Skip to content

Commit e4c86ee

Browse files
TylerLeonhardtrjmholt
authored andcommitted
Add diagnostics (PowerShell#18)
1 parent a6af037 commit e4c86ee

File tree

10 files changed

+324
-30
lines changed

10 files changed

+324
-30
lines changed

src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs

+6
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,12 @@ public void StartLanguageService(
227227

228228
_serviceCollection.AddSingleton<WorkspaceService>();
229229
_serviceCollection.AddSingleton<SymbolsService>();
230+
_serviceCollection.AddSingleton<AnalysisService>(
231+
(provider) => {
232+
// TODO: Fill in settings
233+
return AnalysisService.Create(null, _factory.CreateLogger<AnalysisService>());
234+
}
235+
);
230236

231237
_languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection)
232238
{

src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
using OS = OmniSharp.Extensions.LanguageServer.Server;
88
using System.Security.AccessControl;
99
using OmniSharp.Extensions.LanguageServer.Server;
10-
using PowerShellEditorServices.Engine.Services.Workspace.Handlers;
10+
using PowerShellEditorServices.Engine.Services.Handlers;
1111

1212
namespace Microsoft.PowerShell.EditorServices.Engine
1313
{

src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
using System.Collections;
1414
using Microsoft.Extensions.Logging;
1515

16-
namespace Microsoft.PowerShell.EditorServices.Services
16+
namespace Microsoft.PowerShell.EditorServices
1717
{
1818
/// <summary>
1919
/// Provides a high-level service for performing semantic analysis

src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetVersionHandler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using Microsoft.Extensions.Logging;
55
using Microsoft.PowerShell.EditorServices;
66

7-
namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers
7+
namespace PowerShellEditorServices.Engine.Services.Handlers
88
{
99
public class GetVersionHandler : IGetVersionHandler
1010
{

src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetVersionHandler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using OmniSharp.Extensions.Embedded.MediatR;
22
using OmniSharp.Extensions.JsonRpc;
33

4-
namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers
4+
namespace PowerShellEditorServices.Engine.Services.Handlers
55
{
66
[Serial, Method("powerShell/getVersion")]
77
public interface IGetVersionHandler : IJsonRpcRequestHandler<GetVersionParams, PowerShellVersionDetails> { }

src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/TextDocumentHandler.cs

+251-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Text;
34
using System.Threading;
45
using System.Threading.Tasks;
56
using Microsoft.Extensions.Logging;
@@ -11,14 +12,21 @@
1112
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
1213
using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities;
1314

14-
namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers
15+
namespace PowerShellEditorServices.Engine.Services.Handlers
1516
{
1617
class TextDocumentHandler : ITextDocumentSyncHandler
1718
{
1819

1920
private readonly ILogger _logger;
21+
private readonly ILanguageServer _languageServer;
22+
private readonly AnalysisService _analysisService;
2023
private readonly WorkspaceService _workspaceService;
2124

25+
private Dictionary<string, Dictionary<string, MarkerCorrection>> codeActionsPerFile =
26+
new Dictionary<string, Dictionary<string, MarkerCorrection>>();
27+
28+
private static CancellationTokenSource s_existingRequestCancellation;
29+
2230
private readonly DocumentSelector _documentSelector = new DocumentSelector(
2331
new DocumentFilter()
2432
{
@@ -30,9 +38,11 @@ class TextDocumentHandler : ITextDocumentSyncHandler
3038

3139
public TextDocumentSyncKind Change => TextDocumentSyncKind.Incremental;
3240

33-
public TextDocumentHandler(ILoggerFactory factory, WorkspaceService workspaceService)
41+
public TextDocumentHandler(ILoggerFactory factory, ILanguageServer languageServer, AnalysisService analysisService, WorkspaceService workspaceService)
3442
{
3543
_logger = factory.CreateLogger<TextDocumentHandler>();
44+
_languageServer = languageServer;
45+
_analysisService = analysisService;
3646
_workspaceService = workspaceService;
3747
}
3848

@@ -54,10 +64,7 @@ public Task<Unit> Handle(DidChangeTextDocumentParams notification, CancellationT
5464
}
5565

5666
// TODO: Get all recently edited files in the workspace
57-
// this.RunScriptDiagnosticsAsync(
58-
// changedFiles.ToArray(),
59-
// editorSession,
60-
// eventContext);
67+
RunScriptDiagnosticsAsync(changedFiles.ToArray());
6168
return Unit.Task;
6269
}
6370

@@ -83,10 +90,7 @@ public Task<Unit> Handle(DidOpenTextDocumentParams notification, CancellationTok
8390
notification.TextDocument.Text);
8491

8592
// 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 });
9094

9195
_logger.LogTrace("Finished opening document.");
9296
return Unit.Task;
@@ -108,7 +112,7 @@ public Task<Unit> Handle(DidCloseTextDocumentParams notification, CancellationTo
108112
if (fileToClose != null)
109113
{
110114
_workspaceService.CloseFile(fileToClose);
111-
// await ClearMarkersAsync(fileToClose, eventContext);
115+
ClearMarkers(fileToClose);
112116
}
113117

114118
_logger.LogTrace("Finished closing document.");
@@ -162,14 +166,241 @@ private static FileChange GetFileChangeDetails(Range changeRange, string insertS
162166
};
163167
}
164168

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+
}
174405
}
175406
}

src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
1212
using PowerShellEditorServices.Engine.Utility;
1313

14-
namespace PowerShellEditorServices.Engine.Services.Workspace.Handlers
14+
namespace PowerShellEditorServices.Engine.Services.Handlers
1515
{
1616
public class WorkspaceSymbolsHandler : IWorkspaceSymbolsHandler
1717
{

0 commit comments

Comments
 (0)