@@ -70,8 +70,10 @@ ILanguageServerConfiguration config
70
70
}
71
71
72
72
ScriptPositionAdapter position = request . Position ;
73
- Ast target = FindRenamableSymbol ( scriptFile , position ) ;
73
+ Ast ? target = FindRenamableSymbol ( scriptFile , position ) ;
74
74
if ( target is null ) { return null ; }
75
+
76
+ // Will implicitly convert to RangeOrPlaceholder and adjust to 0-based
75
77
return target switch
76
78
{
77
79
FunctionDefinitionAst funcAst => GetFunctionNameExtent ( funcAst ) ,
@@ -85,7 +87,7 @@ ILanguageServerConfiguration config
85
87
ScriptFile scriptFile = workspaceService . GetFile ( request . TextDocument . Uri ) ;
86
88
ScriptPositionAdapter position = request . Position ;
87
89
88
- Ast tokenToRename = FindRenamableSymbol ( scriptFile , position ) ;
90
+ Ast ? tokenToRename = FindRenamableSymbol ( scriptFile , position ) ;
89
91
if ( tokenToRename is null ) { return null ; }
90
92
91
93
// TODO: Potentially future cross-file support
@@ -151,55 +153,35 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam
151
153
return [ ] ;
152
154
}
153
155
154
-
155
156
/// <summary>
156
- /// Finds a renamable symbol at a given position in a script file.
157
+ /// Finds the most specific renamable symbol at the given position
157
158
/// </summary>
158
159
/// <returns>Ast of the token or null if no renamable symbol was found</returns>
159
- internal static Ast FindRenamableSymbol ( ScriptFile scriptFile , ScriptPositionAdapter position )
160
+ internal static Ast ? FindRenamableSymbol ( ScriptFile scriptFile , ScriptPositionAdapter position )
160
161
{
161
- int line = position . Line ;
162
- int column = position . Column ;
163
-
164
- // Cannot use generic here as our desired ASTs do not share a common parent
165
- Ast token = scriptFile . ScriptAst . Find ( ast =>
162
+ Ast ? ast = scriptFile . ScriptAst . FindAtPosition ( position ,
163
+ [
164
+ // Filters just the ASTs that are candidates for rename
165
+ typeof ( FunctionDefinitionAst ) ,
166
+ typeof ( VariableExpressionAst ) ,
167
+ typeof ( CommandParameterAst ) ,
168
+ typeof ( ParameterAst ) ,
169
+ typeof ( StringConstantExpressionAst ) ,
170
+ typeof ( CommandAst )
171
+ ] ) ;
172
+
173
+ // Special detection for Function calls that dont follow verb-noun syntax e.g. DoThing
174
+ // It's not foolproof but should work in most cases where it is explicit (e.g. not & $x)
175
+ if ( ast is StringConstantExpressionAst stringAst )
166
176
{
167
- // Skip all statements that end before our target line or start after our target line. This is a performance optimization.
168
- if ( ast . Extent . EndLineNumber < line || ast . Extent . StartLineNumber > line ) { return false ; }
169
-
170
- // Supported types, filters out scriptblocks and whatnot
171
- if ( ast is not (
172
- FunctionDefinitionAst
173
- or VariableExpressionAst
174
- or CommandParameterAst
175
- or ParameterAst
176
- or StringConstantExpressionAst
177
- or CommandAst
178
- ) )
179
- {
180
- return false ;
181
- }
182
-
183
- // Special detection for Function calls that dont follow verb-noun syntax e.g. DoThing
184
- // It's not foolproof but should work in most cases where it is explicit (e.g. not & $x)
185
- if ( ast is StringConstantExpressionAst stringAst )
186
- {
187
- if ( stringAst . Parent is not CommandAst parent ) { return false ; }
188
- if ( parent . GetCommandName ( ) != stringAst . Value ) { return false ; }
189
- }
190
-
191
- ScriptExtentAdapter target = ast switch
192
- {
193
- FunctionDefinitionAst funcAst => GetFunctionNameExtent ( funcAst ) ,
194
- _ => new ScriptExtentAdapter ( ast . Extent )
195
- } ;
196
-
197
- return target . Contains ( position ) ;
198
- } , true ) ;
177
+ if ( stringAst . Parent is not CommandAst parent ) { return null ; }
178
+ if ( parent . GetCommandName ( ) != stringAst . Value ) { return null ; }
179
+ }
199
180
200
- return token ;
181
+ return ast ;
201
182
}
202
183
184
+
203
185
private static ScriptExtentAdapter GetFunctionNameExtent ( FunctionDefinitionAst ast )
204
186
{
205
187
string name = ast . Name ;
@@ -221,6 +203,63 @@ public class RenameSymbolOptions
221
203
public bool CreateAlias { get ; set ; }
222
204
}
223
205
206
+
207
+ public static class AstExtensions
208
+ {
209
+ /// <summary>
210
+ /// Finds the most specific Ast at the given script position, or returns null if none found.<br/>
211
+ /// For example, if the position is on a variable expression within a function definition,
212
+ /// the variable will be returned even if the function definition is found first.
213
+ /// </summary>
214
+ internal static Ast ? FindAtPosition ( this Ast ast , IScriptPosition position , Type [ ] ? allowedTypes )
215
+ {
216
+ // Short circuit quickly if the position is not in the provided range, no need to traverse if not
217
+ // TODO: Maybe this should be an exception instead? I mean technically its not found but if you gave a position outside the file something very wrong probably happened.
218
+ if ( ! new ScriptExtentAdapter ( ast . Extent ) . Contains ( position ) ) { return null ; }
219
+
220
+ // This will be updated with each loop, and re-Find to dig deeper
221
+ Ast ? mostSpecificAst = null ;
222
+
223
+ do
224
+ {
225
+ ast = ast . Find ( currentAst =>
226
+ {
227
+ if ( currentAst == mostSpecificAst ) { return false ; }
228
+
229
+ int line = position . LineNumber ;
230
+ int column = position . ColumnNumber ;
231
+
232
+ // Performance optimization, skip statements that don't contain the position
233
+ if (
234
+ currentAst . Extent . EndLineNumber < line
235
+ || currentAst . Extent . StartLineNumber > line
236
+ || ( currentAst . Extent . EndLineNumber == line && currentAst . Extent . EndColumnNumber < column )
237
+ || ( currentAst . Extent . StartLineNumber == line && currentAst . Extent . StartColumnNumber > column )
238
+ )
239
+ {
240
+ return false ;
241
+ }
242
+
243
+ if ( allowedTypes is not null && ! allowedTypes . Contains ( currentAst . GetType ( ) ) )
244
+ {
245
+ return false ;
246
+ }
247
+
248
+ if ( new ScriptExtentAdapter ( currentAst . Extent ) . Contains ( position ) )
249
+ {
250
+ mostSpecificAst = currentAst ;
251
+ return true ; //Stops the find
252
+ }
253
+
254
+ return false ;
255
+ } , true ) ;
256
+ } while ( ast is not null ) ;
257
+
258
+ return mostSpecificAst ;
259
+ }
260
+
261
+ }
262
+
224
263
internal class Utilities
225
264
{
226
265
public static Ast ? GetAstAtPositionOfType ( int StartLineNumber , int StartColumnNumber , Ast ScriptAst , params Type [ ] type )
@@ -385,10 +424,10 @@ public static bool AssertContainsDotSourced(Ast ScriptAst)
385
424
public record ScriptPositionAdapter ( IScriptPosition position ) : IScriptPosition , IComparable < ScriptPositionAdapter > , IComparable < Position > , IComparable < ScriptPosition >
386
425
{
387
426
public int Line => position . LineNumber ;
388
- public int Column => position . ColumnNumber ;
389
- public int Character => position . ColumnNumber ;
390
427
public int LineNumber => position . LineNumber ;
428
+ public int Column => position . ColumnNumber ;
391
429
public int ColumnNumber => position . ColumnNumber ;
430
+ public int Character => position . ColumnNumber ;
392
431
393
432
public string File => position . File ;
394
433
string IScriptPosition . Line => position . Line ;
@@ -457,13 +496,32 @@ internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent
457
496
public IScriptPosition EndScriptPosition => End ;
458
497
public int EndColumnNumber => End . ColumnNumber ;
459
498
public int EndLineNumber => End . LineNumber ;
460
- public int StartOffset => extent . EndOffset ;
499
+ public int StartOffset => extent . StartOffset ;
461
500
public int EndOffset => extent . EndOffset ;
462
501
public string File => extent . File ;
463
502
public int StartColumnNumber => extent . StartColumnNumber ;
464
503
public int StartLineNumber => extent . StartLineNumber ;
465
504
public string Text => extent . Text ;
466
505
467
- public bool Contains ( Position position ) => ContainsPosition ( this , position ) ;
468
- public static bool ContainsPosition ( ScriptExtentAdapter range , ScriptPositionAdapter position ) => Range . ContainsPosition ( range , position ) ;
506
+ public bool Contains ( IScriptPosition position ) => Contains ( ( ScriptPositionAdapter ) position ) ;
507
+
508
+ public bool Contains ( ScriptPositionAdapter position )
509
+ {
510
+ if ( position . Line < Start . Line || position . Line > End . Line )
511
+ {
512
+ return false ;
513
+ }
514
+
515
+ if ( position . Line == Start . Line && position . Character < Start . Character )
516
+ {
517
+ return false ;
518
+ }
519
+
520
+ if ( position . Line == End . Line && position . Character > End . Character )
521
+ {
522
+ return false ;
523
+ }
524
+
525
+ return true ;
526
+ }
469
527
}
0 commit comments