diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 612aa0aa5..de2b092f4 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -115,6 +115,8 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs index d36a0c987..75db008a6 100644 --- a/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs @@ -23,7 +23,7 @@ namespace Microsoft.PowerShell.EditorServices /// Provides a high-level service for performing semantic analysis /// of PowerShell scripts. /// - internal class AnalysisService : IDisposable + public class AnalysisService : IDisposable { #region Static fields diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs new file mode 100644 index 000000000..61b3dae51 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/EvaluateHandler.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class EvaluateHandler : IEvaluateHandler + { + private readonly ILogger _logger; + private readonly PowerShellContextService _powerShellContextService; + + public EvaluateHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService) + { + _logger = factory.CreateLogger(); + _powerShellContextService = powerShellContextService; + } + + public async Task Handle(EvaluateRequestArguments request, CancellationToken cancellationToken) + { + await _powerShellContextService.ExecuteScriptStringAsync( + request.Expression, + writeInputToHost: true, + writeOutputToHost: true, + addToHistory: true); + + return new EvaluateResponseBody + { + Result = "", + VariablesReference = 0 + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs new file mode 100644 index 000000000..e00391880 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/GetCommentHelpHandler.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class GetCommentHelpHandler : IGetCommentHelpHandler + { + private readonly ILogger _logger; + private readonly WorkspaceService _workspaceService; + private readonly AnalysisService _analysisService; + private readonly SymbolsService _symbolsService; + + public GetCommentHelpHandler( + ILoggerFactory factory, + WorkspaceService workspaceService, + AnalysisService analysisService, + SymbolsService symbolsService) + { + _logger = factory.CreateLogger(); + _workspaceService = workspaceService; + _analysisService = analysisService; + _symbolsService = symbolsService; + } + + public async Task Handle(CommentHelpRequestParams request, CancellationToken cancellationToken) + { + var result = new CommentHelpRequestResult(); + + if (!_workspaceService.TryGetFile(request.DocumentUri, out ScriptFile scriptFile)) + { + return result; + } + + int triggerLine = (int) request.TriggerPosition.Line + 1; + + FunctionDefinitionAst functionDefinitionAst = _symbolsService.GetFunctionDefinitionForHelpComment( + scriptFile, + triggerLine, + out string helpLocation); + + if (functionDefinitionAst == null) + { + return result; + } + + IScriptExtent funcExtent = functionDefinitionAst.Extent; + string funcText = funcExtent.Text; + if (helpLocation.Equals("begin")) + { + // check if the previous character is `<` because it invalidates + // the param block the follows it. + IList lines = ScriptFile.GetLinesInternal(funcText); + int relativeTriggerLine0b = triggerLine - funcExtent.StartLineNumber; + if (relativeTriggerLine0b > 0 && lines[relativeTriggerLine0b].IndexOf("<", StringComparison.OrdinalIgnoreCase) > -1) + { + lines[relativeTriggerLine0b] = string.Empty; + } + + funcText = string.Join("\n", lines); + } + + List analysisResults = await _analysisService.GetSemanticMarkersAsync( + funcText, + AnalysisService.GetCommentHelpRuleSettings( + enable: true, + exportedOnly: false, + blockComment: request.BlockComment, + vscodeSnippetCorrection: true, + placement: helpLocation)); + + string helpText = analysisResults?.FirstOrDefault()?.Correction?.Edits[0].Text; + + if (helpText == null) + { + return result; + } + + result.Content = ScriptFile.GetLinesInternal(helpText).ToArray(); + + if (helpLocation != null && + !helpLocation.Equals("before", StringComparison.OrdinalIgnoreCase)) + { + // we need to trim the leading `{` and newline when helpLocation=="begin" + // we also need to trim the leading newline when helpLocation=="end" + result.Content = result.Content.Skip(1).ToArray(); + } + + return result; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs new file mode 100644 index 000000000..1fd053a0f --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IEvaluateHandler.cs @@ -0,0 +1,44 @@ +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + [Serial, Method("evaluate")] + public interface IEvaluateHandler : IJsonRpcRequestHandler { } + + public class EvaluateRequestArguments : IRequest + { + /// + /// The expression to evaluate. + /// + public string Expression { get; set; } + + /// + /// The context in which the evaluate request is run. Possible + /// values are 'watch' if evaluate is run in a watch or 'repl' + /// if run from the REPL console. + /// + public string Context { get; set; } + + /// + /// Evaluate the expression in the context of this stack frame. + /// If not specified, the top most frame is used. + /// + public int FrameId { get; set; } + } + + public class EvaluateResponseBody + { + /// + /// The evaluation result. + /// + public string Result { get; set; } + + /// + /// If variablesReference is > 0, the evaluate result is + /// structured and its children can be retrieved by passing + /// variablesReference to the VariablesRequest + /// + public int VariablesReference { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs new file mode 100644 index 000000000..85c2f00ad --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/Handlers/IGetCommentHelpHandler.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using OmniSharp.Extensions.Embedded.MediatR; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + [Serial, Method("powerShell/getCommentHelp")] + public interface IGetCommentHelpHandler : IJsonRpcRequestHandler { } + + public class CommentHelpRequestResult + { + public string[] Content { get; set; } + } + + public class CommentHelpRequestParams : IRequest + { + public string DocumentUri { get; set; } + public Position TriggerPosition { get; set; } + public bool BlockComment { get; set; } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 3a5ef3b07..216a885d1 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -505,5 +505,97 @@ private ScriptFile[] GetBuiltinCommandScriptFiles( return scriptFiles.ToArray(); } + + /// + /// Finds a function definition that follows or contains the given line number. + /// + /// Open script file. + /// The 1 based line on which to look for function definition. + /// + /// If found, returns the function definition, otherwise, returns null. + public FunctionDefinitionAst GetFunctionDefinitionForHelpComment( + ScriptFile scriptFile, + int lineNumber, + out string helpLocation) + { + // check if the next line contains a function definition + FunctionDefinitionAst funcDefnAst = GetFunctionDefinitionAtLine(scriptFile, lineNumber + 1); + if (funcDefnAst != null) + { + helpLocation = "before"; + return funcDefnAst; + } + + // find all the script definitions that contain the line `lineNumber` + IEnumerable foundAsts = scriptFile.ScriptAst.FindAll( + ast => + { + if (!(ast is FunctionDefinitionAst fdAst)) + { + return false; + } + + return fdAst.Body.Extent.StartLineNumber < lineNumber && + fdAst.Body.Extent.EndLineNumber > lineNumber; + }, + true); + + if (foundAsts == null || !foundAsts.Any()) + { + helpLocation = null; + return null; + } + + // of all the function definitions found, return the innermost function + // definition that contains `lineNumber` + foreach (FunctionDefinitionAst foundAst in foundAsts.Cast()) + { + if (funcDefnAst == null) + { + funcDefnAst = foundAst; + continue; + } + + if (funcDefnAst.Extent.StartOffset >= foundAst.Extent.StartOffset + && funcDefnAst.Extent.EndOffset <= foundAst.Extent.EndOffset) + { + funcDefnAst = foundAst; + } + } + + // TODO use tokens to check for non empty character instead of just checking for line offset + if (funcDefnAst.Body.Extent.StartLineNumber == lineNumber - 1) + { + helpLocation = "begin"; + return funcDefnAst; + } + + if (funcDefnAst.Body.Extent.EndLineNumber == lineNumber + 1) + { + helpLocation = "end"; + return funcDefnAst; + } + + // If we didn't find a function definition, then return null + helpLocation = null; + return null; + } + + /// + /// Gets the function defined on a given line. + /// + /// Open script file. + /// The 1 based line on which to look for function definition. + /// If found, returns the function definition on the given line. Otherwise, returns null. + public FunctionDefinitionAst GetFunctionDefinitionAtLine( + ScriptFile scriptFile, + int lineNumber) + { + Ast functionDefinitionAst = scriptFile.ScriptAst.Find( + ast => ast is FunctionDefinitionAst && ast.Extent.StartLineNumber == lineNumber, + true); + + return functionDefinitionAst as FunctionDefinitionAst; + } } } diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 49b3ea633..2b71ba03f 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -11,6 +11,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Client; using OmniSharp.Extensions.LanguageServer.Protocol.Models; @@ -750,5 +751,51 @@ await LanguageClient.SendRequest( Assert.Equal("New PowerShell Manifest Module", template2.Title); }); } + + [Fact] + public async Task CanSendGetCommentHelpRequest() + { + string scriptPath = NewTestFile(@" +function CanSendGetCommentHelpRequest { + param( + [string] + $myParam + ) +} +"); + + CommentHelpRequestResult commentHelpRequestResult = + await LanguageClient.SendRequest( + "powerShell/getCommentHelp", + new CommentHelpRequestParams + { + DocumentUri = new Uri(scriptPath).ToString(), + BlockComment = false, + TriggerPosition = new Position + { + Line = 0, + Character = 0 + } + }); + + Assert.NotEmpty(commentHelpRequestResult.Content); + Assert.Contains("myParam", commentHelpRequestResult.Content[7]); + } + + [Fact] + public async Task CanSendEvaluateRequest() + { + EvaluateResponseBody evaluateResponseBody = + await LanguageClient.SendRequest( + "evaluate", + new EvaluateRequestArguments + { + Expression = "Get-ChildItem" + }); + + // These always gets returned so this test really just makes sure we get _any_ response. + Assert.Equal("", evaluateResponseBody.Result); + Assert.Equal(0, evaluateResponseBody.VariablesReference); + } } }