From 075f2d58ff9bd6e0f903a910fee1313e13e8ebaf Mon Sep 17 00:00:00 2001 From: nhett Date: Fri, 29 Nov 2024 12:22:09 +0100 Subject: [PATCH 01/10] Folding ranges for comments, imports, regions and other sequences enclosed by matching literals --- AnyText/AnyText.Core/Parser.FoldingRanges.cs | 129 ++++++++++++++++++ AnyText/AnyText.Core/Rules/Rule.cs | 6 + AnyText/AnyText.Core/Rules/SequenceRule.cs | 30 +++- .../AnyText.Lsp/LspServer.FoldingRanges.cs | 35 +++++ AnyText/AnyText.Lsp/LspServer.cs | 4 + .../AnyText/Grammars/AnyTextGrammar.manual.cs | 8 ++ 6 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 AnyText/AnyText.Core/Parser.FoldingRanges.cs create mode 100644 AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs diff --git a/AnyText/AnyText.Core/Parser.FoldingRanges.cs b/AnyText/AnyText.Core/Parser.FoldingRanges.cs new file mode 100644 index 00000000..a23cafea --- /dev/null +++ b/AnyText/AnyText.Core/Parser.FoldingRanges.cs @@ -0,0 +1,129 @@ +using NMF.AnyText.Model; +using NMF.AnyText.Rules; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NMF.AnyText +{ + public partial class Parser + { + public ParsedFoldingRange[] GetFoldingRangesFromRoot() + { + RuleApplication rootApplication = Context.RootRuleApplication; + + if (rootApplication.IsPositive) + { + var parsedFoldingRanges = GetFoldingRangesFromSequence(rootApplication); + return parsedFoldingRanges; + } + + return Array.Empty(); + } + + private ParsedFoldingRange[] GetFoldingRangesFromSequence(RuleApplication ruleApplication) + { + var result = new List(); + + if (ruleApplication.Comments != null) + { + foreach (var commentRuleApplication in ruleApplication.Comments) + { + if (commentRuleApplication.Rule is MultilineCommentRule) + { + var commentsFoldingRange = new ParsedFoldingRange() + { + StartLine = (uint)commentRuleApplication.CurrentPosition.Line, + StartCharacter = (uint)commentRuleApplication.CurrentPosition.Col, + EndLine = (uint)(commentRuleApplication.CurrentPosition.Line + commentRuleApplication.ExaminedTo.Line), + EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can therefore lead to wrap around when casting to uint + Kind = "comment" + }; + + result.Add(commentsFoldingRange); + } + } + } + + if (ruleApplication.Rule is ZeroOrMoreRule zeroOrMoreRule) + { + if (zeroOrMoreRule.InnerRule.IsImports()) + { + var firstInner = (ruleApplication as MultiRuleApplication).Inner.First(); + var lastInner = (ruleApplication as MultiRuleApplication).Inner.Last(); + + var importsFoldingRange = new ParsedFoldingRange() + { + StartLine = (uint)firstInner.CurrentPosition.Line, + StartCharacter = (uint)firstInner.CurrentPosition.Col, + EndLine = (uint)lastInner.CurrentPosition.Line, + EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can therefore lead to wrap around when casting to uint + Kind = "imports" + }; + + result.Add(importsFoldingRange); + } + } + + if (ruleApplication is MultiRuleApplication multiRuleApplication) + { + if (multiRuleApplication.Rule is SequenceRule sequenceRule) + { + var firstInner = multiRuleApplication.Inner.First(); + var lastInner = multiRuleApplication.Inner.Last(); + + if (sequenceRule.IsRegion()) + { + var regionFoldingRange = new ParsedFoldingRange() + { + StartLine = (uint)firstInner.CurrentPosition.Line, + StartCharacter = (uint)firstInner.CurrentPosition.Col, + EndLine = (uint)lastInner.CurrentPosition.Line, + EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can therefore lead to wrap around when casting to uint + Kind = "region" + }; + + result.Add(regionFoldingRange); + } + else if (multiRuleApplication.Rule is ParanthesesRule || sequenceRule.IsFoldingRange()) + { + var foldingRange = new ParsedFoldingRange() + { + StartLine = (uint)firstInner.CurrentPosition.Line, + StartCharacter = (uint)firstInner.CurrentPosition.Col, + EndLine = (uint)lastInner.CurrentPosition.Line, + EndCharacter = 0 // determining the end character using length or examinedTo is inconsistent and can therefore lead to wrap around when casting to uint + }; + + result.Add(foldingRange); + } + } + + foreach (var innerRuleApplication in multiRuleApplication.Inner) + { + var innerFoldingRanges = GetFoldingRangesFromSequence(innerRuleApplication); + result.AddRange(innerFoldingRanges); + } + } + else if (ruleApplication is SingleRuleApplication singleRuleApplication) + { + var innerFoldingRanges = GetFoldingRangesFromSequence(singleRuleApplication.Inner); + result.AddRange(innerFoldingRanges); + } + + return result.ToArray(); + } + + public class ParsedFoldingRange + { + public uint StartLine; + public uint? StartCharacter; + public uint EndLine; + public uint? EndCharacter; + public string? Kind; + } + } +} diff --git a/AnyText/AnyText.Core/Rules/Rule.cs b/AnyText/AnyText.Core/Rules/Rule.cs index 2a65ed3c..e6e0ad07 100644 --- a/AnyText/AnyText.Core/Rules/Rule.cs +++ b/AnyText/AnyText.Core/Rules/Rule.cs @@ -171,6 +171,12 @@ public void Synthesize(object element, ParseContext context, TextWriter writer, /// true, if the rule could start with the given other rule, otherwise false protected internal abstract bool CanStartWith(Rule rule, List trace); + /// + /// Indicates whether the rule is used to define imports + /// + /// true, if the rule is used to define imports, otherwise false + public virtual bool IsImports() => false; + /// /// Gets the token modifiers of /// diff --git a/AnyText/AnyText.Core/Rules/SequenceRule.cs b/AnyText/AnyText.Core/Rules/SequenceRule.cs index 71a7829c..d6a95b0a 100644 --- a/AnyText/AnyText.Core/Rules/SequenceRule.cs +++ b/AnyText/AnyText.Core/Rules/SequenceRule.cs @@ -138,15 +138,39 @@ public override bool CanSynthesize(object semanticElement, ParseContext context) return Array.TrueForAll(Rules, r => r.Rule.CanSynthesize(semanticElement, context)); } - protected virtual bool IsOpeningParanthesis(string literal) + public bool IsRegion() + { + if (Rules.First() is LiteralRule startLiteralRule && Rules.Last() is LiteralRule endLiteralRule) + { + return IsRegionStartLiteral(startLiteralRule.Literal) && IsMatchingEndLiteral(endLiteralRule.Literal, startLiteralRule.Literal); + } + return false; + } + + public bool IsFoldingRange() + { + if (Rules.First() is LiteralRule startLiteralRule && Rules.Last() is LiteralRule endLiteralRule) + { + return IsRangeStartLiteral(startLiteralRule.Literal) && IsMatchingEndLiteral(endLiteralRule.Literal, startLiteralRule.Literal); + } + return false; + } + + protected virtual bool IsRegionStartLiteral(string literal) + { + return literal == "#region"; + } + + protected virtual bool IsRangeStartLiteral(string literal) { return literal == "(" || literal == "[" || literal == "{"; } - protected virtual bool IsMatchingClosingParanthesis(string literal, string openingParanthesis) + protected virtual bool IsMatchingEndLiteral(string literal, string startLiteral) { - switch (openingParanthesis) + switch (startLiteral) { + case "#region": return literal == "#endregion"; case "(": return literal == ")"; case "[": return literal == "]"; case "{": return literal == "}"; diff --git a/AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs b/AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs new file mode 100644 index 00000000..9c9e4f70 --- /dev/null +++ b/AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs @@ -0,0 +1,35 @@ +using LspTypes; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NMF.AnyText +{ + public partial class LspServer + { + public FoldingRange[] QueryFoldingRanges(JToken arg) + { + var foldingRangeParams = arg.ToObject(); + string uri = foldingRangeParams.TextDocument.Uri; + + if (!_documents.TryGetValue(uri, out var document)) + { + return Array.Empty(); + } + + var parsedFoldingRanges = document.GetFoldingRangesFromRoot(); + + return parsedFoldingRanges.Select(parsedFoldingRange => new FoldingRange() + { + StartLine = parsedFoldingRange.StartLine, + StartCharacter = parsedFoldingRange.StartCharacter, + EndLine = parsedFoldingRange.EndLine, + EndCharacter = parsedFoldingRange.EndCharacter, + Kind = parsedFoldingRange.Kind + }).ToArray(); + } + } +} diff --git a/AnyText/AnyText.Lsp/LspServer.cs b/AnyText/AnyText.Lsp/LspServer.cs index 6e605314..a2af3d9c 100644 --- a/AnyText/AnyText.Lsp/LspServer.cs +++ b/AnyText/AnyText.Lsp/LspServer.cs @@ -63,6 +63,10 @@ public InitializeResult Initialize( } }, ReferencesProvider = new ReferenceOptions + { + WorkDoneProgress = false + }, + FoldingRangeProvider = new FoldingRangeOptions { WorkDoneProgress = false } diff --git a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs index 01490d78..91f85a95 100644 --- a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs +++ b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs @@ -133,5 +133,13 @@ public partial class KeywordRule public override string[] TokenModifiers => new [] { "definition" }; } + + public partial class GrammarImportsRule + { + public override bool IsImports() + { + return true; + } + } } } From 2bb1a814ec25ce808892e971721b006c31a35969 Mon Sep 17 00:00:00 2001 From: nhett Date: Fri, 6 Dec 2024 12:51:58 +0100 Subject: [PATCH 02/10] Comment change tests for folding ranges --- .../AnyText/Grammars/AnyTextGrammar.manual.cs | 2 +- .../Tests/AnyText.Tests/CommentChangeTests.cs | 340 ++++++++++++++++++ 2 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 AnyText/Tests/AnyText.Tests/CommentChangeTests.cs diff --git a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs index 91f85a95..46817688 100644 --- a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs +++ b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs @@ -134,7 +134,7 @@ public partial class KeywordRule } - public partial class GrammarImportsRule + public partial class GrammarImportsMetamodelImportRule { public override bool IsImports() { diff --git a/AnyText/Tests/AnyText.Tests/CommentChangeTests.cs b/AnyText/Tests/AnyText.Tests/CommentChangeTests.cs new file mode 100644 index 00000000..7c7fc006 --- /dev/null +++ b/AnyText/Tests/AnyText.Tests/CommentChangeTests.cs @@ -0,0 +1,340 @@ +using NMF.AnyText; +using NMF.AnyText.Grammars; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AnyText.Tests +{ + [TestFixture] + class CommentChangeTests + { + [Test] + public void CommentChange_RespondToLineAddition() + { + var anyText = new AnyTextGrammar(); + var parser = new Parser(new ModelParseContext(anyText)); + var grammar = @"grammar AnyText ( anytext ) +root Grammar +imports https://github.com/NMFCode/NMF/AnyText + +comment '//' +comment '/*' to '*/' + +/* + +this is a multiline comment + +*/ + +Grammar: + 'grammar' Name=ID ( '(' LanguageId=ID ')' )? 'root' StartRule=[ClassRule] Imports+=MetamodelImport* Comments+=CommentRule * Rules+=Rule +; + +CommentRule: + MultilineCommentRule | SinglelineCommentRule; + +SinglelineCommentRule: + 'comment' Start=Keyword; + +MultilineCommentRule: + 'comment' Start=Keyword 'to' End=Keyword; + +MetamodelImport: + 'imports' ( Prefix=ID 'from' )? File=Uri ; + +Rule: + ClassRule | DataRule | FragmentRule | ParanthesisRule | EnumRule; + +ClassRule: + InheritanceRule | ModelRule; + +InheritanceRule: + Name=ID RuleTypeFragment ':' Subtypes+=[ClassRule] ( '|' Subtypes+=[ClassRule] )+ ';' ; + +ModelRule: + Name=ID RuleTypeFragment ':' Expression=ParserExpression ';' ; + +DataRule: + 'terminal' Name=ID RuleTypeFragment ':' Regex=Regex ( 'surround' 'with' SurroundCharacter=Char )? ( 'escape' EscapeRules+=EscapeRule ( ',' EscapeRules+=EscapeRule )* )? ';' ; + +EscapeRule: + Character=Char 'as' Escape=Keyword; + +FragmentRule: + 'fragment' Name=ID 'processes' ( Prefix=ID '.' )? TypeName=ID ':' Expression=ParserExpression ';' ; + +ParanthesisRule: + 'parantheses' Name=ID ':' OpeningParanthesis=KeywordExpression InnerRule=[ClassRule] ClosingParanthesis=KeywordExpression ';' ; + +EnumRule: + 'enum' Name=ID RuleTypeFragment ':' Literals+=LiteralRule+ ';' ; + +LiteralRule: + Literal=ID '=>' Keyword=Keyword ; + +fragment RuleTypeFragment processes Rule : + ( 'returns' ( Prefix=ID '.' )? TypeName=ID )? ; + +fragment FormattingInstructionFragment processes ParserExpression : + FormattingInstructions+=FormattingInstruction*; + +enum FormattingInstruction: + Newline => '' + Indent => '' + Unindent => '' + AvoidSpace => '' + ForbidSpace => '' + ; + +ParserExpression: + ChoiceExpression | SequenceExpression | ConjunctiveParserExpression; + +ConjunctiveParserExpression returns ParserExpression: + PlusExpression | StarExpression | MaybeExpression | BasicParserExpression; + +BasicParserExpression returns ParserExpression: + NegativeLookaheadExpression | KeywordExpression | ReferenceExpression | AssignExpression | AddAssignExpression | ExistsAssignExpression | RuleExpression | ParanthesisExpression; + +parantheses ParanthesisExpression : + '(' ParserExpression ')'; + +SequenceExpression: + InnerExpressions+=ConjunctiveParserExpression InnerExpressions+=ConjunctiveParserExpression+; + +PlusExpression: + Inner=BasicParserExpression '+' FormattingInstructionFragment; + +StarExpression: + Inner=BasicParserExpression '*' FormattingInstructionFragment; + +MaybeExpression: + Inner=BasicParserExpression '?' FormattingInstructionFragment; + +KeywordExpression: + Keyword=Keyword FormattingInstructionFragment; + +ChoiceExpression: + ( Alternatives+=ConjunctiveParserExpression '|' )+ Alternatives+=ConjunctiveParserExpression; + +AssignExpression: + Feature=ID '=' Assigned=BasicParserExpression FormattingInstructionFragment; + +AddAssignExpression: + Feature=ID '+=' Assigned=BasicParserExpression FormattingInstructionFragment; + +ExistsAssignExpression: + Feature=ID '?=' Assigned=BasicParserExpression FormattingInstructionFragment; + +NegativeLookaheadExpression: + '!' Inner=BasicParserExpression; + +RuleExpression: + Rule=[Rule] FormattingInstructionFragment !'='!'+='!'?='; + +ReferenceExpression: + '[' ReferencedRule=[Rule] ']'; + +terminal ID: + /[a-zA-Z]\w*/; + +terminal Keyword: + /(\\\\|\\'|[^'\\])+/ surround with ' escape \ as '\\\\' , ' as '\\\''; + +terminal Regex: + /(\\\/|[^\/])*/ surround with / escape / as '\\/'; + +terminal Uri: + /.+/; + +terminal Char: + /\S/; + +"; + + var parsed = parser.Initialize(SplitIntoLines(grammar)); + Assert.IsNotNull(parsed); + Assert.That(parser.Context.Errors, Is.Empty); + + parser.Update([new TextEdit(new ParsePosition(10, 0), new ParsePosition(10, 0), ["\r\n"])]); + Assert.IsNotNull(parsed); + Assert.That(parser.Context.Errors, Is.Empty); + //var examinedUpdate = new ParsePositionDelta(13, 7); + //AssertAtLeast(parser.Context.RootRuleApplication.ExaminedTo, examinedUpdate); + } + + [Test] + public void CommentChange_RespondToLineRemoval() + { + var anyText = new AnyTextGrammar(); + var parser = new Parser(new ModelParseContext(anyText)); + var grammar = @"grammar AnyText ( anytext ) +root Grammar +imports https://github.com/NMFCode/NMF/AnyText + +comment '//' +comment '/*' to '*/' + +/* + +this is a multiline comment + +*/ + +Grammar: + 'grammar' Name=ID ( '(' LanguageId=ID ')' )? 'root' StartRule=[ClassRule] Imports+=MetamodelImport* Comments+=CommentRule * Rules+=Rule +; + +CommentRule: + MultilineCommentRule | SinglelineCommentRule; + +SinglelineCommentRule: + 'comment' Start=Keyword; + +MultilineCommentRule: + 'comment' Start=Keyword 'to' End=Keyword; + +MetamodelImport: + 'imports' ( Prefix=ID 'from' )? File=Uri ; + +Rule: + ClassRule | DataRule | FragmentRule | ParanthesisRule | EnumRule; + +ClassRule: + InheritanceRule | ModelRule; + +InheritanceRule: + Name=ID RuleTypeFragment ':' Subtypes+=[ClassRule] ( '|' Subtypes+=[ClassRule] )+ ';' ; + +ModelRule: + Name=ID RuleTypeFragment ':' Expression=ParserExpression ';' ; + +DataRule: + 'terminal' Name=ID RuleTypeFragment ':' Regex=Regex ( 'surround' 'with' SurroundCharacter=Char )? ( 'escape' EscapeRules+=EscapeRule ( ',' EscapeRules+=EscapeRule )* )? ';' ; + +EscapeRule: + Character=Char 'as' Escape=Keyword; + +FragmentRule: + 'fragment' Name=ID 'processes' ( Prefix=ID '.' )? TypeName=ID ':' Expression=ParserExpression ';' ; + +ParanthesisRule: + 'parantheses' Name=ID ':' OpeningParanthesis=KeywordExpression InnerRule=[ClassRule] ClosingParanthesis=KeywordExpression ';' ; + +EnumRule: + 'enum' Name=ID RuleTypeFragment ':' Literals+=LiteralRule+ ';' ; + +LiteralRule: + Literal=ID '=>' Keyword=Keyword ; + +fragment RuleTypeFragment processes Rule : + ( 'returns' ( Prefix=ID '.' )? TypeName=ID )? ; + +fragment FormattingInstructionFragment processes ParserExpression : + FormattingInstructions+=FormattingInstruction*; + +enum FormattingInstruction: + Newline => '' + Indent => '' + Unindent => '' + AvoidSpace => '' + ForbidSpace => '' + ; + +ParserExpression: + ChoiceExpression | SequenceExpression | ConjunctiveParserExpression; + +ConjunctiveParserExpression returns ParserExpression: + PlusExpression | StarExpression | MaybeExpression | BasicParserExpression; + +BasicParserExpression returns ParserExpression: + NegativeLookaheadExpression | KeywordExpression | ReferenceExpression | AssignExpression | AddAssignExpression | ExistsAssignExpression | RuleExpression | ParanthesisExpression; + +parantheses ParanthesisExpression : + '(' ParserExpression ')'; + +SequenceExpression: + InnerExpressions+=ConjunctiveParserExpression InnerExpressions+=ConjunctiveParserExpression+; + +PlusExpression: + Inner=BasicParserExpression '+' FormattingInstructionFragment; + +StarExpression: + Inner=BasicParserExpression '*' FormattingInstructionFragment; + +MaybeExpression: + Inner=BasicParserExpression '?' FormattingInstructionFragment; + +KeywordExpression: + Keyword=Keyword FormattingInstructionFragment; + +ChoiceExpression: + ( Alternatives+=ConjunctiveParserExpression '|' )+ Alternatives+=ConjunctiveParserExpression; + +AssignExpression: + Feature=ID '=' Assigned=BasicParserExpression FormattingInstructionFragment; + +AddAssignExpression: + Feature=ID '+=' Assigned=BasicParserExpression FormattingInstructionFragment; + +ExistsAssignExpression: + Feature=ID '?=' Assigned=BasicParserExpression FormattingInstructionFragment; + +NegativeLookaheadExpression: + '!' Inner=BasicParserExpression; + +RuleExpression: + Rule=[Rule] FormattingInstructionFragment !'='!'+='!'?='; + +ReferenceExpression: + '[' ReferencedRule=[Rule] ']'; + +terminal ID: + /[a-zA-Z]\w*/; + +terminal Keyword: + /(\\\\|\\'|[^'\\])+/ surround with ' escape \ as '\\\\' , ' as '\\\''; + +terminal Regex: + /(\\\/|[^\/])*/ surround with / escape / as '\\/'; + +terminal Uri: + /.+/; + +terminal Char: + /\S/; + +"; + + var parsed = parser.Initialize(SplitIntoLines(grammar)); + Assert.IsNotNull(parsed); + Assert.That(parser.Context.Errors, Is.Empty); + + parser.Update([new TextEdit(new ParsePosition(9, 27), new ParsePosition(10, 0), [""])]); + Assert.IsNotNull(parsed); + Assert.That(parser.Context.Errors, Is.Empty); + //var examinedUpdate = new ParsePositionDelta(13, 7); + //AssertAtLeast(parser.Context.RootRuleApplication.ExaminedTo, examinedUpdate); + } + + private static string[] SplitIntoLines(string grammar) + { + var lines = grammar.Split('\n'); + for (int i = 0; i < lines.Length; i++) + { + lines[i] = lines[i].TrimEnd('\r'); + } + return lines; + } + + private static void AssertAtLeast(ParsePositionDelta actual, ParsePositionDelta expected) + { + Assert.That(actual.Line, Is.AtLeast(expected.Line)); + if (actual.Line == expected.Line) + { + Assert.That(actual.Col, Is.AtLeast(expected.Col)); + } + } + } +} From 7e70fd4497a857600618d34dd52c165080c0078e Mon Sep 17 00:00:00 2001 From: Georg Hinkel Date: Fri, 13 Dec 2024 13:12:05 +0100 Subject: [PATCH 03/10] adjusted sequence rule to formatting refactoring --- AnyText/AnyText.Core/Rules/SequenceRule.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AnyText/AnyText.Core/Rules/SequenceRule.cs b/AnyText/AnyText.Core/Rules/SequenceRule.cs index d6a95b0a..bcf10c87 100644 --- a/AnyText/AnyText.Core/Rules/SequenceRule.cs +++ b/AnyText/AnyText.Core/Rules/SequenceRule.cs @@ -140,7 +140,7 @@ public override bool CanSynthesize(object semanticElement, ParseContext context) public bool IsRegion() { - if (Rules.First() is LiteralRule startLiteralRule && Rules.Last() is LiteralRule endLiteralRule) + if (Rules.First().Rule is LiteralRule startLiteralRule && Rules.Last().Rule is LiteralRule endLiteralRule) { return IsRegionStartLiteral(startLiteralRule.Literal) && IsMatchingEndLiteral(endLiteralRule.Literal, startLiteralRule.Literal); } @@ -149,7 +149,7 @@ public bool IsRegion() public bool IsFoldingRange() { - if (Rules.First() is LiteralRule startLiteralRule && Rules.Last() is LiteralRule endLiteralRule) + if (Rules.First().Rule is LiteralRule startLiteralRule && Rules.Last().Rule is LiteralRule endLiteralRule) { return IsRangeStartLiteral(startLiteralRule.Literal) && IsMatchingEndLiteral(endLiteralRule.Literal, startLiteralRule.Literal); } From 682baf2eeaae17e86171f3c3fee046838a221c7e Mon Sep 17 00:00:00 2001 From: nhett Date: Tue, 17 Dec 2024 15:41:06 +0100 Subject: [PATCH 04/10] Rebase fix --- AnyText/AnyText.Lsp/ILspServer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/AnyText/AnyText.Lsp/ILspServer.cs b/AnyText/AnyText.Lsp/ILspServer.cs index 43a0bc0c..ab067e6f 100644 --- a/AnyText/AnyText.Lsp/ILspServer.cs +++ b/AnyText/AnyText.Lsp/ILspServer.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json.Linq; using StreamJsonRpc; using System; +using System.Reflection.PortableExecutable; using System.Threading; namespace NMF.AnyText From 796f6a3097d6d4dc5dc61b69c51636180ac913fe Mon Sep 17 00:00:00 2001 From: nhett Date: Fri, 3 Jan 2025 12:07:08 +0100 Subject: [PATCH 05/10] Consider multiple singleline comments in succession for folding ranges, refactoring --- AnyText/AnyText.Core/Parser.FoldingRanges.cs | 161 ++++++++++++------ .../AnyText.Lsp/ILspServer.FoldingRanges.cs | 22 +++ .../AnyText.Lsp/LspServer.FoldingRanges.cs | 33 +++- 3 files changed, 157 insertions(+), 59 deletions(-) create mode 100644 AnyText/AnyText.Lsp/ILspServer.FoldingRanges.cs diff --git a/AnyText/AnyText.Core/Parser.FoldingRanges.cs b/AnyText/AnyText.Core/Parser.FoldingRanges.cs index a23cafea..8d14fda4 100644 --- a/AnyText/AnyText.Core/Parser.FoldingRanges.cs +++ b/AnyText/AnyText.Core/Parser.FoldingRanges.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Xml.XPath; namespace NMF.AnyText { @@ -30,75 +31,25 @@ private ParsedFoldingRange[] GetFoldingRangesFromSequence(RuleApplication ruleAp if (ruleApplication.Comments != null) { - foreach (var commentRuleApplication in ruleApplication.Comments) - { - if (commentRuleApplication.Rule is MultilineCommentRule) - { - var commentsFoldingRange = new ParsedFoldingRange() - { - StartLine = (uint)commentRuleApplication.CurrentPosition.Line, - StartCharacter = (uint)commentRuleApplication.CurrentPosition.Col, - EndLine = (uint)(commentRuleApplication.CurrentPosition.Line + commentRuleApplication.ExaminedTo.Line), - EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can therefore lead to wrap around when casting to uint - Kind = "comment" - }; - - result.Add(commentsFoldingRange); - } - } + result.AddRange(GetCommentFoldingRanges(ruleApplication)); } - if (ruleApplication.Rule is ZeroOrMoreRule zeroOrMoreRule) + if (ruleApplication.Rule is ZeroOrMoreRule zeroOrMoreRule && zeroOrMoreRule.InnerRule.IsImports()) { - if (zeroOrMoreRule.InnerRule.IsImports()) - { - var firstInner = (ruleApplication as MultiRuleApplication).Inner.First(); - var lastInner = (ruleApplication as MultiRuleApplication).Inner.Last(); - - var importsFoldingRange = new ParsedFoldingRange() - { - StartLine = (uint)firstInner.CurrentPosition.Line, - StartCharacter = (uint)firstInner.CurrentPosition.Col, - EndLine = (uint)lastInner.CurrentPosition.Line, - EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can therefore lead to wrap around when casting to uint - Kind = "imports" - }; - - result.Add(importsFoldingRange); - } + result.Add(GetImportsFoldingRange(ruleApplication)); } if (ruleApplication is MultiRuleApplication multiRuleApplication) { if (multiRuleApplication.Rule is SequenceRule sequenceRule) { - var firstInner = multiRuleApplication.Inner.First(); - var lastInner = multiRuleApplication.Inner.Last(); - if (sequenceRule.IsRegion()) { - var regionFoldingRange = new ParsedFoldingRange() - { - StartLine = (uint)firstInner.CurrentPosition.Line, - StartCharacter = (uint)firstInner.CurrentPosition.Col, - EndLine = (uint)lastInner.CurrentPosition.Line, - EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can therefore lead to wrap around when casting to uint - Kind = "region" - }; - - result.Add(regionFoldingRange); + result.Add(GetRegionFoldingRange(multiRuleApplication)); } else if (multiRuleApplication.Rule is ParanthesesRule || sequenceRule.IsFoldingRange()) { - var foldingRange = new ParsedFoldingRange() - { - StartLine = (uint)firstInner.CurrentPosition.Line, - StartCharacter = (uint)firstInner.CurrentPosition.Col, - EndLine = (uint)lastInner.CurrentPosition.Line, - EndCharacter = 0 // determining the end character using length or examinedTo is inconsistent and can therefore lead to wrap around when casting to uint - }; - - result.Add(foldingRange); + result.Add(GetFoldingRange(multiRuleApplication)); } } @@ -117,6 +68,106 @@ private ParsedFoldingRange[] GetFoldingRangesFromSequence(RuleApplication ruleAp return result.ToArray(); } + private ParsedFoldingRange[] GetCommentFoldingRanges(RuleApplication ruleApplication) + { + var result = new List(); + + for (var i = 0; i < ruleApplication.Comments.Count; i++) + { + var commentRuleApplication = ruleApplication.Comments[i]; + + if (commentRuleApplication.Rule is MultilineCommentRule) + { + var commentsFoldingRange = new ParsedFoldingRange() + { + StartLine = (uint)commentRuleApplication.CurrentPosition.Line, + StartCharacter = (uint)commentRuleApplication.CurrentPosition.Col, + EndLine = (uint)(commentRuleApplication.CurrentPosition.Line + commentRuleApplication.ExaminedTo.Line), + EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint + Kind = "comment" + }; + + result.Add(commentsFoldingRange); + } + else + { + RuleApplication endCommentRuleApplication; + do + { + endCommentRuleApplication = ruleApplication.Comments[i++]; + } + while (endCommentRuleApplication.Rule is not MultilineCommentRule + && endCommentRuleApplication.CurrentPosition.Col == commentRuleApplication.CurrentPosition.Col + && i < ruleApplication.Comments.Count); + + if (commentRuleApplication == endCommentRuleApplication) continue; + + var commentsFoldingRange = new ParsedFoldingRange() + { + StartLine = (uint)commentRuleApplication.CurrentPosition.Line, + StartCharacter = (uint)commentRuleApplication.CurrentPosition.Col, + EndLine = (uint)(endCommentRuleApplication.CurrentPosition.Line), + EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint + Kind = "comment" + }; + + result.Add(commentsFoldingRange); + } + } + + return result.ToArray(); + } + + private ParsedFoldingRange GetImportsFoldingRange(RuleApplication ruleApplication) + { + var firstInner = (ruleApplication as MultiRuleApplication).Inner.First(); + var lastInner = (ruleApplication as MultiRuleApplication).Inner.Last(); + + var importsFoldingRange = new ParsedFoldingRange() + { + StartLine = (uint)firstInner.CurrentPosition.Line, + StartCharacter = (uint)firstInner.CurrentPosition.Col, + EndLine = (uint)lastInner.CurrentPosition.Line, + EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint + Kind = "imports" + }; + + return importsFoldingRange; + } + + private ParsedFoldingRange GetRegionFoldingRange(MultiRuleApplication multiRuleApplication) + { + var firstInner = multiRuleApplication.Inner.First(); + var lastInner = multiRuleApplication.Inner.Last(); + + var regionFoldingRange = new ParsedFoldingRange() + { + StartLine = (uint)firstInner.CurrentPosition.Line, + StartCharacter = (uint)firstInner.CurrentPosition.Col, + EndLine = (uint)lastInner.CurrentPosition.Line, + EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint + Kind = "region" + }; + + return regionFoldingRange; + } + + private ParsedFoldingRange GetFoldingRange(MultiRuleApplication multiRuleApplication) + { + var firstInner = multiRuleApplication.Inner.First(); + var lastInner = multiRuleApplication.Inner.Last(); + + var foldingRange = new ParsedFoldingRange() + { + StartLine = (uint)firstInner.CurrentPosition.Line, + StartCharacter = (uint)firstInner.CurrentPosition.Col, + EndLine = (uint)lastInner.CurrentPosition.Line, + EndCharacter = 0 // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint + }; + + return foldingRange; + } + public class ParsedFoldingRange { public uint StartLine; diff --git a/AnyText/AnyText.Lsp/ILspServer.FoldingRanges.cs b/AnyText/AnyText.Lsp/ILspServer.FoldingRanges.cs new file mode 100644 index 00000000..13ba32a9 --- /dev/null +++ b/AnyText/AnyText.Lsp/ILspServer.FoldingRanges.cs @@ -0,0 +1,22 @@ +using LspTypes; +using Newtonsoft.Json.Linq; +using StreamJsonRpc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NMF.AnyText +{ + public partial interface ILspServer + { + /// + /// Handles the textDocument/foldingRange/full request from the client. This is used to retrieve all folding ranges for a document. + /// + /// The JSON token containing the parameters of the request. (FoldingRangeParams) + /// An array of objects, each containing details on a folding range in the document. + [JsonRpcMethod(Methods.TextDocumentFoldingRangeName)] + FoldingRange[] QueryFoldingRanges(JToken arg); + } +} diff --git a/AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs b/AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs index 9c9e4f70..f185b735 100644 --- a/AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs +++ b/AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs @@ -2,6 +2,8 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -10,6 +12,7 @@ namespace NMF.AnyText { public partial class LspServer { + /// public FoldingRange[] QueryFoldingRanges(JToken arg) { var foldingRangeParams = arg.ToObject(); @@ -19,17 +22,39 @@ public FoldingRange[] QueryFoldingRanges(JToken arg) { return Array.Empty(); } + + var parsedFoldingRanges = document.GetFoldingRangesFromRoot().ToList(); - var parsedFoldingRanges = document.GetFoldingRangesFromRoot(); - - return parsedFoldingRanges.Select(parsedFoldingRange => new FoldingRange() + var foldingRanges = parsedFoldingRanges.Select(parsedFoldingRange => new FoldingRange() { StartLine = parsedFoldingRange.StartLine, StartCharacter = parsedFoldingRange.StartCharacter, EndLine = parsedFoldingRange.EndLine, EndCharacter = parsedFoldingRange.EndCharacter, Kind = parsedFoldingRange.Kind - }).ToArray(); + }); + + return foldingRanges.Distinct(new FoldingRangeEqualityComparer()).ToArray(); + } + + private class FoldingRangeEqualityComparer : IEqualityComparer + { + public bool Equals(FoldingRange x, FoldingRange y) + { + if (ReferenceEquals(x, y)) return true; + else if (x == null || y == null) return false; + + return x.StartLine == y.StartLine + && x.StartCharacter == y.StartCharacter + && x.EndLine == y.EndLine + && x.EndCharacter == y.EndCharacter + && x.Kind.Equals(y.Kind); + } + + public int GetHashCode([DisallowNull] FoldingRange obj) + { + return 0; // always use equals over hash code + } } } } From 8e6a2013377def780207eb63855902c993a754cb Mon Sep 17 00:00:00 2001 From: nhett Date: Sat, 4 Jan 2025 15:46:53 +0100 Subject: [PATCH 06/10] Add folding capabilities for other rules as well --- AnyText/AnyText.Core/Parser.FoldingRanges.cs | 2 +- AnyText/AnyText.Core/Rules/Rule.cs | 5 +++++ AnyText/AnyText.Core/Rules/SequenceRule.cs | 2 +- .../AnyText/Grammars/AnyTextGrammar.manual.cs | 16 +++++++++++----- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/AnyText/AnyText.Core/Parser.FoldingRanges.cs b/AnyText/AnyText.Core/Parser.FoldingRanges.cs index 8d14fda4..033eb24b 100644 --- a/AnyText/AnyText.Core/Parser.FoldingRanges.cs +++ b/AnyText/AnyText.Core/Parser.FoldingRanges.cs @@ -47,7 +47,7 @@ private ParsedFoldingRange[] GetFoldingRangesFromSequence(RuleApplication ruleAp { result.Add(GetRegionFoldingRange(multiRuleApplication)); } - else if (multiRuleApplication.Rule is ParanthesesRule || sequenceRule.IsFoldingRange()) + else if (multiRuleApplication.Rule is ParanthesesRule || sequenceRule.IsFoldable()) { result.Add(GetFoldingRange(multiRuleApplication)); } diff --git a/AnyText/AnyText.Core/Rules/Rule.cs b/AnyText/AnyText.Core/Rules/Rule.cs index e6e0ad07..d2658ef3 100644 --- a/AnyText/AnyText.Core/Rules/Rule.cs +++ b/AnyText/AnyText.Core/Rules/Rule.cs @@ -54,6 +54,11 @@ protected internal virtual void OnDeactivate(RuleApplication application, ParseC /// public virtual bool IsComment => false; + /// + /// True, if the rule element can be folded away (hidden) in the editor + /// + public virtual bool IsFoldable() => false; + /// /// True, if the rule permits trailing whitespaces, otherwise false /// diff --git a/AnyText/AnyText.Core/Rules/SequenceRule.cs b/AnyText/AnyText.Core/Rules/SequenceRule.cs index bcf10c87..74fec9f2 100644 --- a/AnyText/AnyText.Core/Rules/SequenceRule.cs +++ b/AnyText/AnyText.Core/Rules/SequenceRule.cs @@ -147,7 +147,7 @@ public bool IsRegion() return false; } - public bool IsFoldingRange() + public override bool IsFoldable() { if (Rules.First().Rule is LiteralRule startLiteralRule && Rules.Last().Rule is LiteralRule endLiteralRule) { diff --git a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs index 46817688..0aed38cf 100644 --- a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs +++ b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.cs @@ -21,6 +21,7 @@ public partial class RuleRule public partial class InheritanceRuleRule { public override string[] TokenModifiers => new [] { "declaration" }; + public override bool IsFoldable() => true; } public partial class GrammarStartRuleClassRuleRule @@ -57,6 +58,7 @@ public partial class InheritanceRuleSubtypesClassRuleRule public partial class FragmentRuleRule { public override string TokenType => "function"; + public override bool IsFoldable() => true; } public partial class RuleTypeNameIDRule @@ -71,10 +73,12 @@ public partial class DataRuleRegexRegexRule public partial class DataRuleRule { public override string TokenType => "regexp"; + public override bool IsFoldable() => true; } public partial class ParanthesisRuleRule { public override string TokenType => "interface"; + public override bool IsFoldable() => true; } public partial class ParanthesisRuleInnerRuleClassRuleRule { @@ -89,7 +93,7 @@ public partial class EnumRuleRule { public override string TokenType => "enum"; public override string[] TokenModifiers => new [] { "declaration" }; - + public override bool IsFoldable() => true; } public partial class EnumRuleLiteralsLiteralRuleRule { @@ -136,10 +140,12 @@ public partial class KeywordRule public partial class GrammarImportsMetamodelImportRule { - public override bool IsImports() - { - return true; - } + public override bool IsImports() => true; + } + + public partial class ModelRuleRule + { + public override bool IsFoldable() => true; } } } From 0910f88b57b694c065668c98b37d509394b94954 Mon Sep 17 00:00:00 2001 From: nhett Date: Thu, 9 Jan 2025 14:06:36 +0100 Subject: [PATCH 07/10] Move folding range parsing to rule applications, improved tests, documentation and refactoring --- AnyText/AnyText.Core/FoldingRange.cs | 47 +++ AnyText/AnyText.Core/Parser.FoldingRanges.cs | 165 +--------- .../Rules/MultiRuleApplication.cs | 46 ++- AnyText/AnyText.Core/Rules/Rule.cs | 13 +- AnyText/AnyText.Core/Rules/RuleApplication.cs | 52 ++++ .../Rules/SingleRuleApplication.cs | 6 + .../AnyText.Lsp/ILspServer.FoldingRanges.cs | 2 +- .../AnyText.Lsp/LspServer.FoldingRanges.cs | 42 +-- .../Tests/AnyText.Tests/CommentChangeTests.cs | 286 ++---------------- 9 files changed, 204 insertions(+), 455 deletions(-) create mode 100644 AnyText/AnyText.Core/FoldingRange.cs diff --git a/AnyText/AnyText.Core/FoldingRange.cs b/AnyText/AnyText.Core/FoldingRange.cs new file mode 100644 index 00000000..32fad133 --- /dev/null +++ b/AnyText/AnyText.Core/FoldingRange.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NMF.AnyText +{ + /// + /// Denotes a part in a parsed document that can be folded away (hidden). + /// Analogous to the LspTypes FoldingRange interface. + /// + public class FoldingRange + { + /// + /// The zero-based start line of the range to fold. The folded area starts + /// after the line's last character. To be valid, the end must be zero or + /// larger and smaller than the number of lines in the document. + /// + public uint 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 uint StartCharacter { get; set; } + + /// + /// The zero-based end line of the range to fold. The folded area ends with + /// the line's last character. To be valid, the end must be zero or larger + /// and smaller than the number of lines in the document. + /// + public uint 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 uint EndCharacter { get; set; } + + /// + /// Describes the kind of the folding range. + /// Supports values "comment", "imports" and "region". + /// + public string Kind { get; set; } + } +} diff --git a/AnyText/AnyText.Core/Parser.FoldingRanges.cs b/AnyText/AnyText.Core/Parser.FoldingRanges.cs index 033eb24b..d8ef7fa0 100644 --- a/AnyText/AnyText.Core/Parser.FoldingRanges.cs +++ b/AnyText/AnyText.Core/Parser.FoldingRanges.cs @@ -12,169 +12,22 @@ namespace NMF.AnyText { public partial class Parser { - public ParsedFoldingRange[] GetFoldingRangesFromRoot() + /// + /// Parses folding ranges starting from the root rule application + /// + /// An IEnumerable of objects, each containing details on a folding range in the document. + public IEnumerable GetFoldingRangesFromRoot() { RuleApplication rootApplication = Context.RootRuleApplication; if (rootApplication.IsPositive) { - var parsedFoldingRanges = GetFoldingRangesFromSequence(rootApplication); - return parsedFoldingRanges; + var result = new List(); + rootApplication.GetFoldingRanges(result); + return result; } - return Array.Empty(); - } - - private ParsedFoldingRange[] GetFoldingRangesFromSequence(RuleApplication ruleApplication) - { - var result = new List(); - - if (ruleApplication.Comments != null) - { - result.AddRange(GetCommentFoldingRanges(ruleApplication)); - } - - if (ruleApplication.Rule is ZeroOrMoreRule zeroOrMoreRule && zeroOrMoreRule.InnerRule.IsImports()) - { - result.Add(GetImportsFoldingRange(ruleApplication)); - } - - if (ruleApplication is MultiRuleApplication multiRuleApplication) - { - if (multiRuleApplication.Rule is SequenceRule sequenceRule) - { - if (sequenceRule.IsRegion()) - { - result.Add(GetRegionFoldingRange(multiRuleApplication)); - } - else if (multiRuleApplication.Rule is ParanthesesRule || sequenceRule.IsFoldable()) - { - result.Add(GetFoldingRange(multiRuleApplication)); - } - } - - foreach (var innerRuleApplication in multiRuleApplication.Inner) - { - var innerFoldingRanges = GetFoldingRangesFromSequence(innerRuleApplication); - result.AddRange(innerFoldingRanges); - } - } - else if (ruleApplication is SingleRuleApplication singleRuleApplication) - { - var innerFoldingRanges = GetFoldingRangesFromSequence(singleRuleApplication.Inner); - result.AddRange(innerFoldingRanges); - } - - return result.ToArray(); - } - - private ParsedFoldingRange[] GetCommentFoldingRanges(RuleApplication ruleApplication) - { - var result = new List(); - - for (var i = 0; i < ruleApplication.Comments.Count; i++) - { - var commentRuleApplication = ruleApplication.Comments[i]; - - if (commentRuleApplication.Rule is MultilineCommentRule) - { - var commentsFoldingRange = new ParsedFoldingRange() - { - StartLine = (uint)commentRuleApplication.CurrentPosition.Line, - StartCharacter = (uint)commentRuleApplication.CurrentPosition.Col, - EndLine = (uint)(commentRuleApplication.CurrentPosition.Line + commentRuleApplication.ExaminedTo.Line), - EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint - Kind = "comment" - }; - - result.Add(commentsFoldingRange); - } - else - { - RuleApplication endCommentRuleApplication; - do - { - endCommentRuleApplication = ruleApplication.Comments[i++]; - } - while (endCommentRuleApplication.Rule is not MultilineCommentRule - && endCommentRuleApplication.CurrentPosition.Col == commentRuleApplication.CurrentPosition.Col - && i < ruleApplication.Comments.Count); - - if (commentRuleApplication == endCommentRuleApplication) continue; - - var commentsFoldingRange = new ParsedFoldingRange() - { - StartLine = (uint)commentRuleApplication.CurrentPosition.Line, - StartCharacter = (uint)commentRuleApplication.CurrentPosition.Col, - EndLine = (uint)(endCommentRuleApplication.CurrentPosition.Line), - EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint - Kind = "comment" - }; - - result.Add(commentsFoldingRange); - } - } - - return result.ToArray(); - } - - private ParsedFoldingRange GetImportsFoldingRange(RuleApplication ruleApplication) - { - var firstInner = (ruleApplication as MultiRuleApplication).Inner.First(); - var lastInner = (ruleApplication as MultiRuleApplication).Inner.Last(); - - var importsFoldingRange = new ParsedFoldingRange() - { - StartLine = (uint)firstInner.CurrentPosition.Line, - StartCharacter = (uint)firstInner.CurrentPosition.Col, - EndLine = (uint)lastInner.CurrentPosition.Line, - EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint - Kind = "imports" - }; - - return importsFoldingRange; - } - - private ParsedFoldingRange GetRegionFoldingRange(MultiRuleApplication multiRuleApplication) - { - var firstInner = multiRuleApplication.Inner.First(); - var lastInner = multiRuleApplication.Inner.Last(); - - var regionFoldingRange = new ParsedFoldingRange() - { - StartLine = (uint)firstInner.CurrentPosition.Line, - StartCharacter = (uint)firstInner.CurrentPosition.Col, - EndLine = (uint)lastInner.CurrentPosition.Line, - EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint - Kind = "region" - }; - - return regionFoldingRange; - } - - private ParsedFoldingRange GetFoldingRange(MultiRuleApplication multiRuleApplication) - { - var firstInner = multiRuleApplication.Inner.First(); - var lastInner = multiRuleApplication.Inner.Last(); - - var foldingRange = new ParsedFoldingRange() - { - StartLine = (uint)firstInner.CurrentPosition.Line, - StartCharacter = (uint)firstInner.CurrentPosition.Col, - EndLine = (uint)lastInner.CurrentPosition.Line, - EndCharacter = 0 // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint - }; - - return foldingRange; - } - - public class ParsedFoldingRange - { - public uint StartLine; - public uint? StartCharacter; - public uint EndLine; - public uint? EndCharacter; - public string? Kind; + return Enumerable.Empty(); } } } diff --git a/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs b/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs index e0b3bd60..4fda647f 100644 --- a/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs @@ -1,6 +1,8 @@ -using NMF.AnyText.PrettyPrinting; +using NMF.AnyText.Model; +using NMF.AnyText.PrettyPrinting; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.Versioning; @@ -161,6 +163,48 @@ public override object GetValue(ParseContext context) } } + public override void GetFoldingRanges(ICollection result) + { + base.GetFoldingRanges(result); + + if (Rule is ZeroOrMoreRule zeroOrMoreRule && zeroOrMoreRule.InnerRule.IsImports()) + { + GetFoldingRange("imports", result); + } + + if (Rule is SequenceRule sequenceRule) + { + if (sequenceRule.IsRegion()) + { + GetFoldingRange("region", result); + } + else if (Rule is ParanthesesRule || sequenceRule.IsFoldable()) + { + GetFoldingRange(null, result); + } + } + + foreach (var innerRuleApplication in Inner) + { + innerRuleApplication.GetFoldingRanges(result); + } + } + + private void GetFoldingRange(string? kind, ICollection result) + { + if (Inner.Count < 2) return; + + var foldingRange = new FoldingRange() + { + StartLine = (uint)Inner.First().CurrentPosition.Line, + StartCharacter = (uint)Inner.First().CurrentPosition.Col, + EndLine = (uint)Inner.Last().CurrentPosition.Line, + EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint + Kind = kind + }; + + result.Add(foldingRange); + } /// public override void IterateLiterals(Action action) diff --git a/AnyText/AnyText.Core/Rules/Rule.cs b/AnyText/AnyText.Core/Rules/Rule.cs index d2658ef3..4c907baa 100644 --- a/AnyText/AnyText.Core/Rules/Rule.cs +++ b/AnyText/AnyText.Core/Rules/Rule.cs @@ -55,10 +55,15 @@ protected internal virtual void OnDeactivate(RuleApplication application, ParseC public virtual bool IsComment => false; /// - /// True, if the rule element can be folded away (hidden) in the editor + /// True, if the application of this rule can be folded away (hidden) /// public virtual bool IsFoldable() => false; + /// + /// True, if the rule is used to define imports + /// + public virtual bool IsImports() => false; + /// /// True, if the rule permits trailing whitespaces, otherwise false /// @@ -176,12 +181,6 @@ public void Synthesize(object element, ParseContext context, TextWriter writer, /// true, if the rule could start with the given other rule, otherwise false protected internal abstract bool CanStartWith(Rule rule, List trace); - /// - /// Indicates whether the rule is used to define imports - /// - /// true, if the rule is used to define imports, otherwise false - public virtual bool IsImports() => false; - /// /// Gets the token modifiers of /// diff --git a/AnyText/AnyText.Core/Rules/RuleApplication.cs b/AnyText/AnyText.Core/Rules/RuleApplication.cs index ab2ee44e..8f5f6695 100644 --- a/AnyText/AnyText.Core/Rules/RuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/RuleApplication.cs @@ -62,6 +62,58 @@ protected RuleApplication(Rule rule, ParsePosition currentPosition, ParsePositio /// the parsed newPosition public abstract object GetValue(ParseContext context); + /// + /// Gets the folding ranges present in the rule application + /// + /// The IEnumerable to hold the folding ranges + public virtual void GetFoldingRanges(ICollection result) + { + if (Comments != null) + { + GetCommentFoldingRanges(result); + } + } + + private void GetCommentFoldingRanges(ICollection result) + { + for (var i = 0; i < Comments.Count; i++) + { + var commentRuleApplication = Comments[i]; + + uint endLine; + if (commentRuleApplication.Rule is MultilineCommentRule) + { + endLine = (uint)(commentRuleApplication.CurrentPosition.Line + commentRuleApplication.ExaminedTo.Line); + } + else + { + RuleApplication endCommentRuleApplication; + do + { + endCommentRuleApplication = Comments[i++]; + } + while (endCommentRuleApplication.Rule is not MultilineCommentRule + && endCommentRuleApplication.CurrentPosition.Col == commentRuleApplication.CurrentPosition.Col + && i < Comments.Count); + + if (commentRuleApplication == endCommentRuleApplication) continue; + + endLine = (uint)(endCommentRuleApplication.CurrentPosition.Line); + } + + var commentsFoldingRange = new FoldingRange() + { + StartLine = (uint)commentRuleApplication.CurrentPosition.Line, + StartCharacter = (uint)commentRuleApplication.CurrentPosition.Col, + EndLine = endLine, + EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint + Kind = "comment" + }; + + result.Add(commentsFoldingRange); + } + } + /// /// The rule that was matched /// diff --git a/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs b/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs index a2bad5d0..070238bc 100644 --- a/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs @@ -93,6 +93,12 @@ public override object GetValue(ParseContext context) return Inner?.GetValue(context); } + public override void GetFoldingRanges(ICollection result) + { + base.GetFoldingRanges(result); + Inner.GetFoldingRanges(result); + } + /// public override void IterateLiterals(Action action) { diff --git a/AnyText/AnyText.Lsp/ILspServer.FoldingRanges.cs b/AnyText/AnyText.Lsp/ILspServer.FoldingRanges.cs index 13ba32a9..2f7223a1 100644 --- a/AnyText/AnyText.Lsp/ILspServer.FoldingRanges.cs +++ b/AnyText/AnyText.Lsp/ILspServer.FoldingRanges.cs @@ -17,6 +17,6 @@ public partial interface ILspServer /// The JSON token containing the parameters of the request. (FoldingRangeParams) /// An array of objects, each containing details on a folding range in the document. [JsonRpcMethod(Methods.TextDocumentFoldingRangeName)] - FoldingRange[] QueryFoldingRanges(JToken arg); + LspTypes.FoldingRange[] QueryFoldingRanges(JToken arg); } } diff --git a/AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs b/AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs index f185b735..9f28a0b6 100644 --- a/AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs +++ b/AnyText/AnyText.Lsp/LspServer.FoldingRanges.cs @@ -13,48 +13,26 @@ namespace NMF.AnyText public partial class LspServer { /// - public FoldingRange[] QueryFoldingRanges(JToken arg) + public LspTypes.FoldingRange[] QueryFoldingRanges(JToken arg) { var foldingRangeParams = arg.ToObject(); string uri = foldingRangeParams.TextDocument.Uri; if (!_documents.TryGetValue(uri, out var document)) { - return Array.Empty(); + return Array.Empty(); } - - var parsedFoldingRanges = document.GetFoldingRangesFromRoot().ToList(); - var foldingRanges = parsedFoldingRanges.Select(parsedFoldingRange => new FoldingRange() - { - StartLine = parsedFoldingRange.StartLine, - StartCharacter = parsedFoldingRange.StartCharacter, - EndLine = parsedFoldingRange.EndLine, - EndCharacter = parsedFoldingRange.EndCharacter, - Kind = parsedFoldingRange.Kind - }); - - return foldingRanges.Distinct(new FoldingRangeEqualityComparer()).ToArray(); - } + var foldingRanges = document.GetFoldingRangesFromRoot(); - private class FoldingRangeEqualityComparer : IEqualityComparer - { - public bool Equals(FoldingRange x, FoldingRange y) + return foldingRanges.Select(foldingRange => new LspTypes.FoldingRange() { - if (ReferenceEquals(x, y)) return true; - else if (x == null || y == null) return false; - - return x.StartLine == y.StartLine - && x.StartCharacter == y.StartCharacter - && x.EndLine == y.EndLine - && x.EndCharacter == y.EndCharacter - && x.Kind.Equals(y.Kind); - } - - public int GetHashCode([DisallowNull] FoldingRange obj) - { - return 0; // always use equals over hash code - } + StartLine = foldingRange.StartLine, + StartCharacter = foldingRange.StartCharacter, + EndLine = foldingRange.EndLine, + EndCharacter = foldingRange.EndCharacter, + Kind = foldingRange.Kind + }).ToArray(); } } } diff --git a/AnyText/Tests/AnyText.Tests/CommentChangeTests.cs b/AnyText/Tests/AnyText.Tests/CommentChangeTests.cs index 7c7fc006..4404ae51 100644 --- a/AnyText/Tests/AnyText.Tests/CommentChangeTests.cs +++ b/AnyText/Tests/AnyText.Tests/CommentChangeTests.cs @@ -17,12 +17,10 @@ public void CommentChange_RespondToLineAddition() { var anyText = new AnyTextGrammar(); var parser = new Parser(new ModelParseContext(anyText)); - var grammar = @"grammar AnyText ( anytext ) -root Grammar -imports https://github.com/NMFCode/NMF/AnyText -comment '//' -comment '/*' to '*/' + var grammar = @"grammar HelloWorld root Model +Model: + (persons+=Person | greetings+=Greeting)*; /* @@ -30,139 +28,26 @@ this is a multiline comment */ -Grammar: - 'grammar' Name=ID ( '(' LanguageId=ID ')' )? 'root' StartRule=[ClassRule] Imports+=MetamodelImport* Comments+=CommentRule * Rules+=Rule +; +Person: + 'person' name=ID; -CommentRule: - MultilineCommentRule | SinglelineCommentRule; +Greeting: + 'Hello' person=[Person] '!'; -SinglelineCommentRule: - 'comment' Start=Keyword; - -MultilineCommentRule: - 'comment' Start=Keyword 'to' End=Keyword; - -MetamodelImport: - 'imports' ( Prefix=ID 'from' )? File=Uri ; - -Rule: - ClassRule | DataRule | FragmentRule | ParanthesisRule | EnumRule; - -ClassRule: - InheritanceRule | ModelRule; - -InheritanceRule: - Name=ID RuleTypeFragment ':' Subtypes+=[ClassRule] ( '|' Subtypes+=[ClassRule] )+ ';' ; - -ModelRule: - Name=ID RuleTypeFragment ':' Expression=ParserExpression ';' ; - -DataRule: - 'terminal' Name=ID RuleTypeFragment ':' Regex=Regex ( 'surround' 'with' SurroundCharacter=Char )? ( 'escape' EscapeRules+=EscapeRule ( ',' EscapeRules+=EscapeRule )* )? ';' ; - -EscapeRule: - Character=Char 'as' Escape=Keyword; - -FragmentRule: - 'fragment' Name=ID 'processes' ( Prefix=ID '.' )? TypeName=ID ':' Expression=ParserExpression ';' ; - -ParanthesisRule: - 'parantheses' Name=ID ':' OpeningParanthesis=KeywordExpression InnerRule=[ClassRule] ClosingParanthesis=KeywordExpression ';' ; - -EnumRule: - 'enum' Name=ID RuleTypeFragment ':' Literals+=LiteralRule+ ';' ; - -LiteralRule: - Literal=ID '=>' Keyword=Keyword ; - -fragment RuleTypeFragment processes Rule : - ( 'returns' ( Prefix=ID '.' )? TypeName=ID )? ; - -fragment FormattingInstructionFragment processes ParserExpression : - FormattingInstructions+=FormattingInstruction*; - -enum FormattingInstruction: - Newline => '' - Indent => '' - Unindent => '' - AvoidSpace => '' - ForbidSpace => '' - ; - -ParserExpression: - ChoiceExpression | SequenceExpression | ConjunctiveParserExpression; - -ConjunctiveParserExpression returns ParserExpression: - PlusExpression | StarExpression | MaybeExpression | BasicParserExpression; - -BasicParserExpression returns ParserExpression: - NegativeLookaheadExpression | KeywordExpression | ReferenceExpression | AssignExpression | AddAssignExpression | ExistsAssignExpression | RuleExpression | ParanthesisExpression; - -parantheses ParanthesisExpression : - '(' ParserExpression ')'; - -SequenceExpression: - InnerExpressions+=ConjunctiveParserExpression InnerExpressions+=ConjunctiveParserExpression+; - -PlusExpression: - Inner=BasicParserExpression '+' FormattingInstructionFragment; - -StarExpression: - Inner=BasicParserExpression '*' FormattingInstructionFragment; - -MaybeExpression: - Inner=BasicParserExpression '?' FormattingInstructionFragment; - -KeywordExpression: - Keyword=Keyword FormattingInstructionFragment; - -ChoiceExpression: - ( Alternatives+=ConjunctiveParserExpression '|' )+ Alternatives+=ConjunctiveParserExpression; - -AssignExpression: - Feature=ID '=' Assigned=BasicParserExpression FormattingInstructionFragment; - -AddAssignExpression: - Feature=ID '+=' Assigned=BasicParserExpression FormattingInstructionFragment; - -ExistsAssignExpression: - Feature=ID '?=' Assigned=BasicParserExpression FormattingInstructionFragment; - -NegativeLookaheadExpression: - '!' Inner=BasicParserExpression; - -RuleExpression: - Rule=[Rule] FormattingInstructionFragment !'='!'+='!'?='; - -ReferenceExpression: - '[' ReferencedRule=[Rule] ']'; - -terminal ID: - /[a-zA-Z]\w*/; - -terminal Keyword: - /(\\\\|\\'|[^'\\])+/ surround with ' escape \ as '\\\\' , ' as '\\\''; - -terminal Regex: - /(\\\/|[^\/])*/ surround with / escape / as '\\/'; - -terminal Uri: - /.+/; - -terminal Char: - /\S/; - -"; +terminal ID: /[_a-zA-Z][\w_]*/;"; var parsed = parser.Initialize(SplitIntoLines(grammar)); Assert.IsNotNull(parsed); Assert.That(parser.Context.Errors, Is.Empty); - parser.Update([new TextEdit(new ParsePosition(10, 0), new ParsePosition(10, 0), ["\r\n"])]); + parser.Update([new TextEdit(new ParsePosition(7, 0), new ParsePosition(7, 0), ["", ""])]); Assert.IsNotNull(parsed); Assert.That(parser.Context.Errors, Is.Empty); - //var examinedUpdate = new ParsePositionDelta(13, 7); - //AssertAtLeast(parser.Context.RootRuleApplication.ExaminedTo, examinedUpdate); + + var foldingRanges = parser.GetFoldingRangesFromRoot(); + Assert.That(foldingRanges.Any(foldingRange => + foldingRange.StartLine == 4 && foldingRange.EndLine == 9 + )); } [Test] @@ -170,12 +55,10 @@ public void CommentChange_RespondToLineRemoval() { var anyText = new AnyTextGrammar(); var parser = new Parser(new ModelParseContext(anyText)); - var grammar = @"grammar AnyText ( anytext ) -root Grammar -imports https://github.com/NMFCode/NMF/AnyText -comment '//' -comment '/*' to '*/' + var grammar = @"grammar HelloWorld root Model +Model: + (persons+=Person | greetings+=Greeting)*; /* @@ -183,139 +66,26 @@ this is a multiline comment */ -Grammar: - 'grammar' Name=ID ( '(' LanguageId=ID ')' )? 'root' StartRule=[ClassRule] Imports+=MetamodelImport* Comments+=CommentRule * Rules+=Rule +; +Person: + 'person' name=ID; -CommentRule: - MultilineCommentRule | SinglelineCommentRule; +Greeting: + 'Hello' person=[Person] '!'; -SinglelineCommentRule: - 'comment' Start=Keyword; - -MultilineCommentRule: - 'comment' Start=Keyword 'to' End=Keyword; - -MetamodelImport: - 'imports' ( Prefix=ID 'from' )? File=Uri ; - -Rule: - ClassRule | DataRule | FragmentRule | ParanthesisRule | EnumRule; - -ClassRule: - InheritanceRule | ModelRule; - -InheritanceRule: - Name=ID RuleTypeFragment ':' Subtypes+=[ClassRule] ( '|' Subtypes+=[ClassRule] )+ ';' ; - -ModelRule: - Name=ID RuleTypeFragment ':' Expression=ParserExpression ';' ; - -DataRule: - 'terminal' Name=ID RuleTypeFragment ':' Regex=Regex ( 'surround' 'with' SurroundCharacter=Char )? ( 'escape' EscapeRules+=EscapeRule ( ',' EscapeRules+=EscapeRule )* )? ';' ; - -EscapeRule: - Character=Char 'as' Escape=Keyword; - -FragmentRule: - 'fragment' Name=ID 'processes' ( Prefix=ID '.' )? TypeName=ID ':' Expression=ParserExpression ';' ; - -ParanthesisRule: - 'parantheses' Name=ID ':' OpeningParanthesis=KeywordExpression InnerRule=[ClassRule] ClosingParanthesis=KeywordExpression ';' ; - -EnumRule: - 'enum' Name=ID RuleTypeFragment ':' Literals+=LiteralRule+ ';' ; - -LiteralRule: - Literal=ID '=>' Keyword=Keyword ; - -fragment RuleTypeFragment processes Rule : - ( 'returns' ( Prefix=ID '.' )? TypeName=ID )? ; - -fragment FormattingInstructionFragment processes ParserExpression : - FormattingInstructions+=FormattingInstruction*; - -enum FormattingInstruction: - Newline => '' - Indent => '' - Unindent => '' - AvoidSpace => '' - ForbidSpace => '' - ; - -ParserExpression: - ChoiceExpression | SequenceExpression | ConjunctiveParserExpression; - -ConjunctiveParserExpression returns ParserExpression: - PlusExpression | StarExpression | MaybeExpression | BasicParserExpression; - -BasicParserExpression returns ParserExpression: - NegativeLookaheadExpression | KeywordExpression | ReferenceExpression | AssignExpression | AddAssignExpression | ExistsAssignExpression | RuleExpression | ParanthesisExpression; - -parantheses ParanthesisExpression : - '(' ParserExpression ')'; - -SequenceExpression: - InnerExpressions+=ConjunctiveParserExpression InnerExpressions+=ConjunctiveParserExpression+; - -PlusExpression: - Inner=BasicParserExpression '+' FormattingInstructionFragment; - -StarExpression: - Inner=BasicParserExpression '*' FormattingInstructionFragment; - -MaybeExpression: - Inner=BasicParserExpression '?' FormattingInstructionFragment; - -KeywordExpression: - Keyword=Keyword FormattingInstructionFragment; - -ChoiceExpression: - ( Alternatives+=ConjunctiveParserExpression '|' )+ Alternatives+=ConjunctiveParserExpression; - -AssignExpression: - Feature=ID '=' Assigned=BasicParserExpression FormattingInstructionFragment; - -AddAssignExpression: - Feature=ID '+=' Assigned=BasicParserExpression FormattingInstructionFragment; - -ExistsAssignExpression: - Feature=ID '?=' Assigned=BasicParserExpression FormattingInstructionFragment; - -NegativeLookaheadExpression: - '!' Inner=BasicParserExpression; - -RuleExpression: - Rule=[Rule] FormattingInstructionFragment !'='!'+='!'?='; - -ReferenceExpression: - '[' ReferencedRule=[Rule] ']'; - -terminal ID: - /[a-zA-Z]\w*/; - -terminal Keyword: - /(\\\\|\\'|[^'\\])+/ surround with ' escape \ as '\\\\' , ' as '\\\''; - -terminal Regex: - /(\\\/|[^\/])*/ surround with / escape / as '\\/'; - -terminal Uri: - /.+/; - -terminal Char: - /\S/; - -"; +terminal ID: /[_a-zA-Z][\w_]*/;"; var parsed = parser.Initialize(SplitIntoLines(grammar)); Assert.IsNotNull(parsed); Assert.That(parser.Context.Errors, Is.Empty); - parser.Update([new TextEdit(new ParsePosition(9, 27), new ParsePosition(10, 0), [""])]); + parser.Update([new TextEdit(new ParsePosition(6, 27), new ParsePosition(7, 0), [""])]); Assert.IsNotNull(parsed); Assert.That(parser.Context.Errors, Is.Empty); - //var examinedUpdate = new ParsePositionDelta(13, 7); - //AssertAtLeast(parser.Context.RootRuleApplication.ExaminedTo, examinedUpdate); + + var foldingRanges = parser.GetFoldingRangesFromRoot(); + Assert.That(foldingRanges.Any(foldingRange => + foldingRange.StartLine == 4 && foldingRange.EndLine == 7 + )); } private static string[] SplitIntoLines(string grammar) From 3c4920e53c54ec90fb673f48868b2b553d9d4a8e Mon Sep 17 00:00:00 2001 From: nhett Date: Fri, 10 Jan 2025 18:04:09 +0100 Subject: [PATCH 08/10] Moved remaining case distinctions for folding ranges from rule applications to rules --- AnyText/AnyText.Core/Model/ParanthesesRule.cs | 7 +++++ AnyText/AnyText.Core/Parser.FoldingRanges.cs | 2 +- .../Rules/MultiRuleApplication.cs | 27 ++++++------------- AnyText/AnyText.Core/Rules/Rule.cs | 11 ++++++++ AnyText/AnyText.Core/Rules/RuleApplication.cs | 16 ++++++----- AnyText/AnyText.Core/Rules/SequenceRule.cs | 19 +++++++++++++ .../Rules/SingleRuleApplication.cs | 7 ++--- AnyText/AnyText.Core/Rules/ZeroOrMoreRule.cs | 12 +++++++++ 8 files changed, 71 insertions(+), 30 deletions(-) diff --git a/AnyText/AnyText.Core/Model/ParanthesesRule.cs b/AnyText/AnyText.Core/Model/ParanthesesRule.cs index 40ec8e78..26c39a8e 100644 --- a/AnyText/AnyText.Core/Model/ParanthesesRule.cs +++ b/AnyText/AnyText.Core/Model/ParanthesesRule.cs @@ -18,6 +18,13 @@ protected override RuleApplication CreateRuleApplication(ParsePosition currentPo return new ParanthesesRuleApplication(this, currentPosition, inner, length, examined); } + /// + public override bool HasFoldingKind(out string kind) + { + kind = null; + return true; + } + private sealed class ParanthesesRuleApplication : MultiRuleApplication { public ParanthesesRuleApplication(Rule rule, ParsePosition currentPosition, List inner, ParsePositionDelta endsAt, ParsePositionDelta examinedTo) : base(rule, currentPosition, inner, endsAt, examinedTo) diff --git a/AnyText/AnyText.Core/Parser.FoldingRanges.cs b/AnyText/AnyText.Core/Parser.FoldingRanges.cs index d8ef7fa0..3d3521b6 100644 --- a/AnyText/AnyText.Core/Parser.FoldingRanges.cs +++ b/AnyText/AnyText.Core/Parser.FoldingRanges.cs @@ -23,7 +23,7 @@ public IEnumerable GetFoldingRangesFromRoot() if (rootApplication.IsPositive) { var result = new List(); - rootApplication.GetFoldingRanges(result); + rootApplication.AddFoldingRanges(result); return result; } diff --git a/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs b/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs index 4fda647f..e5ce2894 100644 --- a/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs @@ -163,34 +163,23 @@ public override object GetValue(ParseContext context) } } - public override void GetFoldingRanges(ICollection result) + /// + public override void AddFoldingRanges(ICollection result) { - base.GetFoldingRanges(result); - - if (Rule is ZeroOrMoreRule zeroOrMoreRule && zeroOrMoreRule.InnerRule.IsImports()) - { - GetFoldingRange("imports", result); - } + base.AddFoldingRanges(result); - if (Rule is SequenceRule sequenceRule) + if (Rule.HasFoldingKind(out var kind)) { - if (sequenceRule.IsRegion()) - { - GetFoldingRange("region", result); - } - else if (Rule is ParanthesesRule || sequenceRule.IsFoldable()) - { - GetFoldingRange(null, result); - } + AddFoldingRange(kind, result); } foreach (var innerRuleApplication in Inner) { - innerRuleApplication.GetFoldingRanges(result); + innerRuleApplication.AddFoldingRanges(result); } } - private void GetFoldingRange(string? kind, ICollection result) + private void AddFoldingRange(string? kind, ICollection result) { if (Inner.Count < 2) return; @@ -199,7 +188,7 @@ private void GetFoldingRange(string? kind, ICollection result) StartLine = (uint)Inner.First().CurrentPosition.Line, StartCharacter = (uint)Inner.First().CurrentPosition.Col, EndLine = (uint)Inner.Last().CurrentPosition.Line, - EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint + EndCharacter = (uint)(Inner.First().CurrentPosition.Col + Inner.Last().Length.Col), Kind = kind }; diff --git a/AnyText/AnyText.Core/Rules/Rule.cs b/AnyText/AnyText.Core/Rules/Rule.cs index 4c907baa..2d016875 100644 --- a/AnyText/AnyText.Core/Rules/Rule.cs +++ b/AnyText/AnyText.Core/Rules/Rule.cs @@ -64,6 +64,17 @@ protected internal virtual void OnDeactivate(RuleApplication application, ParseC /// public virtual bool IsImports() => false; + /// + /// Returns the folding kind for a rule if one is defined for the rule + /// + /// The folding kind of the rule + /// True, if a folding kind is defined for the rule + public virtual bool HasFoldingKind(out string kind) + { + kind = null; + return false; + } + /// /// True, if the rule permits trailing whitespaces, otherwise false /// diff --git a/AnyText/AnyText.Core/Rules/RuleApplication.cs b/AnyText/AnyText.Core/Rules/RuleApplication.cs index 8f5f6695..afb7bf3b 100644 --- a/AnyText/AnyText.Core/Rules/RuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/RuleApplication.cs @@ -66,24 +66,25 @@ protected RuleApplication(Rule rule, ParsePosition currentPosition, ParsePositio /// Gets the folding ranges present in the rule application /// /// The IEnumerable to hold the folding ranges - public virtual void GetFoldingRanges(ICollection result) + public virtual void AddFoldingRanges(ICollection result) { if (Comments != null) { - GetCommentFoldingRanges(result); + AddCommentFoldingRanges(result); } } - private void GetCommentFoldingRanges(ICollection result) + private void AddCommentFoldingRanges(ICollection result) { for (var i = 0; i < Comments.Count; i++) { var commentRuleApplication = Comments[i]; - uint endLine; + uint endLine, endCol; if (commentRuleApplication.Rule is MultilineCommentRule) { - endLine = (uint)(commentRuleApplication.CurrentPosition.Line + commentRuleApplication.ExaminedTo.Line); + endLine = (uint)(commentRuleApplication.CurrentPosition.Line + commentRuleApplication.Length.Line); + endCol = (uint)(commentRuleApplication.CurrentPosition.Col + commentRuleApplication.Length.Col); } else { @@ -98,7 +99,8 @@ private void GetCommentFoldingRanges(ICollection result) if (commentRuleApplication == endCommentRuleApplication) continue; - endLine = (uint)(endCommentRuleApplication.CurrentPosition.Line); + endLine = (uint)endCommentRuleApplication.CurrentPosition.Line; + endCol = (uint)(commentRuleApplication.CurrentPosition.Col + endCommentRuleApplication.Length.Col); } var commentsFoldingRange = new FoldingRange() @@ -106,7 +108,7 @@ private void GetCommentFoldingRanges(ICollection result) StartLine = (uint)commentRuleApplication.CurrentPosition.Line, StartCharacter = (uint)commentRuleApplication.CurrentPosition.Col, EndLine = endLine, - EndCharacter = 0, // determining the end character using length or examinedTo is inconsistent and can lead to wrap around when casting to uint + EndCharacter = endCol, Kind = "comment" }; diff --git a/AnyText/AnyText.Core/Rules/SequenceRule.cs b/AnyText/AnyText.Core/Rules/SequenceRule.cs index 74fec9f2..9c35cfc0 100644 --- a/AnyText/AnyText.Core/Rules/SequenceRule.cs +++ b/AnyText/AnyText.Core/Rules/SequenceRule.cs @@ -59,6 +59,24 @@ protected internal override bool IsEpsilonAllowed(List trace) return true; } + /// + public override bool HasFoldingKind(out string kind) + { + if (IsRegion()) + { + kind = "region"; + return true; + } + + if (IsFoldable()) + { + kind = null; + return true; + } + + return base.HasFoldingKind(out kind); + } + /// /// The rules that should occur in sequence /// @@ -147,6 +165,7 @@ public bool IsRegion() return false; } + /// public override bool IsFoldable() { if (Rules.First().Rule is LiteralRule startLiteralRule && Rules.Last().Rule is LiteralRule endLiteralRule) diff --git a/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs b/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs index 070238bc..14b71a2d 100644 --- a/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs @@ -93,10 +93,11 @@ public override object GetValue(ParseContext context) return Inner?.GetValue(context); } - public override void GetFoldingRanges(ICollection result) + /// + public override void AddFoldingRanges(ICollection result) { - base.GetFoldingRanges(result); - Inner.GetFoldingRanges(result); + base.AddFoldingRanges(result); + Inner.AddFoldingRanges(result); } /// diff --git a/AnyText/AnyText.Core/Rules/ZeroOrMoreRule.cs b/AnyText/AnyText.Core/Rules/ZeroOrMoreRule.cs index 415f6c84..60212ed9 100644 --- a/AnyText/AnyText.Core/Rules/ZeroOrMoreRule.cs +++ b/AnyText/AnyText.Core/Rules/ZeroOrMoreRule.cs @@ -53,6 +53,18 @@ protected internal override bool IsEpsilonAllowed(List trace) return true; } + /// + public override bool HasFoldingKind(out string kind) + { + if (InnerRule.IsImports()) + { + kind = "imports"; + return true; + } + + return base.HasFoldingKind(out kind); + } + /// /// Gets or sets the inner rule /// From 6063a19f23be3e45b15fc8012ef968512280c3a0 Mon Sep 17 00:00:00 2001 From: nhett Date: Fri, 17 Jan 2025 12:11:16 +0100 Subject: [PATCH 09/10] Minor folding range column fix --- AnyText/AnyText.Core/Rules/RuleApplication.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AnyText/AnyText.Core/Rules/RuleApplication.cs b/AnyText/AnyText.Core/Rules/RuleApplication.cs index afb7bf3b..00eacf87 100644 --- a/AnyText/AnyText.Core/Rules/RuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/RuleApplication.cs @@ -100,7 +100,7 @@ private void AddCommentFoldingRanges(ICollection result) if (commentRuleApplication == endCommentRuleApplication) continue; endLine = (uint)endCommentRuleApplication.CurrentPosition.Line; - endCol = (uint)(commentRuleApplication.CurrentPosition.Col + endCommentRuleApplication.Length.Col); + endCol = (uint)(endCommentRuleApplication.CurrentPosition.Col + endCommentRuleApplication.Length.Col); } var commentsFoldingRange = new FoldingRange() From 16ef284a9152ab32143820016c64e40a33ed5f1f Mon Sep 17 00:00:00 2001 From: nhett Date: Sun, 26 Jan 2025 13:54:52 +0100 Subject: [PATCH 10/10] Combine all comments in immediate succession to folding range instead of only considering single line comments --- AnyText/AnyText.Core/Rules/RuleApplication.cs | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/AnyText/AnyText.Core/Rules/RuleApplication.cs b/AnyText/AnyText.Core/Rules/RuleApplication.cs index 00eacf87..1d6ca23d 100644 --- a/AnyText/AnyText.Core/Rules/RuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/RuleApplication.cs @@ -81,27 +81,17 @@ private void AddCommentFoldingRanges(ICollection result) var commentRuleApplication = Comments[i]; uint endLine, endCol; - if (commentRuleApplication.Rule is MultilineCommentRule) + RuleApplication endCommentRuleApplication; + do { - endLine = (uint)(commentRuleApplication.CurrentPosition.Line + commentRuleApplication.Length.Line); - endCol = (uint)(commentRuleApplication.CurrentPosition.Col + commentRuleApplication.Length.Col); - } - else - { - RuleApplication endCommentRuleApplication; - do - { - endCommentRuleApplication = Comments[i++]; - } - while (endCommentRuleApplication.Rule is not MultilineCommentRule - && endCommentRuleApplication.CurrentPosition.Col == commentRuleApplication.CurrentPosition.Col - && i < Comments.Count); - - if (commentRuleApplication == endCommentRuleApplication) continue; - - endLine = (uint)endCommentRuleApplication.CurrentPosition.Line; - endCol = (uint)(endCommentRuleApplication.CurrentPosition.Col + endCommentRuleApplication.Length.Col); + endCommentRuleApplication = Comments[i++]; } + while (endCommentRuleApplication.CurrentPosition.Col == commentRuleApplication.CurrentPosition.Col && i < Comments.Count); + + if (commentRuleApplication == endCommentRuleApplication) continue; + + endLine = (uint)endCommentRuleApplication.CurrentPosition.Line; + endCol = (uint)(endCommentRuleApplication.CurrentPosition.Col + endCommentRuleApplication.Length.Col); var commentsFoldingRange = new FoldingRange() {