|
2 | 2 | // Licensed under the MIT License.
|
3 | 3 | #nullable enable
|
4 | 4 |
|
5 |
| -using System; |
6 |
| -using System.Collections.Generic; |
7 |
| -using System.Management.Automation.Language; |
| 5 | + |
8 | 6 | using System.Threading;
|
9 | 7 | using System.Threading.Tasks;
|
10 | 8 | using Microsoft.PowerShell.EditorServices.Services;
|
11 |
| -using Microsoft.PowerShell.EditorServices.Services.TextDocument; |
12 |
| -using Microsoft.PowerShell.EditorServices.Refactoring; |
| 9 | + |
13 | 10 | using OmniSharp.Extensions.LanguageServer.Protocol.Document;
|
14 | 11 | using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
15 | 12 | using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
|
16 |
| -using OmniSharp.Extensions.LanguageServer.Protocol; |
17 |
| -using OmniSharp.Extensions.LanguageServer.Protocol.Server; |
| 13 | + |
18 | 14 |
|
19 | 15 | namespace Microsoft.PowerShell.EditorServices.Handlers;
|
20 | 16 |
|
21 | 17 | /// <summary>
|
22 | 18 | /// A handler for <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_prepareRename">textDocument/prepareRename</a>
|
23 | 19 | /// LSP Ref: <see cref="PrepareRename()"/>
|
24 | 20 | /// </summary>
|
25 |
| -internal class PrepareRenameHandler(WorkspaceService workspaceService, ILanguageServerFacade lsp, ILanguageServerConfiguration config) : IPrepareRenameHandler |
| 21 | +internal class PrepareRenameHandler |
| 22 | +( |
| 23 | + IRenameService renameService |
| 24 | +) : IPrepareRenameHandler |
26 | 25 | {
|
27 | 26 | public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new();
|
28 | 27 |
|
29 | 28 | public async Task<RangeOrPlaceholderRange?> Handle(PrepareRenameParams request, CancellationToken cancellationToken)
|
30 |
| - { |
31 |
| - // FIXME: Config actually needs to be read and implemented, this is to make the referencing satisfied |
32 |
| - config.ToString(); |
33 |
| - ShowMessageRequestParams reqParams = new ShowMessageRequestParams |
34 |
| - { |
35 |
| - Type = MessageType.Warning, |
36 |
| - Message = "Test Send", |
37 |
| - Actions = new MessageActionItem[] { |
38 |
| - new MessageActionItem() { Title = "I Accept" }, |
39 |
| - new MessageActionItem() { Title = "I Accept [Workspace]" }, |
40 |
| - new MessageActionItem() { Title = "Decline" } |
41 |
| - } |
42 |
| - }; |
43 |
| - |
44 |
| - MessageActionItem result = await lsp.SendRequest(reqParams, cancellationToken).ConfigureAwait(false); |
45 |
| - if (result.Title == "Test Action") |
46 |
| - { |
47 |
| - // FIXME: Need to accept |
48 |
| - Console.WriteLine("yay"); |
49 |
| - } |
50 |
| - |
51 |
| - ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); |
52 |
| - |
53 |
| - // TODO: Is this too aggressive? We can still rename inside a var/function even if dotsourcing is in use in a file, we just need to be clear it's not supported to take rename actions inside the dotsourced file. |
54 |
| - if (Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)) |
55 |
| - { |
56 |
| - throw new HandlerErrorException("Dot Source detected, this is currently not supported"); |
57 |
| - } |
58 |
| - |
59 |
| - ScriptPositionAdapter position = request.Position; |
60 |
| - Ast target = FindRenamableSymbol(scriptFile, position); |
61 |
| - if (target is null) { return null; } |
62 |
| - return target switch |
63 |
| - { |
64 |
| - FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst), |
65 |
| - _ => new ScriptExtentAdapter(target.Extent) |
66 |
| - }; |
67 |
| - } |
68 |
| - |
69 |
| - private static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast) |
70 |
| - { |
71 |
| - string name = ast.Name; |
72 |
| - // FIXME: Gather dynamically from the AST and include backticks and whatnot that might be present |
73 |
| - int funcLength = "function ".Length; |
74 |
| - ScriptExtentAdapter funcExtent = new(ast.Extent); |
75 |
| - |
76 |
| - // Get a range that represents only the function name |
77 |
| - return funcExtent with |
78 |
| - { |
79 |
| - Start = funcExtent.Start.Delta(0, funcLength), |
80 |
| - End = funcExtent.Start.Delta(0, funcLength + name.Length) |
81 |
| - }; |
82 |
| - } |
83 |
| - |
84 |
| - /// <summary> |
85 |
| - /// Finds a renamable symbol at a given position in a script file. |
86 |
| - /// </summary> |
87 |
| - /// <returns>Ast of the token or null if no renamable symbol was found</returns> |
88 |
| - internal static Ast FindRenamableSymbol(ScriptFile scriptFile, ScriptPositionAdapter position) |
89 |
| - { |
90 |
| - int line = position.Line; |
91 |
| - int column = position.Column; |
92 |
| - |
93 |
| - // Cannot use generic here as our desired ASTs do not share a common parent |
94 |
| - Ast token = scriptFile.ScriptAst.Find(ast => |
95 |
| - { |
96 |
| - // Skip all statements that end before our target line or start after our target line. This is a performance optimization. |
97 |
| - if (ast.Extent.EndLineNumber < line || ast.Extent.StartLineNumber > line) { return false; } |
98 |
| - |
99 |
| - // Supported types, filters out scriptblocks and whatnot |
100 |
| - if (ast is not ( |
101 |
| - FunctionDefinitionAst |
102 |
| - or VariableExpressionAst |
103 |
| - or CommandParameterAst |
104 |
| - or ParameterAst |
105 |
| - or StringConstantExpressionAst |
106 |
| - or CommandAst |
107 |
| - )) |
108 |
| - { |
109 |
| - return false; |
110 |
| - } |
111 |
| - |
112 |
| - // Special detection for Function calls that dont follow verb-noun syntax e.g. DoThing |
113 |
| - // It's not foolproof but should work in most cases where it is explicit (e.g. not & $x) |
114 |
| - if (ast is StringConstantExpressionAst stringAst) |
115 |
| - { |
116 |
| - if (stringAst.Parent is not CommandAst parent) { return false; } |
117 |
| - if (parent.GetCommandName() != stringAst.Value) { return false; } |
118 |
| - } |
119 |
| - |
120 |
| - ScriptExtentAdapter target = ast switch |
121 |
| - { |
122 |
| - FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst), |
123 |
| - _ => new ScriptExtentAdapter(ast.Extent) |
124 |
| - }; |
125 |
| - |
126 |
| - return target.Contains(position); |
127 |
| - }, true); |
128 |
| - |
129 |
| - return token; |
130 |
| - } |
| 29 | + => await renameService.PrepareRenameSymbol(request, cancellationToken).ConfigureAwait(false); |
131 | 30 | }
|
132 | 31 |
|
133 | 32 | /// <summary>
|
134 | 33 | /// A handler for textDocument/prepareRename
|
135 | 34 | /// <para />LSP Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename
|
136 | 35 | /// </summary>
|
137 |
| -internal class RenameHandler(WorkspaceService workspaceService) : IRenameHandler |
| 36 | +internal class RenameHandler( |
| 37 | + IRenameService renameService |
| 38 | +) : IRenameHandler |
138 | 39 | {
|
139 | 40 | // RenameOptions may only be specified if the client states that it supports prepareSupport in its initial initialize request.
|
140 | 41 | public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new();
|
141 | 42 |
|
142 | 43 | public async Task<WorkspaceEdit?> Handle(RenameParams request, CancellationToken cancellationToken)
|
143 |
| - { |
144 |
| - ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); |
145 |
| - ScriptPositionAdapter position = request.Position; |
146 |
| - |
147 |
| - Ast tokenToRename = PrepareRenameHandler.FindRenamableSymbol(scriptFile, position); |
148 |
| - if (tokenToRename is null) { return null; } |
149 |
| - |
150 |
| - // TODO: Potentially future cross-file support |
151 |
| - TextEdit[] changes = tokenToRename switch |
152 |
| - { |
153 |
| - FunctionDefinitionAst or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), |
154 |
| - VariableExpressionAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request), |
155 |
| - // FIXME: Only throw if capability is not prepareprovider |
156 |
| - _ => throw new HandlerErrorException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") |
157 |
| - }; |
158 |
| - |
159 |
| - return new WorkspaceEdit |
160 |
| - { |
161 |
| - Changes = new Dictionary<DocumentUri, IEnumerable<TextEdit>> |
162 |
| - { |
163 |
| - [request.TextDocument.Uri] = changes |
164 |
| - } |
165 |
| - }; |
166 |
| - } |
167 |
| - |
168 |
| - // TODO: We can probably merge these two methods with Generic Type constraints since they are factored into overloading |
169 |
| - |
170 |
| - internal static TextEdit[] RenameFunction(Ast token, Ast scriptAst, RenameParams renameParams) |
171 |
| - { |
172 |
| - ScriptPositionAdapter position = renameParams.Position; |
173 |
| - |
174 |
| - string tokenName = ""; |
175 |
| - if (token is FunctionDefinitionAst funcDef) |
176 |
| - { |
177 |
| - tokenName = funcDef.Name; |
178 |
| - } |
179 |
| - else if (token.Parent is CommandAst CommAst) |
180 |
| - { |
181 |
| - tokenName = CommAst.GetCommandName(); |
182 |
| - } |
183 |
| - IterativeFunctionRename visitor = new( |
184 |
| - tokenName, |
185 |
| - renameParams.NewName, |
186 |
| - position.Line, |
187 |
| - position.Column, |
188 |
| - scriptAst |
189 |
| - ); |
190 |
| - visitor.Visit(scriptAst); |
191 |
| - return visitor.Modifications.ToArray(); |
192 |
| - } |
193 |
| - |
194 |
| - internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) |
195 |
| - { |
196 |
| - if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) |
197 |
| - { |
198 |
| - |
199 |
| - IterativeVariableRename visitor = new( |
200 |
| - requestParams.NewName, |
201 |
| - symbol.Extent.StartLineNumber, |
202 |
| - symbol.Extent.StartColumnNumber, |
203 |
| - scriptAst, |
204 |
| - null //FIXME: Pass through Alias config |
205 |
| - ); |
206 |
| - visitor.Visit(scriptAst); |
207 |
| - return visitor.Modifications.ToArray(); |
208 |
| - |
209 |
| - } |
210 |
| - return []; |
211 |
| - } |
212 |
| -} |
213 |
| - |
214 |
| -public class RenameSymbolOptions |
215 |
| -{ |
216 |
| - public bool CreateAlias { get; set; } |
217 |
| -} |
218 |
| - |
219 |
| -/// <summary> |
220 |
| -/// Represents a position in a script file that adapts and implicitly converts based on context. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. The default line/column constructor is 1-based. |
221 |
| -/// </summary> |
222 |
| -public record ScriptPositionAdapter(IScriptPosition position) : IScriptPosition, IComparable<ScriptPositionAdapter>, IComparable<Position>, IComparable<ScriptPosition> |
223 |
| -{ |
224 |
| - public int Line => position.LineNumber; |
225 |
| - public int Column => position.ColumnNumber; |
226 |
| - public int Character => position.ColumnNumber; |
227 |
| - public int LineNumber => position.LineNumber; |
228 |
| - public int ColumnNumber => position.ColumnNumber; |
229 |
| - |
230 |
| - public string File => position.File; |
231 |
| - string IScriptPosition.Line => position.Line; |
232 |
| - public int Offset => position.Offset; |
233 |
| - |
234 |
| - public ScriptPositionAdapter(int Line, int Column) : this(new ScriptPosition(null, Line, Column, null)) { } |
235 |
| - public ScriptPositionAdapter(ScriptPosition position) : this((IScriptPosition)position) { } |
236 |
| - |
237 |
| - public ScriptPositionAdapter(Position position) : this(position.Line + 1, position.Character + 1) { } |
238 |
| - public static implicit operator ScriptPositionAdapter(Position position) => new(position); |
239 |
| - public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new |
240 |
| - ( |
241 |
| - scriptPosition.position.LineNumber - 1, scriptPosition.position.ColumnNumber - 1 |
242 |
| - ); |
243 |
| - |
244 |
| - |
245 |
| - public static implicit operator ScriptPositionAdapter(ScriptPosition position) => new(position); |
246 |
| - public static implicit operator ScriptPosition(ScriptPositionAdapter position) => position; |
247 |
| - |
248 |
| - internal ScriptPositionAdapter Delta(int LineAdjust, int ColumnAdjust) => new( |
249 |
| - position.LineNumber + LineAdjust, |
250 |
| - position.ColumnNumber + ColumnAdjust |
251 |
| - ); |
252 |
| - |
253 |
| - public int CompareTo(ScriptPositionAdapter other) |
254 |
| - { |
255 |
| - if (position.LineNumber == other.position.LineNumber) |
256 |
| - { |
257 |
| - return position.ColumnNumber.CompareTo(other.position.ColumnNumber); |
258 |
| - } |
259 |
| - return position.LineNumber.CompareTo(other.position.LineNumber); |
260 |
| - } |
261 |
| - public int CompareTo(Position other) => CompareTo((ScriptPositionAdapter)other); |
262 |
| - public int CompareTo(ScriptPosition other) => CompareTo((ScriptPositionAdapter)other); |
263 |
| - public string GetFullScript() => throw new NotImplementedException(); |
264 |
| -} |
265 |
| - |
266 |
| -/// <summary> |
267 |
| -/// Represents a range in a script file that adapts and implicitly converts based on context. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. The default ScriptExtent constructor is 1-based |
268 |
| -/// </summary> |
269 |
| -/// <param name="extent"></param> |
270 |
| -internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent |
271 |
| -{ |
272 |
| - public ScriptPositionAdapter Start = new(extent.StartScriptPosition); |
273 |
| - public ScriptPositionAdapter End = new(extent.EndScriptPosition); |
274 |
| - |
275 |
| - public static implicit operator ScriptExtentAdapter(ScriptExtent extent) => new(extent); |
276 |
| - |
277 |
| - public static implicit operator ScriptExtentAdapter(Range range) => new(new ScriptExtent( |
278 |
| - // Will get shifted to 1-based |
279 |
| - new ScriptPositionAdapter(range.Start), |
280 |
| - new ScriptPositionAdapter(range.End) |
281 |
| - )); |
282 |
| - public static implicit operator Range(ScriptExtentAdapter adapter) => new() |
283 |
| - { |
284 |
| - // Will get shifted to 0-based |
285 |
| - Start = adapter.Start, |
286 |
| - End = adapter.End |
287 |
| - }; |
288 |
| - |
289 |
| - public static implicit operator ScriptExtent(ScriptExtentAdapter adapter) => adapter; |
290 |
| - |
291 |
| - public static implicit operator RangeOrPlaceholderRange(ScriptExtentAdapter adapter) => new((Range)adapter); |
292 |
| - |
293 |
| - public IScriptPosition StartScriptPosition => Start; |
294 |
| - public IScriptPosition EndScriptPosition => End; |
295 |
| - public int EndColumnNumber => End.ColumnNumber; |
296 |
| - public int EndLineNumber => End.LineNumber; |
297 |
| - public int StartOffset => extent.EndOffset; |
298 |
| - public int EndOffset => extent.EndOffset; |
299 |
| - public string File => extent.File; |
300 |
| - public int StartColumnNumber => extent.StartColumnNumber; |
301 |
| - public int StartLineNumber => extent.StartLineNumber; |
302 |
| - public string Text => extent.Text; |
303 |
| - |
304 |
| - public bool Contains(Position position) => ContainsPosition(this, position); |
305 |
| - public static bool ContainsPosition(ScriptExtentAdapter range, ScriptPositionAdapter position) => Range.ContainsPosition(range, position); |
| 44 | + => await renameService.RenameSymbol(request, cancellationToken).ConfigureAwait(false); |
306 | 45 | }
|
0 commit comments