diff --git a/src/PowerShellEditorServices.Host/CodeLens/CodeLensExtensions.cs b/src/PowerShellEditorServices.Host/CodeLens/CodeLensExtensions.cs new file mode 100644 index 000000000..c3be72bfe --- /dev/null +++ b/src/PowerShellEditorServices.Host/CodeLens/CodeLensExtensions.cs @@ -0,0 +1,47 @@ +// +// 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 Microsoft.PowerShell.EditorServices.CodeLenses; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using LanguageServer = Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; + +namespace Microsoft.PowerShell.EditorServices +{ + public static class ICodeLensExtensions + { + public static LanguageServer.CodeLens ToProtocolCodeLens( + this CodeLens codeLens, + JsonSerializer jsonSerializer) + { + return new LanguageServer.CodeLens + { + Range = codeLens.ScriptExtent.ToRange(), + Command = codeLens.Command.ToProtocolCommand(jsonSerializer) + }; + } + + public static LanguageServer.CodeLens ToProtocolCodeLens( + this CodeLens codeLens, + object codeLensData, + JsonSerializer jsonSerializer) + { + LanguageServer.ServerCommand command = null; + + if (codeLens.Command != null) + { + command = codeLens.Command.ToProtocolCommand(jsonSerializer); + } + + return new LanguageServer.CodeLens + { + Range = codeLens.ScriptExtent.ToRange(), + Data = JToken.FromObject(codeLensData, jsonSerializer), + Command = command + }; + } + } +} diff --git a/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs b/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs new file mode 100644 index 000000000..9f6008694 --- /dev/null +++ b/src/PowerShellEditorServices.Host/CodeLens/CodeLensFeature.cs @@ -0,0 +1,159 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Components; +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using LanguageServer = Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + internal class CodeLensFeature : ICodeLenses + { + private EditorSession editorSession; + + private JsonSerializer jsonSerializer = + JsonSerializer.Create( + Constants.JsonSerializerSettings); + + public IFeatureProviderCollection Providers { get; } = + new FeatureProviderCollection(); + + public CodeLensFeature( + EditorSession editorSession, + IMessageHandlers messageHandlers, + ILogger logger) + { + this.editorSession = editorSession; + + messageHandlers.SetRequestHandler( + CodeLensRequest.Type, + this.HandleCodeLensRequest); + + messageHandlers.SetRequestHandler( + CodeLensResolveRequest.Type, + this.HandleCodeLensResolveRequest); + } + + public static CodeLensFeature Create( + IComponentRegistry components, + EditorSession editorSession) + { + var codeLenses = + new CodeLensFeature( + editorSession, + components.Get(), + components.Get()); + + codeLenses.Providers.Add( + new ReferencesCodeLensProvider( + editorSession)); + + + editorSession.Components.Register(codeLenses); + + return codeLenses; + } + + public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) + { + return + this.Providers + .SelectMany(p => p.ProvideCodeLenses(scriptFile)) + .ToArray(); + } + + private async Task HandleCodeLensRequest( + CodeLensRequest codeLensParams, + RequestContext requestContext) + { + JsonSerializer jsonSerializer = + JsonSerializer.Create( + Constants.JsonSerializerSettings); + + var scriptFile = + this.editorSession.Workspace.GetFile( + codeLensParams.TextDocument.Uri); + + 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); + } + + private async Task HandleCodeLensResolveRequest( + LanguageServer.CodeLens codeLens, + RequestContext requestContext) + { + if (codeLens.Data != null) + { + // TODO: Catch deserializtion exception on bad object + CodeLensData codeLensData = codeLens.Data.ToObject(); + + ICodeLensProvider originalProvider = + this.Providers.FirstOrDefault( + provider => provider.ProviderId.Equals(codeLensData.ProviderId)); + + if (originalProvider != null) + { + ScriptFile scriptFile = + this.editorSession.Workspace.GetFile( + codeLensData.Uri); + + ScriptRegion region = new ScriptRegion + { + StartLineNumber = codeLens.Range.Start.Line + 1, + StartColumnNumber = codeLens.Range.Start.Character + 1, + EndLineNumber = codeLens.Range.End.Line + 1, + EndColumnNumber = codeLens.Range.End.Character + 1 + }; + + CodeLens originalCodeLens = + new CodeLens( + originalProvider, + scriptFile, + region); + + var resolvedCodeLens = + await originalProvider.ResolveCodeLensAsync( + originalCodeLens, + CancellationToken.None); + + await requestContext.SendResult( + resolvedCodeLens.ToProtocolCodeLens( + this.jsonSerializer)); + } + else + { + // TODO: Write error! + } + } + } + + private class CodeLensData + { + public string Uri { get; set; } + + public string ProviderId {get; set; } + } + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Host/CodeLens/IScriptExtentExtensions.cs b/src/PowerShellEditorServices.Host/CodeLens/IScriptExtentExtensions.cs new file mode 100644 index 000000000..ca8f49d54 --- /dev/null +++ b/src/PowerShellEditorServices.Host/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 Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; + +namespace Microsoft.PowerShell.EditorServices +{ + public 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.Host/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs new file mode 100644 index 000000000..3d1efc79d --- /dev/null +++ b/src/PowerShellEditorServices.Host/CodeLens/ReferencesCodeLensProvider.cs @@ -0,0 +1,104 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Commands; +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Symbols; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + internal class ReferencesCodeLensProvider : FeatureProviderBase, ICodeLensProvider + { + private EditorSession editorSession; + private IDocumentSymbolProvider symbolProvider; + + public ReferencesCodeLensProvider(EditorSession editorSession) + { + this.editorSession = editorSession; + + // TODO: Pull this from components + this.symbolProvider = + new ScriptDocumentSymbolProvider( + editorSession.PowerShellContext.LocalPowerShellVersion.Version); + } + + 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(); + } + + 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); + + var referenceLocations = + referencesResult + .FoundReferences + .Select( + r => new Location + { + Uri = GetFileUri(r.FilePath), + Range = r.ScriptRegion.ToRange() + }) + .ToArray(); + + 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, + } + )); + } + + private static string GetFileUri(string filePath) + { + // If the file isn't untitled, return a URI-style path + return + !filePath.StartsWith("untitled") + ? new Uri("file://" + filePath).AbsoluteUri + : filePath; + } + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Host/Commands/ClientCommandExtensions.cs b/src/PowerShellEditorServices.Host/Commands/ClientCommandExtensions.cs new file mode 100644 index 000000000..95594d9b6 --- /dev/null +++ b/src/PowerShellEditorServices.Host/Commands/ClientCommandExtensions.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Commands; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using LanguageServer = Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; + +namespace Microsoft.PowerShell.EditorServices +{ + public static class ClientCommandExtensions + { + public static LanguageServer.ServerCommand ToProtocolCommand( + this ClientCommand clientCommand, + JsonSerializer jsonSerializer) + { + return new LanguageServer.ServerCommand + { + Command = clientCommand.Name, + Title = clientCommand.Title, + Arguments = + JArray.FromObject( + clientCommand.Arguments, + jsonSerializer) + }; + } + } +} diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 182166732..8c9c7f9a2 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -3,11 +3,13 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.CodeLenses; using Microsoft.PowerShell.EditorServices.Extensions; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; using Microsoft.PowerShell.EditorServices.Protocol.Server; using Microsoft.PowerShell.EditorServices.Session; +using Microsoft.PowerShell.EditorServices.Symbols; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; @@ -346,6 +348,15 @@ private EditorSession CreateSession( editorSession.StartSession(powerShellContext, hostUserInterface); + // TODO: Move component registrations elsewhere! + editorSession.Components.Register(this.logger); + editorSession.Components.Register(messageHandlers); + editorSession.Components.Register(messageSender); + editorSession.Components.Register(powerShellContext); + + CodeLensFeature.Create(editorSession.Components, editorSession); + DocumentSymbolFeature.Create(editorSession.Components, editorSession); + return editorSession; } diff --git a/src/PowerShellEditorServices.Host/Symbols/DocumentSymbolFeature.cs b/src/PowerShellEditorServices.Host/Symbols/DocumentSymbolFeature.cs new file mode 100644 index 000000000..5f2e9ba9d --- /dev/null +++ b/src/PowerShellEditorServices.Host/Symbols/DocumentSymbolFeature.cs @@ -0,0 +1,114 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Components; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using Servers = Microsoft.PowerShell.EditorServices.Protocol.Server; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + internal class DocumentSymbolFeature : IDocumentSymbols + { + private EditorSession editorSession; + + public IFeatureProviderCollection Providers { get; } = + new FeatureProviderCollection(); + + public DocumentSymbolFeature( + EditorSession editorSession, + IMessageHandlers messageHandlers, + ILogger logger) + { + this.editorSession = editorSession; + + messageHandlers.SetRequestHandler( + DocumentSymbolRequest.Type, + this.HandleDocumentSymbolRequest); + } + + public static DocumentSymbolFeature Create( + IComponentRegistry components, + EditorSession editorSession) + { + var documentSymbols = + new DocumentSymbolFeature( + editorSession, + components.Get(), + components.Get()); + + documentSymbols.Providers.Add( + new ScriptDocumentSymbolProvider( + editorSession.PowerShellContext.LocalPowerShellVersion.Version)); + + documentSymbols.Providers.Add( + new PsdDocumentSymbolProvider()); + + documentSymbols.Providers.Add( + new PesterDocumentSymbolProvider()); + + editorSession.Components.Register(documentSymbols); + + return documentSymbols; + } + + public IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile) + { + throw new NotImplementedException(); + } + + protected async Task HandleDocumentSymbolRequest( + DocumentSymbolParams documentSymbolParams, + RequestContext requestContext) + { + ScriptFile scriptFile = + editorSession.Workspace.GetFile( + documentSymbolParams.TextDocument.Uri); + + IEnumerable foundSymbols = + this.Providers + .SelectMany( + provider => provider.ProvideDocumentSymbols(scriptFile)); + + SymbolInformation[] symbols = null; + + string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); + + if (foundSymbols != null) + { + symbols = + foundSymbols + .Select(r => + { + return new SymbolInformation + { + ContainerName = containerName, + Kind = Servers.LanguageServer.GetSymbolKind(r.SymbolType), + Location = new Location + { + Uri = Servers.LanguageServer.GetFileUri(r.FilePath), + Range = Servers.LanguageServer.GetRangeFromScriptRegion(r.ScriptRegion) + }, + Name = Servers.LanguageServer.GetDecoratedSymbolName(r) + }; + }) + .ToArray(); + } + else + { + symbols = new SymbolInformation[0]; + } + + await requestContext.SendResult(symbols); + } + } +} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/CodeLens.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/CodeLens.cs new file mode 100644 index 000000000..fd12f284d --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/CodeLens.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Newtonsoft.Json.Linq; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + /// + /// Code Lens options. + /// + public class CodeLensOptions + { + /// + /// Code lens has a resolve provider as well. + /// + public bool ResolveProvider { get; set; } + } + + public class CodeLens + { + public Range Range { get; set; } + + public ServerCommand Command { get; set; } + + public JToken Data { get; set; } + } + + /// + /// A code lens represents a command that should be shown along with + /// source text, like the number of references, a way to run tests, etc. + /// + /// A code lens is _unresolved_ when no command is associated to it. For performance + /// reasons the creation of a code lens and resolving should be done in two stages. + /// + public class CodeLensRequest + { + public static readonly + RequestType Type = + RequestType.Create("textDocument/codeLens"); + + /// + /// The document to request code lens for. + /// + public TextDocumentIdentifier TextDocument { get; set; } + } + + public class CodeLensResolveRequest + { + public static readonly + RequestType Type = + RequestType.Create("codeLens/resolve"); + } +} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs index 26fec4ae1..2279e64d6 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs @@ -27,7 +27,7 @@ public class ServerCapabilities public bool? CodeActionProvider { get; set; } - public bool? CodeLensProvider { get; set; } + public CodeLensOptions CodeLensProvider { get; set; } public bool? DocumentFormattingProvider { get; set; } diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCommand.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCommand.cs new file mode 100644 index 000000000..b299b5606 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCommand.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Newtonsoft.Json.Linq; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + public class ServerCommand + { + /// + /// Title of the command, like `save`. + /// + public string Title { get; set; } + + /// + /// The identifier of the actual command handler. + /// + public string Command { get; set; } + + /// + /// Arguments that the command handler should be + /// invoked with. + /// + public JArray Arguments { get; set; } + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 978a9c080..39a38dee1 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -100,7 +100,6 @@ public void Start() this.messageHandlers.SetRequestHandler(SignatureHelpRequest.Type, this.HandleSignatureHelpRequest); this.messageHandlers.SetRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequest); this.messageHandlers.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequest); - this.messageHandlers.SetRequestHandler(DocumentSymbolRequest.Type, this.HandleDocumentSymbolRequest); this.messageHandlers.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); this.messageHandlers.SetRequestHandler(CodeActionRequest.Type, this.HandleCodeActionRequest); @@ -187,6 +186,7 @@ await requestContext.SendResult( WorkspaceSymbolProvider = true, HoverProvider = true, CodeActionProvider = true, + CodeLensProvider = new CodeLensOptions { ResolveProvider = true }, CompletionProvider = new CompletionOptions { ResolveProvider = true, @@ -945,7 +945,7 @@ protected async Task HandleDocumentSymbolRequest( await requestContext.SendResult(symbols); } - private SymbolKind GetSymbolKind(SymbolType symbolType) + public static SymbolKind GetSymbolKind(SymbolType symbolType) { switch (symbolType) { @@ -959,7 +959,7 @@ private SymbolKind GetSymbolKind(SymbolType symbolType) } } - private string GetDecoratedSymbolName(SymbolReference symbolReference) + public static string GetDecoratedSymbolName(SymbolReference symbolReference) { string name = symbolReference.SymbolName; @@ -1206,7 +1206,7 @@ await this.messageSender.SendEvent( #region Helper Methods - private static string GetFileUri(string filePath) + public static string GetFileUri(string filePath) { // If the file isn't untitled, return a URI-style path return @@ -1215,7 +1215,7 @@ private static string GetFileUri(string filePath) : filePath; } - private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) + public static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) { return new Range { diff --git a/src/PowerShellEditorServices/CodeLenses/CodeLens.cs b/src/PowerShellEditorServices/CodeLenses/CodeLens.cs new file mode 100644 index 000000000..2fd251595 --- /dev/null +++ b/src/PowerShellEditorServices/CodeLenses/CodeLens.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 Microsoft.PowerShell.EditorServices.Commands; +using Microsoft.PowerShell.EditorServices.Utility; +using System.Collections.Generic; +using System.Management.Automation.Language; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + /// + /// Defines the data for a "code lens" which is displayed + /// above a symbol in a text document and has an associated + /// command. + /// + public class CodeLens + { + /// + /// Gets the ICodeLensProvider that created this CodeLens. + /// + public ICodeLensProvider Provider { get; private set; } + + /// + /// Gets the ScriptFile for which the CodeLens was created. + /// + public ScriptFile File { get; private set; } + + /// + /// Gets the IScriptExtent for the region which the CodeLens + /// pertains. + /// + public IScriptExtent ScriptExtent { get; private set; } + + /// + /// Gets the command which will be invoked in the editor + /// when the CodeLens is clicked. + /// + public ClientCommand Command { get; private set; } + + /// + /// Creates an instance of the CodeLens class. + /// + /// + /// The ICodeLensProvider which created this CodeLens. + /// + /// + /// The ScriptFile for which the CodeLens was created. + /// + /// + /// The IScriptExtent for the region which the CodeLens + /// pertains. + /// + public CodeLens( + ICodeLensProvider provider, + ScriptFile scriptFile, + IScriptExtent scriptExtent) + : this( + provider, + scriptFile, + scriptExtent, + null) + { + } + + /// + /// Creates an instance of the CodeLens class based on an + /// original CodeLens instance, generally used when resolving + /// the Command for a CodeLens. + /// + /// + /// The original CodeLens upon which this instance is based. + /// + /// + /// The resolved ClientCommand for the original CodeLens. + /// + public CodeLens( + CodeLens originalCodeLens, + ClientCommand resolvedCommand) + { + Validate.IsNotNull(nameof(originalCodeLens), originalCodeLens); + Validate.IsNotNull(nameof(resolvedCommand), resolvedCommand); + + this.Provider = originalCodeLens.Provider; + this.File = originalCodeLens.File; + this.ScriptExtent = originalCodeLens.ScriptExtent; + this.Command = resolvedCommand; + } + + /// + /// Creates an instance of the CodeLens class. + /// + /// + /// The ICodeLensProvider which created this CodeLens. + /// + /// + /// The ScriptFile for which the CodeLens was created. + /// + /// + /// The IScriptExtent for the region which the CodeLens + /// pertains. + /// + /// + /// The ClientCommand to execute when this CodeLens is clicked. + /// If null, this CodeLens will be resolved by the editor when it + /// gets displayed. + /// + public CodeLens( + ICodeLensProvider provider, + ScriptFile scriptFile, + IScriptExtent scriptExtent, + ClientCommand command) + { + Validate.IsNotNull(nameof(provider), provider); + Validate.IsNotNull(nameof(scriptFile), scriptFile); + Validate.IsNotNull(nameof(scriptExtent), scriptExtent); + + this.Provider = provider; + this.File = scriptFile; + this.ScriptExtent = scriptExtent; + this.Command = command; + } + } +} diff --git a/src/PowerShellEditorServices/CodeLenses/ICodeLensProvider.cs b/src/PowerShellEditorServices/CodeLenses/ICodeLensProvider.cs new file mode 100644 index 000000000..71a1c0243 --- /dev/null +++ b/src/PowerShellEditorServices/CodeLenses/ICodeLensProvider.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using System.Collections.Generic; +using System.Management.Automation.Language; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.CodeLenses +{ + /// + /// Specifies the contract for a Code Lens provider. + /// + public interface ICodeLensProvider : IFeatureProvider + { + /// + /// 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. + /// + Task ResolveCodeLensAsync( + CodeLens codeLens, + CancellationToken cancellationToken); + } +} diff --git a/src/PowerShellEditorServices/CodeLenses/ICodeLenses.cs b/src/PowerShellEditorServices/CodeLenses/ICodeLenses.cs new file mode 100644 index 000000000..f63319c1c --- /dev/null +++ b/src/PowerShellEditorServices/CodeLenses/ICodeLenses.cs @@ -0,0 +1,34 @@ +// +// 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; + +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. + /// + IFeatureProviderCollection 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/Commands/ClientCommand.cs b/src/PowerShellEditorServices/Commands/ClientCommand.cs new file mode 100644 index 000000000..42ee9a147 --- /dev/null +++ b/src/PowerShellEditorServices/Commands/ClientCommand.cs @@ -0,0 +1,46 @@ +// +// 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.Commands +{ + /// + /// Provides details for a command which will be executed + /// in the host editor. + /// + public class ClientCommand + { + /// + /// Gets the identifying name of the command. + /// + public string Name { get; private set; } + + /// + /// Gets the display title of the command. + /// + public string Title { get; private set; } + + /// + /// Gets the array of objects which are passed as + /// arguments to the command. + /// + public object[] Arguments { get; private set; } + + /// + /// Creates an instance of the ClientCommand class. + /// + /// The name of the command. + /// The display title of the command. + /// The arguments to be passed to the command. + public ClientCommand( + string commandName, + string commandTitle, + object[] arguments) + { + this.Name = commandName; + this.Title = commandTitle; + this.Arguments = arguments; + } + } +} diff --git a/src/PowerShellEditorServices/Components/ComponentRegistry.cs b/src/PowerShellEditorServices/Components/ComponentRegistry.cs new file mode 100644 index 000000000..48a794174 --- /dev/null +++ b/src/PowerShellEditorServices/Components/ComponentRegistry.cs @@ -0,0 +1,80 @@ +// +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Components +{ + /// + /// Provides a default implementation for the IComponentRegistry + /// interface. + /// + public class ComponentRegistry : IComponentRegistry + { + private Dictionary componentRegistry = + new Dictionary(); + + /// + /// Registers an instance of the specified component type + /// or throws an ArgumentException if an instance has + /// already been registered. + /// + /// + /// The instance of the component to be registered. + /// + /// + /// The provided component instance for convenience in assignment + /// statements. + /// + public TComponent Register(TComponent componentInstance) + where TComponent : class + { + this.componentRegistry.Add(typeof(TComponent), componentInstance); + return componentInstance; + } + + + /// + /// Gets the registered instance of the specified + /// component type or throws a KeyNotFoundException if + /// no instance has been registered. + /// + /// The implementation of the specified type. + public TComponent Get() + where TComponent : class + { + return (TComponent)this.componentRegistry[typeof(TComponent)]; + } + + /// + /// Attempts to retrieve the instance of the specified + /// component type and, if found, stores it in the + /// componentInstance parameter. + /// + /// + /// The out parameter in which the found instance will be stored. + /// + /// + /// True if a registered instance was found, false otherwise. + /// + public bool TryGet(out TComponent componentInstance) + where TComponent : class + { + object componentObject = null; + componentInstance = null; + + if (this.componentRegistry.TryGetValue(typeof(TComponent), out componentObject)) + { + componentInstance = componentObject as TComponent; + return componentInstance != null; + } + + return false; + } + } +} diff --git a/src/PowerShellEditorServices/Components/IComponentRegistry.cs b/src/PowerShellEditorServices/Components/IComponentRegistry.cs new file mode 100644 index 000000000..c2f813b29 --- /dev/null +++ b/src/PowerShellEditorServices/Components/IComponentRegistry.cs @@ -0,0 +1,55 @@ +// +// 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; + +namespace Microsoft.PowerShell.EditorServices.Components +{ + /// + /// Specifies the contract for a registry of component interfaces. + /// + public interface IComponentRegistry + { + /// + /// Registers an instance of the specified component type + /// or throws an ArgumentException if an instance has + /// already been registered. + /// + /// + /// The instance of the component to be registered. + /// + /// + /// The provided component instance for convenience in assignment + /// statements. + /// + TComponent Register(TComponent componentInstance) + where TComponent : class; + + /// + /// Gets the registered instance of the specified + /// component type or throws a KeyNotFoundException if + /// no instance has been registered. + /// + /// The implementation of the specified type. + TComponent Get() + where TComponent : class; + + /// + /// Attempts to retrieve the instance of the specified + /// component type and, if found, stores it in the + /// componentInstance parameter. + /// + /// + /// The out parameter in which the found instance will be stored. + /// + /// + /// True if a registered instance was found, false otherwise. + /// + bool TryGet(out TComponent componentInstance) + where TComponent : class; + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices/Language/AstOperations.cs b/src/PowerShellEditorServices/Language/AstOperations.cs index 48ecf1b26..c28416280 100644 --- a/src/PowerShellEditorServices/Language/AstOperations.cs +++ b/src/PowerShellEditorServices/Language/AstOperations.cs @@ -145,10 +145,20 @@ static public async Task GetCompletions( /// The abstract syntax tree of the given script /// The line number of the cursor for the given script /// The coulumn number of the cursor for the given script + /// Includes full function definition ranges in the search. /// SymbolReference of found symbol - static public SymbolReference FindSymbolAtPosition(Ast scriptAst, int lineNumber, int columnNumber) + static public SymbolReference FindSymbolAtPosition( + Ast scriptAst, + int lineNumber, + int columnNumber, + bool includeFunctionDefinitions = false) { - FindSymbolVisitor symbolVisitor = new FindSymbolVisitor(lineNumber, columnNumber); + FindSymbolVisitor symbolVisitor = + new FindSymbolVisitor( + lineNumber, + columnNumber, + includeFunctionDefinitions); + scriptAst.Visit(symbolVisitor); return symbolVisitor.FoundSymbolReference; diff --git a/src/PowerShellEditorServices/Language/DocumentSymbolProvider.cs b/src/PowerShellEditorServices/Language/DocumentSymbolProvider.cs deleted file mode 100644 index a418cae1d..000000000 --- a/src/PowerShellEditorServices/Language/DocumentSymbolProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.PowerShell.EditorServices -{ - internal abstract class DocumentSymbolProvider - { - public IEnumerable GetSymbols(ScriptFile scriptFile, Version psVersion = null) - { - if (CanProvideFor(scriptFile)) - { - return GetSymbolsImpl(scriptFile, psVersion); - } - - return Enumerable.Empty(); - } - - protected abstract IEnumerable GetSymbolsImpl(ScriptFile scriptFile, Version psVersion); - - protected abstract bool CanProvideFor(ScriptFile scriptFile); - } -} diff --git a/src/PowerShellEditorServices/Language/FindSymbolVisitor.cs b/src/PowerShellEditorServices/Language/FindSymbolVisitor.cs index aeef67abb..b2abea291 100644 --- a/src/PowerShellEditorServices/Language/FindSymbolVisitor.cs +++ b/src/PowerShellEditorServices/Language/FindSymbolVisitor.cs @@ -14,13 +14,18 @@ internal class FindSymbolVisitor : AstVisitor { private int lineNumber; private int columnNumber; + private bool includeFunctionDefinitions; public SymbolReference FoundSymbolReference { get; private set; } - public FindSymbolVisitor(int lineNumber, int columnNumber) + public FindSymbolVisitor( + int lineNumber, + int columnNumber, + bool includeFunctionDefinitions) { this.lineNumber = lineNumber; this.columnNumber = columnNumber; + this.includeFunctionDefinitions = includeFunctionDefinitions; } /// @@ -54,9 +59,14 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) /// or a decision to continue if it wasn't found public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) { - int startColumnNumber = - functionDefinitionAst.Extent.Text.IndexOf( - functionDefinitionAst.Name) + 1; + int startColumnNumber = 1; + + if (!this.includeFunctionDefinitions) + { + startColumnNumber = + functionDefinitionAst.Extent.Text.IndexOf( + functionDefinitionAst.Name) + 1; + } IScriptExtent nameExtent = new ScriptExtent() { diff --git a/src/PowerShellEditorServices/Language/LanguageService.cs b/src/PowerShellEditorServices/Language/LanguageService.cs index 366e7efe1..44439a12b 100644 --- a/src/PowerShellEditorServices/Language/LanguageService.cs +++ b/src/PowerShellEditorServices/Language/LanguageService.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Symbols; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections; @@ -33,7 +34,7 @@ public class LanguageService private string mostRecentRequestFile; private Dictionary> CmdletToAliasDictionary; private Dictionary AliasToCmdletDictionary; - private DocumentSymbolProvider[] documentSymbolProviders; + private IDocumentSymbolProvider[] documentSymbolProviders; const int DefaultWaitTimeoutMilliseconds = 5000; @@ -60,10 +61,10 @@ public LanguageService( this.CmdletToAliasDictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); this.AliasToCmdletDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - this.documentSymbolProviders = new DocumentSymbolProvider[] + this.documentSymbolProviders = new IDocumentSymbolProvider[] { - new ScriptDocumentSymbolProvider(), - new PSDDocumentSymbolProvider(), + new ScriptDocumentSymbolProvider(powerShellContext.LocalPowerShellVersion.Version), + new PsdDocumentSymbolProvider(), new PesterDocumentSymbolProvider() }; } @@ -195,6 +196,35 @@ public SymbolReference FindSymbolAtLocation( return symbolReference; } + /// + /// 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; + } + /// /// Finds the details of the symbol at the given script file location. /// @@ -242,7 +272,7 @@ public FindOccurrencesResult FindSymbolsInFile(ScriptFile scriptFile) return new FindOccurrencesResult { FoundOccurrences = documentSymbolProviders - .SelectMany(p => p.GetSymbols(scriptFile, powerShellContext.LocalPowerShellVersion.Version)) + .SelectMany(p => p.ProvideDocumentSymbols(scriptFile)) .Select(reference => { reference.SourceLine = diff --git a/src/PowerShellEditorServices/Language/PSDDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Language/PSDDocumentSymbolProvider.cs deleted file mode 100644 index 85abd5bd7..000000000 --- a/src/PowerShellEditorServices/Language/PSDDocumentSymbolProvider.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices -{ - internal class PSDDocumentSymbolProvider : DocumentSymbolProvider - { - protected override bool CanProvideFor(ScriptFile scriptFile) - { - return (scriptFile.FilePath != null && - scriptFile.FilePath.EndsWith(".psd1", StringComparison.OrdinalIgnoreCase)) || - AstOperations.IsPowerShellDataFileAst(scriptFile.ScriptAst); - } - protected override IEnumerable GetSymbolsImpl(ScriptFile scriptFile, Version psVersion) - { - var findHashtableSymbolsVisitor = new FindHashtableSymbolsVisitor(); - scriptFile.ScriptAst.Visit(findHashtableSymbolsVisitor); - return findHashtableSymbolsVisitor.SymbolReferences; - } - } -} diff --git a/src/PowerShellEditorServices/Language/PesterDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Language/PesterDocumentSymbolProvider.cs deleted file mode 100644 index 4fb061700..000000000 --- a/src/PowerShellEditorServices/Language/PesterDocumentSymbolProvider.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices -{ - internal class PesterDocumentSymbolProvider : DocumentSymbolProvider - { - protected override bool CanProvideFor(ScriptFile scriptFile) - { - return scriptFile.FilePath.EndsWith("tests.ps1", StringComparison.OrdinalIgnoreCase); - } - - protected override IEnumerable GetSymbolsImpl(ScriptFile scriptFile, Version psVersion) - { - var commandAsts = scriptFile.ScriptAst.FindAll(ast => - { - switch ((ast as CommandAst)?.GetCommandName().ToLower()) - { - case "describe": - case "context": - case "it": - return true; - - default: - return false; - } - }, - true); - - return commandAsts.Select(ast => new SymbolReference( - SymbolType.Function, - ast.Extent, - scriptFile.FilePath, - scriptFile.GetLine(ast.Extent.StartLineNumber))); - } - } -} diff --git a/src/PowerShellEditorServices/Language/ScriptDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Language/ScriptDocumentSymbolProvider.cs deleted file mode 100644 index 74aefa91d..000000000 --- a/src/PowerShellEditorServices/Language/ScriptDocumentSymbolProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices -{ - internal class ScriptDocumentSymbolProvider : DocumentSymbolProvider - { - protected override bool CanProvideFor(ScriptFile scriptFile) - { - return scriptFile != null && - scriptFile.FilePath != null && - (scriptFile.FilePath.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase) || - scriptFile.FilePath.EndsWith(".psm1", StringComparison.OrdinalIgnoreCase)); - } - - protected override IEnumerable GetSymbolsImpl( - ScriptFile scriptFile, - Version psVersion) - { - return AstOperations.FindSymbolsInDocument( - scriptFile.ScriptAst, - psVersion); - - } - } -} diff --git a/src/PowerShellEditorServices/Providers/FeatureProviderBase.cs b/src/PowerShellEditorServices/Providers/FeatureProviderBase.cs new file mode 100644 index 000000000..ecf8e8ce2 --- /dev/null +++ b/src/PowerShellEditorServices/Providers/FeatureProviderBase.cs @@ -0,0 +1,19 @@ +// +// 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 +{ + /// + /// Provides a base implementation of IFeatureProvider. + /// + public abstract class FeatureProviderBase : IFeatureProvider + { + /// + /// Gets the provider class type's FullName as the + /// ProviderId. + /// + public string ProviderId => this.GetType().FullName; + } +} diff --git a/src/PowerShellEditorServices/Providers/FeatureProviderCollection.cs b/src/PowerShellEditorServices/Providers/FeatureProviderCollection.cs new file mode 100644 index 000000000..fd4e1b1c9 --- /dev/null +++ b/src/PowerShellEditorServices/Providers/FeatureProviderCollection.cs @@ -0,0 +1,46 @@ +// +// 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; +using System.Collections.Generic; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides a default implementation of IFeatureProviderCollection. + /// + public class FeatureProviderCollection : IFeatureProviderCollection + where TProvider : IFeatureProvider + { + #region Private Fields + + private List providerList = new List(); + + #endregion + + #region IFeatureProviderCollection Implementation + + void IFeatureProviderCollection.Add(TProvider provider) + { + if (!this.providerList.Contains(provider)) + { + this.providerList.Add(provider); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.providerList.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.providerList.GetEnumerator(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/PowerShellEditorServices/Providers/IFeatureProvider.cs b/src/PowerShellEditorServices/Providers/IFeatureProvider.cs new file mode 100644 index 000000000..bea42b821 --- /dev/null +++ b/src/PowerShellEditorServices/Providers/IFeatureProvider.cs @@ -0,0 +1,19 @@ +// +// 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 +{ + /// + /// Defines the contract for a feature provider, particularly for provider identification. + /// + public interface IFeatureProvider + { + /// + /// Specifies a unique identifier for the feature provider, typically a + /// fully-qualified name like "Microsoft.PowerShell.EditorServices.MyProvider" + /// + string ProviderId { get; } + } +} diff --git a/src/PowerShellEditorServices/Providers/IFeatureProviderCollection.cs b/src/PowerShellEditorServices/Providers/IFeatureProviderCollection.cs new file mode 100644 index 000000000..29351a4a4 --- /dev/null +++ b/src/PowerShellEditorServices/Providers/IFeatureProviderCollection.cs @@ -0,0 +1,22 @@ +// +// 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; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Defines the contract for a collection of provider implementations. + /// + public interface IFeatureProviderCollection : IEnumerable + where TProvider : IFeatureProvider + { + /// + /// Adds a provider to the collection. + /// + /// The provider to be added. + void Add(TProvider provider); + } +} diff --git a/src/PowerShellEditorServices/Session/EditorSession.cs b/src/PowerShellEditorServices/Session/EditorSession.cs index 6c00581b4..36b6db062 100644 --- a/src/PowerShellEditorServices/Session/EditorSession.cs +++ b/src/PowerShellEditorServices/Session/EditorSession.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Components; using Microsoft.PowerShell.EditorServices.Console; using Microsoft.PowerShell.EditorServices.Extensions; using Microsoft.PowerShell.EditorServices.Session; @@ -71,6 +72,11 @@ public class EditorSession /// public RemoteFileManager RemoteFileManager { get; private set; } + /// + /// Gets the IComponentCollection instance for this session. + /// + public IComponentRegistry Components { get; } = new ComponentRegistry(); + #endregion #region Constructors diff --git a/src/PowerShellEditorServices/Symbols/IDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Symbols/IDocumentSymbolProvider.cs new file mode 100644 index 000000000..02638c5b0 --- /dev/null +++ b/src/PowerShellEditorServices/Symbols/IDocumentSymbolProvider.cs @@ -0,0 +1,24 @@ +// +// 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; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Specifies the contract for a document symbols provider. + /// + public interface IDocumentSymbolProvider : IFeatureProvider + { + /// + /// Provides a list of symbols for the given document. + /// + /// + /// The document for which SymbolReferences should be provided. + /// + /// An IEnumerable collection of SymbolReferences. + IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile); + } +} diff --git a/src/PowerShellEditorServices/Symbols/IDocumentSymbols.cs b/src/PowerShellEditorServices/Symbols/IDocumentSymbols.cs new file mode 100644 index 000000000..6c1937627 --- /dev/null +++ b/src/PowerShellEditorServices/Symbols/IDocumentSymbols.cs @@ -0,0 +1,31 @@ +// +// 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; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Specifies the contract for an implementation of + /// the IDocumentSymbols component. + /// + public interface IDocumentSymbols + { + /// + /// Gets the collection of IDocumentSymbolsProvider implementations + /// that are registered with this component. + /// + IFeatureProviderCollection Providers { get; } + + /// + /// Provides a list of symbols for the given document. + /// + /// + /// The document for which SymbolReferences should be provided. + /// + /// An IEnumerable collection of SymbolReferences. + IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile); + } +} diff --git a/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs new file mode 100644 index 000000000..d249dbb9c --- /dev/null +++ b/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs @@ -0,0 +1,51 @@ +// +// 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.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Provides an IDocumentSymbolProvider implementation for + /// enumerating test symbols in Pester test (tests.ps1) files. + /// + public class PesterDocumentSymbolProvider : FeatureProviderBase, IDocumentSymbolProvider + { + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + ScriptFile scriptFile) + { + if (!scriptFile.FilePath.EndsWith( + "tests.ps1", + StringComparison.OrdinalIgnoreCase)) + { + return Enumerable.Empty(); + } + + var commandAsts = scriptFile.ScriptAst.FindAll(ast => + { + switch ((ast as CommandAst)?.GetCommandName()?.ToLower()) + { + case "describe": + case "context": + case "it": + return true; + + default: + return false; + } + }, + true); + + return commandAsts.Select(ast => new SymbolReference( + SymbolType.Function, + ast.Extent, + scriptFile.FilePath, + scriptFile.GetLine(ast.Extent.StartLineNumber))); + } + } +} diff --git a/src/PowerShellEditorServices/Symbols/PsdDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Symbols/PsdDocumentSymbolProvider.cs new file mode 100644 index 000000000..e6e0c4a32 --- /dev/null +++ b/src/PowerShellEditorServices/Symbols/PsdDocumentSymbolProvider.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; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Provides an IDocumentSymbolProvider implementation for + /// enumerating symbols in .psd1 files. + /// + public class PsdDocumentSymbolProvider : FeatureProviderBase, IDocumentSymbolProvider + { + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + ScriptFile scriptFile) + { + if ((scriptFile.FilePath != null && + scriptFile.FilePath.EndsWith(".psd1", StringComparison.OrdinalIgnoreCase)) || + AstOperations.IsPowerShellDataFileAst(scriptFile.ScriptAst)) + { + var findHashtableSymbolsVisitor = new FindHashtableSymbolsVisitor(); + scriptFile.ScriptAst.Visit(findHashtableSymbolsVisitor); + return findHashtableSymbolsVisitor.SymbolReferences; + } + + return Enumerable.Empty(); + } + } +} diff --git a/src/PowerShellEditorServices/Symbols/ScriptDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Symbols/ScriptDocumentSymbolProvider.cs new file mode 100644 index 000000000..9405bf479 --- /dev/null +++ b/src/PowerShellEditorServices/Symbols/ScriptDocumentSymbolProvider.cs @@ -0,0 +1,47 @@ +// +// 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; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Provides an IDocumentSymbolProvider implementation for + /// enumerating symbols in script (.psd1, .psm1) files. + /// + public class ScriptDocumentSymbolProvider : FeatureProviderBase, IDocumentSymbolProvider + { + private Version powerShellVersion; + + /// + /// Creates an instance of the ScriptDocumentSymbolProvider to + /// target the specified PowerShell version. + /// + /// The target PowerShell version. + public ScriptDocumentSymbolProvider(Version powerShellVersion) + { + this.powerShellVersion = powerShellVersion; + } + + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + ScriptFile scriptFile) + { + if (scriptFile != null && + scriptFile.FilePath != null && + (scriptFile.FilePath.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase) || + scriptFile.FilePath.EndsWith(".psm1", StringComparison.OrdinalIgnoreCase))) + { + return + AstOperations.FindSymbolsInDocument( + scriptFile.ScriptAst, + this.powerShellVersion); + } + + return Enumerable.Empty(); + } + } +} diff --git a/src/PowerShellEditorServices/Workspace/ScriptRegion.cs b/src/PowerShellEditorServices/Workspace/ScriptRegion.cs index 803fe8dd4..5775ddee6 100644 --- a/src/PowerShellEditorServices/Workspace/ScriptRegion.cs +++ b/src/PowerShellEditorServices/Workspace/ScriptRegion.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using System.Management.Automation.Language; namespace Microsoft.PowerShell.EditorServices @@ -10,7 +11,7 @@ namespace Microsoft.PowerShell.EditorServices /// /// Contains details about a specific region of text in script file. /// - public sealed class ScriptRegion + public sealed class ScriptRegion : IScriptExtent { #region Properties @@ -54,6 +55,18 @@ public sealed class ScriptRegion /// public int EndOffset { get; set; } + /// + /// Gets the starting IScriptPosition in the script. + /// (Currently unimplemented.) + /// + IScriptPosition IScriptExtent.StartScriptPosition => throw new NotImplementedException(); + + /// + /// Gets the ending IScriptPosition in the script. + /// (Currently unimplemented.) + /// + IScriptPosition IScriptExtent.EndScriptPosition => throw new NotImplementedException(); + #endregion #region Constructors