diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 807b5bcaa..6908cc766 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -111,6 +111,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs new file mode 100644 index 000000000..3604481cc --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolDetails.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Diagnostics; +using System.Management.Automation; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Symbols; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides detailed information for a given symbol. + /// + [DebuggerDisplay("SymbolReference = {SymbolReference.SymbolType}/{SymbolReference.SymbolName}, DisplayString = {DisplayString}")] + public class SymbolDetails + { + #region Properties + + /// + /// Gets the original symbol reference which was used to gather details. + /// + public SymbolReference SymbolReference { get; private set; } + + /// + /// Gets the display string for this symbol. + /// + public string DisplayString { get; private set; } + + /// + /// Gets the documentation string for this symbol. Returns an + /// empty string if the symbol has no documentation. + /// + public string Documentation { get; private set; } + + #endregion + + #region Constructors + + static internal async Task CreateAsync( + SymbolReference symbolReference, + PowerShellContextService powerShellContext) + { + SymbolDetails symbolDetails = new SymbolDetails + { + SymbolReference = symbolReference + }; + + switch (symbolReference.SymbolType) + { + case SymbolType.Function: + CommandInfo commandInfo = await CommandHelpers.GetCommandInfoAsync( + symbolReference.SymbolName, + powerShellContext); + + if (commandInfo != null) + { + symbolDetails.Documentation = + await CommandHelpers.GetCommandSynopsisAsync( + commandInfo, + powerShellContext); + + if (commandInfo.CommandType == CommandTypes.Application) + { + symbolDetails.DisplayString = "(application) " + symbolReference.SymbolName; + return symbolDetails; + } + } + + symbolDetails.DisplayString = "function " + symbolReference.SymbolName; + return symbolDetails; + + case SymbolType.Parameter: + // TODO: Get parameter help + symbolDetails.DisplayString = "(parameter) " + symbolReference.SymbolName; + return symbolDetails; + + case SymbolType.Variable: + symbolDetails.DisplayString = symbolReference.SymbolName; + return symbolDetails; + + default: + return symbolDetails; + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index c9ab22463..e0c1e57be 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -8,6 +8,7 @@ using System.Collections.Specialized; using System.Linq; using System.Runtime.InteropServices; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Symbols; @@ -22,7 +23,7 @@ public class SymbolsService #region Private Fields private readonly ILogger _logger; - + private readonly PowerShellContextService _powerShellContextService; private readonly IDocumentSymbolProvider[] _documentSymbolProviders; #endregion @@ -35,9 +36,11 @@ public class SymbolsService /// /// An ILoggerFactory implementation used for writing log messages. public SymbolsService( - ILoggerFactory factory) + ILoggerFactory factory, + PowerShellContextService powerShellContextService) { _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; _documentSymbolProviders = new IDocumentSymbolProvider[] { new ScriptDocumentSymbolProvider(VersionUtils.PSVersion), @@ -200,6 +203,7 @@ public IReadOnlyList FindOccurrencesInFile( needsAliases: false).ToArray(); } + /// /// Finds a function definition in the script given a file location /// /// The details and contents of a open script file @@ -227,5 +231,36 @@ public SymbolReference FindFunctionDefinitionAtLocation( return symbolReference; } + + /// + /// Finds the details of the symbol at the given script file location. + /// + /// The ScriptFile in which the symbol can be located. + /// The line number at which the symbol can be located. + /// The column number at which the symbol can be located. + /// + public async Task FindSymbolDetailsAtLocationAsync( + ScriptFile scriptFile, + int lineNumber, + int columnNumber) + { + SymbolReference symbolReference = + AstOperations.FindSymbolAtPosition( + scriptFile.ScriptAst, + lineNumber, + columnNumber); + + if (symbolReference == null) + { + return null; + } + + symbolReference.FilePath = scriptFile.FilePath; + SymbolDetails symbolDetails = await SymbolDetails.CreateAsync( + symbolReference, + _powerShellContextService); + + return symbolDetails; + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs new file mode 100644 index 000000000..633ef2900 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/HoverHandler.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +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 HoverHandler : IHoverHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter + { + Language = "powershell" + } + ); + + private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + private readonly PowerShellContextService _powerShellContextService; + + private HoverCapability _capability; + + public HoverHandler( + ILoggerFactory factory, + SymbolsService symbolsService, + WorkspaceService workspaceService, + PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _symbolsService = symbolsService; + _workspaceService = workspaceService; + _powerShellContextService = powerShellContextService; + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions + { + DocumentSelector = _documentSelector, + }; + } + + public async Task Handle(HoverParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = + _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + SymbolDetails symbolDetails = + await _symbolsService.FindSymbolDetailsAtLocationAsync( + scriptFile, + (int) request.Position.Line + 1, + (int) request.Position.Character + 1); + + List symbolInfo = new List(); + Range symbolRange = null; + + if (symbolDetails != null) + { + symbolInfo.Add(new MarkedString("PowerShell", symbolDetails.DisplayString)); + + if (!string.IsNullOrEmpty(symbolDetails.Documentation)) + { + symbolInfo.Add(new MarkedString("markdown", symbolDetails.Documentation)); + } + + symbolRange = GetRangeFromScriptRegion(symbolDetails.SymbolReference.ScriptRegion); + } + + return new Hover + { + Contents = new MarkedStringsOrMarkupContent(symbolInfo), + Range = symbolRange + }; + } + + public void SetCapability(HoverCapability capability) + { + _capability = capability; + } + + private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) + { + return new Range + { + Start = new Position + { + Line = scriptRegion.StartLineNumber - 1, + Character = scriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptRegion.EndLineNumber - 1, + Character = scriptRegion.EndColumnNumber - 1 + } + }; + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index a29563f70..c7593dc4e 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -644,5 +644,25 @@ public async Task CanSendCompletionAndCompletionResolveRequest() Assert.Contains("Writes customized output to a host", updatedCompletionItem.Documentation.String); } + + [Fact] + public async Task CanSendHoverRequest() + { + string filePath = NewTestFile("Write-Host"); + + Hover hover = await LanguageClient.TextDocument.Hover(filePath, line: 0, column: 1); + + Assert.True(hover.Contents.HasMarkedStrings); + Assert.Collection(hover.Contents.MarkedStrings, + str1 => + { + Assert.Equal("function Write-Host", str1.Value); + }, + str2 => + { + Assert.Equal("markdown", str2.Language); + Assert.Equal("Writes customized output to a host.", str2.Value); + }); + } } }