Skip to content

Commit b24e7c4

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.
1 parent af12135 commit b24e7c4

File tree

6 files changed

+266
-193
lines changed

6 files changed

+266
-193
lines changed

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+7-5
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,8 +1382,10 @@ 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,
1385+
ScriptFile script = editorSession.Workspace.GetFile(documentUri);
1386+
foreach (FoldingReference fold in FoldingOperations.FoldableRegions(
1387+
script.ScriptTokens,
1388+
script.ScriptAst,
13871389
this.currentSettings.CodeFolding.ShowLastLine))
13881390
{
13891391
result.Add(new FoldingRange {
@@ -1734,7 +1736,7 @@ await eventSender(
17341736
});
17351737
}
17361738

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

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+
static public 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,121 @@
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.Collections.Generic;
7+
using System.Management.Automation.Language;
8+
9+
using System;
10+
11+
namespace Microsoft.PowerShell.EditorServices
12+
{
13+
/// <summary>
14+
/// The visitor used to find the all folding regions in an AST
15+
/// </summary>
16+
internal class FindFoldsVisitor : AstVisitor
17+
{
18+
private const string RegionKindNone = null;
19+
20+
public List<FoldingReference> FoldableRegions { get; private set; }
21+
22+
public FindFoldsVisitor()
23+
{
24+
this.FoldableRegions = new List<FoldingReference>();
25+
}
26+
27+
/// <summary>
28+
/// Returns whether an Extent could be used as a valid folding region
29+
/// </summary>
30+
private bool IsValidFoldingExtent(
31+
IScriptExtent extent)
32+
{
33+
if (extent.EndLineNumber == extent.StartLineNumber) { return false; }
34+
return true;
35+
}
36+
37+
/// <summary>
38+
/// Creates an instance of a FoldingReference object from a script extent
39+
/// </summary>
40+
private FoldingReference CreateFoldingReference(
41+
IScriptExtent extent,
42+
string matchKind)
43+
{
44+
// Extents are base 1, but LSP is base 0, so minus 1 off all lines and character positions
45+
return new FoldingReference {
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)) { this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone)); }
58+
return AstVisitAction.Continue;
59+
}
60+
61+
public override AstVisitAction VisitHashtable(HashtableAst objAst)
62+
{
63+
if (IsValidFoldingExtent(objAst.Extent)) { this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone)); }
64+
return AstVisitAction.Continue;
65+
}
66+
67+
public override AstVisitAction VisitStatementBlock(StatementBlockAst objAst)
68+
{
69+
// These parent visitors will get this AST Object. No need to process it
70+
if (objAst.Parent == null) { return AstVisitAction.Continue; }
71+
if (objAst.Parent is ArrayExpressionAst) { return AstVisitAction.Continue; }
72+
if (IsValidFoldingExtent(objAst.Extent)) { this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone)); }
73+
return AstVisitAction.Continue;
74+
}
75+
76+
public override AstVisitAction VisitScriptBlock(ScriptBlockAst objAst)
77+
{
78+
// If the Parent object is null then this represents the entire script. We don't want to fold that
79+
if (objAst.Parent == null) { return AstVisitAction.Continue; }
80+
// The ScriptBlockExpressionAst visitor will get this AST Object. No need to process it
81+
if (objAst.Parent is ScriptBlockExpressionAst) { return AstVisitAction.Continue; }
82+
if (IsValidFoldingExtent(objAst.Extent)) { this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone)); }
83+
return AstVisitAction.Continue;
84+
}
85+
86+
public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst objAst)
87+
{
88+
if (IsValidFoldingExtent(objAst.Extent)) {
89+
FoldingReference foldRef = CreateFoldingReference(objAst.ScriptBlock.Extent, RegionKindNone);
90+
if (objAst.Parent == null) { return AstVisitAction.Continue; }
91+
if (objAst.Parent is InvokeMemberExpressionAst) {
92+
// This is a bit naive. The ScriptBlockExpressionAst Extent does not include the actual { and }
93+
// characters so the StartCharacter and EndCharacter indexes are out by one. This could be a bug in
94+
// PowerShell Parser. This is just a workaround
95+
foldRef.StartCharacter--;
96+
foldRef.EndCharacter++;
97+
}
98+
this.FoldableRegions.Add(foldRef);
99+
}
100+
return AstVisitAction.Continue;
101+
}
102+
103+
public override AstVisitAction VisitStringConstantExpression(StringConstantExpressionAst objAst)
104+
{
105+
if (IsValidFoldingExtent(objAst.Extent)) { this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone)); }
106+
return AstVisitAction.Continue;
107+
}
108+
109+
public override AstVisitAction VisitSubExpression(SubExpressionAst objAst)
110+
{
111+
if (IsValidFoldingExtent(objAst.Extent)) { this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone)); }
112+
return AstVisitAction.Continue;
113+
}
114+
115+
public override AstVisitAction VisitVariableExpression(VariableExpressionAst objAst)
116+
{
117+
if (IsValidFoldingExtent(objAst.Extent)) { this.FoldableRegions.Add(CreateFoldingReference(objAst.Extent, RegionKindNone)); }
118+
return AstVisitAction.Continue;
119+
}
120+
}
121+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
bool ShowLastLine)
25+
{
26+
List<FoldingReference> foldableRegions = new List<FoldingReference>();
27+
28+
// Add regions from AST
29+
foldableRegions.AddRange(Microsoft.PowerShell.EditorServices.AstOperations.FindFoldsInDocument(scriptAst));
30+
31+
// Add regions from Tokens
32+
foldableRegions.AddRange(Microsoft.PowerShell.EditorServices.TokenOperations.FoldableRegions(tokens));
33+
34+
// Remove any null entries. Nulls appear if the folding reference is invalid or missing
35+
foldableRegions.RemoveAll(item => item == null);
36+
37+
// Sort the FoldingReferences, starting at the top of the document,
38+
// and ensure that, in the case of multiple ranges starting the same line,
39+
// that the largest range (i.e. most number of lines spanned) is sorted
40+
// first. This is needed to detect duplicate regions. The first in the list
41+
// will be used and subsequent duplicates ignored.
42+
foldableRegions.Sort();
43+
44+
// It's possible to have duplicate or overlapping ranges, that is, regions which have the same starting
45+
// line number as the previous region. Therefore only emit ranges which have a different starting line
46+
// than the previous range.
47+
foldableRegions.RemoveAll( (FoldingReference item) => {
48+
// Note - I'm not happy with searching here, but as the RemoveAll
49+
// doesn't expose the index in the List, we need to calculate it. Fortunately the
50+
// list is sorted at this point, so we can use BinarySearch.
51+
int index = foldableRegions.BinarySearch(item);
52+
if (index == 0) { return false; }
53+
return (item.StartLine == foldableRegions[index - 1].StartLine);
54+
});
55+
56+
// Some editors have different folding UI, sometimes the lastline should be displayed
57+
// If we do want to show the last line, just change the region to be one line less
58+
if (ShowLastLine) {
59+
foldableRegions.ForEach( item => { item.EndLine--; });
60+
}
61+
62+
return foldableRegions.ToArray();
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)