diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index ed92f9083..3ffc3777a 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -1,3 +1,8 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System.IO.Pipes; using System.Reflection; using System.Threading.Tasks; @@ -8,6 +13,7 @@ using System.Security.AccessControl; using OmniSharp.Extensions.LanguageServer.Server; using PowerShellEditorServices.Engine.Services.Handlers; +using Microsoft.PowerShell.EditorServices.TextDocument; namespace Microsoft.PowerShell.EditorServices.Engine { @@ -56,18 +62,26 @@ public async Task StartAsync() _configuration.OutNamedPipeName, out NamedPipeServerStream outNamedPipe); + ILogger logger = options.LoggerFactory.CreateLogger("OptionsStartup"); + + logger.LogInformation("Waiting for connection"); namedPipe.WaitForConnection(); if (outNamedPipe != null) { outNamedPipe.WaitForConnection(); } + logger.LogInformation("Connected"); + options.Input = namedPipe; options.Output = outNamedPipe ?? namedPipe; options.LoggerFactory = _configuration.LoggerFactory; options.MinimumLogLevel = _configuration.MinimumLogLevel; options.Services = _configuration.Services; + + logger.LogInformation("Adding handlers"); + options .WithHandler() .WithHandler() @@ -77,7 +91,10 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() - .WithHandler(); + .WithHandler() + .WithHandler(); + + logger.LogInformation("Handlers added"); }); _serverStart.SetResult(true); diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 071742dea..6e9726f2c 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -1,6 +1,12 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Symbols; @@ -47,8 +53,6 @@ public SymbolsService( #endregion - - /// /// Finds all the symbols in a file. /// @@ -176,5 +180,33 @@ public List FindReferencesOfSymbol( return symbolReferences; } + + /// + /// Finds all the occurences of a symbol in the script given a file location + /// + /// The details and contents of a open script file + /// The line number of the cursor for the given script + /// The coulumn number of the cursor for the given script + /// FindOccurrencesResult + public IReadOnlyList FindOccurrencesInFile( + ScriptFile file, + int symbolLineNumber, + int symbolColumnNumber) + { + SymbolReference foundSymbol = AstOperations.FindSymbolAtPosition( + file.ScriptAst, + symbolLineNumber, + symbolColumnNumber); + + if (foundSymbol == null) + { + return null; + } + + return AstOperations.FindReferencesOfSymbol( + file.ScriptAst, + foundSymbol, + needsAliases: false).ToArray(); + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs new file mode 100644 index 000000000..aafc1c79f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentHighlightHandler.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Symbols; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Engine.Utility; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.TextDocument +{ + public class DocumentHighlightHandler : IDocumentHighlightHandler + { + private static readonly DocumentHighlightContainer s_emptyHighlightContainer = new DocumentHighlightContainer(); + + private readonly ILogger _logger; + + private readonly WorkspaceService _workspaceService; + + private readonly SymbolsService _symbolsService; + + private readonly TextDocumentRegistrationOptions _registrationOptions; + + private DocumentHighlightCapability _capability; + + public DocumentHighlightHandler( + ILoggerFactory loggerFactory, + WorkspaceService workspaceService, + SymbolsService symbolService) + { + _logger = loggerFactory.CreateLogger(); + _workspaceService = workspaceService; + _symbolsService = symbolService; + _registrationOptions = new TextDocumentRegistrationOptions() + { + DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.ps*1" } ) + }; + _logger.LogInformation("highlight handler loaded"); + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return _registrationOptions; + } + + public Task Handle( + DocumentHighlightParams request, + CancellationToken cancellationToken) + { + ScriptFile scriptFile = _workspaceService.GetFile(PathUtils.FromUri(request.TextDocument.Uri)); + + IReadOnlyList symbolOccurrences = _symbolsService.FindOccurrencesInFile( + scriptFile, + (int)request.Position.Line, + (int)request.Position.Character); + + if (symbolOccurrences == null) + { + return Task.FromResult(s_emptyHighlightContainer); + } + + var highlights = new DocumentHighlight[symbolOccurrences.Count]; + for (int i = 0; i < symbolOccurrences.Count; i++) + { + highlights[i] = new DocumentHighlight + { + Kind = DocumentHighlightKind.Write, // TODO: Which symbol types are writable? + Range = symbolOccurrences[i].ScriptRegion.ToRange() + }; + } + + return Task.FromResult(new DocumentHighlightContainer(highlights)); + } + + public void SetCapability(DocumentHighlightCapability capability) + { + _capability = capability; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs index 6da4086ed..9700464dd 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFileMarker.cs @@ -139,20 +139,21 @@ internal static ScriptFileMarker FromDiagnosticRecord(PSObject psObject) if (diagnosticRecord.SuggestedCorrections != null) { - var suggestedCorrections = diagnosticRecord.SuggestedCorrections as dynamic; - List editRegions = new List(); + var editRegions = new List(); string correctionMessage = null; - foreach (var suggestedCorrection in suggestedCorrections) + foreach (dynamic suggestedCorrection in diagnosticRecord.SuggestedCorrections) { - editRegions.Add(new ScriptRegion - { - File = diagnosticRecord.ScriptPath, - Text = suggestedCorrection.Text, - StartLineNumber = suggestedCorrection.StartLineNumber, - StartColumnNumber = suggestedCorrection.StartColumnNumber, - EndLineNumber = suggestedCorrection.EndLineNumber, - EndColumnNumber = suggestedCorrection.EndColumnNumber - }); + editRegions.Add( + new ScriptRegion( + diagnosticRecord.ScriptPath, + suggestedCorrection.Text, + suggestedCorrection.StartLineNumber, + suggestedCorrection.StartColumnNumber, + startOffset: -1, + suggestedCorrection.EndLineNumber, + suggestedCorrection.EndColumnNumber, + endOffset: -1)); + correctionMessage = suggestedCorrection.Description; } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs index 5717b1382..a40814bba 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using OmniSharp.Extensions.LanguageServer.Protocol.Models; using System; using System.Management.Automation.Language; @@ -13,47 +14,109 @@ namespace Microsoft.PowerShell.EditorServices /// public sealed class ScriptRegion : IScriptExtent { + #region Static Methods + + /// + /// Creates a new instance of the ScriptRegion class from an + /// instance of an IScriptExtent implementation. + /// + /// + /// The IScriptExtent to copy into the ScriptRegion. + /// + /// + /// A new ScriptRegion instance with the same details as the IScriptExtent. + /// + public static ScriptRegion Create(IScriptExtent scriptExtent) + { + // IScriptExtent throws an ArgumentOutOfRange exception if Text is null + string scriptExtentText; + try + { + scriptExtentText = scriptExtent.Text; + } + catch (ArgumentOutOfRangeException) + { + scriptExtentText = string.Empty; + } + + return new ScriptRegion( + scriptExtent.File, + scriptExtentText, + scriptExtent.StartLineNumber, + scriptExtent.StartColumnNumber, + scriptExtent.StartOffset, + scriptExtent.EndLineNumber, + scriptExtent.EndColumnNumber, + scriptExtent.EndOffset); + } + + #endregion + + #region Constructors + + public ScriptRegion( + string file, + string text, + int startLineNumber, + int startColumnNumber, + int startOffset, + int endLineNumber, + int endColumnNumber, + int endOffset) + { + File = file; + Text = text; + StartLineNumber = startLineNumber; + StartColumnNumber = startColumnNumber; + StartOffset = startOffset; + EndLineNumber = endLineNumber; + EndColumnNumber = endColumnNumber; + EndOffset = endOffset; + } + + #endregion + #region Properties /// /// Gets the file path of the script file in which this region is contained. /// - public string File { get; set; } + public string File { get; } /// /// Gets or sets the text that is contained within the region. /// - public string Text { get; set; } + public string Text { get; } /// /// Gets or sets the starting line number of the region. /// - public int StartLineNumber { get; set; } + public int StartLineNumber { get; } /// /// Gets or sets the starting column number of the region. /// - public int StartColumnNumber { get; set; } + public int StartColumnNumber { get; } /// /// Gets or sets the starting file offset of the region. /// - public int StartOffset { get; set; } + public int StartOffset { get; } /// /// Gets or sets the ending line number of the region. /// - public int EndLineNumber { get; set; } + public int EndLineNumber { get; } /// /// Gets or sets the ending column number of the region. /// - public int EndColumnNumber { get; set; } + public int EndColumnNumber { get; } /// /// Gets or sets the ending file offset of the region. /// - public int EndOffset { get; set; } + public int EndOffset { get; } /// /// Gets the starting IScriptPosition in the script. @@ -69,41 +132,22 @@ public sealed class ScriptRegion : IScriptExtent #endregion - #region Constructors + #region Methods - /// - /// Creates a new instance of the ScriptRegion class from an - /// instance of an IScriptExtent implementation. - /// - /// - /// The IScriptExtent to copy into the ScriptRegion. - /// - /// - /// A new ScriptRegion instance with the same details as the IScriptExtent. - /// - public static ScriptRegion Create(IScriptExtent scriptExtent) + public Range ToRange() { - // IScriptExtent throws an ArgumentOutOfRange exception if Text is null - string scriptExtentText; - try - { - scriptExtentText = scriptExtent.Text; - } - catch (ArgumentOutOfRangeException) - { - scriptExtentText = string.Empty; - } - - return new ScriptRegion + return new Range { - File = scriptExtent.File, - Text = scriptExtentText, - StartLineNumber = scriptExtent.StartLineNumber, - StartColumnNumber = scriptExtent.StartColumnNumber, - StartOffset = scriptExtent.StartOffset, - EndLineNumber = scriptExtent.EndLineNumber, - EndColumnNumber = scriptExtent.EndColumnNumber, - EndOffset = scriptExtent.EndOffset + Start = new Position + { + Line = StartLineNumber - 1, + Character = StartColumnNumber - 1 + }, + End = new Position + { + Line = EndLineNumber - 1, + Character = EndColumnNumber - 1 + } }; } diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index b363d4ccb..8aaa7511a 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -299,6 +299,34 @@ Get-Bar $response.Result[1].range.end.character | Should -BeExactly 7 } + It "Can handle a textDocument/documentHighlight request" { + $filePath = New-TestFile -Script @' +Write-Host 'Hello!' + +Write-Host 'Goodbye' +'@ + + $documentHighlightParams = @{ + Client = $client + Uri = ([uri]::new($filePath).AbsoluteUri) + LineNumber = 3 + CharacterNumber = 1 + } + $request = Send-LspDocumentHighlightRequest @documentHighlightParams + + $response = Get-LspResponse -Client $client -Id $request.Id + + $response.Result.Count | Should -BeExactly 2 + $response.Result[0].Range.Start.Line | Should -BeExactly 0 + $response.Result[0].Range.Start.Character | Should -BeExactly 0 + $response.Result[0].Range.End.Line | Should -BeExactly 0 + $response.Result[0].Range.End.Character | Should -BeExactly 10 + $response.Result[1].Range.Start.Line | Should -BeExactly 2 + $response.Result[1].Range.Start.Character | Should -BeExactly 0 + $response.Result[1].Range.End.Line | Should -BeExactly 2 + $response.Result[1].Range.End.Character | Should -BeExactly 10 + } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 7010f9a3e..6b9c0987a 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -79,6 +79,7 @@ FunctionsToExport = @( 'Send-LspFormattingRequest', 'Send-LspRangeFormattingRequest', 'Send-LspDocumentSymbolRequest', + 'Send-LspDocumentHighlightRequest', 'Send-LspReferencesRequest', 'Send-LspShutdownRequest', 'Get-LspNotification', diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index bbfec42a4..d15582778 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -453,6 +453,40 @@ function Send-LspReferencesRequest return Send-LspRequest -Client $Client -Method 'textDocument/references' -Parameters $params } +function Send-LspDocumentHighlightRequest +{ + [OutputType([PsesPsClient.LspRequest])] + param( + [Parameter(Position = 0, Mandatory)] + [PsesPsClient.PsesLspClient] + $Client, + + [Parameter(Position = 1, Mandatory)] + [string] + $Uri, + + [Parameter(Mandatory)] + [int] + $LineNumber, + + [Parameter(Mandatory)] + [int] + $CharacterNumber + ) + + $documentHighlightParams = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentPositionParams]@{ + TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ + Uri = $Uri + } + Position = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ + Line = $LineNumber + Character = $CharacterNumber + } + } + + return Send-LspRequest -Client $Client -Method 'textDocument/documentHighlight' -Parameters $documentHighlightParams +} + function Send-LspShutdownRequest { [OutputType([PsesPsClient.LspRequest])]