# 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, "| %s | %s | %s | ", levelText, caller, entry.Message)
} else if !f.FullTimestamp {
fmt.Fprintf(b, "%s | %d | %s | %s | ", 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, "%s | %s | %s | %s | ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, levelColor, html.EscapeString(entry.Message))
} else {
fmt.Fprintf(b, "%s | %s | %s | %s | ", 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, "%s=%s | ", 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")
}
```