-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
459 lines (354 loc) · 15.6 KB
/
main.go
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
package main
import (
"log"
"os"
"strings"
"sync"
"encoding/json"
"net/url"
"go-template-lsp/lsp"
"github.com/yayolande/gota"
"github.com/yayolande/gota/parser"
"github.com/yayolande/gota/lexer"
checker "github.com/yayolande/gota/analyzer"
)
type workSpaceStore struct {
rawFiles map[string][]byte
parsedFiles map[string]*parser.GroupStatementNode
openFilesAnalyzed map[string]*checker.FileDefinition
openedFilesError map[string][]lexer.Error
}
func main() {
// str := "Content-Length: 865\r\n\r\n" + `{"method":"initialize","jsonrpc":"2.0","id":1,"params":{"workspaceFolders":null,"capabilities":{"textDocument":{"completion":{"dynamicRegistration":false,"completionList":{"itemDefaults":["commitCharacters","editRange","insertTextFormat","insertTextMode","data"]},"contextSupport":true,"completionItem":{"snippetSupport":true,"labelDetailsSupport":true,"insertTextModeSupport":{"valueSet":[1,2]},"resolveSupport":{"properties":["documentation","detail","additionalTextEdits","sortText","filterText","insertText","textEdit","insertTextFormat","insertTextMode"]},"insertReplaceSupport":true,"tagSupport":{"valueSet":[1]},"preselectSupport":true,"deprecatedSupport":true,"commitCharactersSupport":true},"insertTextMode":1}}},"rootUri":null,"rootPath":null,"clientInfo":{"version":"0.10.1+v0.10.1","name":"Neovim"},"processId":230750,"workDoneToken":"1","trace":"off"}}`
// scanner := bufio.NewScanner(strings.NewReader(str))
// scanner := bufio.NewScanner(os.Stdin)
// scanner.Split(inputParsingSplitFunc)
configureLogging()
// scanner := lsp.Decode(strings.NewReader(str))
scanner := lsp.ReceiveInput(os.Stdin)
// ******************************************************************************
// WARNING: In under no cirscumstance the 4 variable below should be re-assnigned
// Otherwise, a nasty bug will appear (value not synced with the rest of the app)
// ******************************************************************************
storage := &workSpaceStore{}
rootPathNotication := make(chan string, 2)
textChangedNotification := make(chan bool, 2)
textFromClient := make(map[string][]byte)
muTextFromClient := new(sync.Mutex)
go ProcessDiagnosticNotification(storage, rootPathNotication, textChangedNotification, textFromClient, muTextFromClient)
var request lsp.RequestMessage[any]
var response []byte
var isRequestResponse bool // Response: true <====> Notification: false
var fileURI string
var fileContent []byte
// TODO: What if 'initialize' request is not sent as first request ?
// Check LSP documentation to learn how to handle those issues
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize
// TODO: what if the file to process is out of the project ? For instance what happen if client request diagnostic
// for a file outside of the project ?
for scanner.Scan() {
data := scanner.Bytes()
// TODO: All over the code base, replace 'json' module by a custom made 'stringer' tool
// because it is more performat
json.Unmarshal(data, &request)
// log.Printf("Received json struct: %q\n", data)
log.Printf("Received json struct: %+v\n", request)
// TODO: behavior of the 'method' do not respect the LSP spec. For instance 'initialize' must only happen once
// However there is nothing stoping a rogue program to 'initialize' more than once, or even to not 'initialize' at all
switch request.Method {
case "initialize":
var rootURI string
response, rootURI = lsp.ProcessInitializeRequest(data)
notifyTheRootPath(rootPathNotication, rootURI)
rootPathNotication = nil
isRequestResponse = true
case "initialized":
isRequestResponse = false
lsp.ProcessInitializedNotificatoin(data)
case "textDocument/didOpen":
isRequestResponse = false
fileURI, fileContent = lsp.ProcessDidOpenTextDocumentNotification(data)
insertTextDocumentToDiagnostic(fileURI, fileContent, textChangedNotification, textFromClient, muTextFromClient)
case "textDocument/didChange":
isRequestResponse = false
fileURI, fileContent = lsp.ProcessDidChangeTextDocumentNotification(data)
insertTextDocumentToDiagnostic(fileURI, fileContent, textChangedNotification, textFromClient, muTextFromClient)
case "textDocument/didClose":
// TODO: Not sure what to do
case "textDocument/hover":
isRequestResponse = true
response = lsp.ProcessHoverRequest(data)
case "textDocument/definition":
isRequestResponse = true
response, fileURI, fileContent = lsp.ProcessGoToDefinition(data, storage.openFilesAnalyzed, storage.rawFiles)
insertTextDocumentToDiagnostic(fileURI, fileContent, textChangedNotification, textFromClient, muTextFromClient)
}
if isRequestResponse {
lsp.SendToLspClient(os.Stdout, response)
/*
response = lsp.Encode(response)
lsp.SendOutput(os.Stdout, response)
*/
}
// log.Printf("Sent json struct: %+v", string(response))
response = nil
isRequestResponse = false
}
if scanner.Err() != nil {
log.Printf("error: ", scanner.Err().Error())
}
log.Printf("\n Shutting down custom lsp server")
}
// Queue like system that notify concerned goroutine when new 'text document' is received from the client.
// Not all sent 'text document' are processed in order, or even processed at all.
// In other word, if the same document is inserted many time, only the most recent will be processed when
// concerned goroutine is ready to do so
func insertTextDocumentToDiagnostic(uri string, content []byte, textChangedNotification chan bool, textFromClient map[string][]byte, muTextFromClient *sync.Mutex) {
if content == nil || uri == "" {
return
}
muTextFromClient.Lock()
textFromClient[uri] = content
muTextFromClient.Unlock()
if len(textChangedNotification) == 0 {
textChangedNotification <- true
}
if len(textChangedNotification) >= 2 {
panic("'textChangedNotification' channel size should never exceed 1, otherwise goroutine might be blocked and nasty bug may appear. " +
"as per standard, when there is at least one 'text' from client waiting to be processed, len(textChangedNotification) must remain at 1")
}
}
func notifyTheRootPath(rootPathNotication chan string, rootURI string) {
if rootPathNotication == nil {
panic("unexpected usage of 'rootPathNotication' channel. This channel should be used only once to send root path. " +
"Either it hasn't been initialized at least once, or it has been used more than once (bc. channel set to nil after first use)")
}
if cap(rootPathNotication) == 1 {
panic("'rootPathNotication' channel should be empty at this point. " +
"Either an element have been illegally inserted or the 'initialize' method might be the responsible")
}
if cap(rootPathNotication) == 1 {
panic("'rootPathNotication' channel should have a buffer capacity of at least 2, to have its blocking behavior")
}
rootPathNotication <- rootURI
close(rootPathNotication)
}
// Independently diagnostic code source and send notifications to client
func ProcessDiagnosticNotification(storage *workSpaceStore, rootPathNotication chan string, textChangedNotification chan bool, textFromClient map[string][]byte, muTextFromClient *sync.Mutex) {
if rootPathNotication == nil || textChangedNotification == nil {
panic("channel(s) for 'ProcessDiagnosticNotification()' not properly initialized")
}
if textFromClient == nil {
panic("empty reference to 'textFromClient'. LSP server won't be able to handle text update from client")
}
var rootPath string
var targetExtension string
rootPath, ok := <- rootPathNotication
rootPathNotication = nil
if !ok {
panic("rootPathNotification is closed or nil within 'ProcessDiagnosticNotification()'. " +
"that channel should only emit the root path once, and then be closed right after and then never used again")
}
rootPath = uriToFilePath(rootPath)
targetExtension = ".html"
// TODO: When I use the value below, I get nasty error (panic). Investigate it later on
// targetExtension = ".tmpl"
storage.rawFiles = gota.OpenProjectFiles(rootPath, targetExtension)
// Since the client only recognize URI, it is better to adopt this early
// on the server as well to avoid perpetual conversion from 'uri' to 'path'
storage.rawFiles = moveKeysFromFilePathToUri(storage.rawFiles)
// TODO: should I check: len(rawFiles) == len(storage.parsedFiles)
storage.parsedFiles, _ = gota.ParseFilesInWorkspace(storage.rawFiles)
if storage.parsedFiles == nil {
storage.parsedFiles = make(map[string]*parser.GroupStatementNode)
}
// TODO: For improved perf. also fetch gota.getWorkspaceTemplateDefinition()
// and only recompute it when a specific file change
notification := &lsp.NotificationMessage[lsp.PublishDiagnosticsParams]{
JsonRpc: "2.0",
Method: "textDocument/publishDiagnostics",
Params: lsp.PublishDiagnosticsParams{
Uri: "place_holder_by_lsp_server--should_never_reach_the_client",
Diagnostics: []lsp.Diagnostic{},
},
}
// watch for client edit notification (didChange, ...)
storage.openFilesAnalyzed = make(map[string]*checker.FileDefinition)
storage.openedFilesError = make(map[string][]gota.Error)
cloneTextFromClient := make(map[string][]byte)
for _ = range textChangedNotification {
if len(textFromClient) == 0 {
panic("got a change notification but the text from client was empty. " +
"check that the 'textFromClient' still point to the correct address " +
"or that the notification wasn't fired by accident")
}
// TODO: handle the case where file is not in workspaceFolders
// the lsp should be still working, but it should not use data for the other workspace
muTextFromClient.Lock()
for uri, fileContent := range textFromClient {
cloneTextFromClient[uri] = fileContent
delete(textFromClient, uri)
}
muTextFromClient.Unlock()
for uri, fileContent := range cloneTextFromClient {
// TODO; this code below will one day cause trouble
// In fact, if a file opened by the lsp client is not in the root or haven't the mandatory exntension
// then the condition after the loop below will fail and launch a panic
// if len(cloneTextFromClient) != 0
if ! isFileInsideWorkspace(uri, rootPath, targetExtension) {
log.Printf("oups ... this file is not considerated part of the project ::: file = %s\n", uri)
continue
}
storage.rawFiles[uri] = fileContent
parseTree, localErrs := gota.ParseSingleFile(fileContent)
storage.parsedFiles[uri] = parseTree
storage.openedFilesError[uri] = localErrs
// delete(cloneTextFromClient, uri)
}
for uri, _ := range storage.openedFilesError {
file, localErrs := gota.DefinitionAnalysisSingleFile(uri, storage.parsedFiles)
storage.openFilesAnalyzed[uri] = file
storage.openedFilesError[uri] = append(storage.openedFilesError[uri], localErrs...)
}
var errs []gota.Error
for uri, _ := range cloneTextFromClient {
// file, localErrs := gota.DefinitionAnalysisSingleFile(uri, storage.parsedFiles)
// storage.openFilesAnalyzed[uri] = file
// storage.openedFilesError[uri] = append(storage.openedFilesError[uri], localErrs...)
errs = storage.openedFilesError[uri]
notification = clearPushDiagnosticNotification(notification)
notification = setParseErrosToDiagnosticsNotification(errs, notification)
notification.Params.Uri = uri
response, err := json.Marshal(notification)
if err != nil {
log.Printf("failed marshalling for notification response. \n notification = %#v \n", notification)
panic("Diagnostic Handler is Unable to 'marshall' notification response, " + err.Error())
}
lsp.SendToLspClient(os.Stdout, response)
// log.Printf("sent diagnostic notification: \n %q", response)
// log.Printf("\n\n simpler notif: \n %#v \n", notification)
log.Printf("\n\nmessage sent to client :: msg = %#v\n\n", notification)
}
clear(cloneTextFromClient)
// clear(textFromClient)
}
}
func isFileInsideWorkspace(uri string, rootPath string, allowedFileExntesion string) bool {
path := uri
rootPath = filePathToUri(rootPath)
if ! strings.HasPrefix(path, rootPath) {
return false
}
if ! strings.HasSuffix(path, allowedFileExntesion) {
return false
}
return true
}
func clearPushDiagnosticNotification(notification *lsp.NotificationMessage[lsp.PublishDiagnosticsParams]) *lsp.NotificationMessage[lsp.PublishDiagnosticsParams] {
notification.Params.Diagnostics = []lsp.Diagnostic{}
notification.Params.Uri = ""
return notification
}
func setParseErrosToDiagnosticsNotification(errs []gota.Error, response *lsp.NotificationMessage[lsp.PublishDiagnosticsParams]) *lsp.NotificationMessage[lsp.PublishDiagnosticsParams] {
if response == nil {
panic("diagnostics errors cannot be appended on 'nil' response. first create the the response")
}
response.Params.Diagnostics = []lsp.Diagnostic{}
for _, err := range errs {
if err == nil {
panic("'nil' should not be in the error list. if you to represent an absence of error in the list, just dont insert it")
}
diagnostic := lsp.Diagnostic{
Message: err.GetError(),
Range: *fromParserRangeToLspRange(err.GetRange()),
}
response.Params.Diagnostics = append(response.Params.Diagnostics, diagnostic)
}
return response
}
func fromParserRangeToLspRange(rg lexer.Range) *lsp.Range {
reach := &lsp.Range{
Start: lsp.Position{
Line: uint(rg.Start.Line),
Character: uint(rg.Start.Character),
},
End: lsp.Position{
Line: uint(rg.End.Line),
Character: uint(rg.End.Character),
},
}
return reach
}
// TODO: make this function work for windows path as well
// Undefined behavior when the windows path use special encoding for colon(":")
// Additionally, special character are not always well escaped between client and server
func uriToFilePath(uri string) string {
if uri == "" {
panic("URI to a file cannot be empty")
}
u, err := url.Parse(uri)
if err != nil {
panic("unable to convert from URI to os path, " + err.Error())
}
if u.Scheme == "" {
panic("expected a scheme for the file's 'URI' but found nothing. uri = " + uri)
}
if u.Scheme != "file" {
panic("cannot handle any scheme other than 'file'. uri = " + uri)
}
if u.RawQuery != "" {
panic("'?' character is not permited within a file's 'URI'. uri = " + uri)
}
if u.Fragment != "" {
panic("'#' character is not permited within a file's 'URI'. uri = " + uri)
}
if u.Path == "" {
panic("path to a file cannot be empty (conversion from uri to path)")
}
return u.Path
}
// TODO: make this function work for windows path as well
// Undefined behavior when the windows path use special encoding for colon(":")
// Additionally, special character are not always well escaped between client and server
func filePathToUri(path string) string {
if path == "" {
panic("path to a file cannot be empty")
}
u, err := url.Parse(path)
if err != nil {
panic("unable to convert from os path to URI, " + err.Error())
}
if u.Scheme != "" {
panic("expected empty scheme for the file's 'URI' but found something. uri = ," + path)
}
if u.RawQuery != "" {
panic("'?' character is not permited within a file's 'URI'. uri = " + path)
}
if u.Fragment != "" {
panic("'#' character is not permited within a file's 'URI'. uri = " + path)
}
u.Scheme = "file"
return u.String()
}
func moveKeysFromFilePathToUri(files map[string][]byte) map[string][]byte {
if len(files) == 0 {
return files
}
var uri string
filesWithUriKeys := make(map[string][]byte)
for path, fileContent := range files {
uri = filePathToUri(path)
filesWithUriKeys[uri] = fileContent
}
return filesWithUriKeys
}
func configureLogging() {
logfileName := "log_output.txt"
file, err := os.Create(logfileName)
if err != nil {
panic("Error: " + err.Error())
}
// logger := log.New(file, " :: ", log.Ldate | log.Ltime | log.Lshortfile)
log.SetPrefix(" --> ")
log.SetOutput(file)
}