# Go Lsp Client > func newLspClient(config config) *lspClient { --- # Source: https://github.com/tectiv3/go-lsp-client/blob/master/client.go # client.go ## Go Source Code ```go package main import ( "bufio" "encoding/json" "fmt" "io" "log" "net" "net/textproto" "os/exec" "strconv" "strings" "sync" ) type config struct { stdio bool url string params []string } type lspClient struct { config config reqID int in io.ReadCloser out io.WriteCloser responseChan chan *response crashesCount int sync.Mutex } func newLspClient(config config) *lspClient { client := lspClient{config: config} client.reqID = 0 client.responseChan = make(chan *response) client.connectToServer() return &client } func (p *lspClient) connectToServer() { if p.config.stdio { cmd := exec.Command(p.config.url, p.config.params...) stdin, err := cmd.StdinPipe() checkError(err) p.out = stdin stdout, err := cmd.StdoutPipe() checkError(err) p.in = stdout stderr, err := cmd.StderrPipe() checkError(err) go p.readPipe(stderr) if err := cmd.Start(); err != nil { checkError(err) } go func() { if err := cmd.Wait(); err != nil { p.crashesCount++ if p.crashesCount == 10 { checkError(err) } Log.WithField("err", err).Info("Restarting server after a crash...") go p.connectToServer() p.responseChan <- &response{Method: "restart"} } }() } else { conn, err := net.Dial("tcp", p.config.url) checkError(err) p.in = conn p.out = conn } go p.listen() } func (p *lspClient) listen() { Log.Info("Listening for messages, ^c to exit") for { msg, err := p.receive() if err != nil { Log.Error(err) break } if msg != nil { go p.processMessage(msg) } } Log.Info("Listener finished") } func (p *lspClient) readPipe(conn io.ReadCloser) { reader := bufio.NewReader(conn) for { b, err := reader.ReadByte() if err != nil { Log.Error(err) return } if reader.Buffered() > 0 { var msgData []byte msgData = append(msgData, b) for reader.Buffered() > 0 { // read byte by byte until the buffered data is not empty b, err := reader.ReadByte() if err == nil { msgData = append(msgData, b) } else { log.Println("-------> unreadable caracter...", b) } } // msgData now contain the buffered data... Log.Error(string(msgData)) } } } func (p *lspClient) processMessage(r *response) { if r.Method == "window/logMessage" { Log.Info(r.Params["message"]) } else if r.Method == "serenata/didProgressIndexing" { Log.Info(r.Params["info"]) } else { Log.WithField("method", r.Method).WithField("params", r.Params).Trace(string(r.Result)) p.responseChan <- r } } func (p *lspClient) request(id int, method string, params interface{}) { p.Lock() defer p.Unlock() r := request{id, method, params} Log.Info(method) p.send(r.format()) } func (p *lspClient) notification(method string, params interface{}) { p.Lock() defer p.Unlock() n := notification{method, params} Log.Info(method) p.send(n.format()) } func (p *lspClient) response(id int, method string, res interface{}) { p.Lock() defer p.Unlock() result, _ := json.Marshal(res) r := response{ID: id, Method: method, Result: result} Log.Info(method) p.send(r.format()) } func (p *lspClient) send(msg string) { Log.Trace(msg) fmt.Fprint(p.out, msg) } func (p *lspClient) receive() (*response, error) { reader := bufio.NewReader(p.in) for { str, err := reader.ReadString('\n') if err != nil { Log.Error(err) return nil, err } Log.Trace(str) tp := textproto.NewReader(bufio.NewReader(strings.NewReader(str + "\n"))) mimeHeader, err := tp.ReadMIMEHeader() if err != nil { Log.WithField("str", str).Error(err) break } if l, ok := mimeHeader["Content-Length"]; ok { _, err := reader.ReadString('\n') if err != nil { Log.Error(err) break } jsonLen, _ := strconv.ParseInt(l[0], 10, 32) buf := make([]byte, jsonLen) _, err = io.ReadFull(reader, buf) if err != nil { Log.Error(err) break } Log.Trace(string(buf)) response := response{} if err := json.Unmarshal(buf, &response); err != nil { Log.WithField("err", err).Warn(string(buf)) } return &response, nil } } return nil, nil } ``` --- # Source: https://github.com/tectiv3/go-lsp-client/blob/master/go.mod # Module Dependencies ``` module github.com/tectiv3/go-lsp-client go 1.13 require ( github.com/sirupsen/logrus v1.4.2 golang.org/x/crypto v0.0.0-20191107222254-f4817d981bb6 ) ``` --- # Source: https://github.com/tectiv3/go-lsp-client/blob/master/htmlFormatter.go # htmlFormatter.go ## Go Source Code ```go package main import ( "bytes" "fmt" "html" "runtime" "sort" "strings" "sync" "time" log "github.com/sirupsen/logrus" ) const ( red = "red" yellow = "yellow" blue = "blue" gray = "grey" ) const defaultTimestampFormat = time.RFC3339 var baseTimestamp time.Time func init() { baseTimestamp = time.Now() } // HTMLFormatter formats logs into text type HTMLFormatter struct { // Force disabling colors. DisableColors bool // Force quoting of all values ForceQuote bool // Disable timestamp logging. useful when output is redirected to logging // system that already adds timestamps. DisableTimestamp bool // Enable logging the full timestamp when a TTY is attached instead of just // the time passed since beginning of execution. FullTimestamp bool // TimestampFormat to use for display when a full timestamp is printed TimestampFormat string // The fields are sorted by default for a consistent output. For applications // that log extremely frequently and don't use the JSON formatter this may not // be desired. DisableSorting bool // The keys sorting function, when uninitialized it uses sort.Strings. SortingFunc func([]string) // QuoteEmptyFields will wrap empty fields in quotes if true QuoteEmptyFields bool // Whether the logger's out is to a terminal IsTerminal bool // FieldMap allows users to customize the names of keys for default fields. // As an example: // formatter := &HTMLFormatter{ // FieldMap: FieldMap{ // FieldKeyTime: "@timestamp", // FieldKeyLevel: "@level", // FieldKeyMsg: "@message"}} FieldMap log.FieldMap // CallerPrettyfier can be set by the user to modify the content // of the function and file keys in the data when ReportCaller is // activated. If any of the returned value is the empty string the // corresponding key will be removed from fields. CallerPrettyfier func(*runtime.Frame) (function string, file string) terminalInitOnce sync.Once } func (f *HTMLFormatter) init(entry *log.Entry) { fmt.Fprint(entry.Buffer, "") } func (f *HTMLFormatter) isColored() bool { return !f.DisableColors } // Format renders a single log entry func (f *HTMLFormatter) Format(entry *log.Entry) ([]byte, error) { data := make(log.Fields) for k, v := range entry.Data { data[k] = v } // prefixFieldClashes(data, f.FieldMap, entry.HasCaller()) keys := make([]string, 0, len(data)) for k := range data { keys = append(keys, k) } var funcVal, fileVal string fixedKeys := make([]string, 0, 4+len(data)) if !f.DisableTimestamp { fixedKeys = append(fixedKeys, log.FieldKeyTime) } fixedKeys = append(fixedKeys, log.FieldKeyLevel) if entry.Message != "" { fixedKeys = append(fixedKeys, log.FieldKeyMsg) } // if entry.err != "" { // fixedKeys = append(fixedKeys, log.FieldKeyLogrusError) // } if entry.HasCaller() { if f.CallerPrettyfier != nil { funcVal, fileVal = f.CallerPrettyfier(entry.Caller) } else { funcVal = entry.Caller.Function fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) } if funcVal != "" { fixedKeys = append(fixedKeys, log.FieldKeyFunc) } if fileVal != "" { fixedKeys = append(fixedKeys, log.FieldKeyFile) } } if !f.DisableSorting { if f.SortingFunc == nil { sort.Strings(keys) fixedKeys = append(fixedKeys, keys...) } else { if !f.isColored() { fixedKeys = append(fixedKeys, keys...) f.SortingFunc(fixedKeys) } else { f.SortingFunc(keys) } } } else { fixedKeys = append(fixedKeys, keys...) } var b *bytes.Buffer if entry.Buffer != nil { b = entry.Buffer } else { b = &bytes.Buffer{} } f.terminalInitOnce.Do(func() { f.init(entry) }) timestampFormat := f.TimestampFormat if timestampFormat == "" { timestampFormat = defaultTimestampFormat } if f.isColored() { f.printColored(b, entry, keys, data, timestampFormat) } else { for _, key := range fixedKeys { var value interface{} switch { case key == (log.FieldKeyTime): value = entry.Time.Format(timestampFormat) case key == (log.FieldKeyLevel): value = entry.Level.String() case key == (log.FieldKeyMsg): value = entry.Message // case key == (log.FieldKeyLogrusError): // value = entry.err case key == (log.FieldKeyFunc) && entry.HasCaller(): value = funcVal case key == (log.FieldKeyFile) && entry.HasCaller(): value = fileVal default: value = data[key] } f.appendKeyValue(b, key, value) } } b.WriteByte('\n') return b.Bytes(), nil } func (f *HTMLFormatter) printColored(b *bytes.Buffer, entry *log.Entry, keys []string, data log.Fields, timestampFormat string) { var levelColor string switch entry.Level { case log.DebugLevel, log.TraceLevel: levelColor = gray case log.WarnLevel: levelColor = yellow case log.ErrorLevel, log.FatalLevel, log.PanicLevel: levelColor = red default: levelColor = blue } levelText := strings.ToUpper(entry.Level.String()) // Remove a single newline if it already exists in the message to keep // the behavior of logrus text_formatter the same as the stdlib log package entry.Message = strings.TrimSuffix(entry.Message, "\n") caller := "" if entry.HasCaller() { funcVal := fmt.Sprintf("%s()", entry.Caller.Function) fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) if f.CallerPrettyfier != nil { funcVal, fileVal = f.CallerPrettyfier(entry.Caller) } if fileVal == "" { caller = funcVal } else if funcVal == "" { caller = fileVal } else { caller = fileVal + " " + funcVal } } fmt.Fprint(b, "") if f.DisableTimestamp { fmt.Fprintf(b, "", levelText, caller, entry.Message) } else if !f.FullTimestamp { fmt.Fprintf(b, "", levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, html.EscapeString(entry.Message)) } else { if entry.Level == log.DebugLevel || entry.Level == log.TraceLevel { fmt.Fprintf(b, "", levelColor, levelText, entry.Time.Format(timestampFormat), caller, levelColor, html.EscapeString(entry.Message)) } else { fmt.Fprintf(b, "", levelColor, levelText, entry.Time.Format(timestampFormat), caller, html.EscapeString(entry.Message)) } } for _, k := range keys { v := data[k] if v == nil { continue } vs := f.getValueString(v) if len(vs) == 0 { continue } fmt.Fprintf(b, "", levelColor, k, vs) } fmt.Fprint(b, "") } func (f *HTMLFormatter) needsQuoting(text string) bool { if f.ForceQuote { return true } if f.QuoteEmptyFields && len(text) == 0 { return true } for _, ch := range text { if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') { return true } } return false } func (f *HTMLFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) { if b.Len() > 0 { b.WriteByte(' ') } b.WriteString(key) b.WriteByte('=') vs := f.getValueString(value) b.WriteString(vs) } func (f *HTMLFormatter) getValueString(value interface{}) string { stringVal, ok := value.(string) if !ok { stringVal = fmt.Sprint(value) } stringVal = html.EscapeString(stringVal) if !f.needsQuoting(stringVal) { return stringVal } return fmt.Sprintf("%q", stringVal) } ``` --- # Source: https://github.com/tectiv3/go-lsp-client/blob/master/message.go # message.go ## Go Source Code ```go package main import ( "encoding/json" "fmt" ) const EOL = "\r\n" type request struct { id int method string params interface{} } type notification struct { method string params interface{} } func (r *request) getBody() KeyValue { id := 1 if r.id > 0 { id = r.id } return KeyValue{ "id": id, "method": r.method, "params": r.params, } } func (r *request) format() string { body := r.getBody() body["jsonrpc"] = "2.0" json, _ := json.Marshal(body) headers := fmt.Sprintf("Content-Length: %d", len(json)) + EOL + "Content-Type: application/vscode-jsonrpc; charset=utf-8" return headers + EOL + EOL + string(json) } func (r request) getMethod() string { return r.method } func (r *notification) getBody() KeyValue { return KeyValue{ "method": r.method, "params": r.params, } } func (r *notification) format() string { body := r.getBody() body["jsonrpc"] = "2.0" json, _ := json.Marshal(body) headers := fmt.Sprintf("Content-Length: %d", len(json)) + EOL + "Content-Type: application/vscode-jsonrpc; charset=utf-8" return headers + EOL + EOL + string(json) } func (r notification) getMethod() string { return r.method } type response struct { ID int Method string Params KeyValue Result json.RawMessage Error KeyValue } func (r *response) getBody() KeyValue { return KeyValue{ // "id": r.ID, "method": r.Method, "result": r.Result, } } func (r *response) format() string { body := r.getBody() body["jsonrpc"] = "2.0" json, _ := json.Marshal(body) headers := fmt.Sprintf("Content-Length: %d", len(json)) + EOL + "Content-Type: application/vscode-jsonrpc; charset=utf-8" return headers + EOL + EOL + string(json) } ``` --- # Source: https://github.com/tectiv3/go-lsp-client/blob/master/models.go # models.go ## Go Source Code ```go package main import ( "database/sql/driver" "encoding/json" "fmt" "strings" ) type kvChan chan *KeyValue // KeyValue is basic key:value struct type KeyValue map[string]interface{} // bool returns the value of the given name, assuming the value is a boolean. // If the value isn't found or is not of the type, the defaultValue is returned. func (kv KeyValue) bool(name string, defaultValue bool) bool { if v, found := kv[name]; found { if castValue, is := v.(bool); is { return castValue } } return defaultValue } // int returns the value of the given name, assuming the value is an int. // If the value isn't found or is not of the type, the defaultValue is returned. func (kv KeyValue) int(name string, defaultValue int) int { if v, found := kv[name]; found { if castValue, is := v.(int); is { return castValue } } return defaultValue } // string returns the value of the given name, assuming the value is a string. // If the value isn't found or is not of the type, the defaultValue is returned. func (kv KeyValue) string(name string, defaultValue string) string { if v, found := kv[name]; found { if castValue, is := v.(string); is { return castValue } } return defaultValue } // float64 returns the value of the given name, assuming the value is a float64. // If the value isn't found or is not of the type, the defaultValue is returned. func (kv KeyValue) float64(name string, defaultValue float64) float64 { if v, found := kv[name]; found { if castValue, is := v.(float64); is { return castValue } } return defaultValue } // Value get value of KeyValue func (kv KeyValue) Value() (driver.Value, error) { return json.Marshal(kv) } // Scan scan value into KeyValue func (kv *KeyValue) Scan(value interface{}) error { bytes, ok := value.([]byte) if !ok { return fmt.Errorf("failed to unmarshal JSON value: %v", value) } return json.Unmarshal(bytes, kv) } type Position struct { /** * Line position in a document (zero-based). */ Line int `json:"line"` /** * Character offset on a line in a document (zero-based). */ Character int `json:"character"` } func (p Position) String() string { return fmt.Sprintf("%d:%d", p.Line, p.Character) } type Range struct { /** * The range's start position. */ Start Position `json:"start"` /** * The range's end position. */ End Position `json:"end"` } func (r Range) String() string { return fmt.Sprintf("%s-%s", r.Start, r.End) } type Location struct { URI DocumentURI `json:"uri"` Range Range `json:"range"` } type Diagnostic struct { /** * The range at which the message applies. */ Range Range `json:"range"` /** * The diagnostic's severity. Can be omitted. If omitted it is up to the * client to interpret diagnostics as error, warning, info or hint. */ Severity DiagnosticSeverity `json:"severity,omitempty"` /** * The diagnostic's code. Can be omitted. */ Code int `json:"code,omitempty"` /** * A human-readable string describing the source of this * diagnostic, e.g. 'typescript' or 'super lint'. */ Source string `json:"source,omitempty"` /** * The diagnostic's message. */ Message string `json:"message"` } type DiagnosticSeverity int type Command struct { /** * Title of the command, like `save`. */ Title string `json:"title"` /** * The identifier of the actual command handler. */ Command string `json:"command"` /** * Arguments that the command handler should be * invoked with. */ Arguments []interface{} `json:"arguments"` } type TextEdit struct { /** * The range of the text document to be manipulated. To insert * text into a document create a range where start === end. */ Range Range `json:"range"` /** * The string to be inserted. For delete operations use an * empty string. */ NewText string `json:"newText"` } type TextDocumentIdentifier struct { /** * The text document's URI. */ URI DocumentURI `json:"uri"` } type TextDocumentItem struct { /** * The text document's URI. */ URI DocumentURI `json:"uri"` /** * The text document's language identifier. */ LanguageID string `json:"languageId"` /** * The version number of this document (it will strictly increase after each * change, including undo/redo). */ Version int `json:"version"` /** * The content of the opened text document. */ Text string `json:"text"` } type VersionedTextDocumentIdentifier struct { TextDocumentIdentifier /** * The version number of this document. */ Version int `json:"version"` } type TextDocumentPositionParams struct { /** * The text document. */ TextDocument TextDocumentIdentifier `json:"textDocument"` /** * The position inside the text document. */ Position Position `json:"position"` } type DocumentURI string type InitializeParams struct { ProcessID int `json:"processId,omitempty"` // RootPath is DEPRECATED in favor of the RootURI field. RootPath string `json:"rootPath,omitempty"` RootURI DocumentURI `json:"rootUri,omitempty"` InitializationOptions interface{} `json:"initializationOptions,omitempty"` Capabilities KeyValue `json:"capabilities"` } // Root returns the RootURI if set, or otherwise the RootPath with 'file://' prepended. func (p *InitializeParams) Root() DocumentURI { if p.RootURI != "" { return p.RootURI } if strings.HasPrefix(p.RootPath, "file://") { return DocumentURI(p.RootPath) } return DocumentURI("file://" + p.RootPath) } type CompletionOptions struct { ResolveProvider bool `json:"resolveProvider,omitempty"` TriggerCharacters []string `json:"triggerCharacters,omitempty"` } type DocumentOnTypeFormattingOptions struct { FirstTriggerCharacter string `json:"firstTriggerCharacter"` MoreTriggerCharacter []string `json:"moreTriggerCharacter,omitempty"` } type CodeLensOptions struct { ResolveProvider bool `json:"resolveProvider,omitempty"` } type SignatureHelpOptions struct { TriggerCharacters []string `json:"triggerCharacters,omitempty"` } type ExecuteCommandOptions struct { Commands []string `json:"commands"` } type ExecuteCommandParams struct { Command string `json:"command"` Arguments []interface{} `json:"arguments,omitempty"` } type CompletionItemKind int const ( _ CompletionItemKind = iota CIKText CIKMethod CIKFunction CIKConstructor CIKField CIKVariable CIKClass CIKInterface CIKModule CIKProperty CIKUnit CIKValue CIKEnum CIKKeyword CIKSnippet CIKColor CIKFile CIKReference CIKFolder CIKEnumMember CIKConstant CIKStruct CIKEvent CIKOperator CIKTypeParameter ) func (c CompletionItemKind) String() string { return completionItemKindName[c] } var completionItemKindName = map[CompletionItemKind]string{ CIKText: "text", CIKMethod: "method", CIKFunction: "function", CIKConstructor: "constructor", CIKField: "field", CIKVariable: "variable", CIKClass: "class", CIKInterface: "interface", CIKModule: "module", CIKProperty: "property", CIKUnit: "unit", CIKValue: "value", CIKEnum: "enum", CIKKeyword: "keyword", CIKSnippet: "snippet", CIKColor: "color", CIKFile: "file", CIKReference: "reference", CIKFolder: "folder", CIKEnumMember: "enumMember", CIKConstant: "constant", CIKStruct: "struct", CIKEvent: "event", CIKOperator: "operator", CIKTypeParameter: "typeParameter", } type CompletionItem struct { Label string `json:"label"` Kind CompletionItemKind `json:"kind,omitempty"` Detail string `json:"detail,omitempty"` Documentation string `json:"documentation,omitempty"` SortText string `json:"sortText,omitempty"` FilterText string `json:"filterText,omitempty"` InsertText string `json:"insertText,omitempty"` InsertTextFormat InsertTextFormat `json:"insertTextFormat,omitempty"` TextEdit *TextEdit `json:"textEdit,omitempty"` Data interface{} `json:"data,omitempty"` } type CompletionList struct { IsIncomplete bool `json:"isIncomplete"` Items []CompletionItem `json:"items"` } type CompletionTriggerKind int const ( CTKInvoked CompletionTriggerKind = 1 CTKTriggerCharacter = 2 ) type InsertTextFormat int const ( ITFPlainText InsertTextFormat = 1 ITFSnippet = 2 ) type CompletionContext struct { TriggerKind CompletionTriggerKind `json:"triggerKind"` TriggerCharacter string `json:"triggerCharacter,omitempty"` } type CompletionParams struct { TextDocumentPositionParams Context CompletionContext `json:"context,omitempty"` } type Hover struct { Contents []MarkedString `json:"contents"` Range *Range `json:"range,omitempty"` } type hover Hover func (h Hover) MarshalJSON() ([]byte, error) { if h.Contents == nil { return json.Marshal(hover{ Contents: []MarkedString{}, Range: h.Range, }) } return json.Marshal(hover(h)) } type MarkedString markedString type markedString struct { Language string `json:"language"` Value string `json:"value"` isRawString bool } func (m *MarkedString) UnmarshalJSON(data []byte) error { if d := strings.TrimSpace(string(data)); len(d) > 0 && d[0] == '"' { // Raw string var s string if err := json.Unmarshal(data, &s); err != nil { return err } m.Value = s m.isRawString = true return nil } // Language string ms := (*markedString)(m) return json.Unmarshal(data, ms) } func (m MarkedString) MarshalJSON() ([]byte, error) { if m.isRawString { return json.Marshal(m.Value) } return json.Marshal((markedString)(m)) } // RawMarkedString returns a MarkedString consisting of only a raw // string (i.e., "foo" instead of {"value":"foo", "language":"bar"}). func RawMarkedString(s string) MarkedString { return MarkedString{Value: s, isRawString: true} } type SignatureHelp struct { Signatures []SignatureInformation `json:"signatures"` ActiveSignature int `json:"activeSignature"` ActiveParameter int `json:"activeParameter"` } type SignatureInformation struct { Label string `json:"label"` Documentation string `json:"documentation,omitempty"` Parameters []ParameterInformation `json:"parameters,omitempty"` } type ParameterInformation struct { Label string `json:"label"` Documentation string `json:"documentation,omitempty"` } type ReferenceContext struct { IncludeDeclaration bool `json:"includeDeclaration"` // Sourcegraph extension XLimit int `json:"xlimit,omitempty"` } type ReferenceParams struct { TextDocumentPositionParams Context ReferenceContext `json:"context"` } type DocumentHighlightKind int const ( Text DocumentHighlightKind = 1 Read = 2 Write = 3 ) type DocumentHighlight struct { Range Range `json:"range"` Kind int `json:"kind,omitempty"` } type DocumentSymbolParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` } type SymbolKind int // The SymbolKind values are defined at https://microsoft.github.io/language-server-protocol/specification. const ( SKFile SymbolKind = 1 SKModule SymbolKind = 2 SKNamespace SymbolKind = 3 SKPackage SymbolKind = 4 SKClass SymbolKind = 5 SKMethod SymbolKind = 6 SKProperty SymbolKind = 7 SKField SymbolKind = 8 SKConstructor SymbolKind = 9 SKEnum SymbolKind = 10 SKInterface SymbolKind = 11 SKFunction SymbolKind = 12 SKVariable SymbolKind = 13 SKConstant SymbolKind = 14 SKString SymbolKind = 15 SKNumber SymbolKind = 16 SKBoolean SymbolKind = 17 SKArray SymbolKind = 18 SKObject SymbolKind = 19 SKKey SymbolKind = 20 SKNull SymbolKind = 21 SKEnumMember SymbolKind = 22 SKStruct SymbolKind = 23 SKEvent SymbolKind = 24 SKOperator SymbolKind = 25 SKTypeParameter SymbolKind = 26 ) func (s SymbolKind) String() string { return symbolKindName[s] } var symbolKindName = map[SymbolKind]string{ SKFile: "File", SKModule: "Module", SKNamespace: "Namespace", SKPackage: "Package", SKClass: "Class", SKMethod: "Method", SKProperty: "Property", SKField: "Field", SKConstructor: "Constructor", SKEnum: "Enum", SKInterface: "Interface", SKFunction: "Function", SKVariable: "Variable", SKConstant: "Constant", SKString: "String", SKNumber: "Number", SKBoolean: "Boolean", SKArray: "Array", SKObject: "Object", SKKey: "Key", SKNull: "Null", SKEnumMember: "EnumMember", SKStruct: "Struct", SKEvent: "Event", SKOperator: "Operator", SKTypeParameter: "TypeParameter", } type SymbolInformation struct { Name string `json:"name"` Kind SymbolKind `json:"kind"` Location Location `json:"location"` ContainerName string `json:"containerName,omitempty"` } type WorkspaceSymbolParams struct { Query string `json:"query"` Limit int `json:"limit"` } type ConfigurationParams struct { Items []ConfigurationItem `json:"items"` } type ConfigurationItem struct { ScopeURI string `json:"scopeUri,omitempty"` Section string `json:"section,omitempty"` } type ConfigurationResult []interface{} type CodeActionContext struct { Diagnostics []Diagnostic `json:"diagnostics"` } type CodeActionParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` Range Range `json:"range"` Context CodeActionContext `json:"context"` } type CodeLensParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` } type CodeLens struct { Range Range `json:"range"` Command Command `json:"command,omitempty"` Data interface{} `json:"data,omitempty"` } type DocumentFormattingParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` Options FormattingOptions `json:"options"` } type FormattingOptions struct { TabSize int `json:"tabSize"` InsertSpaces bool `json:"insertSpaces"` Key string `json:"key"` } type RenameParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` Position Position `json:"position"` NewName string `json:"newName"` } type DidOpenTextDocumentParams struct { TextDocument TextDocumentItem `json:"textDocument"` } type DidChangeTextDocumentParams struct { TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"` } type TextDocumentContentChangeEvent struct { Range *Range `json:"range,omitempty"` RangeLength uint `json:"rangeLength,omitempty"` Text string `json:"text"` } type DidCloseTextDocumentParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` } type DidSaveTextDocumentParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` } type DidChangeConfigurationParams struct { Settings interface{} `json:"settings"` } type FileChangeType int const ( Created FileChangeType = 1 Changed = 2 Deleted = 3 ) type FileEvent struct { URI DocumentURI `json:"uri"` Type int `json:"type"` } type DidChangeWatchedFilesParams struct { Changes []FileEvent `json:"changes"` } type PublishDiagnosticsParams struct { URI DocumentURI `json:"uri"` Diagnostics []Diagnostic `json:"diagnostics"` } type DocumentRangeFormattingParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` Range Range `json:"range"` Options FormattingOptions `json:"options"` } type DocumentOnTypeFormattingParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` Position Position `json:"position"` Ch string `json:"ch"` Options FormattingOptions `json:"formattingOptions"` } ``` --- # Source: https://github.com/tectiv3/go-lsp-client/blob/master/server.go # server.go ## Go Source Code ```go package main import ( "encoding/json" "errors" "net/http" "os" "runtime/debug" "strconv" "sync" "time" "github.com/tectiv3/go-lsp-client/events" ) const cacheTime = 5 * time.Second type mateRequest struct { Method string Body json.RawMessage } type mateServer struct { client *lspClient openFiles map[string]time.Time requestID int initialized bool sync.Mutex } func (s *mateServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer func() { if perr := Panicf(recover(), "%v", r.Method); perr != nil { Log.Error(perr) w.WriteHeader(http.StatusInternalServerError) } }() Log.WithField("method", r.Method).WithField("length", r.ContentLength).Debug(r.URL.Path) if r.Method != http.MethodPost { w.WriteHeader(http.StatusNotFound) return } decoder := json.NewDecoder(r.Body) mr := mateRequest{} err := decoder.Decode(&mr) if err != nil { Log.Error(err) w.WriteHeader(http.StatusInternalServerError) return } resultChan := make(kvChan) var result *KeyValue tick := time.After(20 * time.Second) go s.processRequest(mr, resultChan) // block until result or timeout select { case <-tick: w.WriteHeader(http.StatusGatewayTimeout) w.Header().Set("Content-Type", "application/json") Log.WithField("method", mr.Method).Warn("Time out") json.NewEncoder(w).Encode(KeyValue{"result": "error", "message": "time out"}) return case result = <-resultChan: } if result == nil { w.WriteHeader(http.StatusNoContent) return } w.Header().Set("Content-Type", "application/json") tr, _ := json.Marshal(result) Log.WithField("method", mr.Method).Debug(string(tr)) json.NewEncoder(w).Encode(result) } func (s *mateServer) request(method string, params interface{}) int { s.Lock() s.requestID++ reqID := s.requestID s.Unlock() s.client.request(reqID, method, params) return reqID } func (s *mateServer) requestAndWait(method string, params interface{}, cb kvChan) { reqID := s.request(method, params) // block until got response or timeout s.wait("request."+strconv.Itoa(reqID), cb) } func (s *mateServer) wait(event string, cb kvChan) { timer := time.NewTimer(2 * time.Second) var canceled = make(chan struct{}) events.Once(event, func(event string, payload ...interface{}) { Log.Trace(event + " wait once") timer.Stop() canceled <- struct{}{} cb <- &KeyValue{"result": payload[0]} }) select { case <-timer.C: Log.Warn(event + " timed out") cb <- &KeyValue{"result": "error", "message": event + " timed out"} timer.Stop() events.RemoveAllListeners(event) return case <-canceled: } } func (s *mateServer) processRequest(mr mateRequest, cb kvChan) { defer s.handlePanic(mr) Log.WithField("method", mr.Method).Trace(string(mr.Body)) switch mr.Method { case "hover": params := TextDocumentPositionParams{} if err := json.Unmarshal(mr.Body, ¶ms); err != nil { cb <- &KeyValue{"result": "error", "message": err.Error()} return } s.requestAndWait("textDocument/hover", params, cb) case "completion": params := CompletionParams{} if err := json.Unmarshal(mr.Body, ¶ms); err != nil { cb <- &KeyValue{"result": "error", "message": err.Error()} return } s.requestAndWait("textDocument/completion", params, cb) case "definition": params := TextDocumentPositionParams{} if err := json.Unmarshal(mr.Body, ¶ms); err != nil { cb <- &KeyValue{"result": "error", "message": err.Error()} return } s.requestAndWait("textDocument/definition", params, cb) case "initialize": s.onInitialize(mr, cb) case "didOpen": s.onDidOpen(mr, cb) case "didClose": s.onDidClose(mr, cb) default: cb <- &KeyValue{"result": "error", "message": "unknown method"} } Log.WithField("method", mr.Method).Trace("processRequest finished") } func (s *mateServer) onDidOpen(mr mateRequest, cb kvChan) { s.Lock() defer s.Unlock() textDocument := TextDocumentItem{} if err := json.Unmarshal(mr.Body, &textDocument); err != nil { cb <- &KeyValue{"result": "error", "message": err.Error()} return } fn := string(textDocument.URI) if len(fn) == 0 { cb <- &KeyValue{"result": "error", "message": "Invalid document uri"} return } go s.request("textDocument/documentSymbol", DidOpenTextDocumentParams{textDocument}) if _, ok := s.openFiles[fn]; ok { // cb <- &KeyValue{"result": "ok", "message": "already opened"} // return s.client.notification("textDocument/didClose", DocumentSymbolParams{TextDocumentIdentifier{ DocumentURI(fn), }}) time.Sleep(100 * time.Millisecond) } s.openFiles[fn] = time.Now() s.client.notification("textDocument/didOpen", DidOpenTextDocumentParams{textDocument}) Log.Trace("waiting for diagnostics for " + fn) s.wait("diagnostics."+fn, cb) } func (s *mateServer) onDidClose(mr mateRequest, cb kvChan) { s.Lock() defer s.Unlock() textDocument := TextDocumentIdentifier{} if err := json.Unmarshal(mr.Body, &textDocument); err != nil { cb <- &KeyValue{"result": "error", "message": err.Error()} return } fn := string(textDocument.URI) if len(fn) == 0 { cb <- &KeyValue{"result": "error", "message": "Invalid document uri"} return } s.client.notification("textDocument/didClose", DocumentSymbolParams{textDocument}) delete(s.openFiles, fn) cb <- &KeyValue{"result": "ok"} } func (s *mateServer) onInitialize(mr mateRequest, cb kvChan) { s.Lock() defer s.Unlock() if s.initialized { cb <- &KeyValue{"result": "ok", "message": "already initialized"} return } params := KeyValue{} if err := json.Unmarshal(mr.Body, ¶ms); err != nil { cb <- &KeyValue{"result": "error", "message": err.Error()} return } timer := time.NewTimer(10 * time.Second) var canceled = make(chan struct{}) s.initialize(params) // subscribe to initialized response and wait for it events.Once("initialized", func(event string, payload ...interface{}) { canceled <- struct{}{} timer.Stop() s.initialized = true cb <- &KeyValue{"result": "ok"} }) // block until got response for initialized or timeout select { case <-timer.C: s.initialized = true events.RemoveAllListeners("initialized") s.client.notification("initialized", KeyValue{}) // notify server that we are ready s.client.notification("workspace/didChangeConfiguration", DidChangeConfigurationParams{ KeyValue{"intelephense.files.maxSize": 3000000}, }) cb <- &KeyValue{"result": "ok"} return case <-canceled: } } func (s *mateServer) startListeners() { defer s.handlePanic(mateRequest{}) events.On("request.1", func(event string, payload ...interface{}) { s.client.notification("initialized", KeyValue{}) s.client.notification("workspace/didChangeConfiguration", DidChangeConfigurationParams{ KeyValue{"intelephense.files.maxSize": 3000000}, }) events.Emit("initialized") }) // timer := time.NewTicker(30 * time.Second) for { select { case r := <-s.client.responseChan: switch r.Method { case "restart": s.initialized = false s.openFiles = make(map[string]time.Time) case "client/registerCapability": s.client.notification("client/registerCapability", KeyValue{}) case "textDocument/publishDiagnostics": jsParams, _ := json.Marshal(r.Params) params := PublishDiagnosticsParams{} if err := json.Unmarshal(jsParams, ¶ms); err != nil { Log.Warn(err) } else { Log.Debug("diagnostics." + string(params.URI)) events.Emit("diagnostics."+string(params.URI), params.Diagnostics) } case "workspace/configuration": // temporary cfg := KeyValue{ "files": KeyValue{ "maxSize": 300000, "associations": []string{"*.php", "*.phtml"}, "exclude": []string{ "**/.git/**", "**/.svn/**", "**/.hg/**", "**/CVS/**", "**/.DS_Store/**", "**/node_modules/**", "**/bower_components/**", "**/vendor/**/{Test,test,Tests,tests}/**", "**/.git", "**/.svn", "**/.hg", "**/CVS", "**/.DS_Store", "**/nova/tests/**", "**/faker/**", "**/*.log", "**/*.log*", "**/*.min.*", "**/dist", "**/coverage", "**/build/*", "**/nova/public/*", "**/public/*", }, }, "stubs": []string{ "apache", "bcmath", "bz2", "calendar", "com_dotnet", "Core", "ctype", "curl", "date", "dba", "dom", "enchant", "exif", "fileinfo", "filter", "fpm", "ftp", "gd", "hash", "iconv", "imap", "interbase", "intl", "json", "ldap", "libxml", "mbstring", "mcrypt", "meta", "mssql", "mysqli", "oci8", "odbc", "openssl", "pcntl", "pcre", "PDO", "pdo_ibm", "pdo_mysql", "pdo_pgsql", "pdo_sqlite", "pgsql", "Phar", "posix", "pspell", "readline", "recode", "Reflection", "regex", "session", "shmop", "SimpleXML", "snmp", "soap", "sockets", "sodium", "SPL", "sqlite3", "standard", "superglobals", "sybase", "sysvmsg", "sysvsem", "sysvshm", "tidy", "tokenizer", "wddx", "xml", "xmlreader", "xmlrpc", "xmlwriter", "Zend OPcache", "zip", "zlib", }, "completion": KeyValue{ "insertUseDeclaration": true, "fullyQualifyGlobalConstantsAndFunctions": false, "triggerParameterHints": true, "maxItems": 100, }, "format": KeyValue{ "enable": false, }, "environment": KeyValue{ "documentRoot": "", "includePaths": []string{}, }, "runtime": "", "maxMemory": 0, "telemetry": KeyValue{"enabled": false}, "trace": KeyValue{ "server": "verbose", }, } s.client.response(r.ID, "workspace/configuration", []KeyValue{ cfg, cfg, }) default: events.Emit("request."+strconv.Itoa(r.ID), r.Result) } // case <-timer.C: // go s.cleanOpenFiles() } } } func (s *mateServer) cleanOpenFiles() { s.Lock() defer s.Unlock() if len(s.openFiles) == 0 { return } Log.Trace("Cleaning open files...") for fn, openTime := range s.openFiles { if time.Since(openTime).Seconds() > cacheTime.Seconds() { delete(s.openFiles, fn) s.client.notification("textDocument/didClose", DocumentSymbolParams{TextDocumentIdentifier{DocumentURI(fn)}}) } } } func (s *mateServer) initialize(params KeyValue) error { license := params.string("license", "") dir := params.string("dir", "") if len(dir) == 0 { return errors.New("Empty dir") } storagePath := params.string("storage", "/tmp/intelephense/") name := params.string("name", "phpProject") Log.WithField("dir", dir).WithField("name", name).Info("Initialize") s.client.request(1, "initialize", InitializeParams{ ProcessID: os.Getpid(), RootURI: DocumentURI("file://" + dir), RootPath: dir, InitializationOptions: KeyValue{"storagePath": storagePath, "clearCache": true, "isVscode": true, "licenceKey": license}, Capabilities: KeyValue{ "textDocument": KeyValue{ "synchronization": KeyValue{ "dynamicRegistration": true, "didSave": true, "willSaveWaitUntil": false, "willSave": true, }, "publishDiagnostics": true, "completion": KeyValue{ "dynamicRegistration": true, "contextSupport": true, "completionItem": KeyValue{ "snippetSupport": true, "commitCharactersSupport": true, "documentationFormat": []string{"markdown", "plaintext"}, "deprecatedSupport": true, "preselectSupport": true, }, "completionItemKind": KeyValue{ "valueSet": []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}, }, }, "hover": KeyValue{ "dynamicRegistration": true, "contentFormat": []string{"markdown", "plaintext"}, }, "signatureHelp": KeyValue{ "dynamicRegistration": true, "signatureInformation": KeyValue{ "documentationFormat": []string{"markdown", "plaintext"}, "parameterInformation": KeyValue{"labelOffsetSupport": true}, }, }, "codeLens": KeyValue{"dynamicRegistration": true}, "formatting": KeyValue{"dynamicRegistration": true}, "rangeFormatting": KeyValue{"dynamicRegistration": true}, "onTypeFormatting": KeyValue{"dynamicRegistration": true}, "rename": KeyValue{ "dynamicRegistration": true, "prepareSupport": true, }, "documentLink": KeyValue{"dynamicRegistration": true}, "typeDefinition": KeyValue{ "dynamicRegistration": true, "linkSupport": true, }, "implementation": KeyValue{ "dynamicRegistration": true, "linkSupport": true, }, "colorProvider": KeyValue{"dynamicRegistration": true}, "foldingRange": KeyValue{ "dynamicRegistration": true, "rangeLimit": 5000, "lineFoldingOnly": true, }, "declaration": KeyValue{ "dynamicRegistration": true, "linkSupport": true, }, }, "workspace": KeyValue{ "applyEdit": true, // "didChangeConfiguration": KeyValue{"dynamicRegistration": true}, // "configuration": true, "executeCommand": KeyValue{"dynamicRegistration": true}, "workspaceFolders": true, "symbol": KeyValue{ "dynamicRegistration": true, "symbolKind": KeyValue{ "valueSet": []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}, }, }, "workspaceEdit": KeyValue{ "documentChanges": true, "resourceOperations": []string{"create", "rename", "delete"}, "failureHandling": "textOnlyTransactional", }, "didChangeWatchedFiles": KeyValue{"dynamicRegistration": false}, }, "workspaceFolders": []KeyValue{ KeyValue{ "uri": "file://" + dir, "name": name, }, }, }, }) return nil } func (s *mateServer) handlePanic(mr mateRequest) { if err := recover(); err != nil { Log.WithField("method", mr.Method).WithField("bt", string(debug.Stack())).Error("Recovered from:", err) } } func startServer(client *lspClient, port string) { Log.Info("Running webserver on port " + port) server := mateServer{client: client, openFiles: make(map[string]time.Time), requestID: 1, initialized: false} go server.startListeners() Log.Fatal(http.ListenAndServe(":"+port, &server)) } ``` --- # Source: https://github.com/tectiv3/go-lsp-client/blob/master/util.go # util.go ## Go Source Code ```go package main import ( "fmt" "log" "os" "path" "runtime" "strings" "time" ) const megabyte = 1024 * 1024 func runProfiler() { m := &runtime.MemStats{} for { runtime.ReadMemStats(m) Log.Tracef( "goroutines: %v, mem used: %v MB, mem acquired: %v MB. GC runs: %v, GC paused: %v ns", runtime.NumGoroutine(), m.Alloc/megabyte, m.Sys/megabyte, m.NumGC, m.PauseTotalNs, ) time.Sleep(time.Second * 10) } } // Panicf takes the return value of recover() and outputs data to the log with // the stack trace appended. Arguments are handled in the manner of // fmt.Printf. Arguments should format to a string which identifies what the // panic code was doing. Returns a non-nil error if it recovered from a panic. func Panicf(r interface{}, format string, v ...interface{}) error { if r != nil { // Same as net/http const size = 64 << 10 buf := make([]byte, size) buf = buf[:runtime.Stack(buf, false)] id := fmt.Sprintf(format, v...) log.Printf("panic serving %s: %v\n%s", id, r, string(buf)) return fmt.Errorf("unexpected panic: %v", r) } return nil } func checkError(err error) { if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s\n", err.Error()) os.Exit(1) } } func callerPrettyfier(f *runtime.Frame) (string, string) { s := strings.Split(f.Function, ".") funcname := s[len(s)-1] _, filename := path.Split(f.File) return funcname, filename } func userHomeDir() string { if runtime.GOOS == "windows" { home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") if home == "" { home = os.Getenv("USERPROFILE") } return home } else if runtime.GOOS == "linux" { home := os.Getenv("XDG_CONFIG_HOME") if home != "" { return home } } return os.Getenv("HOME") } ```
%s%s%s%s%d%s%s%s%s%s
%s
%s%s%s
%s
%s=%s