From 149df657fa54cc8fef4a315c8d889facb11b1de4 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Wed, 31 Jul 2019 19:18:53 -0700 Subject: [PATCH 1/6] Add initial handler --- .../Handlers/CodeActionHandler.cs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs new file mode 100644 index 000000000..3a4bbf02b --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Engine.Services.Handlers; + +namespace Microsoft.PowerShell.EditorServices.TextDocument +{ + internal class CodeActionHandler : ICodeActionHandler + { + private static readonly CodeActionKind[] s_supportedCodeActions = new[] + { + CodeActionKind.QuickFix + }; + + private readonly CodeActionRegistrationOptions _registrationOptions; + + private readonly ILogger _logger; + + private readonly AnalysisService _analysisService; + + private readonly WorkspaceService _workspaceService; + + public CodeActionHandler(ILoggerFactory factory, AnalysisService analysisService, WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _analysisService = analysisService; + _workspaceService = workspaceService; + _registrationOptions = new CodeActionRegistrationOptions() + { + DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.ps*1" }), + CodeActionKinds = s_supportedCodeActions + }; + } + + public CodeActionRegistrationOptions GetRegistrationOptions() + { + throw new System.NotImplementedException(); + } + + public Task Handle(CodeActionParams request, CancellationToken cancellationToken) + { + MarkerCorrection correction = null; + Dictionary markerIndex = null; + var codeActionCommands = new List(); + + // If there are any code fixes, send these commands first so they appear at top of "Code Fix" menu in the client UI. + if (this.codeActionsPerFile.TryGetValue(codeActionParams.TextDocument.Uri, out markerIndex)) + { + foreach (var diagnostic in codeActionParams.Context.Diagnostics) + { + if (string.IsNullOrEmpty(diagnostic.Code)) + { + _logger.LogWarning( + $"textDocument/codeAction skipping diagnostic with empty Code field: {diagnostic.Source} {diagnostic.Message}"); + + continue; + } + + string diagnosticId = GetUniqueIdFromDiagnostic(diagnostic); + if (markerIndex.TryGetValue(diagnosticId, out correction)) + { + codeActionCommands.Add( + new CodeActionCommand + { + Title = correction.Name, + Command = "PowerShell.ApplyCodeActionEdits", + Arguments = JArray.FromObject(correction.Edits) + }); + } + } + } + + // Add "show documentation" commands last so they appear at the bottom of the client UI. + // These commands do not require code fixes. Sometimes we get a batch of diagnostics + // to create commands for. No need to create multiple show doc commands for the same rule. + var ruleNamesProcessed = new HashSet(); + foreach (var diagnostic in codeActionParams.Context.Diagnostics) + { + if (string.IsNullOrEmpty(diagnostic.Code)) { continue; } + + if (string.Equals(diagnostic.Source, "PSScriptAnalyzer", StringComparison.OrdinalIgnoreCase) && + !ruleNamesProcessed.Contains(diagnostic.Code)) + { + ruleNamesProcessed.Add(diagnostic.Code); + + codeActionCommands.Add( + new CodeActionCommand + { + Title = $"Show documentation for \"{diagnostic.Code}\"", + Command = "PowerShell.ShowCodeActionDocumentation", + Arguments = JArray.FromObject(new[] { diagnostic.Code }) + }); + } + } + + await requestContext.SendResultAsync( + codeActionCommands.ToArray()); + } + + public void SetCapability(CodeActionCapability capability) + { + throw new System.NotImplementedException(); + } + } +} From c1fc3954c19c225ac30562a13345ccb6283bbf55 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Mon, 5 Aug 2019 14:57:28 -0700 Subject: [PATCH 2/6] Add working codeAction implementation --- .../Services/Analysis/AnalysisService.cs | 89 ++++++++++++++---- .../Handlers/CodeActionHandler.cs | 93 +++++++++++-------- .../Server/LanguageServer.cs | 1 - 3 files changed, 125 insertions(+), 58 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs index 992fb65ab..18a9b2410 100644 --- a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs @@ -15,6 +15,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System.Threading; +using System.Collections.Concurrent; namespace Microsoft.PowerShell.EditorServices { @@ -22,7 +23,7 @@ namespace Microsoft.PowerShell.EditorServices /// Provides a high-level service for performing semantic analysis /// of PowerShell scripts. /// - public class AnalysisService : IDisposable + internal class AnalysisService : IDisposable { #region Static fields @@ -54,9 +55,6 @@ public class AnalysisService : IDisposable private static readonly string[] s_emptyGetRuleResult = new string[0]; - private Dictionary> codeActionsPerFile = - new Dictionary>(); - private static CancellationTokenSource s_existingRequestCancellation; /// @@ -96,8 +94,11 @@ public class AnalysisService : IDisposable private PSModuleInfo _pssaModuleInfo; private readonly ILanguageServer _languageServer; + private readonly ConfigurationService _configurationService; + private readonly ConcurrentDictionary)> _mostRecentCorrectionsByFile; + #endregion // Private Fields #region Properties @@ -815,24 +816,55 @@ private void PublishScriptDiagnostics( { List diagnostics = new List(); - // Hold on to any corrections that may need to be applied later - Dictionary fileCorrections = - new Dictionary(); + // Create the entry for this file if it does not already exist + SemaphoreSlim fileLock; + Dictionary fileCorrections; + bool newEntryNeeded = false; + if (_mostRecentCorrectionsByFile.TryGetValue(scriptFile.DocumentUri, out (SemaphoreSlim, Dictionary) fileCorrectionsEntry)) + { + fileLock = fileCorrectionsEntry.Item1; + fileCorrections = fileCorrectionsEntry.Item2; + } + else + { + fileLock = new SemaphoreSlim(initialCount: 1, maxCount: 1); + fileCorrections = new Dictionary(); + newEntryNeeded = true; + } - foreach (var marker in markers) + fileLock.Wait(); + try { - // Does the marker contain a correction? - Diagnostic markerDiagnostic = GetDiagnosticFromMarker(marker); - if (marker.Correction != null) + if (newEntryNeeded) { - string diagnosticId = GetUniqueIdFromDiagnostic(markerDiagnostic); - fileCorrections[diagnosticId] = marker.Correction; + // If we create a new entry, we should do it after acquiring the lock we just created + // to ensure a competing thread can never acquire it first and read invalid information from it + _mostRecentCorrectionsByFile[scriptFile.DocumentUri] = (fileLock, fileCorrections); } + else + { + // Otherwise we need to clear the stale corrections + fileCorrections.Clear(); + } + + foreach (ScriptFileMarker marker in markers) + { + // Does the marker contain a correction? + Diagnostic markerDiagnostic = GetDiagnosticFromMarker(marker); + if (marker.Correction != null) + { + string diagnosticId = GetUniqueIdFromDiagnostic(markerDiagnostic); + fileCorrections[diagnosticId] = marker.Correction; + } - diagnostics.Add(markerDiagnostic); + diagnostics.Add(markerDiagnostic); + } + } + finally + { + fileLock.Release(); } - codeActionsPerFile[scriptFile.DocumentUri] = fileCorrections; var uriBuilder = new UriBuilder() { @@ -850,9 +882,34 @@ private void PublishScriptDiagnostics( }); } + public async Task> GetMostRecentCodeActionsForFileAsync(string documentUri) + { + if (!_mostRecentCorrectionsByFile.TryGetValue(documentUri, out (SemaphoreSlim fileLock, Dictionary corrections) fileCorrectionsEntry)) + { + return null; + } + + await fileCorrectionsEntry.fileLock.WaitAsync(); + // We must copy the dictionary for thread safety + var corrections = new Dictionary(fileCorrectionsEntry.corrections.Count); + try + { + foreach (KeyValuePair correction in fileCorrectionsEntry.corrections) + { + corrections.Add(correction.Key, correction.Value); + } + + return corrections; + } + finally + { + fileCorrectionsEntry.fileLock.Release(); + } + } + // Generate a unique id that is used as a key to look up the associated code action (code fix) when // we receive and process the textDocument/codeAction message. - private static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) + internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) { Position start = diagnostic.Range.Start; Position end = diagnostic.Range.End; diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs index 3a4bbf02b..64f994423 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs @@ -1,7 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.JsonRpc.Client; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; @@ -22,13 +27,12 @@ internal class CodeActionHandler : ICodeActionHandler private readonly AnalysisService _analysisService; - private readonly WorkspaceService _workspaceService; + private CodeActionCapability _capability; - public CodeActionHandler(ILoggerFactory factory, AnalysisService analysisService, WorkspaceService workspaceService) + public CodeActionHandler(ILoggerFactory factory, AnalysisService analysisService) { _logger = factory.CreateLogger(); _analysisService = analysisService; - _workspaceService = workspaceService; _registrationOptions = new CodeActionRegistrationOptions() { DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.ps*1" }), @@ -38,39 +42,52 @@ public CodeActionHandler(ILoggerFactory factory, AnalysisService analysisService public CodeActionRegistrationOptions GetRegistrationOptions() { - throw new System.NotImplementedException(); + return _registrationOptions; } - public Task Handle(CodeActionParams request, CancellationToken cancellationToken) + public void SetCapability(CodeActionCapability capability) { - MarkerCorrection correction = null; - Dictionary markerIndex = null; - var codeActionCommands = new List(); + _capability = capability; + } + + public async Task Handle(CodeActionParams request, CancellationToken cancellationToken) + { + IReadOnlyDictionary corrections = await _analysisService.GetMostRecentCodeActionsForFileAsync(request.TextDocument.Uri.ToString()); + + if (corrections == null) + { + // TODO: Find out if we can cache this empty value + return new CommandOrCodeActionContainer(); + } + + var codeActions = new List(); // If there are any code fixes, send these commands first so they appear at top of "Code Fix" menu in the client UI. - if (this.codeActionsPerFile.TryGetValue(codeActionParams.TextDocument.Uri, out markerIndex)) + foreach (Diagnostic diagnostic in request.Context.Diagnostics) { - foreach (var diagnostic in codeActionParams.Context.Diagnostics) + if (diagnostic.Code.IsLong) { - if (string.IsNullOrEmpty(diagnostic.Code)) - { - _logger.LogWarning( - $"textDocument/codeAction skipping diagnostic with empty Code field: {diagnostic.Source} {diagnostic.Message}"); + _logger.LogWarning( + $"textDocument/codeAction skipping diagnostic with non-string code {diagnostic.Code.Long}: {diagnostic.Source} {diagnostic.Message}"); + } + else if (string.IsNullOrEmpty(diagnostic.Code.String)) + { + _logger.LogWarning( + $"textDocument/codeAction skipping diagnostic with empty Code field: {diagnostic.Source} {diagnostic.Message}"); - continue; - } + continue; + } - string diagnosticId = GetUniqueIdFromDiagnostic(diagnostic); - if (markerIndex.TryGetValue(diagnosticId, out correction)) + + string diagnosticId = AnalysisService.GetUniqueIdFromDiagnostic(diagnostic); + if (corrections.TryGetValue(diagnosticId, out MarkerCorrection correction)) + { + codeActions.Add(new Command() { - codeActionCommands.Add( - new CodeActionCommand - { - Title = correction.Name, - Command = "PowerShell.ApplyCodeActionEdits", - Arguments = JArray.FromObject(correction.Edits) - }); - } + Title = correction.Name, + Name = "PowerShell.ApplyCodeActionEdits", + Arguments = JArray.FromObject(correction.Edits) + }); } } @@ -78,32 +95,26 @@ public Task Handle(CodeActionParams request, Cance // These commands do not require code fixes. Sometimes we get a batch of diagnostics // to create commands for. No need to create multiple show doc commands for the same rule. var ruleNamesProcessed = new HashSet(); - foreach (var diagnostic in codeActionParams.Context.Diagnostics) + foreach (Diagnostic diagnostic in request.Context.Diagnostics) { - if (string.IsNullOrEmpty(diagnostic.Code)) { continue; } + if (!diagnostic.Code.IsString || string.IsNullOrEmpty(diagnostic.Code.String)) { continue; } if (string.Equals(diagnostic.Source, "PSScriptAnalyzer", StringComparison.OrdinalIgnoreCase) && - !ruleNamesProcessed.Contains(diagnostic.Code)) + !ruleNamesProcessed.Contains(diagnostic.Code.String)) { - ruleNamesProcessed.Add(diagnostic.Code); + ruleNamesProcessed.Add(diagnostic.Code.String); - codeActionCommands.Add( - new CodeActionCommand + codeActions.Add( + new Command { Title = $"Show documentation for \"{diagnostic.Code}\"", - Command = "PowerShell.ShowCodeActionDocumentation", + Name = "PowerShell.ShowCodeActionDocumentation", Arguments = JArray.FromObject(new[] { diagnostic.Code }) }); } } - await requestContext.SendResultAsync( - codeActionCommands.ToArray()); - } - - public void SetCapability(CodeActionCapability capability) - { - throw new System.NotImplementedException(); + return codeActions; } } } diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index c8029aed5..06ac6bd7b 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -1732,7 +1732,6 @@ await DelayThenInvokeDiagnosticsAsync( cancellationToken); } - private static async Task DelayThenInvokeDiagnosticsAsync( int delayMilliseconds, ScriptFile[] filesToAnalyze, From 1da8919de9df092134cad32f30d11407b45c2bd2 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Tue, 6 Aug 2019 12:28:12 -0700 Subject: [PATCH 3/6] Crash --- .../Hosting/EditorServicesHost.cs | 2 +- .../LanguageServer/OmnisharpLanguageServer.cs | 3 +- .../Services/Analysis/AnalysisService.cs | 5 +- .../Handlers/CodeActionHandler.cs | 5 +- .../Handlers/FormattingHandlers.cs | 4 +- .../Handlers/ConfigurationHandler.cs | 2 +- .../EditorServices.Integration.Tests.ps1 | 67 ++++++++++++++++--- tools/PsesPsClient/Client.cs | 4 +- tools/PsesPsClient/PsesPsClient.psd1 | 1 + tools/PsesPsClient/PsesPsClient.psm1 | 66 ++++++++++++++++++ 10 files changed, 139 insertions(+), 20 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 9512b7918..b52323d40 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -217,7 +217,7 @@ public void StartLanguageService( EditorServiceTransportConfig config, ProfilePaths profilePaths) { - while (System.Diagnostics.Debugger.IsAttached) + while (!System.Diagnostics.Debugger.IsAttached) { Console.WriteLine($"{Process.GetCurrentProcess().Id}"); Thread.Sleep(2000); diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 8f8792980..ccf221008 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -94,7 +94,8 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() - .WithHandler(); + .WithHandler() + .WithHandler(); logger.LogInformation("Handlers added"); }); diff --git a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs index 18a9b2410..d36a0c987 100644 --- a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs @@ -150,6 +150,7 @@ private AnalysisService( _configurationService = configurationService; _logger = logger; _pssaModuleInfo = pssaModuleInfo; + _mostRecentCorrectionsByFile = new ConcurrentDictionary)>(); } #endregion // constructors @@ -814,7 +815,7 @@ private void PublishScriptDiagnostics( ScriptFile scriptFile, List markers) { - List diagnostics = new List(); + var diagnostics = new List(); // Create the entry for this file if it does not already exist SemaphoreSlim fileLock; @@ -917,7 +918,7 @@ internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) var sb = new StringBuilder(256) .Append(diagnostic.Source ?? "?") .Append("_") - .Append(diagnostic.Code.ToString()) + .Append(diagnostic.Code.IsString ? diagnostic.Code.String : diagnostic.Code.Long.ToString()) .Append("_") .Append(diagnostic.Severity?.ToString() ?? "?") .Append("_") diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs index 64f994423..ab8d3099a 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs @@ -27,12 +27,15 @@ internal class CodeActionHandler : ICodeActionHandler private readonly AnalysisService _analysisService; + private readonly ILanguageServer _languageServer; + private CodeActionCapability _capability; - public CodeActionHandler(ILoggerFactory factory, AnalysisService analysisService) + public CodeActionHandler(ILoggerFactory factory, ILanguageServer languageServer, AnalysisService analysisService) { _logger = factory.CreateLogger(); _analysisService = analysisService; + _languageServer = languageServer; _registrationOptions = new CodeActionRegistrationOptions() { DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.ps*1" }), diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs index b66e581de..57c0744a1 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/FormattingHandlers.cs @@ -10,7 +10,7 @@ namespace PowerShellEditorServices.Engine.Services.Handlers { - public class DocumentFormattingHandler : IDocumentFormattingHandler + internal class DocumentFormattingHandler : IDocumentFormattingHandler { private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() @@ -88,7 +88,7 @@ public void SetCapability(DocumentFormattingCapability capability) } } - public class DocumentRangeFormattingHandler : IDocumentRangeFormattingHandler + internal class DocumentRangeFormattingHandler : IDocumentRangeFormattingHandler { private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs index b97485dc0..c1d672999 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/ConfigurationHandler.cs @@ -10,7 +10,7 @@ namespace Microsoft.PowerShell.EditorServices { - public class ConfigurationHandler : IDidChangeConfigurationHandler + internal class ConfigurationHandler : IDidChangeConfigurationHandler { private readonly ILogger _logger; private readonly AnalysisService _analysisService; diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 2dd7079ba..aa4723dda 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -92,6 +92,7 @@ Describe "Loading and running PowerShellEditorServices" { Import-Module -Force "$PSScriptRoot/../../tools/PsesLogAnalyzer" $logIdx = 0 + Wait-Debugger $psesServer = Start-PsesServer $client = Connect-PsesServer -InPipeName $psesServer.SessionDetails.languageServiceWritePipeName -OutPipeName $psesServer.SessionDetails.languageServiceReadPipeName } @@ -163,20 +164,34 @@ function Get-Foo { It "Can get Diagnostics after changing settings" { $file = New-TestFile -Script 'gci | % { $_ }' - $request = Send-LspDidChangeConfigurationRequest -Client $client -Settings @{ - PowerShell = @{ - ScriptAnalysis = @{ - Enable = $false + try + { + $request = Send-LspDidChangeConfigurationRequest -Client $client -Settings @{ + PowerShell = @{ + ScriptAnalysis = @{ + Enable = $false + } } } - } - # Grab notifications for just the file opened in this test. - $notifications = Get-LspNotification -Client $client | Where-Object { - $_.Params.uri -match ([System.IO.Path]::GetFileName($file.PSPath)) + # Grab notifications for just the file opened in this test. + $notifications = Get-LspNotification -Client $client | Where-Object { + $_.Params.uri -match ([System.IO.Path]::GetFileName($file.PSPath)) + } + $notifications | Should -Not -BeNullOrEmpty + $notifications.Params.diagnostics | Should -BeNullOrEmpty + } + finally + { + # Restore PSSA state + Send-LspDidChangeConfigurationRequest -Client $client -Settings @{ + PowerShell = @{ + ScriptAnalysis = @{ + Enable = $true + } + } + } } - $notifications | Should -Not -BeNullOrEmpty - $notifications.Params.diagnostics | Should -BeNullOrEmpty } It "Can handle folding request" { @@ -407,6 +422,38 @@ Get-Foo $response.Result.command.command | Should -Be 'editor.action.showReferences' } + It "Can handle a textDocument/codeAction request" { + $script = 'gci' + $file = Set-Content -Path (Join-Path $TestDrive "$([System.IO.Path]::GetRandomFileName()).ps1") -Value $script -PassThru -Force + + $request = Send-LspDidOpenTextDocumentRequest -Client $client ` + -Uri ([Uri]::new($file.PSPath).AbsoluteUri) ` + -Text ($file[0].ToString()) + + # There's no response for this message, but we need to call Get-LspResponse + # to increment the counter. + Get-LspResponse -Client $client -Id $request.Id | Out-Null + + # Grab notifications for just the file opened in this test. + $notifications = Get-LspNotification -Client $client | Where-Object { + $_.Params.uri -match ([System.IO.Path]::GetFileName($file.PSPath)) + } + + Wait-Debugger + $codeActionParams = @{ + Client = $client + Uri = $uri + StartLine = 0 + StartCharacter = 0 + EndLine = 0 + EndCharacter = 3 + Diagnostics = $notifications.Params.diagnostics + } + $request = Send-LspCodeActionRequest @codeActionParams + + $response = Get-LspResponse -Client $client -Id $request.Id + } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client diff --git a/tools/PsesPsClient/Client.cs b/tools/PsesPsClient/Client.cs index 35d86b278..7316cfc70 100644 --- a/tools/PsesPsClient/Client.cs +++ b/tools/PsesPsClient/Client.cs @@ -94,8 +94,8 @@ public PsesLspClient(NamedPipeClientStream inPipe, NamedPipeClientStream outPipe /// public void Connect() { - _inPipe.Connect(timeout: 1000); - _outPipe.Connect(timeout: 1000); + _inPipe.Connect(timeout: 10000); + _outPipe.Connect(timeout: 10000); _listener = new MessageStreamListener(new StreamReader(_inPipe, _pipeEncoding)); _writer = new StreamWriter(_outPipe, _pipeEncoding) { diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 5a296e017..34c3921fe 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -74,6 +74,7 @@ FunctionsToExport = @( 'Connect-PsesServer', 'Send-LspRequest', 'Send-LspInitializeRequest', + 'Send-LspCodeActionRequest', 'Send-LspDidOpenTextDocumentRequest', 'Send-LspDidChangeConfigurationRequest', 'Send-LspFormattingRequest', diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index 69404f03a..d233de0e8 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -557,6 +557,72 @@ function Send-LspCodeLensResolveRequest } } + return Send-LspRequest -Client $Client -Method 'textDocument/codeAction' -Parameters $codeActionParams +} + +function Send-LspCodeActionRequest +{ + param( + [Parameter()] + [string] + $Uri, + + [Parameter()] + [int] + $StartLine, + + [Parameter()] + [int] + $StartCharacter, + + [Parameter()] + [int] + $EndLine, + + [Parameter()] + [int] + $EndCharacter, + + [Parameter()] + $Diagnostics + ) + + $codeActionParams = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CodeActionParams]@{ + TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ + Uri = $Uri + } + Range = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Range]@{ + Start = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ + Line = $StartLine + Character = $StartCharacter + } + End = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ + Line = $EndLine + Character = $EndCharacter + } + } + Context = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CodeActionContext]@{ + Diagnostics = $Diagnostics | ForEach-Object { + [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Diagnostic]@{ + Code = $_.code + Severity = $_.severity + Source = $_.source + Message = $_.message + Range = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Range]@{ + Start = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ + Line = $_.range.start.line + Character = $_.range.start.character + } + End = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ + Line = $_.range.end.line + Character = $_.range.end.character + } + } + } + } + } + } + return Send-LspRequest -Client $Client -Method 'codeLens/resolve' -Parameters $params } From 9ff7d09f77a612be46fcefc52cc546c62297d949 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Tue, 6 Aug 2019 17:46:43 -0700 Subject: [PATCH 4/6] Make tests work --- .../Hosting/EditorServicesHost.cs | 2 +- .../PowerShellEditorServices.Engine.csproj | 2 +- .../EditorServices.Integration.Tests.ps1 | 24 ++++++++++++------- tools/PsesPsClient/Client.cs | 4 ++-- tools/PsesPsClient/PsesPsClient.psm1 | 10 +++++--- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index b52323d40..9512b7918 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -217,7 +217,7 @@ public void StartLanguageService( EditorServiceTransportConfig config, ProfilePaths profilePaths) { - while (!System.Diagnostics.Debugger.IsAttached) + while (System.Diagnostics.Debugger.IsAttached) { Console.WriteLine($"{Process.GetCurrentProcess().Id}"); Thread.Sleep(2000); diff --git a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj index c0fb4a3df..032e410d8 100644 --- a/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj +++ b/src/PowerShellEditorServices.Engine/PowerShellEditorServices.Engine.csproj @@ -12,7 +12,7 @@ - + diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index aa4723dda..328c0945c 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -92,7 +92,6 @@ Describe "Loading and running PowerShellEditorServices" { Import-Module -Force "$PSScriptRoot/../../tools/PsesLogAnalyzer" $logIdx = 0 - Wait-Debugger $psesServer = Start-PsesServer $client = Connect-PsesServer -InPipeName $psesServer.SessionDetails.languageServiceWritePipeName -OutPipeName $psesServer.SessionDetails.languageServiceReadPipeName } @@ -223,7 +222,7 @@ $_ $sortedResults[1].endCharacter | Should -Be 2 } - It "can handle a normal formatting request" { + It "Can handle a normal formatting request" { $filePath = New-TestFile -Script ' gci | % { Get-Process @@ -240,7 +239,7 @@ Get-Process $response.Result.newText.Contains("`t") | Should -BeTrue -Because "We expect a tab." } - It "can handle a range formatting request" { + It "Can handle a range formatting request" { $filePath = New-TestFile -Script ' gci | % { Get-Process @@ -439,19 +438,26 @@ Get-Foo $_.Params.uri -match ([System.IO.Path]::GetFileName($file.PSPath)) } - Wait-Debugger $codeActionParams = @{ Client = $client - Uri = $uri - StartLine = 0 - StartCharacter = 0 - EndLine = 0 - EndCharacter = 3 + Uri = $notifications[0].Params.uri + StartLine = 1 + StartCharacter = 1 + EndLine = 1 + EndCharacter = 4 Diagnostics = $notifications.Params.diagnostics } $request = Send-LspCodeActionRequest @codeActionParams $response = Get-LspResponse -Client $client -Id $request.Id + + $edits = $response.Result | Where-Object command -eq 'PowerShell.ApplyCodeActionEdits' + $edits.Count | Should -Be 1 + $edits[0].Arguments.Text | Should -BeExactly 'Get-ChildItem' + $edits[0].Arguments.StartLineNumber | Should -Be 1 + $edits[0].Arguments.StartColumnNumber | Should -Be 1 + $edits[0].Arguments.EndLineNumber | Should -Be 1 + $edits[0].Arguments.EndColumnNumber | Should -Be 4 } # This test MUST be last diff --git a/tools/PsesPsClient/Client.cs b/tools/PsesPsClient/Client.cs index 7316cfc70..35d86b278 100644 --- a/tools/PsesPsClient/Client.cs +++ b/tools/PsesPsClient/Client.cs @@ -94,8 +94,8 @@ public PsesLspClient(NamedPipeClientStream inPipe, NamedPipeClientStream outPipe /// public void Connect() { - _inPipe.Connect(timeout: 10000); - _outPipe.Connect(timeout: 10000); + _inPipe.Connect(timeout: 1000); + _outPipe.Connect(timeout: 1000); _listener = new MessageStreamListener(new StreamReader(_inPipe, _pipeEncoding)); _writer = new StreamWriter(_outPipe, _pipeEncoding) { diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index d233de0e8..099007ad9 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -557,12 +557,16 @@ function Send-LspCodeLensResolveRequest } } - return Send-LspRequest -Client $Client -Method 'textDocument/codeAction' -Parameters $codeActionParams + return Send-LspRequest -Client $Client -Method 'codeLens/resolve' -Parameters $params } function Send-LspCodeActionRequest { param( + [Parameter()] + [PsesPsClient.PsesLspClient] + $Client, + [Parameter()] [string] $Uri, @@ -587,7 +591,7 @@ function Send-LspCodeActionRequest $Diagnostics ) - $codeActionParams = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CodeActionParams]@{ + $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CodeActionParams]@{ TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ Uri = $Uri } @@ -623,7 +627,7 @@ function Send-LspCodeActionRequest } } - return Send-LspRequest -Client $Client -Method 'codeLens/resolve' -Parameters $params + return Send-LspRequest -Client $Client -Method 'textDocument/codeAction' -Parameters $params } function Send-LspShutdownRequest From f99a8221b20cc7ac3a6c0bec9fcb83329ad5e3be Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Tue, 6 Aug 2019 18:01:52 -0700 Subject: [PATCH 5/6] Fix issues --- .../Services/TextDocument/Handlers/CodeActionHandler.cs | 5 +---- test/Pester/EditorServices.Integration.Tests.ps1 | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs index ab8d3099a..64f994423 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeActionHandler.cs @@ -27,15 +27,12 @@ internal class CodeActionHandler : ICodeActionHandler private readonly AnalysisService _analysisService; - private readonly ILanguageServer _languageServer; - private CodeActionCapability _capability; - public CodeActionHandler(ILoggerFactory factory, ILanguageServer languageServer, AnalysisService analysisService) + public CodeActionHandler(ILoggerFactory factory, AnalysisService analysisService) { _logger = factory.CreateLogger(); _analysisService = analysisService; - _languageServer = languageServer; _registrationOptions = new CodeActionRegistrationOptions() { DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.ps*1" }), diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 328c0945c..341f9c036 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -433,11 +433,15 @@ Get-Foo # to increment the counter. Get-LspResponse -Client $client -Id $request.Id | Out-Null + Start-Sleep 1 + # Grab notifications for just the file opened in this test. $notifications = Get-LspNotification -Client $client | Where-Object { $_.Params.uri -match ([System.IO.Path]::GetFileName($file.PSPath)) } + $notifications.Count | Should -BeGreaterOrEqual 1 + $codeActionParams = @{ Client = $client Uri = $notifications[0].Params.uri From 0a1175d9b6d2bc4db4c2b9310d5b378cd8d1e405 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Wed, 7 Aug 2019 14:57:26 -0700 Subject: [PATCH 6/6] Make tests work in WinPS --- .../Services/TextDocument/ScriptFileMarker.cs | 8 ++++---- .../EditorServices.Integration.Tests.ps1 | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs index 9700464dd..a6f518c75 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs @@ -147,11 +147,11 @@ internal static ScriptFileMarker FromDiagnosticRecord(PSObject psObject) new ScriptRegion( diagnosticRecord.ScriptPath, suggestedCorrection.Text, - suggestedCorrection.StartLineNumber, - suggestedCorrection.StartColumnNumber, + startLineNumber: suggestedCorrection.StartLineNumber, + startColumnNumber: suggestedCorrection.StartColumnNumber, + endLineNumber: suggestedCorrection.EndLineNumber, + endColumnNumber: suggestedCorrection.EndColumnNumber, startOffset: -1, - suggestedCorrection.EndLineNumber, - suggestedCorrection.EndColumnNumber, endOffset: -1)); correctionMessage = suggestedCorrection.Description; diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 341f9c036..edc53f684 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -440,11 +440,11 @@ Get-Foo $_.Params.uri -match ([System.IO.Path]::GetFileName($file.PSPath)) } - $notifications.Count | Should -BeGreaterOrEqual 1 + $notifications | Should -Not -BeNullOrEmpty $codeActionParams = @{ Client = $client - Uri = $notifications[0].Params.uri + Uri = $notifications.Params.uri StartLine = 1 StartCharacter = 1 EndLine = 1 @@ -455,13 +455,13 @@ Get-Foo $response = Get-LspResponse -Client $client -Id $request.Id - $edits = $response.Result | Where-Object command -eq 'PowerShell.ApplyCodeActionEdits' - $edits.Count | Should -Be 1 - $edits[0].Arguments.Text | Should -BeExactly 'Get-ChildItem' - $edits[0].Arguments.StartLineNumber | Should -Be 1 - $edits[0].Arguments.StartColumnNumber | Should -Be 1 - $edits[0].Arguments.EndLineNumber | Should -Be 1 - $edits[0].Arguments.EndColumnNumber | Should -Be 4 + $edit = $response.Result | Where-Object command -eq 'PowerShell.ApplyCodeActionEdits' | Select-Object -First 1 + $edit | Should -Not -BeNullOrEmpty + $edit.Arguments.Text | Should -BeExactly 'Get-ChildItem' + $edit.Arguments.StartLineNumber | Should -Be 1 + $edit.Arguments.StartColumnNumber | Should -Be 1 + $edit.Arguments.EndLineNumber | Should -Be 1 + $edit.Arguments.EndColumnNumber | Should -Be 4 } # This test MUST be last