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); + } + } +}