Skip to content

Commit 67d8798

Browse files
committed
Fixup Tests, still a bug in rename logic with functions
1 parent 1081a49 commit 67d8798

File tree

6 files changed

+225
-101
lines changed

6 files changed

+225
-101
lines changed
Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
using System.Management.Automation.Language;
5-
using Microsoft.PowerShell.EditorServices.Services;
4+
// using System.Management.Automation.Language;
5+
// using Microsoft.PowerShell.EditorServices.Services;
66

7-
namespace PowerShellEditorServices.Services.PowerShell.Utility
8-
{
9-
public static class IScriptExtentExtensions
10-
{
11-
public static bool Contains(this IScriptExtent extent, ScriptPositionAdapter position) => ScriptExtentAdapter.ContainsPosition(new(extent), position);
12-
}
13-
}
7+
// namespace PowerShellEditorServices.Services.PowerShell.Utility
8+
// {
9+
// public static class IScriptExtentExtensions
10+
// {
11+
// public static bool Contains(this IScriptExtent extent, IScriptExtent position)
12+
// => ScriptExtentAdapter.ContainsPosition(extent, position);
13+
// }
14+
// }

src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs

Lines changed: 106 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ ILanguageServerConfiguration config
7070
}
7171

7272
ScriptPositionAdapter position = request.Position;
73-
Ast target = FindRenamableSymbol(scriptFile, position);
73+
Ast? target = FindRenamableSymbol(scriptFile, position);
7474
if (target is null) { return null; }
75+
76+
// Will implicitly convert to RangeOrPlaceholder and adjust to 0-based
7577
return target switch
7678
{
7779
FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst),
@@ -85,7 +87,7 @@ ILanguageServerConfiguration config
8587
ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri);
8688
ScriptPositionAdapter position = request.Position;
8789

88-
Ast tokenToRename = FindRenamableSymbol(scriptFile, position);
90+
Ast? tokenToRename = FindRenamableSymbol(scriptFile, position);
8991
if (tokenToRename is null) { return null; }
9092

9193
// TODO: Potentially future cross-file support
@@ -151,55 +153,35 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam
151153
return [];
152154
}
153155

154-
155156
/// <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
157158
/// </summary>
158159
/// <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)
160161
{
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)
166176
{
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+
}
199180

200-
return token;
181+
return ast;
201182
}
202183

184+
203185
private static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast)
204186
{
205187
string name = ast.Name;
@@ -221,6 +203,63 @@ public class RenameSymbolOptions
221203
public bool CreateAlias { get; set; }
222204
}
223205

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+
224263
internal class Utilities
225264
{
226265
public static Ast? GetAstAtPositionOfType(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type)
@@ -385,10 +424,10 @@ public static bool AssertContainsDotSourced(Ast ScriptAst)
385424
public record ScriptPositionAdapter(IScriptPosition position) : IScriptPosition, IComparable<ScriptPositionAdapter>, IComparable<Position>, IComparable<ScriptPosition>
386425
{
387426
public int Line => position.LineNumber;
388-
public int Column => position.ColumnNumber;
389-
public int Character => position.ColumnNumber;
390427
public int LineNumber => position.LineNumber;
428+
public int Column => position.ColumnNumber;
391429
public int ColumnNumber => position.ColumnNumber;
430+
public int Character => position.ColumnNumber;
392431

393432
public string File => position.File;
394433
string IScriptPosition.Line => position.Line;
@@ -457,13 +496,32 @@ internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent
457496
public IScriptPosition EndScriptPosition => End;
458497
public int EndColumnNumber => End.ColumnNumber;
459498
public int EndLineNumber => End.LineNumber;
460-
public int StartOffset => extent.EndOffset;
499+
public int StartOffset => extent.StartOffset;
461500
public int EndOffset => extent.EndOffset;
462501
public string File => extent.File;
463502
public int StartColumnNumber => extent.StartColumnNumber;
464503
public int StartLineNumber => extent.StartLineNumber;
465504
public string Text => extent.Text;
466505

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+
}
469527
}

test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public class RefactorFunctionTestCases
77
{
88
public static RenameTestTarget[] TestCases =
99
[
10-
new("FunctionsSingle.ps1", Line: 1, Column: 5 ),
10+
new("FunctionsSingle.ps1", Line: 1, Column: 11 ),
1111
new("FunctionMultipleOccurrences.ps1", Line: 1, Column: 5 ),
1212
new("FunctionInnerIsNested.ps1", Line: 5, Column: 5, "bar"),
1313
new("FunctionOuterHasNestedFunction.ps1", Line: 10, Column: 1 ),
@@ -19,7 +19,7 @@ public class RefactorFunctionTestCases
1919
new("FunctionLoop.ps1", Line: 5, Column: 5 ),
2020
new("FunctionForeach.ps1", Line: 5, Column: 11 ),
2121
new("FunctionForeachObject.ps1", Line: 5, Column: 11 ),
22-
new("FunctionCallWIthinStringExpression.ps1", Line: 10, Column: 1 ),
23-
new("FunctionNestedRedefinition.ps1", Line: 15, Column: 13 )
22+
new("FunctionCallWIthinStringExpression.ps1", Line: 1, Column: 10 ),
23+
new("FunctionNestedRedefinition.ps1", Line: 13, Column: 15 )
2424
];
2525
}

test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,37 @@ namespace PowerShellEditorServices.Test.Shared.Refactoring;
88
/// <summary>
99
/// Describes a test case for renaming a file
1010
/// </summary>
11-
/// <param name="FileName">The test case file name e.g. testScript.ps1</param>
12-
/// <param name="Line">The line where the cursor should be positioned for the rename</param>
13-
/// <param name="Column">The column/character indent where ther cursor should be positioned for the rename</param>
14-
/// <param name="NewName">What the target symbol represented by the line and column should be renamed to. Defaults to "Renamed" if not specified</param>
15-
public record RenameTestTarget(string FileName = "UNKNOWN", int Line = -1, int Column = -1, string NewName = "Renamed")
11+
public class RenameTestTarget
1612
{
13+
/// <summary>
14+
/// The test case file name e.g. testScript.ps1
15+
/// </summary>
16+
public string FileName { get; set; } = "UNKNOWN";
17+
/// <summary>
18+
/// The line where the cursor should be positioned for the rename
19+
/// </summary>
20+
public int Line { get; set; } = -1;
21+
/// <summary>
22+
/// The column/character indent where ther cursor should be positioned for the rename
23+
/// </summary>
24+
public int Column { get; set; } = -1;
25+
/// <summary>
26+
/// What the target symbol represented by the line and column should be renamed to. Defaults to "Renamed" if not specified
27+
/// </summary>
28+
public string NewName = "Renamed";
29+
30+
/// <param name="FileName">The test case file name e.g. testScript.ps1</param>
31+
/// <param name="Line">The line where the cursor should be positioned for the rename</param>
32+
/// <param name="Column">The column/character indent where ther cursor should be positioned for the rename</param>
33+
/// <param name="NewName">What the target symbol represented by the line and column should be renamed to. Defaults to "Renamed" if not specified</param>
34+
public RenameTestTarget(string FileName, int Line, int Column, string NewName = "Renamed")
35+
{
36+
this.FileName = FileName;
37+
this.Line = Line;
38+
this.Column = Column;
39+
this.NewName = NewName;
40+
}
41+
public RenameTestTarget() { }
42+
1743
public override string ToString() => $"{FileName.Substring(0, FileName.Length - 4)}";
1844
}

0 commit comments

Comments
 (0)