Skip to content

Commit 8549cb1

Browse files
committed
(todo) Tests and extractFoldableRegions done
1 parent 318cec9 commit 8549cb1

File tree

3 files changed

+457
-49
lines changed

3 files changed

+457
-49
lines changed

src/PowerShellEditorServices/Language/FoldingReference.cs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@
44
//
55

66
using System;
7-
using System.Diagnostics;
8-
using System.Management.Automation.Language;
97

108
namespace Microsoft.PowerShell.EditorServices
119
{
1210
/// <summary>
1311
/// A class that holds the information for a foldable region of text in a document
1412
/// </summary>
15-
public class FoldingReference
13+
public class FoldingReference: IComparable<FoldingReference>
1614
{
1715
/// <summary>
1816
/// The zero-based line number from where the folded range starts.
@@ -38,5 +36,54 @@ public class FoldingReference
3836
/// Describes the kind of the folding range such as `comment' or 'region'.
3937
/// <summary>
4038
public string kind { get; set; }
39+
40+
// TODO: Do constructors go at the top?
41+
public FoldingReference(
42+
int startLine,
43+
int startCharacter,
44+
int endLine,
45+
int endCharacter,
46+
string kind)
47+
{
48+
this.endCharacter = endCharacter;
49+
this.endLine = endLine;
50+
this.kind = kind;
51+
this.startCharacter = startCharacter;
52+
this.startLine = startLine;
53+
}
54+
55+
public FoldingReference(
56+
int startLine,
57+
int endLine,
58+
string kind)
59+
{
60+
this.endCharacter = 0;
61+
this.endLine = endLine;
62+
this.kind = kind;
63+
this.startCharacter = 0;
64+
this.startLine = startLine;
65+
}
66+
67+
/// <summary>
68+
/// A custom comparable method which can properly sort FoldingReference objects
69+
/// </summary>
70+
public int CompareTo(FoldingReference that) {
71+
// Initially look at the start line
72+
if (this.startLine < that.startLine) { return -1; }
73+
if (this.startLine > that.startLine) { return 1; }
74+
// They have the same start line so now consider the end line.
75+
// The biggest line range is sorted first
76+
if (this.endLine > that.endLine) { return -1; }
77+
if (this.endLine < that.endLine) { return 1; }
78+
// They have the same lines, but what about character offsets
79+
if (this.startCharacter < that.startCharacter) { return -1; }
80+
if (this.startCharacter > that.startCharacter) { return 1; }
81+
if (this.endCharacter < that.endCharacter) { return -1; }
82+
if (this.endCharacter > that.endCharacter) { return 1; }
83+
// They're the same range, but what about kind
84+
// Check for nulls
85+
if ((this.kind == null) & (that.kind == null)) { return 0; }
86+
return this.kind.CompareTo(that.kind);
87+
}
4188
}
4289
}

src/PowerShellEditorServices/Language/TokenOperations.cs

Lines changed: 242 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,259 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6-
// using Microsoft.PowerShell.EditorServices.Utility;
7-
// using System;
8-
// using System.Diagnostics;
9-
// using System.Collections.Generic;
10-
// using System.Linq;
11-
// using System.Reflection;
12-
// using System.Threading;
13-
// using System.Threading.Tasks;
6+
using System.Collections.Generic;
7+
using System.Text.RegularExpressions;
148
using System.Management.Automation.Language;
15-
// using System.Management.Automation.Runspaces;
169

1710
namespace Microsoft.PowerShell.EditorServices
1811
{
19-
using System.Management.Automation;
2012

2113
/// <summary>
2214
/// Provides common operations for the tokens of a parsed script.
2315
/// </summary>
2416
internal static class TokenOperations
2517
{
18+
/// <summary>
19+
/// Extracts all of the unique foldable regions in a script given the list tokens
20+
/// </summary>
2621
static public FoldingReference[] FoldableRegions(Token[] tokens) {
27-
FoldingReference[] result = new FoldingReference[] {};
22+
List<FoldingReference> foldableRegions = new List<FoldingReference>();
23+
24+
// Find matching braces { -> }
25+
foldableRegions.AddRange(
26+
MatchTokenElements(tokens, TokenKind.LCurly, TokenKind.RCurly, null)
27+
);
28+
29+
// Find matching braces ( -> )
30+
foldableRegions.AddRange(
31+
MatchTokenElements(tokens, TokenKind.LParen, TokenKind.RParen, null)
32+
);
33+
34+
// Find matching arrays @( -> )
35+
foldableRegions.AddRange(
36+
MatchTokenElements(tokens, TokenKind.AtParen, TokenKind.RParen, null)
37+
);
38+
39+
// Find matching hashes @{ -> }
40+
foldableRegions.AddRange(
41+
MatchTokenElements(tokens, TokenKind.AtCurly, TokenKind.RParen, null)
42+
);
43+
44+
// Find contiguous here strings @' -> '@
45+
foldableRegions.AddRange(
46+
MatchTokenElement(tokens, TokenKind.HereStringLiteral, null)
47+
);
48+
49+
// Find contiguous here strings @" -> "@
50+
foldableRegions.AddRange(
51+
MatchTokenElement(tokens, TokenKind.HereStringExpandable, null)
52+
);
53+
54+
// Find matching comment regions #region -> #endregion
55+
foldableRegions.AddRange(
56+
MatchCustomCommentRegionTokenElements(tokens, "region")
57+
);
58+
59+
// Find blocks of line comments # comment1\n# comment2\n...
60+
foldableRegions.AddRange(
61+
MatchBlockCommentTokenElement(tokens, "comment")
62+
);
63+
64+
// Find comments regions <# -> #>
65+
foldableRegions.AddRange(
66+
MatchTokenElement(tokens, TokenKind.Comment, "comment")
67+
);
68+
69+
// Remove any null entries. Nulls appear if the folding reference is invalid
70+
// or missing
71+
foldableRegions.RemoveAll(item => item == null);
72+
73+
// Sort the FoldingReferences, starting at the top of the document,
74+
// and ensure that, in the case of multiple ranges starting the same line,
75+
// that the largest range (i.e. most number of lines spanned) is sorted
76+
// first. This is needed to detect duplicate regions. The first in the list
77+
// will be used and subsequent duplicates ignored.
78+
foldableRegions.Sort();
79+
80+
// It's possible to have duplicate or overlapping ranges, that is, regions which have the same starting
81+
// line number as the previous region. Therefore only emit ranges which have a different starting line
82+
// than the previous range.
83+
foldableRegions.RemoveAll( (FoldingReference item) => {
84+
// Note - I'm not happy with searching here, but as the RemoveAll
85+
// doesn't expose the index in the List, we need to calculate it. Fortunately the
86+
// list is sorted at this point, so we can use BinarySearch.
87+
int index = foldableRegions.BinarySearch(item);
88+
if (index == 0) { return false; }
89+
return (item.startLine == foldableRegions[index - 1].startLine);
90+
});
91+
92+
return foldableRegions.ToArray();
93+
}
94+
95+
/// <summary>
96+
/// Creates an instance of a FoldingReference object from a start and end langauge Token
97+
/// Returns null if the line range is invalid
98+
/// </summary>
99+
static private FoldingReference CreateFoldingReference(
100+
Token startToken,
101+
Token endToken,
102+
string matchKind)
103+
{
104+
if (endToken.Extent.EndLineNumber == startToken.Extent.StartLineNumber) { return null; }
105+
return new FoldingReference(
106+
startToken.Extent.StartLineNumber - 1, // Extents are base 1, but LSP is base 0
107+
startToken.Extent.StartColumnNumber - 1, // Extents are base 1, but LSP is base 0
108+
endToken.Extent.EndLineNumber - 1, // Extents are base 1, but LSP is base 0
109+
endToken.Extent.EndColumnNumber - 1, // Extents are base 1, but LSP is base 0
110+
matchKind
111+
);
112+
}
113+
114+
/// <summary>
115+
/// Creates an instance of a FoldingReference object from a start token and an end line
116+
/// Returns null if the line range is invalid
117+
/// </summary>
118+
static private FoldingReference CreateFoldingReference(
119+
Token startToken,
120+
int endLine,
121+
string matchKind)
122+
{
123+
if (endLine == (startToken.Extent.StartLineNumber - 1)) { return null; }
124+
return new FoldingReference(
125+
startToken.Extent.StartLineNumber - 1, // Extents are base 1, but LSP is base 0
126+
startToken.Extent.StartColumnNumber - 1, // Extents are base 1, but LSP is base 0
127+
endLine,
128+
0,
129+
matchKind
130+
);
131+
}
132+
133+
/// <summary>
134+
/// Given a array tokens finds matching regions which start and end with a different TokenKind
135+
/// </summary>
136+
static private List<FoldingReference> MatchTokenElements(
137+
Token[] tokens,
138+
TokenKind startTokenKind,
139+
TokenKind endTokenKind,
140+
string matchKind)
141+
{
142+
List<FoldingReference> result = new List<FoldingReference>();
143+
Stack<Token> tokenStack = new Stack<Token>();
144+
foreach (Token token in tokens)
145+
{
146+
if (token.Kind == startTokenKind) {
147+
tokenStack.Push(token);
148+
}
149+
if ((tokenStack.Count > 0) & (token.Kind == endTokenKind)) {
150+
result.Add(CreateFoldingReference(tokenStack.Pop(), token, matchKind));
151+
}
152+
}
153+
return result;
154+
}
155+
156+
/// <summary>
157+
/// Given a array tokens finds a specific token
158+
/// </summary>
159+
static private List<FoldingReference> MatchTokenElement(
160+
Token[] tokens,
161+
TokenKind tokenKind,
162+
string matchKind)
163+
{
164+
List<FoldingReference> result = new List<FoldingReference>();
165+
foreach (Token token in tokens)
166+
{
167+
if ((token.Kind == tokenKind) & (token.Extent.StartLineNumber != token.Extent.EndLineNumber)) {
168+
result.Add(CreateFoldingReference(token, token, matchKind));
169+
}
170+
}
171+
return result;
172+
}
173+
174+
/// <summary>
175+
/// Returns true if a Token is a block comment;
176+
/// - Must be a TokenKind.comment
177+
/// - Must be preceeded by TokenKind.NewLine
178+
/// - Token text must start with a '#'.false This is because comment regions
179+
/// start with '<#' but have the same TokenKind
180+
/// </summary>
181+
static private bool IsBlockComment(int index, Token[] tokens) {
182+
Token thisToken = tokens[index];
183+
if (thisToken.Kind != TokenKind.Comment) { return false; }
184+
if (index == 0) { return true; }
185+
if (tokens[index - 1].Kind != TokenKind.NewLine) { return false; }
186+
return thisToken.Text.StartsWith("#");
187+
}
188+
189+
/// <summary>
190+
/// Finding blocks of comment tokens is more complicated as the newline characters are not
191+
/// classed as comments. To workaround this we search for valid block comments (See IsBlockCmment)
192+
/// and then determine contiguous line numbers from there
193+
/// </summary>
194+
static private List<FoldingReference> MatchBlockCommentTokenElement(
195+
Token[] tokens,
196+
string matchKind)
197+
{
198+
// This regular expressions is used to detect a line comment (as opposed to an inline comment), that is not a region
199+
// block directive i.e.
200+
// - No text between the beginning of the line and `#`
201+
// - Comment does start with region
202+
// - Comment does start with endregion
203+
string lineCommentText = @"\s*#(?!region\b|endregion\b)";
204+
205+
List<FoldingReference> result = new List<FoldingReference>();
206+
Token startToken = null;
207+
int nextLine = -1;
208+
for (int index = 0; index < tokens.Length; index++)
209+
{
210+
Token thisToken = tokens[index];
211+
if ((IsBlockComment(index, tokens)) & (Regex.IsMatch(thisToken.Text, lineCommentText, RegexOptions.IgnoreCase))) {
212+
int thisLine = thisToken.Extent.StartLineNumber - 1;
213+
if ((startToken != null) & (thisLine != nextLine)) {
214+
result.Add(CreateFoldingReference(startToken, nextLine - 1, matchKind));
215+
startToken = thisToken;
216+
}
217+
if (startToken == null) { startToken = thisToken; }
218+
nextLine = thisLine + 1;
219+
}
220+
}
221+
// If we exit the token array and we're still processing comment lines, then the
222+
// comment block simply ends at the end of document
223+
if (startToken != null) {
224+
result.Add(CreateFoldingReference(startToken, nextLine - 1, matchKind));
225+
}
226+
return result;
227+
}
228+
229+
/// <summary>
230+
/// Given a list of tokens, find the tokens that are comments and
231+
/// the comment text is either `# region` or `# endregion`, and then use a stack to determine
232+
/// the ranges they span
233+
/// </summary>
234+
static private List<FoldingReference> MatchCustomCommentRegionTokenElements(
235+
Token[] tokens,
236+
string matchKind)
237+
{
238+
// These regular expressions are used to match lines which mark the start and end of region comment in a PowerShell
239+
// script. They are based on the defaults in the VS Code Language Configuration at;
240+
// https://github.com/Microsoft/vscode/blob/64186b0a26/extensions/powershell/language-configuration.json#L26-L31
241+
string startRegionText = @"^\s*#region\b";
242+
string endRegionText = @"^\s*#endregion\b";
243+
244+
List<FoldingReference> result = new List<FoldingReference>();
245+
Stack<Token> tokenStack = new Stack<Token>();
246+
for (int index = 0; index < tokens.Length; index++)
247+
{
248+
if (IsBlockComment(index, tokens)) {
249+
Token token = tokens[index];
250+
if (Regex.IsMatch(token.Text, startRegionText, RegexOptions.IgnoreCase)) {
251+
tokenStack.Push(token);
252+
}
253+
if ((tokenStack.Count > 0) & (Regex.IsMatch(token.Text, endRegionText, RegexOptions.IgnoreCase))) {
254+
result.Add(CreateFoldingReference(tokenStack.Pop(), token, matchKind));
255+
}
256+
}
257+
}
28258
return result;
29259
}
30-
}
260+
}
31261
}

0 commit comments

Comments
 (0)