1- import process from "process" ;
1+ import process , { allowedNodeEnvironmentFlags } from "process" ;
22import * as p from "vscode-languageserver-protocol" ;
33import * as t from "vscode-languageserver-types" ;
44import * as j from "vscode-jsonrpc" ;
55import * as m from "vscode-jsonrpc/lib/messages" ;
66import * as v from "vscode-languageserver" ;
77import * as path from 'path' ;
88import fs from 'fs' ;
9- import { DidOpenTextDocumentNotification , DidChangeTextDocumentNotification , DidCloseTextDocumentNotification } from 'vscode-languageserver-protocol' ;
9+ // TODO: check DidChangeWatchedFilesNotification. Check DidChangeTextDocumentNotification. Do they fire on uninitialized files?
10+ import { DidOpenTextDocumentNotification , DidChangeTextDocumentNotification , DidCloseTextDocumentNotification , DidChangeWatchedFilesNotification , CompletionResolveRequest } from 'vscode-languageserver-protocol' ;
1011import { uriToFsPath , URI } from 'vscode-uri' ;
1112import * as utils from './utils' ;
1213import * as c from './constants' ;
1314import * as chokidar from 'chokidar'
15+ import { assert } from 'console' ;
16+ // TODO: what's this?
17+ import { fileURLToPath } from 'url' ;
1418
1519// https://microsoft.github.io/language-server-protocol/specification#initialize
1620// According to the spec, there could be requests before the 'initialize' request. Link in comment tells how to handle them.
1721let initialized = false ;
1822// https://microsoft.github.io/language-server-protocol/specification#exit
1923let shutdownRequestAlreadyReceived = false ;
20- // congrats. A simple UI problem is now a distributed system problem
21- let stupidFileContentCache : { [ key : string ] : string } = { }
22- let previouslyDiagnosedFiles : Set < string > = new Set ( )
23- let compilerLogPaths : Set < string > = new Set ( )
24+ let stupidFileContentCache : Map < string , string > = new Map ( )
25+ /*
26+ Map {
27+ '/foo/lib/bs/.compiler.log': Map {
28+ '/foo/src/A.res': {
29+ trackedContent: 'let a = 1',
30+ hasDiagnostics: false
31+ }
32+ '/foo/src/B.res': {
33+ trackedContent: null,
34+ hasDiagnostics: true
35+ }
36+ },
37+ }
38+ */
39+ let projectsFiles : Map <
40+ string ,
41+ {
42+ openFiles : Set < string > ,
43+ filesWithDiagnostics : Set < string > ,
44+ } >
45+ = new Map ( )
46+ // ^ caching AND states AND distributed system. Why does LSP has to be stupid like this
2447
2548let sendUpdatedDiagnostics = ( ) => {
26- let diagnosedFiles : utils . filesDiagnostics = { }
27- compilerLogPaths . forEach ( compilerLogPath => {
49+ projectsFiles . forEach ( ( { filesWithDiagnostics } , compilerLogPath ) => {
2850 let content = fs . readFileSync ( compilerLogPath , { encoding : 'utf-8' } ) ;
29- console . log ( "new log content: " , content )
30- let { done : buildDone , result : filesAndErrors } = utils . parseCompilerLogOutput ( content )
31- // A file can only belong to one .compiler.log root. So we're not overriding
32- // any existing key here
33- Object . assign ( diagnosedFiles , filesAndErrors )
34- } ) ;
51+ console . log ( "new log content: " , compilerLogPath , content )
52+ let { done, result : filesAndErrors } = utils . parseCompilerLogOutput ( content )
3553
36- // Send new diagnostic, wipe old ones
37- let diagnosedFilePaths = Object . keys ( diagnosedFiles )
38- diagnosedFilePaths . forEach ( file => {
39- let params : p . PublishDiagnosticsParams = {
40- uri : file ,
41- // there's a new optional version param from https://github.com/microsoft/language-server-protocol/issues/201
42- // not using it for now, sigh
43- diagnostics : diagnosedFiles [ file ] ,
44- }
45- let notification : m . NotificationMessage = {
46- jsonrpc : c . jsonrpcVersion ,
47- method : 'textDocument/publishDiagnostics' ,
48- params : params ,
49- } ;
50- process . send ! ( notification ) ;
54+ // diff
55+ Object . keys ( filesAndErrors ) . forEach ( file => {
56+ // send diagnostic
57+ let params : p . PublishDiagnosticsParams = {
58+ uri : file ,
59+ diagnostics : filesAndErrors [ file ] ,
60+ }
61+ let notification : m . NotificationMessage = {
62+ jsonrpc : c . jsonrpcVersion ,
63+ method : 'textDocument/publishDiagnostics' ,
64+ params : params ,
65+ } ;
66+ process . send ! ( notification ) ;
5167
52- // this file's taken care of already now. Remove from old diagnostic files
53- previouslyDiagnosedFiles . delete ( file )
54- } )
55- // wipe the errors from the files that are no longer erroring
56- previouslyDiagnosedFiles . forEach ( remainingPreviousFile => {
57- let params : p . PublishDiagnosticsParams = {
58- uri : remainingPreviousFile ,
59- diagnostics : [ ] ,
68+ filesWithDiagnostics . add ( file )
69+ } )
70+ if ( done ) {
71+ // clear old files
72+ filesWithDiagnostics . forEach ( file => {
73+ if ( filesAndErrors [ file ] == null ) {
74+ // Doesn't exist in the new diagnostics. Clear this diagnostic
75+ let params : p . PublishDiagnosticsParams = {
76+ uri : file ,
77+ diagnostics : [ ] ,
78+ }
79+ let notification : m . NotificationMessage = {
80+ jsonrpc : c . jsonrpcVersion ,
81+ method : 'textDocument/publishDiagnostics' ,
82+ params : params ,
83+ } ;
84+ process . send ! ( notification ) ;
85+ filesWithDiagnostics . delete ( file )
86+ }
87+ } )
6088 }
61- let notification : m . NotificationMessage = {
62- jsonrpc : c . jsonrpcVersion ,
63- method : 'textDocument/publishDiagnostics' ,
64- params : params ,
65- } ;
66- process . send ! ( notification ) ;
67- } )
68- previouslyDiagnosedFiles = new Set ( diagnosedFilePaths )
89+ } ) ;
90+ }
91+ let deleteProjectDiagnostics = ( compilerLogPath : string ) => {
92+ let compilerLog = projectsFiles . get ( compilerLogPath )
93+ if ( compilerLog != null ) {
94+ compilerLog . filesWithDiagnostics . forEach ( file => {
95+ let params : p . PublishDiagnosticsParams = {
96+ uri : file ,
97+ diagnostics : [ ] ,
98+ }
99+ let notification : m . NotificationMessage = {
100+ jsonrpc : c . jsonrpcVersion ,
101+ method : 'textDocument/publishDiagnostics' ,
102+ params : params ,
103+ } ;
104+ process . send ! ( notification ) ;
105+ } )
106+
107+ projectsFiles . delete ( compilerLogPath )
108+ }
69109}
70110
71111let compilerLogsWatcher = chokidar . watch ( [ ] )
72112 . on ( 'all' , ( _e , changedPath ) => {
73113 console . log ( 'new log change' , changedPath , Math . random ( ) )
74114 sendUpdatedDiagnostics ( )
75115 } )
116+ let stopWatchingCompilerLog = ( ) => {
117+ // TODO: cleanup of compilerLogs?
118+ compilerLogsWatcher . close ( )
119+ }
76120
77- let addCompilerLogToWatch = ( fileUri : string ) => {
121+ let openedFile = ( fileUri : string , fileContent : string ) => {
78122 let filePath = uriToFsPath ( URI . parse ( fileUri ) , true ) ;
123+
124+ stupidFileContentCache . set ( filePath , fileContent )
125+
79126 let compilerLogDir = utils . findDirOfFileNearFile ( c . compilerLogPartialPath , filePath )
80127 if ( compilerLogDir != null ) {
81128 let compilerLogPath = path . join ( compilerLogDir , c . compilerLogPartialPath ) ;
82- if ( ! compilerLogPaths . has ( compilerLogPath ) ) {
83- console . log ( "added new " , compilerLogPath , "from file: " , compilerLogDir )
84- compilerLogPaths . add ( compilerLogPath )
129+ if ( ! projectsFiles . has ( compilerLogPath ) ) {
130+ projectsFiles . set ( compilerLogPath , { openFiles : new Set ( ) , filesWithDiagnostics : new Set ( ) } )
85131 compilerLogsWatcher . add ( compilerLogPath )
86- // no need to call sendUpdatedDiagnostics() here; the watcher add will
87- // call the listener which calls it
88132 }
133+ let compilerLog = projectsFiles . get ( compilerLogPath ) !
134+ compilerLog . openFiles . add ( filePath )
135+ // no need to call sendUpdatedDiagnostics() here; the watcher add will
136+ // call the listener which calls it
89137 }
90138}
91- let removeCompilerLogToWatch = ( fileUri : string ) => {
139+ let closedFile = ( fileUri : string ) => {
92140 let filePath = uriToFsPath ( URI . parse ( fileUri ) , true ) ;
141+
142+ stupidFileContentCache . delete ( filePath )
143+
93144 let compilerLogDir = utils . findDirOfFileNearFile ( c . compilerLogPartialPath , filePath )
94145 if ( compilerLogDir != null ) {
95146 let compilerLogPath = path . join ( compilerLogDir , c . compilerLogPartialPath ) ;
96- if ( compilerLogPaths . has ( compilerLogPath ) ) {
97- console . log ( "remove log path " , compilerLogPath )
98- compilerLogPaths . delete ( compilerLogPath )
99- compilerLogsWatcher . unwatch ( compilerLogPath )
100- sendUpdatedDiagnostics ( )
147+ let compilerLog = projectsFiles . get ( compilerLogPath )
148+ if ( compilerLog != null ) {
149+ compilerLog . openFiles . delete ( filePath )
150+ // clear diagnostics too if no open files open in said project
151+ if ( compilerLog . openFiles . size === 0 ) {
152+ compilerLogsWatcher . unwatch ( compilerLogPath )
153+ deleteProjectDiagnostics ( compilerLogPath )
154+ }
101155 }
102156 }
103157}
104- let stopWatchingCompilerLog = ( ) => {
105- compilerLogsWatcher . close ( )
158+ let updateOpenedFile = ( fileUri : string , fileContent : string ) => {
159+ let filePath = uriToFsPath ( URI . parse ( fileUri ) , true )
160+ assert ( stupidFileContentCache . has ( filePath ) )
161+ stupidFileContentCache . set ( filePath , fileContent )
162+ }
163+ let getOpenedFileContent = ( fileUri : string ) => {
164+ let filePath = uriToFsPath ( URI . parse ( fileUri ) , true )
165+ let content = stupidFileContentCache . get ( filePath ) !
166+ assert ( content != null )
167+ return content
106168}
107169
108170process . on ( 'message' , ( a : ( m . RequestMessage | m . NotificationMessage ) ) => {
@@ -125,8 +187,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
125187 let extName = path . extname ( params . textDocument . uri )
126188 if ( extName === c . resExt || extName === c . resiExt ) {
127189 console . log ( "new file coming" , params . textDocument . uri )
128- stupidFileContentCache [ params . textDocument . uri ] = params . textDocument . text ;
129- addCompilerLogToWatch ( params . textDocument . uri )
190+ openedFile ( params . textDocument . uri , params . textDocument . text )
130191 }
131192 } else if ( aa . method === DidChangeTextDocumentNotification . method ) {
132193 let params = ( aa . params as p . DidChangeTextDocumentParams ) ;
@@ -137,13 +198,12 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
137198 // no change?
138199 } else {
139200 // we currently only support full changes
140- stupidFileContentCache [ params . textDocument . uri ] = changes [ changes . length - 1 ] . text ;
201+ updateOpenedFile ( params . textDocument . uri , changes [ changes . length - 1 ] . text )
141202 }
142203 }
143204 } else if ( aa . method === DidCloseTextDocumentNotification . method ) {
144205 let params = ( aa . params as p . DidCloseTextDocumentParams ) ;
145- delete stupidFileContentCache [ params . textDocument . uri ] ;
146- removeCompilerLogToWatch ( params . textDocument . uri )
206+ closedFile ( params . textDocument . uri )
147207 }
148208 } else {
149209 // this is a request message, aka client sent request, waits for our reply
@@ -235,7 +295,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
235295 process . send ! ( response ) ;
236296 } else {
237297 // code will always be defined here, even though technically it can be undefined
238- let code = stupidFileContentCache [ params . textDocument . uri ] ;
298+ let code = getOpenedFileContent ( params . textDocument . uri )
239299 let formattedResult = utils . formatUsingValidBscPath (
240300 code ,
241301 path . join ( nodeModulesParentPath , c . bscPartialPath ) ,
0 commit comments