Skip to content

Commit 3754d99

Browse files
Comment Help and Evaluate (#1015)
* Support for Comment Help generator * add evaluate handler
1 parent 48b8d21 commit 3754d99

File tree

8 files changed

+345
-1
lines changed

8 files changed

+345
-1
lines changed

src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs

+2
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ public async Task StartAsync()
115115
.WithHandler<SignatureHelpHandler>()
116116
.WithHandler<DefinitionHandler>()
117117
.WithHandler<TemplateHandlers>()
118+
.WithHandler<GetCommentHelpHandler>()
119+
.WithHandler<EvaluateHandler>()
118120
.OnInitialize(
119121
async (languageServer, request) =>
120122
{

src/PowerShellEditorServices.Engine/Services/Analysis/AnalysisService.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ namespace Microsoft.PowerShell.EditorServices
2323
/// Provides a high-level service for performing semantic analysis
2424
/// of PowerShell scripts.
2525
/// </summary>
26-
internal class AnalysisService : IDisposable
26+
public class AnalysisService : IDisposable
2727
{
2828
#region Static fields
2929

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Microsoft.Extensions.Logging;
5+
using Microsoft.PowerShell.EditorServices;
6+
7+
namespace PowerShellEditorServices.Engine.Services.Handlers
8+
{
9+
public class EvaluateHandler : IEvaluateHandler
10+
{
11+
private readonly ILogger _logger;
12+
private readonly PowerShellContextService _powerShellContextService;
13+
14+
public EvaluateHandler(ILoggerFactory factory, PowerShellContextService powerShellContextService)
15+
{
16+
_logger = factory.CreateLogger<EvaluateHandler>();
17+
_powerShellContextService = powerShellContextService;
18+
}
19+
20+
public async Task<EvaluateResponseBody> Handle(EvaluateRequestArguments request, CancellationToken cancellationToken)
21+
{
22+
await _powerShellContextService.ExecuteScriptStringAsync(
23+
request.Expression,
24+
writeInputToHost: true,
25+
writeOutputToHost: true,
26+
addToHistory: true);
27+
28+
return new EvaluateResponseBody
29+
{
30+
Result = "",
31+
VariablesReference = 0
32+
};
33+
}
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Management.Automation.Language;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.PowerShell.EditorServices;
9+
using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer;
10+
11+
namespace PowerShellEditorServices.Engine.Services.Handlers
12+
{
13+
public class GetCommentHelpHandler : IGetCommentHelpHandler
14+
{
15+
private readonly ILogger _logger;
16+
private readonly WorkspaceService _workspaceService;
17+
private readonly AnalysisService _analysisService;
18+
private readonly SymbolsService _symbolsService;
19+
20+
public GetCommentHelpHandler(
21+
ILoggerFactory factory,
22+
WorkspaceService workspaceService,
23+
AnalysisService analysisService,
24+
SymbolsService symbolsService)
25+
{
26+
_logger = factory.CreateLogger<GetCommentHelpHandler>();
27+
_workspaceService = workspaceService;
28+
_analysisService = analysisService;
29+
_symbolsService = symbolsService;
30+
}
31+
32+
public async Task<CommentHelpRequestResult> Handle(CommentHelpRequestParams request, CancellationToken cancellationToken)
33+
{
34+
var result = new CommentHelpRequestResult();
35+
36+
if (!_workspaceService.TryGetFile(request.DocumentUri, out ScriptFile scriptFile))
37+
{
38+
return result;
39+
}
40+
41+
int triggerLine = (int) request.TriggerPosition.Line + 1;
42+
43+
FunctionDefinitionAst functionDefinitionAst = _symbolsService.GetFunctionDefinitionForHelpComment(
44+
scriptFile,
45+
triggerLine,
46+
out string helpLocation);
47+
48+
if (functionDefinitionAst == null)
49+
{
50+
return result;
51+
}
52+
53+
IScriptExtent funcExtent = functionDefinitionAst.Extent;
54+
string funcText = funcExtent.Text;
55+
if (helpLocation.Equals("begin"))
56+
{
57+
// check if the previous character is `<` because it invalidates
58+
// the param block the follows it.
59+
IList<string> lines = ScriptFile.GetLinesInternal(funcText);
60+
int relativeTriggerLine0b = triggerLine - funcExtent.StartLineNumber;
61+
if (relativeTriggerLine0b > 0 && lines[relativeTriggerLine0b].IndexOf("<", StringComparison.OrdinalIgnoreCase) > -1)
62+
{
63+
lines[relativeTriggerLine0b] = string.Empty;
64+
}
65+
66+
funcText = string.Join("\n", lines);
67+
}
68+
69+
List<ScriptFileMarker> analysisResults = await _analysisService.GetSemanticMarkersAsync(
70+
funcText,
71+
AnalysisService.GetCommentHelpRuleSettings(
72+
enable: true,
73+
exportedOnly: false,
74+
blockComment: request.BlockComment,
75+
vscodeSnippetCorrection: true,
76+
placement: helpLocation));
77+
78+
string helpText = analysisResults?.FirstOrDefault()?.Correction?.Edits[0].Text;
79+
80+
if (helpText == null)
81+
{
82+
return result;
83+
}
84+
85+
result.Content = ScriptFile.GetLinesInternal(helpText).ToArray();
86+
87+
if (helpLocation != null &&
88+
!helpLocation.Equals("before", StringComparison.OrdinalIgnoreCase))
89+
{
90+
// we need to trim the leading `{` and newline when helpLocation=="begin"
91+
// we also need to trim the leading newline when helpLocation=="end"
92+
result.Content = result.Content.Skip(1).ToArray();
93+
}
94+
95+
return result;
96+
}
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using OmniSharp.Extensions.Embedded.MediatR;
2+
using OmniSharp.Extensions.JsonRpc;
3+
4+
namespace PowerShellEditorServices.Engine.Services.Handlers
5+
{
6+
[Serial, Method("evaluate")]
7+
public interface IEvaluateHandler : IJsonRpcRequestHandler<EvaluateRequestArguments, EvaluateResponseBody> { }
8+
9+
public class EvaluateRequestArguments : IRequest<EvaluateResponseBody>
10+
{
11+
/// <summary>
12+
/// The expression to evaluate.
13+
/// </summary>
14+
public string Expression { get; set; }
15+
16+
/// <summary>
17+
/// The context in which the evaluate request is run. Possible
18+
/// values are 'watch' if evaluate is run in a watch or 'repl'
19+
/// if run from the REPL console.
20+
/// </summary>
21+
public string Context { get; set; }
22+
23+
/// <summary>
24+
/// Evaluate the expression in the context of this stack frame.
25+
/// If not specified, the top most frame is used.
26+
/// </summary>
27+
public int FrameId { get; set; }
28+
}
29+
30+
public class EvaluateResponseBody
31+
{
32+
/// <summary>
33+
/// The evaluation result.
34+
/// </summary>
35+
public string Result { get; set; }
36+
37+
/// <summary>
38+
/// If variablesReference is > 0, the evaluate result is
39+
/// structured and its children can be retrieved by passing
40+
/// variablesReference to the VariablesRequest
41+
/// </summary>
42+
public int VariablesReference { get; set; }
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using OmniSharp.Extensions.Embedded.MediatR;
7+
using OmniSharp.Extensions.JsonRpc;
8+
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
9+
10+
namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer
11+
{
12+
[Serial, Method("powerShell/getCommentHelp")]
13+
public interface IGetCommentHelpHandler : IJsonRpcRequestHandler<CommentHelpRequestParams, CommentHelpRequestResult> { }
14+
15+
public class CommentHelpRequestResult
16+
{
17+
public string[] Content { get; set; }
18+
}
19+
20+
public class CommentHelpRequestParams : IRequest<CommentHelpRequestResult>
21+
{
22+
public string DocumentUri { get; set; }
23+
public Position TriggerPosition { get; set; }
24+
public bool BlockComment { get; set; }
25+
}
26+
}

src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs

+92
Original file line numberDiff line numberDiff line change
@@ -505,5 +505,97 @@ private ScriptFile[] GetBuiltinCommandScriptFiles(
505505

506506
return scriptFiles.ToArray();
507507
}
508+
509+
/// <summary>
510+
/// Finds a function definition that follows or contains the given line number.
511+
/// </summary>
512+
/// <param name="scriptFile">Open script file.</param>
513+
/// <param name="lineNumber">The 1 based line on which to look for function definition.</param>
514+
/// <param name="helpLocation"></param>
515+
/// <returns>If found, returns the function definition, otherwise, returns null.</returns>
516+
public FunctionDefinitionAst GetFunctionDefinitionForHelpComment(
517+
ScriptFile scriptFile,
518+
int lineNumber,
519+
out string helpLocation)
520+
{
521+
// check if the next line contains a function definition
522+
FunctionDefinitionAst funcDefnAst = GetFunctionDefinitionAtLine(scriptFile, lineNumber + 1);
523+
if (funcDefnAst != null)
524+
{
525+
helpLocation = "before";
526+
return funcDefnAst;
527+
}
528+
529+
// find all the script definitions that contain the line `lineNumber`
530+
IEnumerable<Ast> foundAsts = scriptFile.ScriptAst.FindAll(
531+
ast =>
532+
{
533+
if (!(ast is FunctionDefinitionAst fdAst))
534+
{
535+
return false;
536+
}
537+
538+
return fdAst.Body.Extent.StartLineNumber < lineNumber &&
539+
fdAst.Body.Extent.EndLineNumber > lineNumber;
540+
},
541+
true);
542+
543+
if (foundAsts == null || !foundAsts.Any())
544+
{
545+
helpLocation = null;
546+
return null;
547+
}
548+
549+
// of all the function definitions found, return the innermost function
550+
// definition that contains `lineNumber`
551+
foreach (FunctionDefinitionAst foundAst in foundAsts.Cast<FunctionDefinitionAst>())
552+
{
553+
if (funcDefnAst == null)
554+
{
555+
funcDefnAst = foundAst;
556+
continue;
557+
}
558+
559+
if (funcDefnAst.Extent.StartOffset >= foundAst.Extent.StartOffset
560+
&& funcDefnAst.Extent.EndOffset <= foundAst.Extent.EndOffset)
561+
{
562+
funcDefnAst = foundAst;
563+
}
564+
}
565+
566+
// TODO use tokens to check for non empty character instead of just checking for line offset
567+
if (funcDefnAst.Body.Extent.StartLineNumber == lineNumber - 1)
568+
{
569+
helpLocation = "begin";
570+
return funcDefnAst;
571+
}
572+
573+
if (funcDefnAst.Body.Extent.EndLineNumber == lineNumber + 1)
574+
{
575+
helpLocation = "end";
576+
return funcDefnAst;
577+
}
578+
579+
// If we didn't find a function definition, then return null
580+
helpLocation = null;
581+
return null;
582+
}
583+
584+
/// <summary>
585+
/// Gets the function defined on a given line.
586+
/// </summary>
587+
/// <param name="scriptFile">Open script file.</param>
588+
/// <param name="lineNumber">The 1 based line on which to look for function definition.</param>
589+
/// <returns>If found, returns the function definition on the given line. Otherwise, returns null.</returns>
590+
public FunctionDefinitionAst GetFunctionDefinitionAtLine(
591+
ScriptFile scriptFile,
592+
int lineNumber)
593+
{
594+
Ast functionDefinitionAst = scriptFile.ScriptAst.Find(
595+
ast => ast is FunctionDefinitionAst && ast.Extent.StartLineNumber == lineNumber,
596+
true);
597+
598+
return functionDefinitionAst as FunctionDefinitionAst;
599+
}
508600
}
509601
}

test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs

+47
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Reflection;
1212
using System.Threading;
1313
using System.Threading.Tasks;
14+
using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer;
1415
using Newtonsoft.Json.Linq;
1516
using OmniSharp.Extensions.LanguageServer.Client;
1617
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
@@ -750,5 +751,51 @@ await LanguageClient.SendRequest<GetProjectTemplatesResponse>(
750751
Assert.Equal("New PowerShell Manifest Module", template2.Title);
751752
});
752753
}
754+
755+
[Fact]
756+
public async Task CanSendGetCommentHelpRequest()
757+
{
758+
string scriptPath = NewTestFile(@"
759+
function CanSendGetCommentHelpRequest {
760+
param(
761+
[string]
762+
$myParam
763+
)
764+
}
765+
");
766+
767+
CommentHelpRequestResult commentHelpRequestResult =
768+
await LanguageClient.SendRequest<CommentHelpRequestResult>(
769+
"powerShell/getCommentHelp",
770+
new CommentHelpRequestParams
771+
{
772+
DocumentUri = new Uri(scriptPath).ToString(),
773+
BlockComment = false,
774+
TriggerPosition = new Position
775+
{
776+
Line = 0,
777+
Character = 0
778+
}
779+
});
780+
781+
Assert.NotEmpty(commentHelpRequestResult.Content);
782+
Assert.Contains("myParam", commentHelpRequestResult.Content[7]);
783+
}
784+
785+
[Fact]
786+
public async Task CanSendEvaluateRequest()
787+
{
788+
EvaluateResponseBody evaluateResponseBody =
789+
await LanguageClient.SendRequest<EvaluateResponseBody>(
790+
"evaluate",
791+
new EvaluateRequestArguments
792+
{
793+
Expression = "Get-ChildItem"
794+
});
795+
796+
// These always gets returned so this test really just makes sure we get _any_ response.
797+
Assert.Equal("", evaluateResponseBody.Result);
798+
Assert.Equal(0, evaluateResponseBody.VariablesReference);
799+
}
753800
}
754801
}

0 commit comments

Comments
 (0)