├── .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 | 
10 | - textDocument/definition
11 | - textDocument/hover
12 | 
13 | - textDocument/publishDiagnostics
14 | 
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 |
--------------------------------------------------------------------------------