diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Folding.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Folding.cs
new file mode 100644
index 000000000..f7b1d8f01
--- /dev/null
+++ b/src/PowerShellEditorServices.Protocol/LanguageServer/Folding.cs
@@ -0,0 +1,69 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
+
+namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer
+{
+ public class FoldingRangeRequest
+ {
+ ///
+ /// A request to provide folding ranges in a document. The request's
+ /// parameter is of type [FoldingRangeParams](#FoldingRangeParams), the
+ /// response is of type [FoldingRangeList](#FoldingRangeList) or a Thenable
+ /// that resolves to such.
+ /// Ref: https://github.com/Microsoft/vscode-languageserver-node/blob/5350bc2ffe8afb17357c1a66fbdd3845fa05adfd/protocol/src/protocol.foldingRange.ts#L112-L120
+ ///
+ public static readonly
+ RequestType Type =
+ RequestType.Create("textDocument/foldingRange");
+ }
+
+ ///
+ /// Parameters for a [FoldingRangeRequest](#FoldingRangeRequest).
+ /// Ref: https://github.com/Microsoft/vscode-languageserver-node/blob/5350bc2ffe8afb17357c1a66fbdd3845fa05adfd/protocol/src/protocol.foldingRange.ts#L102-L110
+ ///
+ public class FoldingRangeParams
+ {
+ ///
+ /// The text document
+ ///
+ public TextDocumentIdentifier TextDocument { get; set; }
+ }
+
+ ///
+ /// Represents a folding range.
+ /// Ref: https://github.com/Microsoft/vscode-languageserver-node/blob/5350bc2ffe8afb17357c1a66fbdd3845fa05adfd/protocol/src/protocol.foldingRange.ts#L69-L100
+ ///
+ public class FoldingRange
+ {
+ ///
+ /// The zero-based line number from where the folded range starts.
+ ///
+ public int StartLine { get; set; }
+
+ ///
+ /// The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line.
+ ///
+ public int StartCharacter { get; set; }
+
+ ///
+ /// The zero-based line number where the folded range ends.
+ ///
+ public int EndLine { get; set; }
+
+ ///
+ /// The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line.
+ ///
+ public int EndCharacter { get; set; }
+
+ ///
+ /// Describes the kind of the folding range such as `comment' or 'region'. The kind
+ /// is used to categorize folding ranges and used by commands like 'Fold all comments'. See
+ /// [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds.
+ ///
+ public string Kind { get; set; }
+ }
+}
diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs
index 2279e64d6..e53ca4f6c 100644
--- a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs
+++ b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs
@@ -42,6 +42,8 @@ public class ServerCapabilities
public ExecuteCommandOptions ExecuteCommandProvider { get; set; }
public object Experimental { get; set; }
+
+ public bool FoldingRangeProvider { get; set; } = false;
}
///
diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs
index ab3e7a040..2a7cbf01c 100644
--- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs
+++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs
@@ -131,6 +131,7 @@ public void Start()
this.messageHandlers.SetRequestHandler(
DocumentRangeFormattingRequest.Type,
this.HandleDocumentRangeFormattingRequest);
+ this.messageHandlers.SetRequestHandler(FoldingRangeRequest.Type, this.HandleFoldingRangeRequestAsync);
this.messageHandlers.SetRequestHandler(ShowOnlineHelpRequest.Type, this.HandleShowOnlineHelpRequest);
this.messageHandlers.SetRequestHandler(ShowHelpRequest.Type, this.HandleShowHelpRequest);
@@ -239,7 +240,8 @@ await requestContext.SendResult(
},
DocumentFormattingProvider = false,
DocumentRangeFormattingProvider = false,
- RenameProvider = false
+ RenameProvider = false,
+ FoldingRangeProvider = true
}
});
}
@@ -1250,6 +1252,13 @@ await requestContext.SendResult(new TextEdit[1]
});
}
+ protected async Task HandleFoldingRangeRequestAsync(
+ FoldingRangeParams foldingParams,
+ RequestContext requestContext)
+ {
+ await requestContext.SendResult(Fold(foldingParams.TextDocument.Uri));
+ }
+
protected Task HandleEvaluateRequest(
DebugAdapterMessages.EvaluateRequestArguments evaluateParams,
RequestContext requestContext)
@@ -1288,6 +1297,27 @@ protected Task HandleEvaluateRequest(
#region Event Handlers
+ private FoldingRange[] Fold(
+ string documentUri)
+ {
+ // TODO Should be using dynamic registrations
+ if (!this.currentSettings.CodeFolding.Enable) { return null; }
+ var result = new List();
+ foreach (FoldingReference fold in TokenOperations.FoldableRegions(
+ editorSession.Workspace.GetFile(documentUri).ScriptTokens,
+ this.currentSettings.CodeFolding.ShowLastLine))
+ {
+ result.Add(new FoldingRange {
+ EndCharacter = fold.EndCharacter,
+ EndLine = fold.EndLine,
+ Kind = fold.Kind,
+ StartCharacter = fold.StartCharacter,
+ StartLine = fold.StartLine
+ });
+ }
+ return result.ToArray();
+ }
+
private async Task> Format(
string documentUri,
FormattingOptions options,
diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs
index 05b3e6e06..cee52d904 100644
--- a/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs
+++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs
@@ -20,10 +20,13 @@ public class LanguageServerSettings
public CodeFormattingSettings CodeFormatting { get; set; }
+ public CodeFoldingSettings CodeFolding { get; set; }
+
public LanguageServerSettings()
{
this.ScriptAnalysis = new ScriptAnalysisSettings();
this.CodeFormatting = new CodeFormattingSettings();
+ this.CodeFolding = new CodeFoldingSettings();
}
public void Update(
@@ -39,6 +42,7 @@ public void Update(
workspaceRootPath,
logger);
this.CodeFormatting = new CodeFormattingSettings(settings.CodeFormatting);
+ this.CodeFolding.Update(settings.CodeFolding, logger);
}
}
}
@@ -261,6 +265,41 @@ private Hashtable GetCustomPSSASettingsHashtable(int tabSize, bool insertSpaces)
}
}
+ ///
+ /// Code folding settings
+ ///
+ public class CodeFoldingSettings
+ {
+ ///
+ /// Whether the folding is enabled. Default is true as per VSCode
+ ///
+ public bool Enable { get; set; } = true;
+
+ ///
+ /// Whether to show or hide the last line of a folding region. Default is true as per VSCode
+ ///
+ public bool ShowLastLine { get; set; } = true;
+
+ ///
+ /// Update these settings from another settings object
+ ///
+ public void Update(
+ CodeFoldingSettings settings,
+ ILogger logger)
+ {
+ if (settings != null) {
+ if (this.Enable != settings.Enable) {
+ this.Enable = settings.Enable;
+ logger.Write(LogLevel.Verbose, string.Format("Using Code Folding Enabled - {0}", this.Enable));
+ }
+ if (this.ShowLastLine != settings.ShowLastLine) {
+ this.ShowLastLine = settings.ShowLastLine;
+ logger.Write(LogLevel.Verbose, string.Format("Using Code Folding ShowLastLine - {0}", this.ShowLastLine));
+ }
+ }
+ }
+ }
+
public class LanguageServerSettingsWrapper
{
// NOTE: This property is capitalized as 'Powershell' because the
diff --git a/src/PowerShellEditorServices/Language/FoldingReference.cs b/src/PowerShellEditorServices/Language/FoldingReference.cs
new file mode 100644
index 000000000..54e3401df
--- /dev/null
+++ b/src/PowerShellEditorServices/Language/FoldingReference.cs
@@ -0,0 +1,63 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+
+namespace Microsoft.PowerShell.EditorServices
+{
+ ///
+ /// A class that holds the information for a foldable region of text in a document
+ ///
+ public class FoldingReference: IComparable
+ {
+ ///
+ /// The zero-based line number from where the folded range starts.
+ ///
+ public int StartLine { get; set; }
+
+ ///
+ /// The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line.
+ ///
+ public int StartCharacter { get; set; } = 0;
+
+ ///
+ /// The zero-based line number where the folded range ends.
+ ///
+ public int EndLine { get; set; }
+
+ ///
+ /// The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line.
+ ///
+ public int EndCharacter { get; set; } = 0;
+
+ ///
+ /// Describes the kind of the folding range such as `comment' or 'region'.
+ ///
+ public string Kind { get; set; }
+
+ ///
+ /// A custom comparable method which can properly sort FoldingReference objects
+ ///
+ public int CompareTo(FoldingReference that) {
+ // Initially look at the start line
+ if (this.StartLine < that.StartLine) { return -1; }
+ if (this.StartLine > that.StartLine) { return 1; }
+
+ // They have the same start line so now consider the end line.
+ // The biggest line range is sorted first
+ if (this.EndLine > that.EndLine) { return -1; }
+ if (this.EndLine < that.EndLine) { return 1; }
+
+ // They have the same lines, but what about character offsets
+ if (this.StartCharacter < that.StartCharacter) { return -1; }
+ if (this.StartCharacter > that.StartCharacter) { return 1; }
+ if (this.EndCharacter < that.EndCharacter) { return -1; }
+ if (this.EndCharacter > that.EndCharacter) { return 1; }
+
+ // They're the same range, but what about kind
+ return string.Compare(this.Kind, that.Kind);
+ }
+ }
+}
diff --git a/src/PowerShellEditorServices/Language/TokenOperations.cs b/src/PowerShellEditorServices/Language/TokenOperations.cs
new file mode 100644
index 000000000..236c05f16
--- /dev/null
+++ b/src/PowerShellEditorServices/Language/TokenOperations.cs
@@ -0,0 +1,278 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System.Collections.Generic;
+using System.Management.Automation.Language;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.PowerShell.EditorServices
+{
+
+ ///
+ /// Provides common operations for the tokens of a parsed script.
+ ///
+ internal static class TokenOperations
+ {
+ private const string RegionKindComment = "comment";
+ private const string RegionKindRegion = "region";
+ private const string RegionKindNone = null;
+
+ ///
+ /// Extracts all of the unique foldable regions in a script given the list tokens
+ ///
+ internal static FoldingReference[] FoldableRegions(
+ Token[] tokens,
+ bool ShowLastLine)
+ {
+ List foldableRegions = new List();
+
+ // Find matching braces { -> }
+ foldableRegions.AddRange(
+ MatchTokenElements(tokens, TokenKind.LCurly, TokenKind.RCurly, RegionKindNone)
+ );
+
+ // Find matching braces ( -> )
+ foldableRegions.AddRange(
+ MatchTokenElements(tokens, TokenKind.LParen, TokenKind.RParen, RegionKindNone)
+ );
+
+ // Find matching arrays @( -> )
+ foldableRegions.AddRange(
+ MatchTokenElements(tokens, TokenKind.AtParen, TokenKind.RParen, RegionKindNone)
+ );
+
+ // Find matching hashes @{ -> }
+ foldableRegions.AddRange(
+ MatchTokenElements(tokens, TokenKind.AtCurly, TokenKind.RParen, RegionKindNone)
+ );
+
+ // Find contiguous here strings @' -> '@
+ foldableRegions.AddRange(
+ MatchTokenElement(tokens, TokenKind.HereStringLiteral, RegionKindNone)
+ );
+
+ // Find contiguous here strings @" -> "@
+ foldableRegions.AddRange(
+ MatchTokenElement(tokens, TokenKind.HereStringExpandable, RegionKindNone)
+ );
+
+ // Find matching comment regions #region -> #endregion
+ foldableRegions.AddRange(
+ MatchCustomCommentRegionTokenElements(tokens, RegionKindRegion)
+ );
+
+ // Find blocks of line comments # comment1\n# comment2\n...
+ foldableRegions.AddRange(
+ MatchBlockCommentTokenElement(tokens, RegionKindComment)
+ );
+
+ // Find comments regions <# -> #>
+ foldableRegions.AddRange(
+ MatchTokenElement(tokens, TokenKind.Comment, RegionKindComment)
+ );
+
+ // Remove any null entries. Nulls appear if the folding reference is invalid
+ // or missing
+ foldableRegions.RemoveAll(item => item == null);
+
+ // Sort the FoldingReferences, starting at the top of the document,
+ // and ensure that, in the case of multiple ranges starting the same line,
+ // that the largest range (i.e. most number of lines spanned) is sorted
+ // first. This is needed to detect duplicate regions. The first in the list
+ // will be used and subsequent duplicates ignored.
+ foldableRegions.Sort();
+
+ // It's possible to have duplicate or overlapping ranges, that is, regions which have the same starting
+ // line number as the previous region. Therefore only emit ranges which have a different starting line
+ // than the previous range.
+ foldableRegions.RemoveAll( (FoldingReference item) => {
+ // Note - I'm not happy with searching here, but as the RemoveAll
+ // doesn't expose the index in the List, we need to calculate it. Fortunately the
+ // list is sorted at this point, so we can use BinarySearch.
+ int index = foldableRegions.BinarySearch(item);
+ if (index == 0) { return false; }
+ return (item.StartLine == foldableRegions[index - 1].StartLine);
+ });
+
+ // Some editors have different folding UI, sometimes the lastline should be displayed
+ // If we do want to show the last line, just change the region to be one line less
+ if (ShowLastLine) {
+ foldableRegions.ForEach( item => { item.EndLine--; });
+ }
+
+ return foldableRegions.ToArray();
+ }
+
+ ///
+ /// Creates an instance of a FoldingReference object from a start and end langauge Token
+ /// Returns null if the line range is invalid
+ ///
+ static private FoldingReference CreateFoldingReference(
+ Token startToken,
+ Token endToken,
+ string matchKind)
+ {
+ if (endToken.Extent.EndLineNumber == startToken.Extent.StartLineNumber) { return null; }
+ // Extents are base 1, but LSP is base 0, so minus 1 off all lines and character positions
+ return new FoldingReference {
+ StartLine = startToken.Extent.StartLineNumber - 1,
+ StartCharacter = startToken.Extent.StartColumnNumber - 1,
+ EndLine = endToken.Extent.EndLineNumber - 1,
+ EndCharacter = endToken.Extent.EndColumnNumber - 1,
+ Kind = matchKind
+ };
+ }
+
+ ///
+ /// Creates an instance of a FoldingReference object from a start token and an end line
+ /// Returns null if the line range is invalid
+ ///
+ static private FoldingReference CreateFoldingReference(
+ Token startToken,
+ int endLine,
+ string matchKind)
+ {
+ if (endLine == (startToken.Extent.StartLineNumber - 1)) { return null; }
+ // Extents are base 1, but LSP is base 0, so minus 1 off all lines and character positions
+ return new FoldingReference {
+ StartLine = startToken.Extent.StartLineNumber - 1,
+ StartCharacter = startToken.Extent.StartColumnNumber - 1,
+ EndLine = endLine,
+ EndCharacter = 0,
+ Kind = matchKind
+ };
+ }
+
+ ///
+ /// Given an array of tokens, find matching regions which start and end with a different TokenKind
+ ///
+ static private List MatchTokenElements(
+ Token[] tokens,
+ TokenKind startTokenKind,
+ TokenKind endTokenKind,
+ string matchKind)
+ {
+ List result = new List();
+ Stack tokenStack = new Stack();
+ foreach (Token token in tokens)
+ {
+ if (token.Kind == startTokenKind) {
+ tokenStack.Push(token);
+ }
+ if ((tokenStack.Count > 0) && (token.Kind == endTokenKind)) {
+ result.Add(CreateFoldingReference(tokenStack.Pop(), token, matchKind));
+ }
+ }
+ return result;
+ }
+
+ ///
+ /// Given an array of token, finds a specific token
+ ///
+ static private List MatchTokenElement(
+ Token[] tokens,
+ TokenKind tokenKind,
+ string matchKind)
+ {
+ List result = new List();
+ foreach (Token token in tokens)
+ {
+ if ((token.Kind == tokenKind) && (token.Extent.StartLineNumber != token.Extent.EndLineNumber)) {
+ result.Add(CreateFoldingReference(token, token, matchKind));
+ }
+ }
+ return result;
+ }
+
+ ///
+ /// Returns true if a Token is a block comment;
+ /// - Must be a TokenKind.comment
+ /// - Must be preceeded by TokenKind.NewLine
+ /// - Token text must start with a '#'.false This is because comment regions
+ /// start with '<#' but have the same TokenKind
+ ///
+ static private bool IsBlockComment(int index, Token[] tokens) {
+ Token thisToken = tokens[index];
+ if (thisToken.Kind != TokenKind.Comment) { return false; }
+ if (index == 0) { return true; }
+ if (tokens[index - 1].Kind != TokenKind.NewLine) { return false; }
+ return thisToken.Text.StartsWith("#");
+ }
+
+ // This regular expressions is used to detect a line comment (as opposed to an inline comment), that is not a region
+ // block directive i.e.
+ // - No text between the beginning of the line and `#`
+ // - Comment does start with region
+ // - Comment does start with endregion
+ static private readonly Regex s_nonRegionLineCommentRegex = new Regex(
+ @"\s*#(?!region\b|endregion\b)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ ///
+ /// Finding blocks of comment tokens is more complicated as the newline characters are not
+ /// classed as comments. To workaround this we search for valid block comments (See IsBlockCmment)
+ /// and then determine contiguous line numbers from there
+ ///
+ static private List MatchBlockCommentTokenElement(
+ Token[] tokens,
+ string matchKind)
+ {
+ List result = new List();
+ Token startToken = null;
+ int nextLine = -1;
+ for (int index = 0; index < tokens.Length; index++)
+ {
+ Token thisToken = tokens[index];
+ if (IsBlockComment(index, tokens) && s_nonRegionLineCommentRegex.IsMatch(thisToken.Text)) {
+ int thisLine = thisToken.Extent.StartLineNumber - 1;
+ if ((startToken != null) && (thisLine != nextLine)) {
+ result.Add(CreateFoldingReference(startToken, nextLine - 1, matchKind));
+ startToken = thisToken;
+ }
+ if (startToken == null) { startToken = thisToken; }
+ nextLine = thisLine + 1;
+ }
+ }
+ // If we exit the token array and we're still processing comment lines, then the
+ // comment block simply ends at the end of document
+ if (startToken != null) {
+ result.Add(CreateFoldingReference(startToken, nextLine - 1, matchKind));
+ }
+ return result;
+ }
+
+ ///
+ /// Given a list of tokens, find the tokens that are comments and
+ /// the comment text is either `# region` or `# endregion`, and then use a stack to determine
+ /// the ranges they span
+ ///
+ static private List MatchCustomCommentRegionTokenElements(
+ Token[] tokens,
+ string matchKind)
+ {
+ // These regular expressions are used to match lines which mark the start and end of region comment in a PowerShell
+ // script. They are based on the defaults in the VS Code Language Configuration at;
+ // https://github.com/Microsoft/vscode/blob/64186b0a26/extensions/powershell/language-configuration.json#L26-L31
+ string startRegionText = @"^\s*#region\b";
+ string endRegionText = @"^\s*#endregion\b";
+
+ List result = new List();
+ Stack tokenStack = new Stack();
+ for (int index = 0; index < tokens.Length; index++)
+ {
+ if (IsBlockComment(index, tokens)) {
+ Token token = tokens[index];
+ if (Regex.IsMatch(token.Text, startRegionText, RegexOptions.IgnoreCase)) {
+ tokenStack.Push(token);
+ }
+ if ((tokenStack.Count > 0) && (Regex.IsMatch(token.Text, endRegionText, RegexOptions.IgnoreCase))) {
+ result.Add(CreateFoldingReference(tokenStack.Pop(), token, matchKind));
+ }
+ }
+ }
+ return result;
+ }
+ }
+}
diff --git a/test/PowerShellEditorServices.Test/Language/TokenOperationsTests.cs b/test/PowerShellEditorServices.Test/Language/TokenOperationsTests.cs
new file mode 100644
index 000000000..9a192a666
--- /dev/null
+++ b/test/PowerShellEditorServices.Test/Language/TokenOperationsTests.cs
@@ -0,0 +1,221 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+using Xunit;
+
+namespace Microsoft.PowerShell.EditorServices.Test.Language
+{
+ public class TokenOperationsTests
+ {
+ ///
+ /// Helper method to create a stub script file and then call FoldableRegions
+ ///
+ private FoldingReference[] GetRegions(string text, bool showLastLine = true) {
+ ScriptFile scriptFile = new ScriptFile(
+ "testfile",
+ "clienttestfile",
+ text,
+ Version.Parse("5.0"));
+ return Microsoft.PowerShell.EditorServices.TokenOperations.FoldableRegions(scriptFile.ScriptTokens, showLastLine);
+ }
+
+ ///
+ /// Helper method to create FoldingReference objects with less typing
+ ///
+ private static FoldingReference CreateFoldingReference(int startLine, int startCharacter, int endLine, int endCharacter, string matchKind) {
+ return new FoldingReference {
+ StartLine = startLine,
+ StartCharacter = startCharacter,
+ EndLine = endLine,
+ EndCharacter = endCharacter,
+ Kind = matchKind
+ };
+ }
+
+ // This PowerShell script will exercise all of the
+ // folding regions and regions which should not be
+ // detected. Due to file encoding this could be CLRF or LF line endings
+ private const string allInOneScript =
+@"#RegIon This should fold
+<#
+Nested different comment types. This should fold
+#>
+#EnDReGion
+
+# region This should not fold due to whitespace
+$shouldFold = $false
+# endRegion
+function short-func-not-fold {};
+<#
+.SYNOPSIS
+ This whole comment block should fold, not just the SYNOPSIS
+.EXAMPLE
+ This whole comment block should fold, not just the EXAMPLE
+#>
+function New-VSCodeShouldFold {
+<#
+.SYNOPSIS
+ This whole comment block should fold, not just the SYNOPSIS
+.EXAMPLE
+ This whole comment block should fold, not just the EXAMPLE
+#>
+ $I = @'
+herestrings should fold
+
+'@
+
+$I = @""
+double quoted herestrings should also fold
+
+""@
+
+ # this won't be folded
+
+ # This block of comments should be foldable as a single block
+ # This block of comments should be foldable as a single block
+ # This block of comments should be foldable as a single block
+
+ #region This fools the indentation folding.
+ Write-Host ""Hello""
+ #region Nested regions should be foldable
+ Write-Host ""Hello""
+ # comment1
+ Write-Host ""Hello""
+ #endregion
+ Write-Host ""Hello""
+ # comment2
+ Write-Host ""Hello""
+ #endregion
+
+ $c = {
+ Write-Host ""Script blocks should be foldable""
+ }
+
+ # Array fools indentation folding
+ $d = @(
+ 'should fold1',
+ 'should fold2'
+ )
+}
+
+# Make sure contiguous comment blocks can be folded properly
+
+# Comment Block 1
+# Comment Block 1
+# Comment Block 1
+#region Comment Block 3
+# Comment Block 2
+# Comment Block 2
+# Comment Block 2
+$something = $true
+#endregion Comment Block 3";
+ private FoldingReference[] expectedAllInOneScriptFolds = {
+ CreateFoldingReference(0, 0, 3, 10, "region"),
+ CreateFoldingReference(1, 0, 2, 2, "comment"),
+ CreateFoldingReference(10, 0, 14, 2, "comment"),
+ CreateFoldingReference(16, 30, 59, 1, null),
+ CreateFoldingReference(17, 0, 21, 2, "comment"),
+ CreateFoldingReference(23, 7, 25, 2, null),
+ CreateFoldingReference(28, 5, 30, 2, null),
+ CreateFoldingReference(35, 2, 36, 0, "comment"),
+ CreateFoldingReference(39, 2, 48, 14, "region"),
+ CreateFoldingReference(41, 4, 44, 14, "region"),
+ CreateFoldingReference(51, 7, 52, 3, null),
+ CreateFoldingReference(56, 7, 58, 3, null),
+ CreateFoldingReference(64, 0, 65, 0, "comment"),
+ CreateFoldingReference(67, 0, 71, 26, "region"),
+ CreateFoldingReference(68, 0, 69, 0, "comment")
+ };
+
+ ///
+ /// Assertion helper to compare two FoldingReference arrays.
+ ///
+ private void AssertFoldingReferenceArrays(
+ FoldingReference[] expected,
+ FoldingReference[] actual)
+ {
+ for (int index = 0; index < expected.Length; index++)
+ {
+ Assert.Equal(expected[index], actual[index]);
+ }
+ Assert.Equal(expected.Length, actual.Length);
+ }
+
+ [Fact]
+ public void LaguageServiceFindsFoldablRegionsWithLF() {
+ // Remove and CR characters
+ string testString = allInOneScript.Replace("\r", "");
+ // Ensure that there are no CR characters in the string
+ Assert.True(testString.IndexOf("\r\n") == -1, "CRLF should not be present in the test string");
+ FoldingReference[] result = GetRegions(testString);
+ AssertFoldingReferenceArrays(expectedAllInOneScriptFolds, result);
+ }
+
+ [Fact]
+ public void LaguageServiceFindsFoldablRegionsWithCRLF() {
+ // The Foldable regions should be the same regardless of line ending type
+ // Enforce CRLF line endings, if none exist
+ string testString = allInOneScript;
+ if (testString.IndexOf("\r\n") == -1) {
+ testString = testString.Replace("\n", "\r\n");
+ }
+ // Ensure that there are CRLF characters in the string
+ Assert.True(testString.IndexOf("\r\n") != -1, "CRLF should be present in the teststring");
+ FoldingReference[] result = GetRegions(testString);
+ AssertFoldingReferenceArrays(expectedAllInOneScriptFolds, result);
+ }
+
+ [Fact]
+ public void LaguageServiceFindsFoldablRegionsWithoutLastLine() {
+ FoldingReference[] result = GetRegions(allInOneScript, false);
+ // Incrememnt the end line of the expected regions by one as we will
+ // be hiding the last line
+ FoldingReference[] expectedFolds = expectedAllInOneScriptFolds.Clone() as FoldingReference[];
+ for (int index = 0; index < expectedFolds.Length; index++)
+ {
+ expectedFolds[index].EndLine++;
+ }
+ AssertFoldingReferenceArrays(expectedFolds, result);
+ }
+
+ [Fact]
+ public void LaguageServiceFindsFoldablRegionsWithMismatchedRegions() {
+ string testString =
+@"#endregion should not fold - mismatched
+
+#region This should fold
+$something = 'foldable'
+#endregion
+
+#region should not fold - mismatched
+";
+ FoldingReference[] expectedFolds = {
+ CreateFoldingReference(2, 0, 3, 10, "region")
+ };
+
+ FoldingReference[] result = GetRegions(testString);
+ AssertFoldingReferenceArrays(expectedFolds, result);
+ }
+
+ [Fact]
+ public void LaguageServiceFindsFoldablRegionsWithDuplicateRegions() {
+ string testString =
+@"# This script causes duplicate/overlapping ranges due to the `(` and `{` characters
+$AnArray = @(Get-ChildItem -Path C:\ -Include *.ps1 -File).Where({
+ $_.FullName -ne 'foo'}).ForEach({
+ # Do Something
+})
+";
+ FoldingReference[] expectedFolds = {
+ CreateFoldingReference(1, 64, 1, 27, null),
+ CreateFoldingReference(2, 35, 3, 2, null)
+ };
+
+ FoldingReference[] result = GetRegions(testString);
+ AssertFoldingReferenceArrays(expectedFolds, result);
+ }
+ }
+}