forked from PowerShell/PowerShellEditorServices
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathAstOperations.cs
184 lines (165 loc) · 8.14 KB
/
AstOperations.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
namespace Microsoft.PowerShell.EditorServices.Services.Symbols
{
/// <summary>
/// Provides common operations for the syntax tree of a parsed script.
/// </summary>
internal static class AstOperations
{
private static readonly Func<IScriptPosition, int, IScriptPosition> s_clonePositionWithNewOffset;
static AstOperations()
{
Type internalScriptPositionType = typeof(PSObject).GetTypeInfo().Assembly
.GetType("System.Management.Automation.Language.InternalScriptPosition");
MethodInfo cloneWithNewOffsetMethod = internalScriptPositionType.GetMethod("CloneWithNewOffset", BindingFlags.Instance | BindingFlags.NonPublic);
ParameterExpression originalPosition = Expression.Parameter(typeof(IScriptPosition));
ParameterExpression newOffset = Expression.Parameter(typeof(int));
ParameterExpression[] parameters = new ParameterExpression[] { originalPosition, newOffset };
s_clonePositionWithNewOffset = Expression.Lambda<Func<IScriptPosition, int, IScriptPosition>>(
Expression.Call(
Expression.Convert(originalPosition, internalScriptPositionType),
cloneWithNewOffsetMethod,
newOffset),
parameters).Compile();
}
/// <summary>
/// Gets completions for the symbol found in the Ast at
/// the given file offset.
/// </summary>
/// <param name="scriptAst">
/// The Ast which will be traversed to find a completable symbol.
/// </param>
/// <param name="currentTokens">
/// The array of tokens corresponding to the scriptAst parameter.
/// </param>
/// <param name="fileOffset">
/// The 1-based file offset at which a symbol will be located.
/// </param>
/// <param name="executionService">
/// The PowerShellContext to use for gathering completions.
/// </param>
/// <param name="logger">An ILogger implementation used for writing log messages.</param>
/// <param name="cancellationToken">
/// A CancellationToken to cancel completion requests.
/// </param>
/// <returns>
/// A CommandCompletion instance that contains completions for the
/// symbol at the given offset.
/// </returns>
public static async Task<CommandCompletion> GetCompletionsAsync(
Ast scriptAst,
Token[] currentTokens,
int fileOffset,
IInternalPowerShellExecutionService executionService,
ILogger logger,
CancellationToken cancellationToken)
{
IScriptPosition cursorPosition = s_clonePositionWithNewOffset(scriptAst.Extent.StartScriptPosition, fileOffset);
Stopwatch stopwatch = new();
logger.LogTrace($"Getting completions at offset {fileOffset} (line: {cursorPosition.LineNumber}, column: {cursorPosition.ColumnNumber})");
CommandCompletion commandCompletion = await executionService.ExecuteDelegateAsync(
representation: "CompleteInput",
new ExecutionOptions { Priority = ExecutionPriority.Next },
(pwsh, _) =>
{
stopwatch.Start();
// If the current runspace is not out of process, then we call TabExpansion2 so
// that we have the ability to issue pipeline stop requests on cancellation.
if (executionService is PsesInternalHost psesInternalHost
&& !psesInternalHost.Runspace.RunspaceIsRemote)
{
IReadOnlyList<CommandCompletion> completionResults = new SynchronousPowerShellTask<CommandCompletion>(
logger,
psesInternalHost,
new PSCommand()
.AddCommand("TabExpansion2")
.AddParameter("ast", scriptAst)
.AddParameter("tokens", currentTokens)
.AddParameter("positionOfCursor", cursorPosition),
executionOptions: null,
cancellationToken)
.ExecuteAndGetResult(cancellationToken);
if (completionResults is { Count: > 0 })
{
return completionResults[0];
}
return null;
}
// If the current runspace is out of process, we can't call TabExpansion2
// because the output will be serialized.
return CommandCompletion.CompleteInput(
scriptAst,
currentTokens,
cursorPosition,
options: null,
powershell: pwsh);
},
cancellationToken).ConfigureAwait(false);
stopwatch.Stop();
if (commandCompletion is null)
{
logger.LogError("Error Occurred in TabExpansion2");
}
else
{
logger.LogTrace(
"IntelliSense completed in {elapsed}ms - WordToComplete: \"{word}\" MatchCount: {count}",
stopwatch.ElapsedMilliseconds,
commandCompletion.ReplacementLength > 0
? scriptAst.Extent.StartScriptPosition.GetFullScript()?.Substring(
commandCompletion.ReplacementIndex,
commandCompletion.ReplacementLength)
: null,
commandCompletion.CompletionMatches.Count);
}
return commandCompletion;
}
internal static bool TryGetInferredValue(ExpandableStringExpressionAst expandableStringExpressionAst, out string value)
{
// Currently we only support inferring the value of `$PSScriptRoot`. We could potentially
// expand this to parts of `$MyInvocation` and some basic constant folding.
if (string.IsNullOrEmpty(expandableStringExpressionAst.Extent.File))
{
value = null;
return false;
}
string psScriptRoot = System.IO.Path.GetDirectoryName(expandableStringExpressionAst.Extent.File);
if (string.IsNullOrEmpty(psScriptRoot))
{
value = null;
return false;
}
string path = expandableStringExpressionAst.Value;
foreach (ExpressionAst nestedExpression in expandableStringExpressionAst.NestedExpressions)
{
// If the string contains the variable $PSScriptRoot, we replace it with the corresponding value.
if (!(nestedExpression is VariableExpressionAst variableAst
&& variableAst.VariablePath.UserPath.Equals("PSScriptRoot", StringComparison.OrdinalIgnoreCase)))
{
value = null;
return false;
}
// TODO: This should use offsets from the extent rather than a blind replace. In
// practice it won't hurt anything because $ is not valid in paths, but if we expand
// this functionality, this will be problematic.
path = path.Replace(variableAst.ToString(), psScriptRoot);
}
value = path;
return true;
}
}
}