1
1
using System ;
2
2
using System . Collections . Generic ;
3
+ using System . Text ;
3
4
using System . Threading ;
4
5
using System . Threading . Tasks ;
5
6
using Microsoft . Extensions . Logging ;
11
12
using OmniSharp . Extensions . LanguageServer . Protocol . Server ;
12
13
using OmniSharp . Extensions . LanguageServer . Protocol . Server . Capabilities ;
13
14
14
- namespace PowerShellEditorServices . Engine . Services . Workspace . Handlers
15
+ namespace PowerShellEditorServices . Engine . Services . Handlers
15
16
{
16
17
class TextDocumentHandler : ITextDocumentSyncHandler
17
18
{
18
19
19
20
private readonly ILogger _logger ;
21
+ private readonly ILanguageServer _languageServer ;
22
+ private readonly AnalysisService _analysisService ;
20
23
private readonly WorkspaceService _workspaceService ;
21
24
25
+ private Dictionary < string , Dictionary < string , MarkerCorrection > > codeActionsPerFile =
26
+ new Dictionary < string , Dictionary < string , MarkerCorrection > > ( ) ;
27
+
28
+ private static CancellationTokenSource s_existingRequestCancellation ;
29
+
22
30
private readonly DocumentSelector _documentSelector = new DocumentSelector (
23
31
new DocumentFilter ( )
24
32
{
@@ -30,9 +38,11 @@ class TextDocumentHandler : ITextDocumentSyncHandler
30
38
31
39
public TextDocumentSyncKind Change => TextDocumentSyncKind . Incremental ;
32
40
33
- public TextDocumentHandler ( ILoggerFactory factory , WorkspaceService workspaceService )
41
+ public TextDocumentHandler ( ILoggerFactory factory , ILanguageServer languageServer , AnalysisService analysisService , WorkspaceService workspaceService )
34
42
{
35
43
_logger = factory . CreateLogger < TextDocumentHandler > ( ) ;
44
+ _languageServer = languageServer ;
45
+ _analysisService = analysisService ;
36
46
_workspaceService = workspaceService ;
37
47
}
38
48
@@ -54,10 +64,7 @@ public Task<Unit> Handle(DidChangeTextDocumentParams notification, CancellationT
54
64
}
55
65
56
66
// TODO: Get all recently edited files in the workspace
57
- // this.RunScriptDiagnosticsAsync(
58
- // changedFiles.ToArray(),
59
- // editorSession,
60
- // eventContext);
67
+ RunScriptDiagnosticsAsync ( changedFiles . ToArray ( ) ) ;
61
68
return Unit . Task ;
62
69
}
63
70
@@ -83,10 +90,7 @@ public Task<Unit> Handle(DidOpenTextDocumentParams notification, CancellationTok
83
90
notification . TextDocument . Text ) ;
84
91
85
92
// TODO: Get all recently edited files in the workspace
86
- // this.RunScriptDiagnosticsAsync(
87
- // new ScriptFile[] { openedFile },
88
- // editorSession,
89
- // eventContext);
93
+ RunScriptDiagnosticsAsync ( new ScriptFile [ ] { openedFile } ) ;
90
94
91
95
_logger . LogTrace ( "Finished opening document." ) ;
92
96
return Unit . Task ;
@@ -108,7 +112,7 @@ public Task<Unit> Handle(DidCloseTextDocumentParams notification, CancellationTo
108
112
if ( fileToClose != null )
109
113
{
110
114
_workspaceService . CloseFile ( fileToClose ) ;
111
- // await ClearMarkersAsync (fileToClose, eventContext );
115
+ ClearMarkers ( fileToClose ) ;
112
116
}
113
117
114
118
_logger . LogTrace ( "Finished closing document." ) ;
@@ -162,14 +166,241 @@ private static FileChange GetFileChangeDetails(Range changeRange, string insertS
162
166
} ;
163
167
}
164
168
165
- // private async Task ClearMarkersAsync(ScriptFile scriptFile, EventContext eventContext)
166
- // {
167
- // // send empty diagnostic markers to clear any markers associated with the given file
168
- // await PublishScriptDiagnosticsAsync(
169
- // scriptFile,
170
- // new List<ScriptFileMarker>(),
171
- // this.codeActionsPerFile,
172
- // eventContext);
173
- // }
169
+ private Task RunScriptDiagnosticsAsync (
170
+ ScriptFile [ ] filesToAnalyze )
171
+ {
172
+ // If there's an existing task, attempt to cancel it
173
+ try
174
+ {
175
+ if ( s_existingRequestCancellation != null )
176
+ {
177
+ // Try to cancel the request
178
+ s_existingRequestCancellation . Cancel ( ) ;
179
+
180
+ // If cancellation didn't throw an exception,
181
+ // clean up the existing token
182
+ s_existingRequestCancellation . Dispose ( ) ;
183
+ s_existingRequestCancellation = null ;
184
+ }
185
+ }
186
+ catch ( Exception e )
187
+ {
188
+ // TODO: Catch a more specific exception!
189
+ _logger . LogError (
190
+ string . Format (
191
+ "Exception while canceling analysis task:\n \n {0}" ,
192
+ e . ToString ( ) ) ) ;
193
+
194
+ TaskCompletionSource < bool > cancelTask = new TaskCompletionSource < bool > ( ) ;
195
+ cancelTask . SetCanceled ( ) ;
196
+ return cancelTask . Task ;
197
+ }
198
+
199
+ // If filesToAnalzye is empty, nothing to do so return early.
200
+ if ( filesToAnalyze . Length == 0 )
201
+ {
202
+ return Task . FromResult ( true ) ;
203
+ }
204
+
205
+ // Create a fresh cancellation token and then start the task.
206
+ // We create this on a different TaskScheduler so that we
207
+ // don't block the main message loop thread.
208
+ // TODO: Is there a better way to do this?
209
+ s_existingRequestCancellation = new CancellationTokenSource ( ) ;
210
+ // TODO use settings service
211
+ Task . Factory . StartNew (
212
+ ( ) =>
213
+ DelayThenInvokeDiagnosticsAsync (
214
+ 750 ,
215
+ filesToAnalyze ,
216
+ true ,
217
+ this . codeActionsPerFile ,
218
+ _logger ,
219
+ s_existingRequestCancellation . Token ) ,
220
+ CancellationToken . None ,
221
+ TaskCreationOptions . None ,
222
+ TaskScheduler . Default ) ;
223
+
224
+ return Task . FromResult ( true ) ;
225
+ }
226
+
227
+ private async Task DelayThenInvokeDiagnosticsAsync (
228
+ int delayMilliseconds ,
229
+ ScriptFile [ ] filesToAnalyze ,
230
+ bool isScriptAnalysisEnabled ,
231
+ Dictionary < string , Dictionary < string , MarkerCorrection > > correctionIndex ,
232
+ ILogger Logger ,
233
+ CancellationToken cancellationToken )
234
+ {
235
+ // First of all, wait for the desired delay period before
236
+ // analyzing the provided list of files
237
+ try
238
+ {
239
+ await Task . Delay ( delayMilliseconds , cancellationToken ) ;
240
+ }
241
+ catch ( TaskCanceledException )
242
+ {
243
+ // If the task is cancelled, exit directly
244
+ foreach ( var script in filesToAnalyze )
245
+ {
246
+ PublishScriptDiagnostics (
247
+ script ,
248
+ script . DiagnosticMarkers ,
249
+ correctionIndex ) ;
250
+ }
251
+
252
+ return ;
253
+ }
254
+
255
+ // If we've made it past the delay period then we don't care
256
+ // about the cancellation token anymore. This could happen
257
+ // when the user stops typing for long enough that the delay
258
+ // period ends but then starts typing while analysis is going
259
+ // on. It makes sense to send back the results from the first
260
+ // delay period while the second one is ticking away.
261
+
262
+ // Get the requested files
263
+ foreach ( ScriptFile scriptFile in filesToAnalyze )
264
+ {
265
+ List < ScriptFileMarker > semanticMarkers = null ;
266
+ if ( isScriptAnalysisEnabled && _analysisService != null )
267
+ {
268
+ semanticMarkers = await _analysisService . GetSemanticMarkersAsync ( scriptFile ) ;
269
+ }
270
+ else
271
+ {
272
+ // Semantic markers aren't available if the AnalysisService
273
+ // isn't available
274
+ semanticMarkers = new List < ScriptFileMarker > ( ) ;
275
+ }
276
+
277
+ scriptFile . DiagnosticMarkers . AddRange ( semanticMarkers ) ;
278
+
279
+ PublishScriptDiagnostics (
280
+ scriptFile ,
281
+ // Concat script analysis errors to any existing parse errors
282
+ scriptFile . DiagnosticMarkers ,
283
+ correctionIndex ) ;
284
+ }
285
+ }
286
+
287
+ private void ClearMarkers ( ScriptFile scriptFile )
288
+ {
289
+ // send empty diagnostic markers to clear any markers associated with the given file
290
+ PublishScriptDiagnostics (
291
+ scriptFile ,
292
+ new List < ScriptFileMarker > ( ) ,
293
+ this . codeActionsPerFile ) ;
294
+ }
295
+
296
+ private void PublishScriptDiagnostics (
297
+ ScriptFile scriptFile ,
298
+ List < ScriptFileMarker > markers ,
299
+ Dictionary < string , Dictionary < string , MarkerCorrection > > correctionIndex )
300
+ {
301
+ List < Diagnostic > diagnostics = new List < Diagnostic > ( ) ;
302
+
303
+ // Hold on to any corrections that may need to be applied later
304
+ Dictionary < string , MarkerCorrection > fileCorrections =
305
+ new Dictionary < string , MarkerCorrection > ( ) ;
306
+
307
+ foreach ( var marker in markers )
308
+ {
309
+ // Does the marker contain a correction?
310
+ Diagnostic markerDiagnostic = GetDiagnosticFromMarker ( marker ) ;
311
+ if ( marker . Correction != null )
312
+ {
313
+ string diagnosticId = GetUniqueIdFromDiagnostic ( markerDiagnostic ) ;
314
+ fileCorrections . Add ( diagnosticId , marker . Correction ) ;
315
+ }
316
+
317
+ diagnostics . Add ( markerDiagnostic ) ;
318
+ }
319
+
320
+ correctionIndex [ scriptFile . DocumentUri ] = fileCorrections ;
321
+
322
+ var uriBuilder = new UriBuilder ( )
323
+ {
324
+ Scheme = Uri . UriSchemeFile ,
325
+ Path = scriptFile . FilePath ,
326
+ Host = string . Empty ,
327
+ } ;
328
+
329
+ // Always send syntax and semantic errors. We want to
330
+ // make sure no out-of-date markers are being displayed.
331
+ _languageServer . Document . PublishDiagnostics ( new PublishDiagnosticsParams ( )
332
+ {
333
+ Uri = uriBuilder . Uri ,
334
+ Diagnostics = new Container < Diagnostic > ( diagnostics ) ,
335
+ } ) ;
336
+ }
337
+
338
+ // Generate a unique id that is used as a key to look up the associated code action (code fix) when
339
+ // we receive and process the textDocument/codeAction message.
340
+ private static string GetUniqueIdFromDiagnostic ( Diagnostic diagnostic )
341
+ {
342
+ Position start = diagnostic . Range . Start ;
343
+ Position end = diagnostic . Range . End ;
344
+
345
+ var sb = new StringBuilder ( 256 )
346
+ . Append ( diagnostic . Source ?? "?" )
347
+ . Append ( "_" )
348
+ . Append ( diagnostic . Code . ToString ( ) )
349
+ . Append ( "_" )
350
+ . Append ( diagnostic . Severity ? . ToString ( ) ?? "?" )
351
+ . Append ( "_" )
352
+ . Append ( start . Line )
353
+ . Append ( ":" )
354
+ . Append ( start . Character )
355
+ . Append ( "-" )
356
+ . Append ( end . Line )
357
+ . Append ( ":" )
358
+ . Append ( end . Character ) ;
359
+
360
+ var id = sb . ToString ( ) ;
361
+ return id ;
362
+ }
363
+
364
+ private static Diagnostic GetDiagnosticFromMarker ( ScriptFileMarker scriptFileMarker )
365
+ {
366
+ return new Diagnostic
367
+ {
368
+ Severity = MapDiagnosticSeverity ( scriptFileMarker . Level ) ,
369
+ Message = scriptFileMarker . Message ,
370
+ Code = scriptFileMarker . RuleName ,
371
+ Source = scriptFileMarker . Source ,
372
+ Range = new Range
373
+ {
374
+ Start = new Position
375
+ {
376
+ Line = scriptFileMarker . ScriptRegion . StartLineNumber - 1 ,
377
+ Character = scriptFileMarker . ScriptRegion . StartColumnNumber - 1
378
+ } ,
379
+ End = new Position
380
+ {
381
+ Line = scriptFileMarker . ScriptRegion . EndLineNumber - 1 ,
382
+ Character = scriptFileMarker . ScriptRegion . EndColumnNumber - 1
383
+ }
384
+ }
385
+ } ;
386
+ }
387
+
388
+ private static DiagnosticSeverity MapDiagnosticSeverity ( ScriptFileMarkerLevel markerLevel )
389
+ {
390
+ switch ( markerLevel )
391
+ {
392
+ case ScriptFileMarkerLevel . Error :
393
+ return DiagnosticSeverity . Error ;
394
+
395
+ case ScriptFileMarkerLevel . Warning :
396
+ return DiagnosticSeverity . Warning ;
397
+
398
+ case ScriptFileMarkerLevel . Information :
399
+ return DiagnosticSeverity . Information ;
400
+
401
+ default :
402
+ return DiagnosticSeverity . Error ;
403
+ }
404
+ }
174
405
}
175
406
}
0 commit comments