├── .gitignore ├── README.md ├── default.nix ├── go.mod ├── go.sum ├── images ├── completion.png ├── definition.png └── diagnostic.png ├── main.go ├── neuron ├── query.go ├── query_test.go └── query_types.go ├── server.go ├── server_test.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | result 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neuron-language-server 2 | Language server for [neuron](https://github.com/srid/neuron). 3 | 4 | Neuron will embed language server in neuron. Check [sric/neuron#213](https://github.com/srid/neuron/issues/213) for updates.
5 | This is just an personal experiment. 6 | 7 | Supports 8 | - textDocument/completion(search by title/id)
9 | ![completion](./images/completion.png) 10 | - textDocument/definition 11 | - textDocument/hover
12 | ![definition](./images/definition.png) 13 | - textDocument/publishDiagnostics
14 | ![diagnostic](./images/diagnostic.png)
15 | Virtual text is available with [nvim-lsp](https://github.com/neovim/nvim-lsp) 16 | 17 | 18 | #### TODO 19 | * [Renaming tag](https://github.com/srid/neuron/issues/213#issuecomment-648885576) 20 | 21 | * improve completion 22 | 23 | * LSP snippets 24 | - header snippets? 25 | 26 | * textDocument/codeAction
27 | https://github.com/felko/neuron-mode has some awesome features that might be converted to code actions. 28 | 29 | 30 | #### Prerequisites 31 | - neuron 32 | 33 | #### Installation 34 | ``` 35 | go get -u github.com/aca/neuron-language-server 36 | ``` 37 | 38 | #### LSP client settings 39 | - vim/neovim, [coc.nvim](https://github.com/neoclide/coc.nvim) 40 | ``` 41 | "languageserver": { 42 | "neuron": { 43 | "command": "neuron-language-server", 44 | "filetypes": ["markdown"] 45 | }, 46 | ``` 47 | - neovim, [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) 48 | ```lua 49 | local nvim_lsp = require'lspconfig' 50 | local configs = require'lspconfig/configs' 51 | 52 | configs.neuron_ls = { 53 | default_config = { 54 | cmd = {'neuron-language-server'}; 55 | filetypes = {'markdown'}; 56 | root_dir = function() 57 | return vim.loop.cwd() 58 | end; 59 | settings = {}; 60 | }; 61 | } 62 | 63 | nvim_lsp.neuron_ls.setup{} 64 | ``` 65 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import (builtins.fetchTarball { 2 | url = "https://github.com/nixos/nixpkgs/archive/ccd458053b0e.tar.gz"; 3 | sha256 = "1qwnmmb2p7mj1h1ffz1wvkr1v55qbhzvxr79i3a15blq622r4al9"; 4 | }) { } 5 | }: 6 | 7 | pkgs.buildGoModule { 8 | pname = "neuron-language-server"; 9 | version = "0.1.1"; 10 | 11 | src = ./.; 12 | 13 | vendorSha256 = "0pjjkw0633l8qbvwzy57rx76zjn3w3kf5f7plxnpxih9zj0q258l"; 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aca/neuron-language-server 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d 7 | github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 2 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 3 | github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d h1:afLbh+ltiygTOB37ymZVwKlJwWZn+86syPTbrrOAydY= 4 | github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d/go.mod h1:SULmZY7YNBsvNiQbrb/BEDdEJ84TGnfyUQxaHt8t8rY= 5 | github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37 h1:marA1XQDC7N870zmSFIoHZpIUduK80USeY0Rkuflgp4= 6 | github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= 7 | -------------------------------------------------------------------------------- /images/completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aca/neuron-language-server/450a7cff71c14e291ee85ff8a0614fa9d4dd5145/images/completion.png -------------------------------------------------------------------------------- /images/definition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aca/neuron-language-server/450a7cff71c14e291ee85ff8a0614fa9d4dd5145/images/definition.png -------------------------------------------------------------------------------- /images/diagnostic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aca/neuron-language-server/450a7cff71c14e291ee85ff8a0614fa9d4dd5145/images/diagnostic.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "log" 7 | "net" 8 | "os" 9 | 10 | "github.com/aca/neuron-language-server/neuron" 11 | "github.com/sourcegraph/go-lsp" 12 | "github.com/sourcegraph/jsonrpc2" 13 | ) 14 | 15 | type DebugLogger struct { 16 | conn net.Conn 17 | } 18 | 19 | func main() { 20 | var connOpt []jsonrpc2.ConnOpt 21 | 22 | // Setup a debug logger, 23 | // You can send a log to client, but each clients implement different way to see / debug log. 24 | // So we just use simple trick to debug server. 25 | // 26 | // $ nc -lp 3000 27 | // Use netcat on the terminal like this, and this server would write log to it. 28 | logConn, err := net.Dial("tcp", "localhost:3000") 29 | var logger *log.Logger 30 | if err == nil { 31 | logger = log.New(logConn, "", log.LstdFlags|log.Lshortfile) 32 | } else { 33 | logger = log.New(ioutil.Discard, "", log.LstdFlags) 34 | } 35 | 36 | connOpt = append(connOpt, jsonrpc2.LogMessages(logger)) 37 | 38 | s := &server{ 39 | logger: logger, 40 | documents: make(map[lsp.DocumentURI]string), 41 | neuronMeta: make(map[string]neuron.Result), 42 | diagnosticChan: make(chan lsp.DocumentURI), 43 | } 44 | 45 | s.logger.Print("start") 46 | 47 | handler := jsonrpc2.HandlerWithError(s.handle) 48 | 49 | <-jsonrpc2.NewConn( 50 | context.Background(), 51 | jsonrpc2.NewBufferedStream(stdrwc{}, jsonrpc2.VSCodeObjectCodec{}), 52 | handler, connOpt...).DisconnectNotify() 53 | 54 | s.logger.Print("shutdown") 55 | } 56 | 57 | type stdrwc struct{} 58 | 59 | func (stdrwc) Read(p []byte) (int, error) { 60 | return os.Stdin.Read(p) 61 | } 62 | 63 | func (c stdrwc) Write(p []byte) (int, error) { 64 | return os.Stdout.Write(p) 65 | } 66 | 67 | func (c stdrwc) Close() error { 68 | if err := os.Stdin.Close(); err != nil { 69 | return err 70 | } 71 | return os.Stdout.Close() 72 | } 73 | -------------------------------------------------------------------------------- /neuron/query.go: -------------------------------------------------------------------------------- 1 | package neuron 2 | 3 | import ( 4 | "encoding/json" 5 | "os/exec" 6 | ) 7 | 8 | func Query(arg ...string) (*QueryResult, error) { 9 | // TODO: to avoid creating .neuron directory 10 | // This should be optional with flags 11 | // path, err := os.Getwd() 12 | // if err != nil { 13 | // return nil, err 14 | // } 15 | // 16 | // _, err = os.Stat(filepath.Join(path, ".neuron")) 17 | // if os.IsNotExist(err) { 18 | // return nil, nil 19 | // } else if err != nil { 20 | // return nil, err 21 | // } 22 | 23 | arg = append([]string{"query"}, arg...) 24 | cmd := exec.Command("neuron", arg...) 25 | output, err := cmd.Output() 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | result := new(QueryResult) 31 | err = json.Unmarshal(output, result) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return result, nil 37 | } 38 | -------------------------------------------------------------------------------- /neuron/query_test.go: -------------------------------------------------------------------------------- 1 | package neuron 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestQuery(t *testing.T) { 8 | _, err := Query() 9 | if err != nil { 10 | t.Fatal(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /neuron/query_types.go: -------------------------------------------------------------------------------- 1 | package neuron 2 | 3 | type QueryResult struct { 4 | Skipped Skipped `json:"skipped"` 5 | Result []Result `json:"result"` 6 | // Query []NeuronQuery `json:"query"` 7 | } 8 | 9 | type QueryClass struct { 10 | ZettelsViewLinkView ZettelsViewLinkView `json:"zettelsViewLinkView"` 11 | ZettelsViewGroupByTag bool `json:"zettelsViewGroupByTag"` 12 | } 13 | 14 | type Result struct { 15 | ZettelTags []string `json:"zettelTags"` 16 | ZettelDay string `json:"zettelDay"` 17 | ZettelID string `json:"zettelID"` 18 | ZettelError ZettelError `json:"zettelError"` 19 | ZettelContent []interface{} `json:"zettelContent"` 20 | // ZettelQueries [][]ResultZettelQuery `json:"zettelQueries"` 21 | ZettelFormat ZettelFormat `json:"zettelFormat"` 22 | ZettelPath string `json:"zettelPath"` 23 | ZettelTitle string `json:"zettelTitle"` 24 | ZettelTitleInBody bool `json:"zettelTitleInBody"` 25 | } 26 | 27 | type ZettelError struct { 28 | Right []interface{} `json:"Right"` 29 | } 30 | 31 | type Skipped struct { 32 | } 33 | 34 | type ZettelsViewLinkView string 35 | 36 | const ( 37 | LinkViewDefault ZettelsViewLinkView = "LinkView_Default" 38 | LinkViewShowDate ZettelsViewLinkView = "LinkView_ShowDate" 39 | ) 40 | 41 | type QueryEnum string 42 | 43 | const ( 44 | ZettelQueryZettelByID QueryEnum = "ZettelQuery_ZettelByID" 45 | ZettelQueryZettelsByTag QueryEnum = "ZettelQuery_ZettelsByTag" 46 | ) 47 | 48 | type ZettelFormat string 49 | 50 | const ( 51 | Markdown ZettelFormat = "markdown" 52 | ) 53 | 54 | type NeuronQuery struct { 55 | Enum *QueryEnum 56 | UnionArray []QueryQueryUnion 57 | } 58 | 59 | type QueryQueryUnion struct { 60 | AnythingArray []interface{} 61 | QueryClass *QueryClass 62 | } 63 | 64 | type ResultZettelQuery struct { 65 | Enum *QueryEnum 66 | UnionArray []ZettelQueryZettelQuery 67 | } 68 | 69 | type ZettelQueryZettelQuery struct { 70 | QueryClass *QueryClass 71 | String *string 72 | StringArray []string 73 | } 74 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/url" 9 | "os" 10 | "path/filepath" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/aca/neuron-language-server/neuron" 15 | "github.com/sourcegraph/go-lsp" 16 | "github.com/sourcegraph/jsonrpc2" 17 | ) 18 | 19 | type server struct { 20 | conn *jsonrpc2.Conn 21 | rootURI string // from initliaze param 22 | rootDir string 23 | logger *log.Logger 24 | neuronMeta map[string]neuron.Result 25 | documents map[lsp.DocumentURI]string 26 | diagnosticChan chan lsp.DocumentURI 27 | } 28 | 29 | func (s *server) update(uri lsp.DocumentURI) { 30 | select { 31 | case s.diagnosticChan <- lsp.DocumentURI(uri): 32 | default: 33 | s.logger.Println("skip diagnostic") 34 | } 35 | } 36 | 37 | // Wiki-style links https://github.com/srid/neuron/pull/351 38 | var neuronLinkRegex = regexp.MustCompile(`\[?\[\[([^]\s]+)\]?\]\]`) 39 | 40 | func (s *server) findLinks(txt string) []lsp.Diagnostic { 41 | lines := strings.Split(txt, "\n") 42 | 43 | diagnostics := []lsp.Diagnostic{} 44 | 45 | for ln, lt := range lines { 46 | matches := neuronLinkRegex.FindAllStringIndex(lt, -1) 47 | 48 | chars := []rune(lt) 49 | 50 | for _, match := range matches { 51 | matchStr := string(chars[match[0]:match[1]]) 52 | matchStr = strings.ReplaceAll(matchStr, "[[[", "") 53 | matchStr = strings.ReplaceAll(matchStr, "]]]", "") 54 | matchStr = strings.ReplaceAll(matchStr, "[[", "") 55 | matchStr = strings.ReplaceAll(matchStr, "]]", "") 56 | 57 | matchLink, ok := s.neuronMeta[matchStr] 58 | if !ok { 59 | continue 60 | } 61 | diagnostics = append(diagnostics, lsp.Diagnostic{ 62 | Range: lsp.Range{ 63 | Start: lsp.Position{Line: ln, Character: match[0]}, 64 | End: lsp.Position{Line: ln, Character: match[1]}, 65 | }, 66 | Message: matchLink.ZettelTitle, 67 | Severity: 4, 68 | }) 69 | } 70 | } 71 | 72 | return diagnostics 73 | } 74 | 75 | func (s *server) diagnostic() { 76 | for { 77 | s.logger.Println("diagnostic start") 78 | uri, _ := <-s.diagnosticChan 79 | 80 | diagnostics := s.findLinks(s.documents[uri]) 81 | 82 | s.conn.Notify( 83 | context.Background(), 84 | "textDocument/publishDiagnostics", 85 | &lsp.PublishDiagnosticsParams{ 86 | URI: uri, 87 | Diagnostics: diagnostics, 88 | // Version: , 89 | }) 90 | } 91 | } 92 | 93 | func (s *server) handleTextDocumentDidOpen(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { 94 | if req.Params == nil { 95 | return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} 96 | } 97 | 98 | var params lsp.DidOpenTextDocumentParams 99 | err = json.Unmarshal(*req.Params, ¶ms) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | s.documents[params.TextDocument.URI] = params.TextDocument.Text 105 | s.update(params.TextDocument.URI) 106 | return nil, nil 107 | } 108 | 109 | func (s *server) handleTextDocumentDidChange(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { 110 | if req.Params == nil { 111 | return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} 112 | } 113 | 114 | var params lsp.DidChangeTextDocumentParams 115 | err = json.Unmarshal(*req.Params, ¶ms) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | if len(params.ContentChanges) != 1 { 121 | return nil, fmt.Errorf("len(params.ContentChanges) = %v", len(params.ContentChanges)) 122 | } 123 | 124 | s.documents[params.TextDocument.URI] = params.ContentChanges[0].Text 125 | s.update(params.TextDocument.URI) 126 | return nil, nil 127 | } 128 | 129 | func (s *server) handleTextDocumentCompletion(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { 130 | if req.Params == nil { 131 | return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} 132 | } 133 | 134 | var params lsp.CompletionParams 135 | err = json.Unmarshal(*req.Params, ¶ms) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | items := make([]lsp.CompletionItem, 0) 141 | 142 | w := s.WordAt(params.TextDocument.URI, params.Position) 143 | 144 | w = strings.ReplaceAll(w, "[[[", "") 145 | w = strings.ReplaceAll(w, "]]]", "") 146 | 147 | w = strings.ReplaceAll(w, "[[", "") 148 | w = strings.ReplaceAll(w, "]]", "") 149 | 150 | for id, m := range s.neuronMeta { 151 | 152 | if w == "" { 153 | item := lsp.CompletionItem{ 154 | Label: fmt.Sprintf("%v:%v", id, m.ZettelTitle), 155 | InsertText: id, 156 | Detail: m.ZettelDay, 157 | } 158 | items = append(items, item) 159 | continue 160 | } 161 | 162 | w = strings.ToLower(w) 163 | 164 | zid := strings.ToLower(m.ZettelID) 165 | if strings.Contains(zid, w) { 166 | item := lsp.CompletionItem{ 167 | Label: fmt.Sprintf("%v:%v", id, m.ZettelTitle), 168 | InsertText: id, 169 | Detail: m.ZettelDay, 170 | } 171 | items = append(items, item) 172 | continue 173 | } 174 | 175 | ztitle := strings.ToLower(m.ZettelTitle) 176 | if strings.Contains(ztitle, w) { 177 | item := lsp.CompletionItem{ 178 | Label: fmt.Sprintf("%v:%v", id, m.ZettelTitle), 179 | InsertText: id, 180 | Detail: m.ZettelDay, 181 | } 182 | items = append(items, item) 183 | continue 184 | } 185 | } 186 | 187 | return items, nil 188 | } 189 | 190 | func (s *server) handleTextDocumentDefinition(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { 191 | if req.Params == nil { 192 | return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} 193 | } 194 | 195 | var params lsp.TextDocumentPositionParams 196 | if err := json.Unmarshal(*req.Params, ¶ms); err != nil { 197 | return nil, err 198 | } 199 | 200 | w := s.WordAt(params.TextDocument.URI, params.Position) 201 | 202 | matches := neuronLinkRegex.FindStringSubmatch(w) 203 | if matches == nil || len(matches) != 2 { 204 | s.logger.Printf("%s not found", w) 205 | return nil, nil 206 | } 207 | 208 | w = strings.ReplaceAll(matches[1], "?cf", "") 209 | 210 | neuronResult, ok := s.neuronMeta[w] 211 | if !ok { 212 | s.logger.Printf("%v doesn't exist", w) 213 | return nil, nil 214 | } 215 | 216 | p := filepath.Join(s.rootDir, neuronResult.ZettelPath) 217 | 218 | return lsp.Location{ 219 | URI: "file://" + lsp.DocumentURI(p), 220 | }, nil 221 | } 222 | 223 | func (s *server) handleTextDocumentHover(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { 224 | if req.Params == nil { 225 | return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} 226 | } 227 | 228 | var params lsp.TextDocumentPositionParams 229 | if err := json.Unmarshal(*req.Params, ¶ms); err != nil { 230 | return nil, err 231 | } 232 | 233 | w := s.WordAt(params.TextDocument.URI, params.Position) 234 | 235 | matches := neuronLinkRegex.FindStringSubmatch(w) 236 | if matches == nil || len(matches) != 2 { 237 | s.logger.Printf("%v not found: ", w) 238 | return nil, nil 239 | } 240 | 241 | w = matches[1] 242 | 243 | neuronResult, ok := s.neuronMeta[w] 244 | if !ok { 245 | s.logger.Printf("%v doesn't exist", w) 246 | return nil, nil 247 | } 248 | 249 | msgl := []string{ 250 | fmt.Sprintf("[%s](%v)\n", neuronResult.ZettelTitle, neuronResult.ZettelPath), 251 | } 252 | 253 | if len(neuronResult.ZettelTags) != 0 { 254 | msgl = append(msgl, fmt.Sprintf("tags: %v", strings.Join(neuronResult.ZettelTags, ","))) 255 | } 256 | 257 | if neuronResult.ZettelDay != "" { 258 | msgl = append(msgl, fmt.Sprintf("date: %v", neuronResult.ZettelDay)) 259 | } 260 | 261 | msg := strings.Join(msgl, "\n") 262 | 263 | return lsp.Hover{ 264 | Contents: []lsp.MarkedString{ 265 | { 266 | Language: `markdown`, 267 | Value: msg, 268 | }, 269 | }, 270 | }, nil 271 | } 272 | 273 | func (s *server) handleInitialize(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { 274 | s.logger.Print("handleInitialize") 275 | if req.Params == nil { 276 | return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} 277 | } 278 | 279 | s.conn = conn 280 | 281 | var params lsp.InitializeParams 282 | if err := json.Unmarshal(*req.Params, ¶ms); err != nil { 283 | return nil, err 284 | } 285 | 286 | u, err := url.ParseRequestURI(string(params.RootURI)) 287 | if err != nil { 288 | return nil, err 289 | } 290 | 291 | s.rootDir = u.EscapedPath() 292 | s.logger.Printf("neuron: query -d %v", s.rootDir) 293 | queryResult, err := neuron.Query("-d", s.rootDir) 294 | if err != nil { 295 | s.logger.Println(queryResult) 296 | return nil, err 297 | } 298 | 299 | s.logger.Printf("neuron: %v found", len(queryResult.Result)) 300 | 301 | for _, result := range queryResult.Result { 302 | s.logger.Printf("neuron: added %s", result.ZettelID) 303 | s.neuronMeta[result.ZettelID] = result 304 | } 305 | 306 | initializeResult := lsp.InitializeResult{ 307 | Capabilities: lsp.ServerCapabilities{ 308 | TextDocumentSync: &lsp.TextDocumentSyncOptionsOrKind{ 309 | Options: &lsp.TextDocumentSyncOptions{ 310 | OpenClose: true, 311 | Change: lsp.TDSKFull, 312 | }, 313 | }, 314 | DefinitionProvider: true, 315 | HoverProvider: true, 316 | CompletionProvider: &lsp.CompletionOptions{ 317 | ResolveProvider: true, 318 | TriggerCharacters: []string{"[", "[["}, 319 | }, 320 | }, 321 | } 322 | 323 | go s.diagnostic() 324 | 325 | return initializeResult, nil 326 | } 327 | 328 | func (s *server) handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { 329 | switch req.Method { 330 | case "initialize": 331 | return s.handleInitialize(ctx, conn, req) 332 | case "initialized": 333 | return 334 | case "shutdown": 335 | os.Exit(0) 336 | return 337 | case "textDocument/didOpen": 338 | return s.handleTextDocumentDidOpen(ctx, conn, req) 339 | case "textDocument/didChange": 340 | return s.handleTextDocumentDidChange(ctx, conn, req) 341 | case "textDocument/completion": 342 | return s.handleTextDocumentCompletion(ctx, conn, req) 343 | case "textDocument/definition": 344 | return s.handleTextDocumentDefinition(ctx, conn, req) 345 | case "textDocument/hover": 346 | return s.handleTextDocumentHover(ctx, conn, req) 347 | // case "textDocument/didSave": 348 | // return s.handleTextDocumentDidSave(ctx, conn, req) 349 | // case "textDocument/didClose": 350 | // return s.handleTextDocumentDidClose(ctx, conn, req) 351 | // case "textDocument/formatting": 352 | // return s.handleTextDocumentFormatting(ctx, conn, req) 353 | // case "textDocument/documentSymbol": 354 | // return s.handleTextDocumentSymbol(ctx, conn, req) 355 | // case "textDocument/codeAction": 356 | // return s.handleTextDocumentCodeAction(ctx, conn, req) 357 | // case "workspace/executeCommand": 358 | // return s.handleWorkspaceExecuteCommand(ctx, conn, req) 359 | // case "workspace/didCs.ngeConfiguration": 360 | // return s.handleWorkspaceDidChangeConfiguration(ctx, conn, req) 361 | // case "workspace/workspaceFolders": 362 | // return s.handleWorkspaceWorkspaceFolders(ctx, conn, req) 363 | } 364 | 365 | return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeMethodNotFound, Message: fmt.Sprintf("method not supported: %s", req.Method)} 366 | } 367 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_findLinks(t *testing.T) { 8 | } 9 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/sourcegraph/go-lsp" 8 | ) 9 | 10 | var nonEmptyString = regexp.MustCompile(`\S+`) 11 | 12 | // WordAt returns word at certain postition 13 | func (s *server) WordAt(uri lsp.DocumentURI, pos lsp.Position) string { 14 | text, ok := s.documents[uri] 15 | if !ok { 16 | return "" 17 | } 18 | lines := strings.Split(text, "\n") 19 | if pos.Line < 0 || pos.Line > len(lines) { 20 | return "" 21 | } 22 | 23 | curLine := lines[pos.Line] 24 | wordIdxs := nonEmptyString.FindAllStringIndex(curLine, -1) 25 | for _, wordIdx := range wordIdxs { 26 | if wordIdx[0] <= pos.Character && pos.Character < wordIdx[1] { 27 | return curLine[wordIdx[0]:wordIdx[1]] 28 | } 29 | } 30 | 31 | return "" 32 | } 33 | --------------------------------------------------------------------------------