diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 3ffc3777a..73da0e447 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -92,9 +92,10 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() - .WithHandler(); + .WithHandler() + .WithHandler(); - logger.LogInformation("Handlers added"); + logger.LogInformation("Handlers added"); }); _serverStart.SetResult(true); diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs new file mode 100644 index 000000000..15fff728d --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/CodeLensData.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + /// + /// Represents data expected back in an LSP CodeLens response. + /// + internal class CodeLensData + { + public string Uri { get; set; } + + public string ProviderId { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs new file mode 100644 index 000000000..fab101580 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLensProvider.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + /// + /// Specifies the contract for a Code Lens provider. + /// + public interface ICodeLensProvider + { + /// + /// Specifies a unique identifier for the feature provider, typically a + /// fully-qualified name like "Microsoft.PowerShell.EditorServices.MyProvider" + /// + string ProviderId { get; } + + /// + /// Provides a collection of CodeLenses for the given + /// document. + /// + /// + /// The document for which CodeLenses should be provided. + /// + /// An array of CodeLenses. + CodeLens[] ProvideCodeLenses(ScriptFile scriptFile); + + /// + /// Resolves a CodeLens that was created without a Command. + /// + /// + /// The CodeLens to resolve. + /// + /// + /// A CancellationToken which can be used to cancel the + /// request. + /// + /// + /// A Task which returns the resolved CodeLens when completed. + /// + CodeLens ResolveCodeLens( + CodeLens codeLens, + ScriptFile scriptFile); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs new file mode 100644 index 000000000..6bca0c9be --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/ICodeLenses.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + /// + /// Specifies the contract for an implementation of + /// the ICodeLenses component. + /// + public interface ICodeLenses + { + /// + /// Gets the collection of ICodeLensProvider implementations + /// that are registered with this component. + /// + List Providers { get; } + + /// + /// Provides a collection of CodeLenses for the given + /// document. + /// + /// + /// The document for which CodeLenses should be provided. + /// + /// An array of CodeLenses. + CodeLens[] ProvideCodeLenses(ScriptFile scriptFile); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/IScriptExtentExtensions.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/IScriptExtentExtensions.cs new file mode 100644 index 000000000..1f8ee1da1 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/IScriptExtentExtensions.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation.Language; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices +{ + internal static class IScriptExtentExtensions + { + public static Range ToRange(this IScriptExtent scriptExtent) + { + return new Range + { + Start = new Position + { + Line = scriptExtent.StartLineNumber - 1, + Character = scriptExtent.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptExtent.EndLineNumber - 1, + Character = scriptExtent.EndColumnNumber - 1 + } + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs new file mode 100644 index 000000000..3d7d85556 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/PesterCodeLensProvider.cs @@ -0,0 +1,127 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Symbols; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + internal class PesterCodeLensProvider : ICodeLensProvider + { + + /// + /// The symbol provider to get symbols from to build code lenses with. + /// + private readonly IDocumentSymbolProvider _symbolProvider; + + /// + /// Specifies a unique identifier for the feature provider, typically a + /// fully-qualified name like "Microsoft.PowerShell.EditorServices.MyProvider" + /// + public string ProviderId => nameof(PesterCodeLensProvider); + + /// + /// Create a new Pester CodeLens provider for a given editor session. + /// + public PesterCodeLensProvider() + { + _symbolProvider = new PesterDocumentSymbolProvider(); + } + + /// + /// Get the Pester CodeLenses for a given Pester symbol. + /// + /// The Pester symbol to get CodeLenses for. + /// The script file the Pester symbol comes from. + /// All CodeLenses for the given Pester symbol. + private CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, ScriptFile scriptFile) + { + + var codeLensResults = new CodeLens[] + { + new CodeLens() + { + Range = pesterSymbol.ScriptRegion.ToRange(), + Data = JToken.FromObject(new { + Uri = scriptFile.DocumentUri, + ProviderId = nameof(PesterCodeLensProvider) + }), + Command = new Command() + { + Name = "PowerShell.RunPesterTests", + Title = "Run tests", + Arguments = JArray.FromObject(new object[] { + scriptFile.DocumentUri, + false /* No debug */, + pesterSymbol.TestName, + pesterSymbol.ScriptRegion?.StartLineNumber }) + } + }, + + new CodeLens() + { + Range = pesterSymbol.ScriptRegion.ToRange(), + Data = JToken.FromObject(new { + Uri = scriptFile.DocumentUri, + ProviderId = nameof(PesterCodeLensProvider) + }), + Command = new Command() + { + Name = "PowerShell.RunPesterTests", + Title = "Debug tests", + Arguments = JArray.FromObject(new object[] { + scriptFile.DocumentUri, + true /* No debug */, + pesterSymbol.TestName, + pesterSymbol.ScriptRegion?.StartLineNumber }) + } + } + }; + + return codeLensResults; + } + + /// + /// Get all Pester CodeLenses for a given script file. + /// + /// The script file to get Pester CodeLenses for. + /// All Pester CodeLenses for the given script file. + public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) + { + var lenses = new List(); + foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile)) + { + if (symbol is PesterSymbolReference pesterSymbol) + { + if (pesterSymbol.Command != PesterCommandType.Describe) + { + continue; + } + + lenses.AddRange(GetPesterLens(pesterSymbol, scriptFile)); + } + } + + return lenses.ToArray(); + } + + /// + /// Resolve the CodeLens provision asynchronously -- just wraps the CodeLens argument in a task. + /// + /// The code lens to resolve. + /// The script file. + /// The given CodeLens, wrapped in a task. + public CodeLens ResolveCodeLens(CodeLens codeLens, ScriptFile scriptFile) + { + // This provider has no specific behavior for + // resolving CodeLenses. + return codeLens; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs new file mode 100644 index 000000000..5e1c16581 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/CodeLens/ReferencesCodeLensProvider.cs @@ -0,0 +1,179 @@ +// +// 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.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Symbols; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using PowerShellEditorServices.Engine.Utility; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + /// + /// Provides the "reference" code lens by extracting document symbols. + /// + internal class ReferencesCodeLensProvider : ICodeLensProvider + { + private static readonly Location[] s_emptyLocationArray = new Location[0]; + + /// + /// The document symbol provider to supply symbols to generate the code lenses. + /// + private IDocumentSymbolProvider _symbolProvider; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + + /// + /// Specifies a unique identifier for the feature provider, typically a + /// fully-qualified name like "Microsoft.PowerShell.EditorServices.MyProvider" + /// + public string ProviderId => nameof(ReferencesCodeLensProvider); + + /// + /// Construct a new ReferencesCodeLensProvider for a given EditorSession. + /// + /// + /// + public ReferencesCodeLensProvider(WorkspaceService workspaceService, SymbolsService symbolsService) + { + _workspaceService = workspaceService; + _symbolsService = symbolsService; + // TODO: Pull this from components + _symbolProvider = new ScriptDocumentSymbolProvider( + VersionUtils.PSVersion); + } + + /// + /// Get all reference code lenses for a given script file. + /// + /// The PowerShell script file to get code lenses for. + /// An array of CodeLenses describing all functions in the given script file. + public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) + { + var acc = new List(); + foreach (SymbolReference sym in _symbolProvider.ProvideDocumentSymbols(scriptFile)) + { + if (sym.SymbolType == SymbolType.Function) + { + //acc.Add(new CodeLens(this, scriptFile, sym.ScriptRegion)); + acc.Add(new CodeLens() + { + Data = JToken.FromObject(new + { + Uri = scriptFile.DocumentUri, + ProviderId = nameof(ReferencesCodeLensProvider) + }), + Range = sym.ScriptRegion.ToRange() + }); + } + } + + return acc.ToArray(); + } + + /// + /// Take a codelens and create a new codelens object with updated references. + /// + /// The old code lens to get updated references for. + /// A new code lens object describing the same data as the old one but with updated references. + public CodeLens ResolveCodeLens(CodeLens codeLens, ScriptFile scriptFile) + { + + ScriptFile[] references = _workspaceService.ExpandScriptReferences( + scriptFile); + + SymbolReference foundSymbol = _symbolsService.FindFunctionDefinitionAtLocation( + scriptFile, + (int)codeLens.Range.Start.Line + 1, + (int)codeLens.Range.Start.Character + 1); + + List referencesResult = _symbolsService.FindReferencesOfSymbol( + foundSymbol, + references, + _workspaceService); + + Location[] referenceLocations; + if (referencesResult == null) + { + referenceLocations = s_emptyLocationArray; + } + else + { + var acc = new List(); + foreach (SymbolReference foundReference in referencesResult) + { + if (!NotReferenceDefinition(foundSymbol, foundReference)) + { + continue; + } + + acc.Add(new Location + { + Uri = PathUtils.ToUri(foundReference.FilePath), + Range = foundReference.ScriptRegion.ToRange() + }); + } + referenceLocations = acc.ToArray(); + } + + return new CodeLens() + { + Data = codeLens.Data, + Range = codeLens.Range, + Command = new Command + { + Name = "editor.action.showReferences", + Title = GetReferenceCountHeader(referenceLocations.Length), + Arguments = JArray.FromObject(new object[] + { + scriptFile.DocumentUri, + codeLens.Range.Start, + referenceLocations + }) + } + }; + } + + /// + /// Check whether a SymbolReference is not a reference to another defined symbol. + /// + /// The symbol definition that may be referenced. + /// The reference symbol to check. + /// True if the reference is not a reference to the definition, false otherwise. + private static bool NotReferenceDefinition( + SymbolReference definition, + SymbolReference reference) + { + return + definition.ScriptRegion.StartLineNumber != reference.ScriptRegion.StartLineNumber + || definition.SymbolType != reference.SymbolType + || !string.Equals(definition.SymbolName, reference.SymbolName, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Get the code lens header for the number of references on a definition, + /// given the number of references. + /// + /// The number of references found for a given definition. + /// The header string for the reference code lens. + private static string GetReferenceCountHeader(int referenceCount) + { + if (referenceCount == 1) + { + return "1 reference"; + } + + var sb = new StringBuilder(14); // "100 references".Length = 14 + sb.Append(referenceCount); + sb.Append(" references"); + return sb.ToString(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 6e9726f2c..92508adb2 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -35,10 +35,7 @@ public class SymbolsService /// Constructs an instance of the SymbolsService class and uses /// the given Runspace to execute language service operations. /// - /// - /// The PowerShellContext in which language service operations will be executed. - /// - /// An ILogger implementation used for writing log messages. + /// An ILoggerFactory implementation used for writing log messages. public SymbolsService( ILoggerFactory factory) { @@ -208,5 +205,33 @@ public IReadOnlyList FindOccurrencesInFile( foundSymbol, needsAliases: false).ToArray(); } + + /// Finds a function definition 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 + /// A SymbolReference of the symbol found at the given location + /// or null if there is no symbol at that location + /// + public SymbolReference FindFunctionDefinitionAtLocation( + ScriptFile scriptFile, + int lineNumber, + int columnNumber) + { + SymbolReference symbolReference = + AstOperations.FindSymbolAtPosition( + scriptFile.ScriptAst, + lineNumber, + columnNumber, + includeFunctionDefinitions: true); + + if (symbolReference != null) + { + symbolReference.FilePath = scriptFile.FilePath; + } + + return symbolReference; + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs new file mode 100644 index 000000000..463fbf767 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CodeLensHandlers.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.CodeLenses; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class CodeLensHandlers : ICodeLensHandler, ICodeLensResolveHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + + private readonly ICodeLensProvider[] _providers; + + private CodeLensCapability _capability; + + public CodeLensHandlers(ILoggerFactory factory, SymbolsService symbolsService, WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _workspaceService = workspaceService; + _symbolsService = symbolsService; + _providers = new ICodeLensProvider[] + { + new ReferencesCodeLensProvider(_workspaceService, _symbolsService), + new PesterCodeLensProvider() + }; + } + + CodeLensRegistrationOptions IRegistration.GetRegistrationOptions() + { + return new CodeLensRegistrationOptions() + { + DocumentSelector = _documentSelector, + ResolveProvider = true + }; + } + + public Task Handle(CodeLensParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + CodeLens[] codeLensResults = ProvideCodeLenses(scriptFile); + + return Task.FromResult(new CodeLensContainer(codeLensResults)); + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions() + { + DocumentSelector = _documentSelector, + }; + } + + public bool CanResolve(CodeLens value) + { + CodeLensData codeLensData = value.Data.ToObject(); + return value?.Data != null && _providers.Any(provider => provider.ProviderId.Equals(codeLensData.ProviderId)); + } + + public Task Handle(CodeLens request, CancellationToken cancellationToken) + { + // TODO: Catch deserializtion exception on bad object + CodeLensData codeLensData = request.Data.ToObject(); + + ICodeLensProvider originalProvider = + _providers.FirstOrDefault( + provider => provider.ProviderId.Equals(codeLensData.ProviderId)); + + ScriptFile scriptFile = + _workspaceService.GetFile( + codeLensData.Uri); + + var resolvedCodeLens = originalProvider.ResolveCodeLens(request, scriptFile); + return Task.FromResult(resolvedCodeLens); + } + + public void SetCapability(CodeLensCapability capability) + { + _capability = capability; + } + + /// + /// Get all the CodeLenses for a given script file. + /// + /// The PowerShell script file to get CodeLenses for. + /// All generated CodeLenses for the given script file. + private CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) + { + return InvokeProviders(provider => provider.ProvideCodeLenses(scriptFile)) + .SelectMany(codeLens => codeLens) + .ToArray(); + } + + /// + /// Invokes the given function synchronously against all + /// registered providers. + /// + /// The function to be invoked. + /// + /// An IEnumerable containing the results of all providers + /// that were invoked successfully. + /// + private IEnumerable InvokeProviders( + Func invokeFunc) + { + Stopwatch invokeTimer = new Stopwatch(); + List providerResults = new List(); + + foreach (var provider in this._providers) + { + try + { + invokeTimer.Restart(); + + providerResults.Add(invokeFunc(provider)); + + invokeTimer.Stop(); + + this._logger.LogTrace( + $"Invocation of provider '{provider.GetType().Name}' completed in {invokeTimer.ElapsedMilliseconds}ms."); + } + catch (Exception e) + { + this._logger.LogException( + $"Exception caught while invoking provider {provider.GetType().Name}:", + e); + } + } + + return providerResults; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs index a40814bba..d91ec865e 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptRegion.cs @@ -131,26 +131,5 @@ public ScriptRegion( IScriptPosition IScriptExtent.EndScriptPosition => throw new NotImplementedException(); #endregion - - #region Methods - - public Range ToRange() - { - return new Range - { - Start = new Position - { - Line = StartLineNumber - 1, - Character = StartColumnNumber - 1 - }, - End = new Position - { - Line = EndLineNumber - 1, - Character = EndColumnNumber - 1 - } - }; - } - - #endregion } } diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 8aaa7511a..78d8de1a7 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -60,10 +60,14 @@ function New-TestFile param( [Parameter(Mandatory)] [string] - $Script + $Script, + + [Parameter()] + [string] + $FileName = "$([System.IO.Path]::GetRandomFileName()).ps1" ) - $file = Set-Content -Path (Join-Path $TestDrive "$([System.IO.Path]::GetRandomFileName()).ps1") -Value $Script -PassThru -Force + $file = Set-Content -Path (Join-Path $TestDrive $FileName) -Value $Script -PassThru -Force $request = Send-LspDidOpenTextDocumentRequest -Client $client ` -Uri ([Uri]::new($file.PSPath).AbsoluteUri) ` @@ -327,6 +331,60 @@ Write-Host 'Goodbye' $response.Result[1].Range.End.Character | Should -BeExactly 10 } + It "Can handle a textDocument/codeLens Pester request" { + $filePath = New-TestFile -FileName ("$([System.IO.Path]::GetRandomFileName()).Tests.ps1") -Script ' +Describe "DescribeName" { + Context "ContextName" { + It "ItName" { + 1 | Should -Be 1 + } + } +} +' + + $request = Send-LspCodeLensRequest -Client $client ` + -Uri ([Uri]::new($filePath).AbsoluteUri) + + $response = Get-LspResponse -Client $client -Id $request.Id + $response.Result.Count | Should -BeExactly 2 + + # Both commands will have the same values for these so we can check them like so. + $response.Result.range.start.line | Should -Be @(1, 1) + $response.Result.range.start.character | Should -Be @(0, 0) + $response.Result.range.end.line | Should -Be @(7, 7) + $response.Result.range.end.character | Should -Be @(1, 1) + + $response.Result.command.title[0] | Should -Be "Run tests" + $response.Result.command.title[1] | Should -Be "Debug tests" + } + + It "Can handle a textDocument/codeLens and codeLens/resolve References request" { + $filePath = New-TestFile -Script ' +function Get-Foo { + +} + +Get-Foo +' + + $request = Send-LspCodeLensRequest -Client $client ` + -Uri ([Uri]::new($filePath).AbsoluteUri) + + $response = Get-LspResponse -Client $client -Id $request.Id + $response.Result.Count | Should -BeExactly 1 + $response.Result.data.data.ProviderId | Should -Be ReferencesCodeLensProvider + $response.Result.range.start.line | Should -BeExactly 1 + $response.Result.range.start.character | Should -BeExactly 0 + $response.Result.range.end.line | Should -BeExactly 3 + $response.Result.range.end.character | Should -BeExactly 1 + + $request = Send-LspCodeLensResolveRequest -Client $client -CodeLens $response.Result[0] + $response = Get-LspResponse -Client $client -Id $request.Id + + $response.Result.command.title | Should -Be '1 reference' + $response.Result.command.command | Should -Be 'editor.action.showReferences' + } + # 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 6b9c0987a..822ef602d 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -81,6 +81,8 @@ FunctionsToExport = @( 'Send-LspDocumentSymbolRequest', 'Send-LspDocumentHighlightRequest', 'Send-LspReferencesRequest', + 'Send-LspCodeLensRequest', + 'Send-LspCodeLensResolveRequest', 'Send-LspShutdownRequest', 'Get-LspNotification', 'Get-LspResponse' diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index d15582778..059b1ee17 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -487,6 +487,58 @@ function Send-LspDocumentHighlightRequest return Send-LspRequest -Client $Client -Method 'textDocument/documentHighlight' -Parameters $documentHighlightParams } +function Send-LspCodeLensRequest +{ + [OutputType([PsesPsClient.LspRequest])] + param( + [Parameter(Position = 0, Mandatory)] + [PsesPsClient.PsesLspClient] + $Client, + + [Parameter(Mandatory)] + [string] + $Uri + ) + + $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CodeLensRequest]@{ + TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ + Uri = $Uri + } + } + return Send-LspRequest -Client $Client -Method 'textDocument/codeLens' -Parameters $params +} + +function Send-LspCodeLensResolveRequest +{ + [OutputType([PsesPsClient.LspRequest])] + param( + [Parameter(Position = 0, Mandatory)] + [PsesPsClient.PsesLspClient] + $Client, + + [Parameter(Mandatory)] + # Expects to be passed in a single item from the `Result` collection from + # Send-LspCodeLensRequest + $CodeLens + ) + + $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.CodeLens]@{ + Data = [Newtonsoft.Json.Linq.JToken]::Parse(($CodeLens.data.data | ConvertTo-Json)) + Range = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Range]@{ + Start = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ + Line = $CodeLens.range.start.line + Character = $CodeLens.range.start.character + } + End = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ + Line = $CodeLens.range.end.line + Character = $CodeLens.range.end.character + } + } + } + + return Send-LspRequest -Client $Client -Method 'codeLens/resolve' -Parameters $params +} + function Send-LspShutdownRequest { [OutputType([PsesPsClient.LspRequest])]