Skip to content

Commit e33ba46

Browse files
committed
(PowerShellGH-813) Use AST for code folding
The AST contains the most correct version of how a script is interpreted. This includes regions of text. Currently the code folder only uses the Tokens which requires the folder to re-implement some of the AST behaviour e.g. matching token pairs for arrays etc. The code folder should be implemented using as much of the AST as possible. This commit; * Moves most of the region detection to use the AST Extents and uses a new FindFoldsASTVisitor. * Modifies the tests and language server to use the new method fold detection class. * Moved the code to modify the end line of folding regions to the language server code.
1 parent e3179e4 commit e33ba46

File tree

6 files changed

+314
-246
lines changed

6 files changed

+314
-246
lines changed

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+11-7
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ private async Task HandleGetCommandRequestAsync(
535535
{
536536
PSCommand psCommand = new PSCommand();
537537
if (!string.IsNullOrEmpty(param))
538-
{
538+
{
539539
psCommand.AddCommand("Microsoft.PowerShell.Core\\Get-Command").AddArgument(param);
540540
}
541541
else
@@ -1267,7 +1267,7 @@ protected async Task HandleCodeActionRequest(
12671267
}
12681268
}
12691269

1270-
// Add "show documentation" commands last so they appear at the bottom of the client UI.
1270+
// Add "show documentation" commands last so they appear at the bottom of the client UI.
12711271
// These commands do not require code fixes. Sometimes we get a batch of diagnostics
12721272
// to create commands for. No need to create multiple show doc commands for the same rule.
12731273
var ruleNamesProcessed = new HashSet<string>();
@@ -1382,13 +1382,17 @@ private FoldingRange[] Fold(
13821382
// TODO Should be using dynamic registrations
13831383
if (!this.currentSettings.CodeFolding.Enable) { return null; }
13841384
var result = new List<FoldingRange>();
1385-
foreach (FoldingReference fold in TokenOperations.FoldableRegions(
1386-
editorSession.Workspace.GetFile(documentUri).ScriptTokens,
1387-
this.currentSettings.CodeFolding.ShowLastLine))
1385+
ScriptFile script = editorSession.Workspace.GetFile(documentUri);
1386+
int endLineOffset = 0;
1387+
// If we're showing the last line, decrement the Endline of all regions by one.
1388+
if (this.currentSettings.CodeFolding.ShowLastLine) { endLineOffset = -1; }
1389+
foreach (FoldingReference fold in FoldingOperations.FoldableRegions(
1390+
script.ScriptTokens,
1391+
script.ScriptAst))
13881392
{
13891393
result.Add(new FoldingRange {
13901394
EndCharacter = fold.EndCharacter,
1391-
EndLine = fold.EndLine,
1395+
EndLine = fold.EndLine + endLineOffset,
13921396
Kind = fold.Kind,
13931397
StartCharacter = fold.StartCharacter,
13941398
StartLine = fold.StartLine
@@ -1734,7 +1738,7 @@ await eventSender(
17341738
});
17351739
}
17361740

1737-
// Generate a unique id that is used as a key to look up the associated code action (code fix) when
1741+
// Generate a unique id that is used as a key to look up the associated code action (code fix) when
17381742
// we receive and process the textDocument/codeAction message.
17391743
private static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic)
17401744
{

src/PowerShellEditorServices/Language/AstOperations.cs

+14
Original file line numberDiff line numberDiff line change
@@ -330,5 +330,19 @@ static public string[] FindDotSourcedIncludes(Ast scriptAst, string psScriptRoot
330330

331331
return dotSourcedVisitor.DotSourcedFiles.ToArray();
332332
}
333+
334+
/// <summary>
335+
/// Finds all foldable regions in a script based on AST
336+
/// </summary>
337+
/// <param name="scriptAst">The abstract syntax tree of the given script</param>
338+
/// <returns>A collection of FoldingReference objects</returns>
339+
public static IEnumerable<FoldingReference> FindFoldsInDocument(Ast scriptAst)
340+
{
341+
FindFoldsVisitor findFoldsVisitor = new FindFoldsVisitor();
342+
scriptAst.Visit(findFoldsVisitor);
343+
344+
return findFoldsVisitor.FoldableRegions;
345+
}
346+
333347
}
334348
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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 System;
7+
using System.Collections.Generic;
8+
using System.Management.Automation.Language;
9+
10+
namespace Microsoft.PowerShell.EditorServices
11+
{
12+
/// <summary>
13+
/// The visitor used to find the all folding regions in an AST
14+
/// </summary>
15+
internal class FindFoldsVisitor : AstVisitor
16+
{
17+
private const string RegionKindNone = null;
18+
19+
public List<FoldingReference> FoldableRegions { get; }
20+
21+
public FindFoldsVisitor()
22+
{
23+
this.FoldableRegions = new List<FoldingReference>();
24+
}
25+
26+
/// <summary>
27+
/// Returns whether an Extent could be used as a valid folding region
28+
/// </summary>
29+
private bool IsValidFoldingExtent(
30+
IScriptExtent extent)
31+
{
32+
// The extent must span at least one line
33+
return extent.EndLineNumber > extent.StartLineNumber;
34+
}
35+
36+
/// <summary>
37+
/// Creates an instance of a FoldingReference object from a script extent
38+
/// </summary>
39+
private FoldingReference CreateFoldingReference(
40+
IScriptExtent extent,
41+
string matchKind)
42+
{
43+
// Extents are base 1, but LSP is base 0, so minus 1 off all lines and character positions
44+
return new FoldingReference {
45+
StartLine = extent.StartLineNumber - 1,
46+
StartCharacter = extent.StartColumnNumber - 1,
47+
EndLine = extent.EndLineNumber - 1,
48+
EndCharacter = extent.EndColumnNumber - 1,
49+
Kind = matchKind
50+
};
51+
}
52+
53+
// AST object visitor methods
54+
public override AstVisitAction VisitArrayExpression(ArrayExpressionAst objAst)
55+
{
56+
if (IsValidFoldingExtent(objAst.Extent))
57+
{
58+
this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
59+
}
60+
return AstVisitAction.Continue;
61+
}
62+
63+
public override AstVisitAction VisitHashtable(HashtableAst objAst)
64+
{
65+
if (IsValidFoldingExtent(objAst.Extent))
66+
{
67+
this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
68+
}
69+
return AstVisitAction.Continue;
70+
}
71+
72+
public override AstVisitAction VisitStatementBlock(StatementBlockAst objAst)
73+
{
74+
// These parent visitors will get this AST Object. No need to process it
75+
if (objAst.Parent == null) { return AstVisitAction.Continue; }
76+
if (objAst.Parent is ArrayExpressionAst) { return AstVisitAction.Continue; }
77+
if (IsValidFoldingExtent(objAst.Extent))
78+
{
79+
this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
80+
}
81+
return AstVisitAction.Continue;
82+
}
83+
84+
public override AstVisitAction VisitScriptBlock(ScriptBlockAst objAst)
85+
{
86+
// If the Parent object is null then this represents the entire script. We don't want to fold that
87+
if (objAst.Parent == null) { return AstVisitAction.Continue; }
88+
// The ScriptBlockExpressionAst visitor will get this AST Object. No need to process it
89+
if (objAst.Parent is ScriptBlockExpressionAst) { return AstVisitAction.Continue; }
90+
if (IsValidFoldingExtent(objAst.Extent))
91+
{
92+
this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
93+
}
94+
return AstVisitAction.Continue;
95+
}
96+
97+
public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst objAst)
98+
{
99+
if (IsValidFoldingExtent(objAst.Extent)) {
100+
FoldingReference foldRef = CreateFoldingReference(objAst.ScriptBlock.Extent, RegionKindNone);
101+
if (objAst.Parent == null) { return AstVisitAction.Continue; }
102+
if (objAst.Parent is InvokeMemberExpressionAst) {
103+
// This is a bit naive. The ScriptBlockExpressionAst Extent does not include the actual { and }
104+
// characters so the StartCharacter and EndCharacter indexes are out by one. This could be a bug in
105+
// PowerShell Parser. This is just a workaround
106+
foldRef.StartCharacter--;
107+
foldRef.EndCharacter++;
108+
}
109+
this.FoldableRegions.Add(foldRef);
110+
}
111+
return AstVisitAction.Continue;
112+
}
113+
114+
public override AstVisitAction VisitStringConstantExpression(StringConstantExpressionAst objAst)
115+
{
116+
if (IsValidFoldingExtent(objAst.Extent))
117+
{
118+
this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
119+
}
120+
121+
return AstVisitAction.Continue;
122+
}
123+
124+
public override AstVisitAction VisitSubExpression(SubExpressionAst objAst)
125+
{
126+
if (IsValidFoldingExtent(objAst.Extent))
127+
{
128+
this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
129+
}
130+
return AstVisitAction.Continue;
131+
}
132+
133+
public override AstVisitAction VisitVariableExpression(VariableExpressionAst objAst)
134+
{
135+
if (IsValidFoldingExtent(objAst.Extent))
136+
{
137+
this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
138+
}
139+
return AstVisitAction.Continue;
140+
}
141+
}
142+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 System;
7+
using System.Collections.Generic;
8+
using System.Management.Automation.Language;
9+
10+
namespace Microsoft.PowerShell.EditorServices
11+
{
12+
/// <summary>
13+
/// Provides common operations for code folding in a script
14+
/// </summary>
15+
internal static class FoldingOperations
16+
{
17+
/// <summary>
18+
/// Extracts all of the unique foldable regions in a script given a script AST and the list tokens
19+
/// used to generate the AST
20+
/// </summary>
21+
internal static FoldingReference[] FoldableRegions(
22+
Token[] tokens,
23+
Ast scriptAst)
24+
{
25+
List<FoldingReference> foldableRegions = new List<FoldingReference>();
26+
27+
// Add regions from AST
28+
foldableRegions.AddRange(AstOperations.FindFoldsInDocument(scriptAst));
29+
30+
// Add regions from Tokens
31+
foldableRegions.AddRange(TokenOperations.FoldableRegions(tokens));
32+
33+
// Sort the FoldingReferences, starting at the top of the document,
34+
// and ensure that, in the case of multiple ranges starting the same line,
35+
// that the largest range (i.e. most number of lines spanned) is sorted
36+
// first. This is needed to detect duplicate regions. The first in the list
37+
// will be used and subsequent duplicates ignored.
38+
foldableRegions.Sort();
39+
40+
// It's possible to have duplicate or overlapping ranges, that is, regions which have the same starting
41+
// line number as the previous region. Therefore only emit ranges which have a different starting line
42+
// than the previous range.
43+
foldableRegions.RemoveAll( (FoldingReference item) => {
44+
// Note - I'm not happy with searching here, but as the RemoveAll
45+
// doesn't expose the index in the List, we need to calculate it. Fortunately the
46+
// list is sorted at this point, so we can use BinarySearch.
47+
int index = foldableRegions.BinarySearch(item);
48+
if (index == 0) { return false; }
49+
return (item.StartLine == foldableRegions[index - 1].StartLine);
50+
});
51+
52+
return foldableRegions.ToArray();
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)