@@ -7,21 +7,28 @@ import * as m from "vscode-jsonrpc/lib/messages";
77import * as v from "vscode-languageserver" ;
88import { attachPartialResult } from 'vscode-languageserver/lib/progress' ;
99import * as path from 'path' ;
10- import fs from 'fs' ;
10+ import fs , { open } from 'fs' ;
1111import * as childProcess from 'child_process' ;
1212import { DidOpenTextDocumentNotification , DidChangeTextDocumentNotification , DidCloseTextDocumentNotification } from 'vscode-languageserver-protocol' ;
1313import * as tmp from 'tmp' ;
14+ import { type } from 'os' ;
15+ import { Range } from 'vscode-languageserver-textdocument' ;
16+ import { strict } from 'assert' ;
1417
1518// See https://microsoft.github.io/language-server-protocol/specification Abstract Message
1619// version is fixed to 2.0
17- let jsonrpcVersion = '2.0'
18- let bscPartialPath = path . join ( 'node_modules' , '.bin' , 'bsc' )
20+ let jsonrpcVersion = '2.0' ;
21+ let bscPartialPath = path . join ( 'node_modules' , '.bin' , 'bsc' ) ;
22+ let bsbLogPartialPath = 'bsb.log' ;
23+ let resExt = '.res' ;
24+ let resiExt = '.resi' ;
1925
2026// https://microsoft.github.io/language-server-protocol/specification#initialize
2127// According to the spec, there could be requests before the 'initialize' request. Link in comment tells how to handle them.
2228let initialized = false ;
2329// https://microsoft.github.io/language-server-protocol/specification#exit
2430let shutdownRequestAlreadyReceived = false ;
31+ let diagnosisTimer : null | NodeJS . Timeout = null ;
2532
2633// congrats. A simple UI problem is now a distributed system problem
2734let stupidFileContentCache : { [ key : string ] : string } = {
@@ -53,7 +60,7 @@ let formatUsingValidBscPath = (code: string, bscPath: p.DocumentUri, isInterface
5360 let result ;
5461 // library cleans up after itself. No need to manually remove temp file
5562 let tmpobj = tmp . fileSync ( ) ;
56- let extension = isInterface ? '.resi' : '.res' ;
63+ let extension = isInterface ? resiExt : resExt ;
5764 let fileToFormat = tmpobj . name + extension ;
5865 fs . writeFileSync ( fileToFormat , code , { encoding : 'utf-8' } ) ;
5966 try {
@@ -70,13 +77,158 @@ let formatUsingValidBscPath = (code: string, bscPath: p.DocumentUri, isInterface
7077 }
7178}
7279
73- let startWatchingBsbOutputFile = ( root : p . DocumentUri ) => {
74- console . log ( root )
75- // fs.watch()
80+ let parseBsbOutputLocation = ( location : string ) : Range => {
81+ // example bsb output location:
82+ // 3:9
83+ // 3:9-6:1
84+ // language-server position is 0-based. Ours is 1-based. Don't forget to convert
85+ let isMultiline = location . indexOf ( '-' ) >= 0
86+ if ( isMultiline ) {
87+ let [ from , to ] = location . split ( '-' )
88+ let [ fromLine , fromChar ] = from . split ( ':' )
89+ let [ toLine , toChar ] = to . split ( ':' )
90+ return {
91+ start : { line : parseInt ( fromLine ) - 1 , character : parseInt ( fromChar ) } ,
92+ end : { line : parseInt ( toLine ) - 1 , character : parseInt ( toChar ) } ,
93+ }
94+ } else {
95+ let [ line , char ] = location . split ( ':' )
96+ let start = { line : parseInt ( line ) - 1 , character : parseInt ( char ) - 1 }
97+ return {
98+ start : start ,
99+ end : start ,
100+ }
101+ }
102+ }
103+ type diagnosis = { range : Range , diagnosis : string }
104+ let parseBsbLogOutput = ( content : string ) => {
105+ /* example bsb.log file content:
106+
107+ Cleaning... 6 files.
108+ Cleaning... 87 files.
109+ [1/5] [34mBuilding[39m [2msrc/TestFramework.reiast[22m
110+ [2/5] [34mBuilding[39m [2msrc/TestFramework.reast[22m
111+ FAILED: src/test.cmj src/test.cmi
112+
113+ We've found a bug for you!
114+ /Users/chenglou/github/reason-react/src/test.res 3:9
115+
116+ 1 │ let a = 1
117+ 2 │ let b = "hi"
118+ 3 │ let a = b + 1
119+
120+ This has type:
121+ string
122+
123+ But somewhere wanted:
124+ int
125+
126+
127+ [15/62] [34mBuilding[39m [2msrc/ReactDOMServer.reast[22m
128+ */
129+
130+ // we're gonna chop that
131+ let res : string [ ] [ ] = [ ] ;
132+ let lines = content . split ( '\n' ) ;
133+ lines . forEach ( line => {
134+ if ( line . startsWith ( ' We\'ve found a bug for you!' ) ) {
135+ res . push ( [ ] )
136+ } else if ( / ^ [ 0 - 9 ] + / . test ( line ) ) {
137+ // code display. Swallow
138+ } else if ( line . startsWith ( ' ' ) ) {
139+ res [ res . length - 1 ] . push ( line )
140+ }
141+ } )
142+
143+ // map of file path to list of diagnosis
144+ let ret : { [ key : string ] : diagnosis [ ] } = { }
145+ res . forEach ( diagnosisLines => {
146+ let [ fileAndLocation , ...diagnosisMessage ] = diagnosisLines
147+ let lastSpace = fileAndLocation . lastIndexOf ( ' ' )
148+ let file = fileAndLocation . substring ( 2 , lastSpace )
149+ let location = fileAndLocation . substring ( lastSpace )
150+ if ( ret [ file ] == null ) {
151+ ret [ file ] = [ ]
152+ }
153+ let cleanedUpDiagnosis = diagnosisMessage
154+ . map ( line => {
155+ // remove the spaces in front
156+ return line . slice ( 2 )
157+ } )
158+ . join ( '\n' )
159+ // remove start and end whitespaces/newlines
160+ . trim ( ) ;
161+ ret [ file ] . push ( {
162+ range : parseBsbOutputLocation ( location ) ,
163+ diagnosis : cleanedUpDiagnosis ,
164+ } )
165+ } )
166+
167+ return ret
168+ }
169+
170+ let startWatchingBsbOutputFile = ( root : p . DocumentUri , process : NodeJS . Process ) => {
171+ // console.log(root);
172+ // TOOD: setTimeout instead
173+ let id = setInterval ( ( ) => {
174+ let openFiles = Object . keys ( stupidFileContentCache ) ;
175+ let bsbLogDirs : Set < p . DocumentUri > = new Set ( ) ;
176+ openFiles . forEach ( openFile => {
177+ // TODO: remove this hack
178+ let filePath = openFile . replace ( 'file://' , '' ) ;
179+ let bsbLogDir = findDirOfFileNearFile ( bsbLogPartialPath , filePath )
180+ if ( bsbLogDir != null ) {
181+ bsbLogDirs . add ( bsbLogDir ) ;
182+ }
183+ } ) ;
184+
185+ let files : { [ key : string ] : diagnosis [ ] } = { }
186+
187+ let res = Array . from ( bsbLogDirs )
188+ . forEach ( bsbLogDir => {
189+ let bsbLogPath = path . join ( bsbLogDir , bsbLogPartialPath ) ;
190+ let content = fs . readFileSync ( bsbLogPath , { encoding : 'utf-8' } ) ;
191+ let filesAndErrors = parseBsbLogOutput ( content )
192+ Object . keys ( filesAndErrors ) . forEach ( file => {
193+ files [ file ] = filesAndErrors [ file ]
194+ } )
195+ // content.split('\n').forEach(line => {
196+ // let [file, diagnosis] = line.split(': ');
197+
198+ // if (files[file] == null) {
199+ // files[file] = []
200+ // }
201+ // files[file].push(diagnosis)
202+ // })
203+ } ) ;
204+
205+ Object . keys ( files ) . forEach ( file => {
206+ let params : p . PublishDiagnosticsParams = {
207+ uri : file ,
208+ // there's a new optional version param from https://github.com/microsoft/language-server-protocol/issues/201
209+ // not using it for now, sigh
210+ diagnostics :
211+ files [ file ] . map ( ( { range, diagnosis } ) => {
212+ return {
213+ range : range ,
214+ message : diagnosis ,
215+ }
216+ } ) ,
217+ }
218+ let notification : m . NotificationMessage = {
219+ jsonrpc : jsonrpcVersion ,
220+ method : 'textDocument/publishDiagnostics' ,
221+ params : params ,
222+ } ;
223+ process . send ! ( notification ) ;
224+ } )
225+ } , 1000 ) ;
226+
227+ return id ;
228+ }
229+ let stopWatchingBsbOutputFile = ( timerId : NodeJS . Timeout ) => {
230+ clearInterval ( timerId ) ;
76231}
77- // let stopWatchingBsbOutputFile = () => {
78- // fs.unwatchFile()
79- // }
80232
81233process . on ( 'message' , ( a : ( m . RequestMessage | m . NotificationMessage ) ) => {
82234 if ( ( a as m . RequestMessage ) . id == null ) {
@@ -95,15 +247,21 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
95247 }
96248 } else if ( aa . method === DidOpenTextDocumentNotification . method ) {
97249 let params = ( aa . params as p . DidOpenTextDocumentParams ) ;
98- stupidFileContentCache [ params . textDocument . uri ] = params . textDocument . text ;
250+ let extName = path . extname ( params . textDocument . uri )
251+ if ( extName === resExt || extName === resiExt ) {
252+ stupidFileContentCache [ params . textDocument . uri ] = params . textDocument . text ;
253+ }
99254 } else if ( aa . method === DidChangeTextDocumentNotification . method ) {
100255 let params = ( aa . params as p . DidChangeTextDocumentParams ) ;
101- let changes = params . contentChanges
102- if ( changes . length === 0 ) {
103- // no change?
104- } else {
105- // we currently only support full changes
106- stupidFileContentCache [ params . textDocument . uri ] = changes [ changes . length - 1 ] . text ;
256+ let extName = path . extname ( params . textDocument . uri )
257+ if ( extName === resExt || extName === resiExt ) {
258+ let changes = params . contentChanges
259+ if ( changes . length === 0 ) {
260+ // no change?
261+ } else {
262+ // we currently only support full changes
263+ stupidFileContentCache [ params . textDocument . uri ] = changes [ changes . length - 1 ] . text ;
264+ }
107265 }
108266 } else if ( aa . method === DidCloseTextDocumentNotification . method ) {
109267 let params = ( aa . params as p . DidCloseTextDocumentParams ) ;
@@ -121,15 +279,15 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
121279 message : "Server not initialized."
122280 }
123281 } ;
124- ( < any > process ) . send ( response ) ;
282+ process . send ! ( response ) ;
125283 } else if ( aa . method === 'initialize' ) {
126284 let param : p . InitializeParams = aa . params
127285 let root = param . rootUri
128286 if ( root == null ) {
129287 // TODO: handle single file
130288 console . log ( "not handling single file" )
131289 } else {
132- startWatchingBsbOutputFile ( root )
290+ diagnosisTimer = startWatchingBsbOutputFile ( root , process )
133291 }
134292 // send the list of things we support
135293 let result : p . InitializeResult = {
@@ -145,16 +303,15 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
145303 result : result ,
146304 } ;
147305 initialized = true ;
148- ( < any > process ) . send ( response ) ;
306+ process . send ! ( response ) ;
149307 } else if ( aa . method === 'initialized' ) {
150- console . log ( 'inited' )
151308 // sent from client after initialize. Nothing to do for now
152309 let response : m . ResponseMessage = {
153310 jsonrpc : jsonrpcVersion ,
154311 id : aa . id ,
155312 result : null ,
156313 } ;
157- ( < any > process ) . send ( response ) ;
314+ process . send ! ( response ) ;
158315 } else if ( aa . method === 'shutdown' ) {
159316 // https://microsoft.github.io/language-server-protocol/specification#shutdown
160317 if ( shutdownRequestAlreadyReceived ) {
@@ -166,31 +323,34 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
166323 message : `Language server already received the shutdown request`
167324 }
168325 } ;
169- ( < any > process ) . send ( response ) ;
326+ process . send ! ( response ) ;
170327 } else {
171328 shutdownRequestAlreadyReceived = true
329+ if ( diagnosisTimer != null ) {
330+ stopWatchingBsbOutputFile ( diagnosisTimer )
331+ }
172332 let response : m . ResponseMessage = {
173333 jsonrpc : jsonrpcVersion ,
174334 id : aa . id ,
175335 result : null ,
176336 } ;
177- ( < any > process ) . send ( response ) ;
337+ process . send ! ( response ) ;
178338 }
179339 } else if ( aa . method === p . DocumentFormattingRequest . method ) {
180340 let params = ( aa . params as p . DocumentFormattingParams )
181341 // TODO: remove this hack
182- let filePath = params . textDocument . uri . replace ( 'file:' , '' )
342+ let filePath = params . textDocument . uri . replace ( 'file:// ' , '' )
183343 let extension = path . extname ( params . textDocument . uri ) ;
184- if ( extension !== '.res' && extension !== '.resi' ) {
344+ if ( extension !== resExt && extension !== resiExt ) {
185345 let response : m . ResponseMessage = {
186346 jsonrpc : jsonrpcVersion ,
187347 id : aa . id ,
188348 error : {
189349 code : m . ErrorCodes . InvalidRequest ,
190- message : `Not a .res or .resi file.`
350+ message : `Not a ${ resExt } or ${ resiExt } file.`
191351 }
192352 } ;
193- ( < any > process ) . send ( response ) ;
353+ process . send ! ( response ) ;
194354 } else {
195355 let nodeModulesParentPath = findDirOfFileNearFile ( bscPartialPath , filePath )
196356 if ( nodeModulesParentPath == null ) {
@@ -202,7 +362,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
202362 message : `Cannot find a nearby ${ bscPartialPath } . It's needed for formatting.` ,
203363 }
204364 } ;
205- ( < any > process ) . send ( response ) ;
365+ process . send ! ( response ) ;
206366 } else {
207367 // file to format potentially doesn't exist anymore because of races. But that's ok, the error from bsc should handle it
208368 let code = stupidFileContentCache [ params . textDocument . uri ] ;
@@ -213,7 +373,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
213373 let formattedResult = formatUsingValidBscPath (
214374 code ,
215375 path . join ( nodeModulesParentPath , bscPartialPath ) ,
216- extension === '.resi' ,
376+ extension === resiExt ,
217377 ) ;
218378 if ( formattedResult . kind === 'success' ) {
219379 let result : p . TextEdit [ ] = [ {
@@ -228,7 +388,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
228388 id : aa . id ,
229389 result : result ,
230390 } ;
231- ( < any > process ) . send ( response ) ;
391+ process . send ! ( response ) ;
232392 } else {
233393 let response : m . ResponseMessage = {
234394 jsonrpc : jsonrpcVersion ,
@@ -238,7 +398,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
238398 message : formattedResult . error ,
239399 }
240400 } ;
241- ( < any > process ) . send ( response ) ;
401+ process . send ! ( response ) ;
242402 }
243403 }
244404 }
@@ -252,7 +412,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
252412 message : "Unrecognized editor request."
253413 }
254414 } ;
255- ( < any > process ) . send ( response ) ;
415+ process . send ! ( response ) ;
256416 }
257417 }
258418} )
0 commit comments