Skip to content

Commit 3be84ba

Browse files
Add setting support (PowerShell#19)
* Added Diagnostics * didChangeConfiguration message and general settings support * Apply suggestions from code review Co-Authored-By: Robert Holt <[email protected]>
1 parent e4c86ee commit 3be84ba

File tree

10 files changed

+857
-249
lines changed

10 files changed

+857
-249
lines changed

src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,13 @@ public void StartLanguageService(
227227

228228
_serviceCollection.AddSingleton<WorkspaceService>();
229229
_serviceCollection.AddSingleton<SymbolsService>();
230+
_serviceCollection.AddSingleton<ConfigurationService>();
230231
_serviceCollection.AddSingleton<AnalysisService>(
231232
(provider) => {
232-
// TODO: Fill in settings
233-
return AnalysisService.Create(null, _factory.CreateLogger<AnalysisService>());
233+
return AnalysisService.Create(
234+
provider.GetService<ConfigurationService>(),
235+
provider.GetService<OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer>(),
236+
_factory.CreateLogger<AnalysisService>());
234237
}
235238
);
236239

src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public async Task StartAsync()
7171
options.WithHandler<WorkspaceSymbolsHandler>();
7272
options.WithHandler<TextDocumentHandler>();
7373
options.WithHandler<GetVersionHandler>();
74+
options.WithHandler<ConfigurationHandler>();
7475
});
7576

7677
_serverStart.SetResult(true);

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

+245-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
using System.Text;
1313
using System.Collections;
1414
using Microsoft.Extensions.Logging;
15+
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
16+
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
17+
using System.Threading;
1518

1619
namespace Microsoft.PowerShell.EditorServices
1720
{
@@ -51,6 +54,11 @@ public class AnalysisService : IDisposable
5154

5255
private static readonly string[] s_emptyGetRuleResult = new string[0];
5356

57+
private Dictionary<string, Dictionary<string, MarkerCorrection>> codeActionsPerFile =
58+
new Dictionary<string, Dictionary<string, MarkerCorrection>>();
59+
60+
private static CancellationTokenSource s_existingRequestCancellation;
61+
5462
/// <summary>
5563
/// The indentation to add when the logger lists errors.
5664
/// </summary>
@@ -87,6 +95,9 @@ public class AnalysisService : IDisposable
8795
/// </summary>
8896
private PSModuleInfo _pssaModuleInfo;
8997

98+
private readonly ILanguageServer _languageServer;
99+
private readonly ConfigurationService _configurationService;
100+
90101
#endregion // Private Fields
91102

92103
#region Properties
@@ -126,12 +137,16 @@ private AnalysisService(
126137
RunspacePool analysisRunspacePool,
127138
string pssaSettingsPath,
128139
IEnumerable<string> activeRules,
140+
ILanguageServer languageServer,
141+
ConfigurationService configurationService,
129142
ILogger logger,
130143
PSModuleInfo pssaModuleInfo = null)
131144
{
132145
_analysisRunspacePool = analysisRunspacePool;
133146
SettingsPath = pssaSettingsPath;
134147
ActiveRules = activeRules.ToArray();
148+
_languageServer = languageServer;
149+
_configurationService = configurationService;
135150
_logger = logger;
136151
_pssaModuleInfo = pssaModuleInfo;
137152
}
@@ -150,8 +165,9 @@ private AnalysisService(
150165
/// A new analysis service instance with a freshly imported PSScriptAnalyzer module and runspace pool.
151166
/// Returns null if problems occur. This method should never throw.
152167
/// </returns>
153-
public static AnalysisService Create(string settingsPath, ILogger logger)
168+
public static AnalysisService Create(ConfigurationService configurationService, ILanguageServer languageServer, ILogger logger)
154169
{
170+
string settingsPath = configurationService.CurrentSettings.ScriptAnalysis.SettingsPath;
155171
try
156172
{
157173
RunspacePool analysisRunspacePool;
@@ -184,6 +200,8 @@ public static AnalysisService Create(string settingsPath, ILogger logger)
184200
analysisRunspacePool,
185201
settingsPath,
186202
s_includedRules,
203+
languageServer,
204+
configurationService,
187205
logger,
188206
pssaModuleInfo);
189207

@@ -673,6 +691,232 @@ public PowerShellResult(
673691

674692
public bool HasErrors { get; }
675693
}
694+
695+
internal async Task RunScriptDiagnosticsAsync(
696+
ScriptFile[] filesToAnalyze)
697+
{
698+
// If there's an existing task, attempt to cancel it
699+
try
700+
{
701+
if (s_existingRequestCancellation != null)
702+
{
703+
// Try to cancel the request
704+
s_existingRequestCancellation.Cancel();
705+
706+
// If cancellation didn't throw an exception,
707+
// clean up the existing token
708+
s_existingRequestCancellation.Dispose();
709+
s_existingRequestCancellation = null;
710+
}
711+
}
712+
catch (Exception e)
713+
{
714+
// TODO: Catch a more specific exception!
715+
_logger.LogError(
716+
string.Format(
717+
"Exception while canceling analysis task:\n\n{0}",
718+
e.ToString()));
719+
720+
TaskCompletionSource<bool> cancelTask = new TaskCompletionSource<bool>();
721+
cancelTask.SetCanceled();
722+
return;
723+
}
724+
725+
// If filesToAnalzye is empty, nothing to do so return early.
726+
if (filesToAnalyze.Length == 0)
727+
{
728+
return;
729+
}
730+
731+
// Create a fresh cancellation token and then start the task.
732+
// We create this on a different TaskScheduler so that we
733+
// don't block the main message loop thread.
734+
// TODO: Is there a better way to do this?
735+
s_existingRequestCancellation = new CancellationTokenSource();
736+
await Task.Factory.StartNew(
737+
() =>
738+
DelayThenInvokeDiagnosticsAsync(
739+
750,
740+
filesToAnalyze,
741+
_configurationService.CurrentSettings.ScriptAnalysis.Enable ?? false,
742+
s_existingRequestCancellation.Token),
743+
CancellationToken.None,
744+
TaskCreationOptions.None,
745+
TaskScheduler.Default);
746+
}
747+
748+
private async Task DelayThenInvokeDiagnosticsAsync(
749+
int delayMilliseconds,
750+
ScriptFile[] filesToAnalyze,
751+
bool isScriptAnalysisEnabled,
752+
CancellationToken cancellationToken)
753+
{
754+
// First of all, wait for the desired delay period before
755+
// analyzing the provided list of files
756+
try
757+
{
758+
await Task.Delay(delayMilliseconds, cancellationToken);
759+
}
760+
catch (TaskCanceledException)
761+
{
762+
// If the task is cancelled, exit directly
763+
foreach (var script in filesToAnalyze)
764+
{
765+
PublishScriptDiagnostics(
766+
script,
767+
script.DiagnosticMarkers);
768+
}
769+
770+
return;
771+
}
772+
773+
// If we've made it past the delay period then we don't care
774+
// about the cancellation token anymore. This could happen
775+
// when the user stops typing for long enough that the delay
776+
// period ends but then starts typing while analysis is going
777+
// on. It makes sense to send back the results from the first
778+
// delay period while the second one is ticking away.
779+
780+
// Get the requested files
781+
foreach (ScriptFile scriptFile in filesToAnalyze)
782+
{
783+
List<ScriptFileMarker> semanticMarkers = null;
784+
if (isScriptAnalysisEnabled)
785+
{
786+
semanticMarkers = await GetSemanticMarkersAsync(scriptFile);
787+
}
788+
else
789+
{
790+
// Semantic markers aren't available if the AnalysisService
791+
// isn't available
792+
semanticMarkers = new List<ScriptFileMarker>();
793+
}
794+
795+
scriptFile.DiagnosticMarkers.AddRange(semanticMarkers);
796+
797+
PublishScriptDiagnostics(
798+
scriptFile,
799+
// Concat script analysis errors to any existing parse errors
800+
scriptFile.DiagnosticMarkers);
801+
}
802+
}
803+
804+
internal void ClearMarkers(ScriptFile scriptFile)
805+
{
806+
// send empty diagnostic markers to clear any markers associated with the given file
807+
PublishScriptDiagnostics(
808+
scriptFile,
809+
new List<ScriptFileMarker>());
810+
}
811+
812+
private void PublishScriptDiagnostics(
813+
ScriptFile scriptFile,
814+
List<ScriptFileMarker> markers)
815+
{
816+
List<Diagnostic> diagnostics = new List<Diagnostic>();
817+
818+
// Hold on to any corrections that may need to be applied later
819+
Dictionary<string, MarkerCorrection> fileCorrections =
820+
new Dictionary<string, MarkerCorrection>();
821+
822+
foreach (var marker in markers)
823+
{
824+
// Does the marker contain a correction?
825+
Diagnostic markerDiagnostic = GetDiagnosticFromMarker(marker);
826+
if (marker.Correction != null)
827+
{
828+
string diagnosticId = GetUniqueIdFromDiagnostic(markerDiagnostic);
829+
fileCorrections[diagnosticId] = marker.Correction;
830+
}
831+
832+
diagnostics.Add(markerDiagnostic);
833+
}
834+
835+
codeActionsPerFile[scriptFile.DocumentUri] = fileCorrections;
836+
837+
var uriBuilder = new UriBuilder()
838+
{
839+
Scheme = Uri.UriSchemeFile,
840+
Path = scriptFile.FilePath,
841+
Host = string.Empty,
842+
};
843+
844+
// Always send syntax and semantic errors. We want to
845+
// make sure no out-of-date markers are being displayed.
846+
_languageServer.Document.PublishDiagnostics(new PublishDiagnosticsParams()
847+
{
848+
Uri = uriBuilder.Uri,
849+
Diagnostics = new Container<Diagnostic>(diagnostics),
850+
});
851+
}
852+
853+
// Generate a unique id that is used as a key to look up the associated code action (code fix) when
854+
// we receive and process the textDocument/codeAction message.
855+
private static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic)
856+
{
857+
Position start = diagnostic.Range.Start;
858+
Position end = diagnostic.Range.End;
859+
860+
var sb = new StringBuilder(256)
861+
.Append(diagnostic.Source ?? "?")
862+
.Append("_")
863+
.Append(diagnostic.Code.ToString())
864+
.Append("_")
865+
.Append(diagnostic.Severity?.ToString() ?? "?")
866+
.Append("_")
867+
.Append(start.Line)
868+
.Append(":")
869+
.Append(start.Character)
870+
.Append("-")
871+
.Append(end.Line)
872+
.Append(":")
873+
.Append(end.Character);
874+
875+
var id = sb.ToString();
876+
return id;
877+
}
878+
879+
private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker)
880+
{
881+
return new Diagnostic
882+
{
883+
Severity = MapDiagnosticSeverity(scriptFileMarker.Level),
884+
Message = scriptFileMarker.Message,
885+
Code = scriptFileMarker.RuleName,
886+
Source = scriptFileMarker.Source,
887+
Range = new Range
888+
{
889+
Start = new Position
890+
{
891+
Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1,
892+
Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1
893+
},
894+
End = new Position
895+
{
896+
Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1,
897+
Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1
898+
}
899+
}
900+
};
901+
}
902+
903+
private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel)
904+
{
905+
switch (markerLevel)
906+
{
907+
case ScriptFileMarkerLevel.Error:
908+
return DiagnosticSeverity.Error;
909+
910+
case ScriptFileMarkerLevel.Warning:
911+
return DiagnosticSeverity.Warning;
912+
913+
case ScriptFileMarkerLevel.Information:
914+
return DiagnosticSeverity.Information;
915+
916+
default:
917+
return DiagnosticSeverity.Error;
918+
}
919+
}
676920
}
677921

678922
/// <summary>

0 commit comments

Comments
 (0)