Skip to content

Commit 73b6030

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 73b6030

File tree

6 files changed

+352
-246
lines changed

6 files changed

+352
-246
lines changed

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+10-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,16 @@ 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+
// If we're showing the last line, decrement the Endline of all regions by one.
1387+
int endLineOffset = this.currentSettings.CodeFolding.ShowLastLine ? -1 : 0;
1388+
foreach (FoldingReference fold in FoldingOperations.FoldableRegions(
1389+
script.ScriptTokens,
1390+
script.ScriptAst))
13881391
{
13891392
result.Add(new FoldingRange {
13901393
EndCharacter = fold.EndCharacter,
1391-
EndLine = fold.EndLine,
1394+
EndLine = fold.EndLine + endLineOffset,
13921395
Kind = fold.Kind,
13931396
StartCharacter = fold.StartCharacter,
13941397
StartLine = fold.StartLine
@@ -1734,7 +1737,7 @@ await eventSender(
17341737
});
17351738
}
17361739

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

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,152 @@
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 : AstVisitor2
16+
{
17+
private const string RegionKindNone = null;
18+
19+
public List<FoldingReference> FoldableRegions { get; }
20+
21+
public FindFoldsVisitor()
22+
{
23+
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+
{
46+
StartLine = extent.StartLineNumber - 1,
47+
StartCharacter = extent.StartColumnNumber - 1,
48+
EndLine = extent.EndLineNumber - 1,
49+
EndCharacter = extent.EndColumnNumber - 1,
50+
Kind = matchKind
51+
};
52+
}
53+
54+
// AST object visitor methods
55+
public override AstVisitAction VisitArrayExpression(ArrayExpressionAst objAst)
56+
{
57+
if (IsValidFoldingExtent(objAst.Extent))
58+
{
59+
FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
60+
}
61+
return AstVisitAction.Continue;
62+
}
63+
64+
public override AstVisitAction VisitHashtable(HashtableAst objAst)
65+
{
66+
if (IsValidFoldingExtent(objAst.Extent))
67+
{
68+
FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
69+
}
70+
return AstVisitAction.Continue;
71+
}
72+
73+
public override AstVisitAction VisitStatementBlock(StatementBlockAst objAst)
74+
{
75+
// These parent visitors will get this AST Object. No need to process it
76+
if (objAst.Parent == null) { return AstVisitAction.Continue; }
77+
if (objAst.Parent is ArrayExpressionAst) { return AstVisitAction.Continue; }
78+
if (IsValidFoldingExtent(objAst.Extent))
79+
{
80+
FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
81+
}
82+
return AstVisitAction.Continue;
83+
}
84+
85+
public override AstVisitAction VisitScriptBlock(ScriptBlockAst objAst)
86+
{
87+
// If the Parent object is null then this represents the entire script. We don't want to fold that
88+
if (objAst.Parent == null) { return AstVisitAction.Continue; }
89+
// The ScriptBlockExpressionAst visitor will get this AST Object. No need to process it
90+
if (objAst.Parent is ScriptBlockExpressionAst) { return AstVisitAction.Continue; }
91+
if (IsValidFoldingExtent(objAst.Extent))
92+
{
93+
FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
94+
}
95+
return AstVisitAction.Continue;
96+
}
97+
98+
public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst objAst)
99+
{
100+
if (IsValidFoldingExtent(objAst.Extent)) {
101+
FoldingReference foldRef = CreateFoldingReference(objAst.ScriptBlock.Extent, RegionKindNone);
102+
if (objAst.Parent == null) { return AstVisitAction.Continue; }
103+
if (objAst.Parent is InvokeMemberExpressionAst) {
104+
// This is a bit naive. The ScriptBlockExpressionAst Extent does not include the actual { and }
105+
// characters so the StartCharacter and EndCharacter indexes are out by one. This could be a bug in
106+
// PowerShell Parser. This is just a workaround
107+
foldRef.StartCharacter--;
108+
foldRef.EndCharacter++;
109+
}
110+
FoldableRegions.Add(foldRef);
111+
}
112+
return AstVisitAction.Continue;
113+
}
114+
115+
public override AstVisitAction VisitStringConstantExpression(StringConstantExpressionAst objAst)
116+
{
117+
if (IsValidFoldingExtent(objAst.Extent))
118+
{
119+
FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
120+
}
121+
122+
return AstVisitAction.Continue;
123+
}
124+
125+
public override AstVisitAction VisitSubExpression(SubExpressionAst objAst)
126+
{
127+
if (IsValidFoldingExtent(objAst.Extent))
128+
{
129+
FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
130+
}
131+
return AstVisitAction.Continue;
132+
}
133+
134+
public override AstVisitAction VisitTypeDefinition(TypeDefinitionAst objAst)
135+
{
136+
if (IsValidFoldingExtent(objAst.Extent))
137+
{
138+
FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
139+
}
140+
return AstVisitAction.Continue;
141+
}
142+
143+
public override AstVisitAction VisitVariableExpression(VariableExpressionAst objAst)
144+
{
145+
if (IsValidFoldingExtent(objAst.Extent))
146+
{
147+
FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone));
148+
}
149+
return AstVisitAction.Continue;
150+
}
151+
}
152+
}
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+
var 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)