diff --git a/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs b/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs index 4392523ac..ac5f388a3 100644 --- a/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs +++ b/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs @@ -9,6 +9,7 @@ using Microsoft.PowerShell.EditorServices.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -17,33 +18,23 @@ namespace Microsoft.PowerShell.EditorServices.CodeLenses { + /// + /// Implements the CodeLens feature for EditorServices. + /// internal class CodeLensFeature : FeatureComponentBase, ICodeLenses { - private EditorSession editorSession; - - private JsonSerializer jsonSerializer = - JsonSerializer.Create( - Constants.JsonSerializerSettings); - - public CodeLensFeature( - EditorSession editorSession, - IMessageHandlers messageHandlers, - ILogger logger) - : base(logger) - { - this.editorSession = editorSession; - - messageHandlers.SetRequestHandler( - CodeLensRequest.Type, - this.HandleCodeLensRequest); - - messageHandlers.SetRequestHandler( - CodeLensResolveRequest.Type, - this.HandleCodeLensResolveRequest); - } + /// + /// Create a new CodeLens instance around a given editor session + /// from the component registry. + /// + /// + /// The component registry to provider other components and to register the CodeLens provider in. + /// + /// The editor session context of the CodeLens provider. + /// A new CodeLens provider for the given editor session. public static CodeLensFeature Create( IComponentRegistry components, EditorSession editorSession) @@ -51,9 +42,19 @@ public static CodeLensFeature Create( var codeLenses = new CodeLensFeature( editorSession, - components.Get(), + JsonSerializer.Create(Constants.JsonSerializerSettings), components.Get()); + var messageHandlers = components.Get(); + + messageHandlers.SetRequestHandler( + CodeLensRequest.Type, + codeLenses.HandleCodeLensRequest); + + messageHandlers.SetRequestHandler( + CodeLensResolveRequest.Type, + codeLenses.HandleCodeLensResolveRequest); + codeLenses.Providers.Add( new ReferencesCodeLensProvider( editorSession)); @@ -67,42 +68,78 @@ public static CodeLensFeature Create( return codeLenses; } + /// + /// The editor session context to get workspace and language server data from. + /// + private readonly EditorSession _editorSession; + + /// + /// The json serializer instance for CodeLens object translation. + /// + private readonly JsonSerializer _jsonSerializer; + + /// + /// + /// + /// + /// + /// + private CodeLensFeature( + EditorSession editorSession, + JsonSerializer jsonSerializer, + ILogger logger) + : base(logger) + { + _editorSession = editorSession; + _jsonSerializer = jsonSerializer; + } + + /// + /// 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. public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) { - return - this.InvokeProviders(p => p.ProvideCodeLenses(scriptFile)) - .SelectMany(r => r) - .ToArray(); + return InvokeProviders(provider => provider.ProvideCodeLenses(scriptFile)) + .SelectMany(codeLens => codeLens) + .ToArray(); } + /// + /// Handles a request for CodeLenses from VSCode. + /// + /// Parameters on the CodeLens request that was received. + /// private async Task HandleCodeLensRequest( CodeLensRequest codeLensParams, RequestContext requestContext) { - JsonSerializer jsonSerializer = - JsonSerializer.Create( - Constants.JsonSerializerSettings); + ScriptFile scriptFile = _editorSession.Workspace.GetFile( + codeLensParams.TextDocument.Uri); - var scriptFile = - this.editorSession.Workspace.GetFile( - codeLensParams.TextDocument.Uri); + CodeLens[] codeLensResults = ProvideCodeLenses(scriptFile); - var codeLenses = - this.ProvideCodeLenses(scriptFile) - .Select( - codeLens => - codeLens.ToProtocolCodeLens( - new CodeLensData - { - Uri = codeLens.File.ClientFilePath, - ProviderId = codeLens.Provider.ProviderId - }, - this.jsonSerializer)) - .ToArray(); - - await requestContext.SendResult(codeLenses); + var codeLensResponse = new LanguageServer.CodeLens[codeLensResults.Length]; + for (int i = 0; i < codeLensResults.Length; i++) + { + codeLensResponse[i] = codeLensResults[i].ToProtocolCodeLens( + new CodeLensData + { + Uri = codeLensResults[i].File.ClientFilePath, + ProviderId = codeLensResults[i].Provider.ProviderId + }, + _jsonSerializer); + } + + await requestContext.SendResult(codeLensResponse); } + /// + /// Handle a CodeLens resolve request from VSCode. + /// + /// The CodeLens to be resolved/updated. + /// private async Task HandleCodeLensResolveRequest( LanguageServer.CodeLens codeLens, RequestContext requestContext) @@ -113,13 +150,13 @@ private async Task HandleCodeLensResolveRequest( CodeLensData codeLensData = codeLens.Data.ToObject(); ICodeLensProvider originalProvider = - this.Providers.FirstOrDefault( + Providers.FirstOrDefault( provider => provider.ProviderId.Equals(codeLensData.ProviderId)); if (originalProvider != null) { ScriptFile scriptFile = - this.editorSession.Workspace.GetFile( + _editorSession.Workspace.GetFile( codeLensData.Uri); ScriptRegion region = new ScriptRegion @@ -143,7 +180,7 @@ await originalProvider.ResolveCodeLensAsync( await requestContext.SendResult( resolvedCodeLens.ToProtocolCodeLens( - this.jsonSerializer)); + _jsonSerializer)); } else { @@ -153,6 +190,9 @@ await requestContext.SendError( } } + /// + /// Represents data expected back in an LSP CodeLens response. + /// private class CodeLensData { public string Uri { get; set; } diff --git a/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs b/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs index 61d700bb9..619ca0a49 100644 --- a/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs +++ b/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs @@ -15,71 +15,90 @@ namespace Microsoft.PowerShell.EditorServices.CodeLenses { internal class PesterCodeLensProvider : FeatureProviderBase, ICodeLensProvider { - private static char[] QuoteChars = new char[] { '\'', '"'}; + /// + /// The editor session context to provide CodeLenses for. + /// + private EditorSession _editorSession; - private EditorSession editorSession; - private IDocumentSymbolProvider symbolProvider; + /// + /// The symbol provider to get symbols from to build code lenses with. + /// + private IDocumentSymbolProvider _symbolProvider; + /// + /// Create a new Pester CodeLens provider for a given editor session. + /// + /// The editor session context for which to provide Pester CodeLenses. public PesterCodeLensProvider(EditorSession editorSession) { - this.editorSession = editorSession; - this.symbolProvider = new PesterDocumentSymbolProvider(); + _editorSession = editorSession; + _symbolProvider = new PesterDocumentSymbolProvider(); } - private IEnumerable GetPesterLens( + /// + /// 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 clientCommands = new ClientCommand[] + var codeLensResults = new CodeLens[] { - new ClientCommand( - "PowerShell.RunPesterTests", - "Run tests", - new object[] - { - scriptFile.ClientFilePath, - false, // Don't debug - pesterSymbol.TestName, - }), + new CodeLens( + this, + scriptFile, + pesterSymbol.ScriptRegion, + new ClientCommand( + "PowerShell.RunPesterTests", + "Run tests", + new object[] { scriptFile.ClientFilePath, false /* No debug */, pesterSymbol.TestName })), - new ClientCommand( - "PowerShell.RunPesterTests", - "Debug tests", - new object[] - { - scriptFile.ClientFilePath, - true, // Run in debugger - pesterSymbol.TestName, - }), + new CodeLens( + this, + scriptFile, + pesterSymbol.ScriptRegion, + new ClientCommand( + "PowerShell.RunPesterTests", + "Debug tests", + new object[] { scriptFile.ClientFilePath, true /* Run in debugger */, pesterSymbol.TestName })), }; - return - clientCommands.Select( - command => - new CodeLens( - this, - scriptFile, - pesterSymbol.ScriptRegion, - command)); + 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 symbols = - this.symbolProvider - .ProvideDocumentSymbols(scriptFile); + var lenses = new List(); + foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile)) + { + if (symbol is PesterSymbolReference pesterSymbol) + { + if (pesterSymbol.Command != PesterCommandType.Describe) + { + continue; + } - var lenses = - symbols - .OfType() - .Where(s => s.Command == PesterCommandType.Describe) - .SelectMany(s => this.GetPesterLens(s, scriptFile)) - .Where(codeLens => codeLens != null) - .ToArray(); + lenses.AddRange(GetPesterLens(pesterSymbol, scriptFile)); + } + } - return lenses; + return lenses.ToArray(); } + /// + /// Resolve the CodeLens provision asynchronously -- just wraps the CodeLens argument in a task. + /// + /// The code lens to resolve. + /// + /// The given CodeLens, wrapped in a task. public Task ResolveCodeLensAsync( CodeLens codeLens, CancellationToken cancellationToken) diff --git a/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs index abe29a997..c833aec27 100644 --- a/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs +++ b/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Commands; @@ -14,87 +15,122 @@ namespace Microsoft.PowerShell.EditorServices.CodeLenses { + /// + /// Provides the "reference" code lens by extracting document symbols. + /// internal class ReferencesCodeLensProvider : FeatureProviderBase, ICodeLensProvider { - private EditorSession editorSession; - private IDocumentSymbolProvider symbolProvider; + private static readonly Location[] s_emptyLocationArray = new Location[0]; + /// + /// The editor session code lenses are being provided from. + /// + private EditorSession _editorSession; + + /// + /// The document symbol provider to supply symbols to generate the code lenses. + /// + private IDocumentSymbolProvider _symbolProvider; + + /// + /// Construct a new ReferencesCodeLensProvider for a given EditorSession. + /// + /// public ReferencesCodeLensProvider(EditorSession editorSession) { - this.editorSession = editorSession; + _editorSession = editorSession; // TODO: Pull this from components - this.symbolProvider = - new ScriptDocumentSymbolProvider( - editorSession.PowerShellContext.LocalPowerShellVersion.Version); + _symbolProvider = new ScriptDocumentSymbolProvider( + editorSession.PowerShellContext.LocalPowerShellVersion.Version); } + /// + /// 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) { - return - this.symbolProvider - .ProvideDocumentSymbols(scriptFile) - .Where(symbol => symbol.SymbolType == SymbolType.Function) - .Select( - symbol => - new CodeLens( - this, - scriptFile, - symbol.ScriptRegion)) - .ToArray(); + var acc = new List(); + foreach (SymbolReference sym in _symbolProvider.ProvideDocumentSymbols(scriptFile)) + { + if (sym.SymbolType == SymbolType.Function) + { + acc.Add(new CodeLens(this, scriptFile, sym.ScriptRegion)); + } + } + + return acc.ToArray(); } + /// + /// Take a codelens and create a new codelens object with updated references. + /// + /// The old code lens to get updated references for. + /// The cancellation token for this request. + /// A new code lens object describing the same data as the old one but with updated references. public async Task ResolveCodeLensAsync( CodeLens codeLens, CancellationToken cancellationToken) { - ScriptFile[] references = - editorSession.Workspace.ExpandScriptReferences( - codeLens.File); - - var foundSymbol = - this.editorSession.LanguageService.FindFunctionDefinitionAtLocation( - codeLens.File, - codeLens.ScriptExtent.StartLineNumber, - codeLens.ScriptExtent.StartColumnNumber); - - FindReferencesResult referencesResult = - await editorSession.LanguageService.FindReferencesOfSymbol( - foundSymbol, - references, - editorSession.Workspace); - - Location[] referenceLocations = - referencesResult == null - ? new Location[0] - : referencesResult - .FoundReferences - .Where(r => NotReferenceDefinition(foundSymbol, r)) - .Select( - r => new Location - { - Uri = GetFileUri(r.FilePath), - Range = r.ScriptRegion.ToRange() - }) - .ToArray(); + ScriptFile[] references = _editorSession.Workspace.ExpandScriptReferences( + codeLens.File); - return - new CodeLens( - codeLens, - new ClientCommand( - "editor.action.showReferences", - referenceLocations.Length == 1 - ? "1 reference" - : $"{referenceLocations.Length} references", - new object[] - { - codeLens.File.ClientFilePath, - codeLens.ScriptExtent.ToRange().Start, - referenceLocations, - } + SymbolReference foundSymbol = _editorSession.LanguageService.FindFunctionDefinitionAtLocation( + codeLens.File, + codeLens.ScriptExtent.StartLineNumber, + codeLens.ScriptExtent.StartColumnNumber); + + FindReferencesResult referencesResult = await _editorSession.LanguageService.FindReferencesOfSymbol( + foundSymbol, + references, + _editorSession.Workspace); + + Location[] referenceLocations; + if (referencesResult == null) + { + referenceLocations = s_emptyLocationArray; + } + else + { + var acc = new List(); + foreach (SymbolReference foundReference in referencesResult.FoundReferences) + { + if (!NotReferenceDefinition(foundSymbol, foundReference)) + { + continue; + } + + acc.Add(new Location + { + Uri = GetFileUri(foundReference.FilePath), + Range = foundReference.ScriptRegion.ToRange() + }); + } + referenceLocations = acc.ToArray(); + } + + return new CodeLens( + codeLens, + new ClientCommand( + "editor.action.showReferences", + GetReferenceCountHeader(referenceLocations.Length), + new object[] + { + codeLens.File.ClientFilePath, + codeLens.ScriptExtent.ToRange().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) @@ -105,6 +141,11 @@ private static bool NotReferenceDefinition( || !string.Equals(definition.SymbolName, reference.SymbolName, StringComparison.OrdinalIgnoreCase); } + /// + /// Get a URI for a given file path. + /// + /// A file path that may be prefixed with URI scheme already. + /// A URI to the file. private static string GetFileUri(string filePath) { // If the file isn't untitled, return a URI-style path @@ -113,5 +154,24 @@ private static string GetFileUri(string filePath) ? new Uri("file://" + filePath).AbsoluteUri : filePath; } + + /// + /// 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(); + } } }