From 3606c62ae2c4c11a50ab99e46b69f6c6488816af Mon Sep 17 00:00:00 2001 From: David Sisco Date: Mon, 5 Jan 2026 21:25:59 -0800 Subject: [PATCH] refactor block queries to remove InsertionQuery system --- .../SyntaxTreeBenchmarks.cs | 2 +- TinyTokenizer.E2ETests/GlslEditorTests.cs | 44 ++-- TinyTokenizer.Tests/SyntaxEditorTests.cs | 65 +++--- TinyTokenizer.Tests/SyntaxNodeTests.cs | 8 +- TinyTokenizer.Tests/SyntaxTreeTests.cs | 20 +- TinyTokenizer/Ast/IBlockContainerNode.cs | 171 +++++++++++++-- TinyTokenizer/Ast/NodeQuery.cs | 147 ------------- TinyTokenizer/Ast/NodeQueryTypes.cs | 134 +++++++++++- TinyTokenizer/Ast/SyntaxEditor.cs | 206 +++++++++++++----- 9 files changed, 504 insertions(+), 293 deletions(-) diff --git a/TinyTokenizer.Benchmarks/SyntaxTreeBenchmarks.cs b/TinyTokenizer.Benchmarks/SyntaxTreeBenchmarks.cs index 9fe47ae..8af608c 100644 --- a/TinyTokenizer.Benchmarks/SyntaxTreeBenchmarks.cs +++ b/TinyTokenizer.Benchmarks/SyntaxTreeBenchmarks.cs @@ -430,7 +430,7 @@ public SyntaxTree EditInsert() { var tree = SyntaxTree.Parse(SmallInput, DefaultSchema); tree.CreateEditor() - .Insert(Q.BraceBlock.First().InnerStart(), "/* inserted */") + .InsertAfter(Q.BraceBlock.First().Start(), "/* inserted */") .Commit(); return tree; } diff --git a/TinyTokenizer.E2ETests/GlslEditorTests.cs b/TinyTokenizer.E2ETests/GlslEditorTests.cs index d4dad5b..5d262b0 100644 --- a/TinyTokenizer.E2ETests/GlslEditorTests.cs +++ b/TinyTokenizer.E2ETests/GlslEditorTests.cs @@ -255,7 +255,7 @@ public void InsertCommentAboveMainMethod() tree.CreateEditor() - .Insert(mainFuncQuery.Before(), "// Entry point for the fragment shader\r\n") + .InsertBefore(mainFuncQuery, "// Entry point for the fragment shader\r\n") .Commit(); var result = NormalizeLineEndings(tree.Root.ToText()); @@ -277,7 +277,7 @@ public void InsertSampleFromTextureAtTopOfMain() // Use INamedNode + IBlockContainerNode APIs to locate injection point tree.CreateEditor() - .Insert(Query.Syntax().Named("main").InnerStart("body"), "\n vec4 sample = texture(tex, uv);") + .InsertAfter(Query.Syntax().Named("main").InnerStart("body"), "\n vec4 sample = texture(tex, uv);") .Commit(); var result = NormalizeLineEndings(tree.Root.ToText()); @@ -299,7 +299,7 @@ public void InsertWriteToOutBufferAtEndOfMain() // Use INamedNode + IBlockContainerNode APIs to locate injection point tree.CreateEditor() - .Insert(Query.Syntax().Named("main").InnerEnd("body"), "\n fragColor = color;") + .InsertBefore(Query.Syntax().Named("main").InnerEnd("body"), "\n fragColor = color;") .Commit(); var result = NormalizeLineEndings(tree.Root.ToText()); @@ -324,7 +324,7 @@ public void InsertCommentAfterMainMethod() Assert.NotNull(mainFunc); tree.CreateEditor() - .Insert(mainQuery.After(), "\n// End of main function\n") + .InsertAfter(mainQuery, "\n// End of main function\n") .Commit(); var result = NormalizeLineEndings(tree.Root.ToText()); @@ -346,7 +346,7 @@ public void InsertImportBelowVersionDirective() // Use INamedNode API to find the #version directive and insert after it tree.CreateEditor() - .Insert(Query.Syntax().Named("version").After(), "\n@import \"my-include.glsl\"") + .InsertAfter(Query.Syntax().Named("version"), "\n@import \"my-include.glsl\"") .Commit(); var result = NormalizeLineEndings(tree.Root.ToText()); @@ -368,7 +368,7 @@ public void InsertCommentAboveFooMethod() // Use INamedNode API to find foo function by name tree.CreateEditor() - .Insert(Query.Syntax().Named("foo").Before(), "/* foo comment */\n") + .InsertBefore(Query.Syntax().Named("foo"), "/* foo comment */\n") .Commit(); var result = NormalizeLineEndings(tree.Root.ToText()); @@ -395,17 +395,17 @@ public void ApplyAllEditsInSingleCommit() tree.CreateEditor() // 1. Comment above main - .Insert(mainQuery.Before(), "// Entry point for the fragment shader\n") + .InsertBefore(mainQuery, "// Entry point for the fragment shader\n") // 2. Sample from texture at top of main body - .Insert(mainQuery.InnerStart("body"), "\n vec4 sample = texture(tex, uv);") + .InsertAfter(mainQuery.InnerStart("body"), "\n vec4 sample = texture(tex, uv);") // 3. Write to out buffer at end of main body - .Insert(mainQuery.InnerEnd("body"), "\n fragColor = sample;") + .InsertBefore(mainQuery.InnerEnd("body"), "\n fragColor = sample;") // 4. Comment after main - .Insert(mainQuery.After(), "\n// End of main function\n") + .InsertAfter(mainQuery, "\n// End of main function\n") // 5. Import below #version directive - .Insert(versionQuery.After(), "\n@import \"my-include.glsl\"") + .InsertAfter(versionQuery, "\n@import \"my-include.glsl\"") // 6. Comment above foo - .Insert(fooQuery.Before(), "/* foo comment */\n") + .InsertBefore(fooQuery, "/* foo comment */\n") .Commit(); var result = NormalizeLineEndings(tree.Root.ToText()); @@ -441,7 +441,7 @@ public void UndoRevertsEdits() Assert.NotNull(mainFunc); tree.CreateEditor() - .Insert(mainQuery.Before(), "// This comment will be undone\n") + .InsertBefore(mainQuery, "// This comment will be undone\n") .Commit(); var modifiedText = tree.Root.ToText(); @@ -498,7 +498,7 @@ public void InnerStart_WithBlockName_InsertsIntoBody() var mainQuery = Query.Syntax().Named("main"); tree.CreateEditor() - .Insert(mainQuery.InnerStart("body"), "\n // Body start") + .InsertAfter(mainQuery.InnerStart("body"), "\n // Body start") .Commit(); var result = NormalizeLineEndings(tree.Root.ToText()); @@ -516,7 +516,7 @@ public void InnerEnd_WithDefaultBlock_InsertsIntoBody() var mainQuery = Query.Syntax().Named("main"); tree.CreateEditor() - .Insert(mainQuery.InnerEnd(), "\n // Body end") + .InsertBefore(mainQuery.InnerEnd(), "\n // Body end") .Commit(); var result = NormalizeLineEndings(tree.Root.ToText()); @@ -588,7 +588,7 @@ void main() { // Insert a new import after the #version directive var versionQuery = Query.Syntax().Named("version"); tree.CreateEditor() - .Insert(versionQuery.After(), "\n@import \"utils.glsl\"") + .InsertAfter(versionQuery, "\n@import \"utils.glsl\"") .Commit(); // The inserted text is present in serialized content @@ -838,7 +838,7 @@ public void TriviaPreservation_AfterEdits_UnmodifiedSectionsKeepExactIndentation // Make edits to calculateLight var calcLightQuery = Query.Syntax().Named("calculateLight"); tree.CreateEditor() - .Insert(calcLightQuery.InnerStart("body"), "\n // INJECTED") + .InsertAfter(calcLightQuery.InnerStart("body"), "\n // INJECTED") .Commit(); var result = NormalizeLineEndings(tree.ToText()); @@ -871,9 +871,9 @@ public void TriviaPreservation_UndoRestoresExactOriginal() // Make edits var mainQuery = Query.Syntax().Named("main"); tree.CreateEditor() - .Insert(mainQuery.Before(), "// Comment\n") - .Insert(mainQuery.InnerStart("body"), "\n // Start") - .Insert(mainQuery.InnerEnd("body"), "\n // End") + .InsertBefore(mainQuery, "// Comment\n") + .InsertAfter(mainQuery.InnerStart("body"), "\n // Start") + .InsertBefore(mainQuery.InnerEnd("body"), "\n // End") .Commit(); // Verify edits were applied @@ -930,7 +930,7 @@ void main() { // === STEP 1: Insert @import after #version === tree.CreateEditor() - .Insert(versionQuery.After(), "\n@import \"utils.glsl\"") + .InsertAfter(versionQuery, "\n@import \"utils.glsl\"") .Commit(); _output.WriteLine("\n=== After inserting @import ==="); @@ -964,7 +964,7 @@ void main() { _output.WriteLine($"\n#version after mutations: '{NormalizeLineEndings(versionAfterMutations!.ToText())}'"); tree.CreateEditor() - .Insert(versionQuery.After(), "\n#define DEBUG 1") + .InsertAfter(versionQuery, "\n#define DEBUG 1") .Commit(); _output.WriteLine("\n=== After inserting #define ==="); diff --git a/TinyTokenizer.Tests/SyntaxEditorTests.cs b/TinyTokenizer.Tests/SyntaxEditorTests.cs index 1d54acd..ab9591e 100644 --- a/TinyTokenizer.Tests/SyntaxEditorTests.cs +++ b/TinyTokenizer.Tests/SyntaxEditorTests.cs @@ -188,7 +188,7 @@ public void Insert_Before_InsertsBeforeNode() var tree = SyntaxTree.Parse("world"); tree.CreateEditor() - .Insert(Q.AnyIdent.First().Before(), "hello ") + .InsertBefore(Q.AnyIdent.First(), "hello ") .Commit(); Assert.Equal("hello world", tree.ToText()); @@ -200,7 +200,7 @@ public void Insert_After_InsertsAfterNode() var tree = SyntaxTree.Parse("hello"); tree.CreateEditor() - .Insert(Q.AnyIdent.First().After(), " world") + .InsertAfter(Q.AnyIdent.First(), " world") .Commit(); Assert.Equal("hello world", tree.ToText()); @@ -212,7 +212,7 @@ public void Insert_InnerStart_InsertsAtBlockStart() var tree = SyntaxTree.Parse("{b}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().InnerStart(), "a ") + .InsertAfter(Q.BraceBlock.First().Start(), "a ") .Commit(); Assert.Equal("{a b}", tree.ToText()); @@ -224,7 +224,7 @@ public void Insert_InnerEnd_InsertsAtBlockEnd() var tree = SyntaxTree.Parse("{a}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().InnerEnd(), " b") + .InsertBefore(Q.BraceBlock.First().End(), " b") .Commit(); Assert.Equal("{a b}", tree.ToText()); @@ -234,11 +234,10 @@ public void Insert_InnerEnd_InsertsAtBlockEnd() public void Insert_WithGreenNodes_InsertsProvidedNodes() { var tree = SyntaxTree.Parse("x"); - var lexer = new GreenLexer(); - var nodes = lexer.ParseToGreenNodes(" inserted"); + // The new API uses string parsing instead of pre-built GreenNodes tree.CreateEditor() - .Insert(Q.AnyIdent.First().After(), nodes) + .InsertAfter(Q.AnyIdent.First(), " inserted") .Commit(); Assert.Equal("x inserted", tree.ToText()); @@ -250,7 +249,7 @@ public void Insert_BeforeMultiple_InsertsBeforeEach() var tree = SyntaxTree.Parse("a b c"); tree.CreateEditor() - .Insert(Q.AnyIdent.Before(), "_") + .InsertBefore(Q.AnyIdent, "_") .Commit(); // With leading trivia transfer: inserted content takes target's leading trivia @@ -268,7 +267,7 @@ public void Insert_IntoEmptyBlock_AddsContent() var tree = SyntaxTree.Parse("{}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().InnerStart(), "content") + .InsertAfter(Q.BraceBlock.First().Start(), "content") .Commit(); Assert.Equal("{content}", tree.ToText()); @@ -467,7 +466,7 @@ public void MixedOperations_ApplyCorrectly() tree.CreateEditor() .Remove(Q.AnyIdent.WithText("a")) - .Insert(Q.BraceBlock.First().InnerEnd(), " extra") + .InsertBefore(Q.BraceBlock.First().End(), " extra") .Replace(Q.AnyIdent.WithText("c"), "z") .Commit(); @@ -489,7 +488,7 @@ public void FluentApi_ReturnsSameEditor() var result1 = editor.Replace(Q.AnyIdent.First(), "a"); var result2 = result1.Remove(Q.AnyIdent.Last()); - var result3 = result2.Insert(Q.AnyIdent.First().Before(), "b"); + var result3 = result2.InsertBefore(Q.AnyIdent.First(), "b"); Assert.Same(editor, result1); Assert.Same(editor, result2); @@ -502,8 +501,8 @@ public void FluentApi_FullChain() var tree = SyntaxTree.Parse("{x}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().InnerStart(), "start ") - .Insert(Q.BraceBlock.First().InnerEnd(), " end") + .InsertAfter(Q.BraceBlock.First().Start(), "start ") + .InsertBefore(Q.BraceBlock.First().End(), " end") .Replace(Q.AnyIdent.WithText("x"), "middle") .Commit(); @@ -560,8 +559,8 @@ public void Insert_AdjacentPositions_HandlesCorrectly() var tree = SyntaxTree.Parse("x"); tree.CreateEditor() - .Insert(Q.AnyIdent.First().Before(), "A") - .Insert(Q.AnyIdent.First().After(), "B") + .InsertBefore(Q.AnyIdent.First(), "A") + .InsertAfter(Q.AnyIdent.First(), "B") .Commit(); Assert.Equal("AxB", tree.ToText()); @@ -637,7 +636,7 @@ public void Insert_BracketBlock_InnerStart() var tree = SyntaxTree.Parse("[b]"); tree.CreateEditor() - .Insert(Q.BracketBlock.First().InnerStart(), "a ") + .InsertAfter(Q.BracketBlock.First().Start(), "a ") .Commit(); Assert.Equal("[a b]", tree.ToText()); @@ -649,7 +648,7 @@ public void Insert_ParenBlock_InnerEnd() var tree = SyntaxTree.Parse("(a)"); tree.CreateEditor() - .Insert(Q.ParenBlock.First().InnerEnd(), " b") + .InsertBefore(Q.ParenBlock.First().End(), " b") .Commit(); Assert.Equal("(a b)", tree.ToText()); @@ -667,7 +666,7 @@ public void Insert_BeforeFunctionBlock_InsertsBeforeOpeningBrace() var tree = SyntaxTree.Parse("function {body}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().Before(), "/* comment */") + .InsertBefore(Q.BraceBlock.First(), "/* comment */") .Commit(); // /* comment */ takes the space from {, so result is: "function /* comment */{body}" @@ -682,7 +681,7 @@ public void Insert_AtFunctionStart_InsertsAfterOpeningBrace() var tree = SyntaxTree.Parse("function {existing}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().InnerStart(), "first; ") + .InsertAfter(Q.BraceBlock.First().Start(), "first; ") .Commit(); Assert.Equal("function {first; existing}", tree.ToText()); @@ -695,7 +694,7 @@ public void Insert_AtFunctionEnd_InsertsBeforeClosingBrace() var tree = SyntaxTree.Parse("function {existing}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().InnerEnd(), " return") + .InsertBefore(Q.BraceBlock.First().End(), " return") .Commit(); Assert.Equal("function {existing return}", tree.ToText()); @@ -708,7 +707,7 @@ public void Insert_AfterFunctionBlock_InsertsAfterClosingBrace() var tree = SyntaxTree.Parse("function {body}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().After(), " nextFunction") + .InsertAfter(Q.BraceBlock.First(), " nextFunction") .Commit(); Assert.Equal("function {body} nextFunction", tree.ToText()); @@ -721,10 +720,10 @@ public void Insert_MultipleFunctionPositions_AllInsertCorrectly() var tree = SyntaxTree.Parse("fn {body}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().Before(), "/* before */ ") - .Insert(Q.BraceBlock.First().InnerStart(), "start; ") - .Insert(Q.BraceBlock.First().InnerEnd(), " end;") - .Insert(Q.BraceBlock.First().After(), " /* after */") + .InsertBefore(Q.BraceBlock.First(), "/* before */ ") + .InsertAfter(Q.BraceBlock.First().Start(), "start; ") + .InsertBefore(Q.BraceBlock.First().End(), " end;") + .InsertAfter(Q.BraceBlock.First(), " /* after */") .Commit(); var text = tree.ToText(); @@ -742,7 +741,7 @@ public void Insert_NestedFunctionBlocks_InsertsAtCorrectLevel() // Insert at the outer function's start tree.CreateEditor() - .Insert(Q.BraceBlock.First().InnerStart(), "first; ") + .InsertAfter(Q.BraceBlock.First().Start(), "first; ") .Commit(); var text = tree.ToText(); @@ -761,7 +760,7 @@ public void Insert_InnerBlockStart_InsertsInNestedBlock() // Use Nth(1) to get the second (inner) block tree.CreateEditor() - .Insert(Q.BraceBlock.Nth(1).InnerStart(), "nested; ") + .InsertAfter(Q.BraceBlock.Nth(1).Start(), "nested; ") .Commit(); var text = tree.ToText(); @@ -776,7 +775,7 @@ public void Insert_BeforeAndAfterMultipleFunctions_HandlesCorrectly() var tree = SyntaxTree.Parse("{first} {second}"); tree.CreateEditor() - .Insert(Q.BraceBlock.Before(), "/* fn */") + .InsertBefore(Q.BraceBlock, "/* fn */") .Commit(); var text = tree.ToText(); @@ -792,7 +791,7 @@ public void Insert_EmptyFunctionBody_InsertsCorrectly() var tree = SyntaxTree.Parse("function {}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().InnerStart(), "statement;") + .InsertAfter(Q.BraceBlock.First().Start(), "statement;") .Commit(); Assert.Equal("function {statement;}", tree.ToText()); @@ -806,7 +805,7 @@ public void Insert_FunctionWithWhitespace_PreservesFormatting() var tree = SyntaxTree.Parse("fn { body }"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().InnerStart(), "new; ") + .InsertAfter(Q.BraceBlock.First().Start(), "new; ") .Commit(); var text = tree.ToText(); @@ -821,7 +820,7 @@ public void Insert_BeforeFirstBlock_InsertsAtDocumentStart() var tree = SyntaxTree.Parse("{only}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().Before(), "prefix ") + .InsertBefore(Q.BraceBlock.First(), "prefix ") .Commit(); Assert.Equal("prefix {only}", tree.ToText()); @@ -833,7 +832,7 @@ public void Insert_AfterLastBlock_InsertsAtDocumentEnd() var tree = SyntaxTree.Parse("{only}"); tree.CreateEditor() - .Insert(Q.BraceBlock.First().After(), " suffix") + .InsertAfter(Q.BraceBlock.First(), " suffix") .Commit(); Assert.Equal("{only} suffix", tree.ToText()); @@ -1758,7 +1757,7 @@ public void Insert_WithSchemaCommentStyles_InsertsComment() var tree = SyntaxTree.Parse("before\n@tag \"value\"", schema); tree.CreateEditor() - .Insert(Q.Syntax().Before(), "// comment\n") + .InsertBefore(Q.Syntax(), "// comment\n") .Commit(); var result = tree.ToText(); diff --git a/TinyTokenizer.Tests/SyntaxNodeTests.cs b/TinyTokenizer.Tests/SyntaxNodeTests.cs index 376fff0..3b94bb5 100644 --- a/TinyTokenizer.Tests/SyntaxNodeTests.cs +++ b/TinyTokenizer.Tests/SyntaxNodeTests.cs @@ -456,7 +456,7 @@ public void SyntaxBinder_Rebind_DoesNotDoubleWrapSyntaxNodes() // Make an edit that triggers rebinding tree.CreateEditor() - .Insert(Query.Syntax().After(), " bar()") + .InsertAfter(Query.Syntax(), " bar()") .Commit(); // After rebinding, the original FunctionCallSyntax should still be valid @@ -483,7 +483,7 @@ public void SyntaxBinder_MultipleRebinds_MaintainsCorrectStructure() // First edit tree.CreateEditor() - .Insert(Query.Syntax().After(), " bar()") + .InsertAfter(Query.Syntax(), " bar()") .Commit(); // Verify after first edit @@ -505,7 +505,7 @@ public void SyntaxBinder_MultipleRebinds_MaintainsCorrectStructure() // Third edit - insert at beginning tree.CreateEditor() - .Insert(Query.Syntax().First().Before(), "qux() ") + .InsertBefore(Query.Syntax().First(), "qux() ") .Commit(); // Verify after third edit @@ -544,7 +544,7 @@ public void SyntaxBinder_RebindsNestedChildrenInsideSyntaxNode() // Edit: Insert another function call inside the arguments of outer() // This tests that rebinding correctly processes children of existing syntax nodes tree.CreateEditor() - .Insert(Query.Syntax().Where(f => f.Name == "inner").After(), ", added()") + .InsertAfter(Query.Syntax().Where(f => f.Name == "inner"), ", added()") .Commit(); // Verify outer still exists and is correct diff --git a/TinyTokenizer.Tests/SyntaxTreeTests.cs b/TinyTokenizer.Tests/SyntaxTreeTests.cs index c7c6d0b..906291e 100644 --- a/TinyTokenizer.Tests/SyntaxTreeTests.cs +++ b/TinyTokenizer.Tests/SyntaxTreeTests.cs @@ -1721,12 +1721,12 @@ public void BlockQuery_First_PreservesBlockMethods() { var tree = SyntaxTree.Parse("{a} {b}"); - // First() on BlockNodeQuery should return BlockNodeQuery with InnerStart/InnerEnd + // First() on BlockNodeQuery should return BlockNodeQuery with Start/End var firstBlockQuery = Q.BraceBlock.First(); - var insertQuery = firstBlockQuery.InnerStart(); + var boundaryQuery = firstBlockQuery.Start(); - var positions = insertQuery.ResolvePositions(tree).ToList(); - Assert.Single(positions); + var results = boundaryQuery.Select(tree).ToList(); + Assert.Single(results); } [Fact] @@ -1735,10 +1735,10 @@ public void BlockQuery_Last_PreservesBlockMethods() var tree = SyntaxTree.Parse("{a} {b}"); var lastBlockQuery = Q.BraceBlock.Last(); - var insertQuery = lastBlockQuery.InnerEnd(); + var boundaryQuery = lastBlockQuery.End(); - var positions = insertQuery.ResolvePositions(tree).ToList(); - Assert.Single(positions); + var results = boundaryQuery.Select(tree).ToList(); + Assert.Single(results); } [Fact] @@ -1747,10 +1747,10 @@ public void BlockQuery_Nth_PreservesBlockMethods() var tree = SyntaxTree.Parse("{a} {b} {c}"); var nthBlockQuery = Q.BraceBlock.Nth(1); - var insertQuery = nthBlockQuery.InnerStart(); + var boundaryQuery = nthBlockQuery.Start(); - var positions = insertQuery.ResolvePositions(tree).ToList(); - Assert.Single(positions); + var results = boundaryQuery.Select(tree).ToList(); + Assert.Single(results); } #endregion diff --git a/TinyTokenizer/Ast/IBlockContainerNode.cs b/TinyTokenizer/Ast/IBlockContainerNode.cs index 96087f6..cebd3eb 100644 --- a/TinyTokenizer/Ast/IBlockContainerNode.cs +++ b/TinyTokenizer/Ast/IBlockContainerNode.cs @@ -2,20 +2,19 @@ namespace TinyTokenizer.Ast; /// /// Marker interface for syntax nodes that contain one or more named blocks. -/// Enables the and -/// extension methods -/// for concise block content insertion. +/// Enables the extension method +/// for accessing named blocks within syntax nodes. /// /// /// /// public sealed class FunctionSyntax : SyntaxNode, IBlockContainerNode /// { -/// public RedBlock Parameters => GetTypedChild<RedBlock>(2); -/// public RedBlock Body => GetTypedChild<RedBlock>(3); +/// public SyntaxBlock Parameters => GetTypedChild<SyntaxBlock>(2); +/// public SyntaxBlock Body => GetTypedChild<SyntaxBlock>(3); /// /// public IReadOnlyList<string> BlockNames => ["params", "body"]; /// -/// public RedBlock GetBlock(string? name) => name switch +/// public SyntaxBlock GetBlock(string? name) => name switch /// { /// null or "body" => Body, // default block /// "params" => Parameters, @@ -24,8 +23,8 @@ namespace TinyTokenizer.Ast; /// } /// /// // Usage: -/// Query.Syntax<FunctionSyntax>().Named("main").InnerStart("body") -/// Query.Syntax<FunctionSyntax>().Named("main").InnerEnd() // defaults to first block +/// editor.InsertAfter(Query.Syntax<FunctionSyntax>().Block("body").Start(), "// start") +/// editor.InsertBefore(Query.Syntax<FunctionSyntax>().Block("body").End(), "// end") /// /// public interface IBlockContainerNode @@ -57,34 +56,162 @@ public interface IBlockContainerNode public static class BlockContainerQueryExtensions { /// - /// Creates an insertion query for the start of a named block's content. + /// Creates a query that selects a named block from matched syntax nodes. + /// Use with methods like + /// and for block content insertion. /// /// The syntax node type that implements . - /// The query to create an insertion point from. + /// The query to create a block query from. /// /// The name of the block, or null to use the default block. /// For nodes with multiple blocks, null throws if ambiguous. /// - /// An insertion query for the block's inner start position. - public static InsertionQuery InnerStart(this SyntaxNodeQuery query, string? blockName = null) + /// A query that selects the named block from matched syntax nodes. + /// + /// + /// // Insert at the start of a function's body block + /// editor.InsertAfter(Query.Syntax<FunctionSyntax>().Block("body").Start(), "// start") + /// + /// // Insert at the end of the default block + /// editor.InsertBefore(Query.Syntax<FunctionSyntax>().Block().End(), "// end") + /// + /// + public static NamedBlockQuery Block(this SyntaxNodeQuery query, string? blockName = null) where T : SyntaxNode, IBlockContainerNode { - return new InsertionQuery(query, InsertionPoint.NamedBlockInnerStart, blockName); + return new NamedBlockQuery(query, blockName); } +} + +/// +/// A query that selects a named block from matched syntax nodes. +/// +public sealed record NamedBlockQuery : INodeQuery +{ + /// Gets the underlying syntax node query. + public INodeQuery InnerQuery { get; } + + /// Gets the block name (null for default block). + public string? BlockName { get; } + + internal NamedBlockQuery(INodeQuery innerQuery, string? blockName) + { + InnerQuery = innerQuery; + BlockName = blockName; + } + + /// + public IEnumerable Select(SyntaxTree tree) => Select(tree.Root); + + /// + public IEnumerable Select(SyntaxNode root) + { + foreach (var node in InnerQuery.Select(root)) + { + if (node is IBlockContainerNode container) + { + yield return container.GetBlock(BlockName); + } + } + } + + /// + public bool Matches(SyntaxNode node) + { + // A named block query matches if the node is a block that is a child of + // a syntax node that matches the inner query + if (node is not SyntaxBlock block) + return false; + + var parent = block.Parent; + if (parent is not IBlockContainerNode container) + return false; + + if (!InnerQuery.Matches(parent)) + return false; + + // Check if this is the right named block + try + { + var namedBlock = container.GetBlock(BlockName); + return ReferenceEquals(namedBlock.Green, block.Green) && namedBlock.Position == block.Position; + } + catch + { + return false; + } + } + + /// + public bool TryMatch(SyntaxNode startNode, out int consumedCount) + { + if (Matches(startNode)) + { + consumedCount = 1; + return true; + } + consumedCount = 0; + return false; + } + + /// + /// Returns a query that selects the opening delimiter (start) of matched blocks. + /// Use with InsertAfter to insert at the beginning of block content. + /// + public BoundaryQuery Start() => new BoundaryQuery(this, BoundarySide.Start); /// - /// Creates an insertion query for the end of a named block's content. + /// Returns a query that selects the closing delimiter (end) of matched blocks. + /// Use with InsertBefore to insert at the end of block content. + /// + public BoundaryQuery End() => new BoundaryQuery(this, BoundarySide.End); +} + +/// +/// Extension methods for directly accessing inner start/end of named blocks in block container queries. +/// These provide a convenient shorthand for the more explicit Block().Start() / Block().End() pattern. +/// +public static class BlockContainerInsertExtensions +{ + /// + /// Returns a boundary query for the inner start of a named block. + /// Shorthand for .Block(blockName).Start(). + /// Use with InsertAfter to insert at the beginning of block content. /// /// The syntax node type that implements . - /// The query to create an insertion point from. - /// - /// The name of the block, or null to use the default block. - /// For nodes with multiple blocks, null throws if ambiguous. - /// - /// An insertion query for the block's inner end position. - public static InsertionQuery InnerEnd(this SyntaxNodeQuery query, string? blockName = null) + /// The query to create a boundary from. + /// The name of the block, or null for the default block. + /// A boundary query for the block's inner start position. + /// + /// + /// // Insert at the start of a function's body block + /// editor.InsertAfter(Query.Syntax<FunctionSyntax>().Named("main").InnerStart("body"), "// first line") + /// + /// + public static BoundaryQuery InnerStart(this SyntaxNodeQuery query, string? blockName = null) + where T : SyntaxNode, IBlockContainerNode + { + return query.Block(blockName).Start(); + } + + /// + /// Returns a boundary query for the inner end of a named block. + /// Shorthand for .Block(blockName).End(). + /// Use with InsertBefore to insert at the end of block content. + /// + /// The syntax node type that implements . + /// The query to create a boundary from. + /// The name of the block, or null for the default block. + /// A boundary query for the block's inner end position. + /// + /// + /// // Insert at the end of a function's body block + /// editor.InsertBefore(Query.Syntax<FunctionSyntax>().Named("main").InnerEnd("body"), "// last line") + /// + /// + public static BoundaryQuery InnerEnd(this SyntaxNodeQuery query, string? blockName = null) where T : SyntaxNode, IBlockContainerNode { - return new InsertionQuery(query, InsertionPoint.NamedBlockInnerEnd, blockName); + return query.Block(blockName).End(); } } diff --git a/TinyTokenizer/Ast/NodeQuery.cs b/TinyTokenizer/Ast/NodeQuery.cs index 313a497..7315f0a 100644 --- a/TinyTokenizer/Ast/NodeQuery.cs +++ b/TinyTokenizer/Ast/NodeQuery.cs @@ -206,20 +206,6 @@ public TSelf WithTextEndingWith(string suffix) => #endregion - #region Position Modifiers - - /// - /// Returns a position query for inserting before each matched node. - /// - public InsertionQuery Before() => new InsertionQuery(this, InsertionPoint.Before); - - /// - /// Returns a position query for inserting after each matched node. - /// - public InsertionQuery After() => new InsertionQuery(this, InsertionPoint.After); - - #endregion - #region Composition Operators /// @@ -238,136 +224,3 @@ public TSelf WithTextEndingWith(string suffix) => #endregion } - -/// -/// Specifies where to insert relative to a matched node. -/// -public enum InsertionPoint -{ - /// Insert before the matched node. - Before, - /// Insert after the matched node. - After, - /// Insert at the start of a block's content (after opening delimiter). - InnerStart, - /// Insert at the end of a block's content (before closing delimiter). - InnerEnd, - /// Insert at the start of a named block's content (for IBlockContainerNode). - NamedBlockInnerStart, - /// Insert at the end of a named block's content (for IBlockContainerNode). - NamedBlockInnerEnd, -} - -/// -/// A query that specifies an insertion position relative to matched nodes. -/// -public sealed record InsertionQuery -{ - /// Gets the underlying node query. - public INodeQuery InnerQuery { get; } - - /// Gets the insertion point relative to matched nodes. - public InsertionPoint Point { get; } - - /// Gets the block name for named block insertion points, or null. - public string? BlockName { get; } - - internal InsertionQuery(INodeQuery inner, InsertionPoint point, string? blockName = null) - { - InnerQuery = inner; - Point = point; - BlockName = blockName; - } - - /// - /// Resolves insertion positions for all matched nodes. - /// Returns tuples containing path, index, position, and trivia context for proper insertion. - /// - internal IEnumerable ResolvePositions(SyntaxTree tree) - { - foreach (var node in InnerQuery.Select(tree)) - { - var position = ResolvePosition(node); - if (position.HasValue) - yield return position.Value; - } - } - - private InsertionPosition? ResolvePosition(SyntaxNode node) - { - var parent = node.Parent; - if (parent == null) - return null; // Can't insert relative to root - - // Use sibling index directly since red nodes are ephemeral - int childIndex = node.SiblingIndex; - - if (childIndex < 0) - return null; - - var parentPath = NodePath.FromNode(parent); - var targetPath = NodePath.FromNode(node); - - // Get trivia from target node for Before/After insertions - var (targetLeading, targetTrailing) = GetNodeTrivia(node); - - return Point switch - { - InsertionPoint.Before => new InsertionPosition( - parentPath, childIndex, node.Position, Point, targetPath, targetLeading, targetTrailing), - InsertionPoint.After => new InsertionPosition( - parentPath, childIndex + 1, node.EndPosition, Point, targetPath, targetLeading, targetTrailing), - InsertionPoint.InnerStart when node is SyntaxBlock block => new InsertionPosition( - NodePath.FromNode(node), 0, block.Position + 1, Point, null, - ImmutableArray.Empty, ImmutableArray.Empty), - InsertionPoint.InnerEnd when node is SyntaxBlock block => new InsertionPosition( - NodePath.FromNode(node), block.ChildCount, block.EndPosition - 1, Point, null, - ImmutableArray.Empty, ImmutableArray.Empty), - InsertionPoint.NamedBlockInnerStart when node is IBlockContainerNode container => - ResolveNamedBlockPosition(node, container.GetBlock(BlockName), isStart: true), - InsertionPoint.NamedBlockInnerEnd when node is IBlockContainerNode container => - ResolveNamedBlockPosition(node, container.GetBlock(BlockName), isStart: false), - _ => null - }; - } - - private static InsertionPosition ResolveNamedBlockPosition(SyntaxNode syntaxNode, SyntaxBlock block, bool isStart) - { - var blockPath = NodePath.FromNode(block); - - if (isStart) - { - return new InsertionPosition( - blockPath, 0, block.Position + 1, InsertionPoint.InnerStart, null, - ImmutableArray.Empty, ImmutableArray.Empty); - } - else - { - return new InsertionPosition( - blockPath, block.ChildCount, block.EndPosition - 1, InsertionPoint.InnerEnd, null, - ImmutableArray.Empty, ImmutableArray.Empty); - } - } - - private static (ImmutableArray Leading, ImmutableArray Trailing) GetNodeTrivia(SyntaxNode node) - { - return node.Green switch - { - GreenLeaf leaf => (leaf.LeadingTrivia, leaf.TrailingTrivia), - GreenBlock block => (block.LeadingTrivia, block.TrailingTrivia), - _ => (ImmutableArray.Empty, ImmutableArray.Empty) - }; - } -} - -/// -/// Contains all information needed to perform an insertion. -/// -internal readonly record struct InsertionPosition( - NodePath ParentPath, - int ChildIndex, - int Position, - InsertionPoint Point, - NodePath? TargetPath, - ImmutableArray TargetLeadingTrivia, - ImmutableArray TargetTrailingTrivia); diff --git a/TinyTokenizer/Ast/NodeQueryTypes.cs b/TinyTokenizer/Ast/NodeQueryTypes.cs index f154f41..dcdd1d2 100644 --- a/TinyTokenizer/Ast/NodeQueryTypes.cs +++ b/TinyTokenizer/Ast/NodeQueryTypes.cs @@ -176,14 +176,140 @@ protected override BlockNodeQuery CreateFiltered(Func predicat a == null ? b : n => a(n) && b(n); /// - /// Returns a position query for inserting at the start of block content. + /// Returns a query that selects the opening delimiter (start) of matched blocks. + /// Use with InsertAfter to insert at the beginning of block content. /// - public InsertionQuery InnerStart() => new InsertionQuery(this, InsertionPoint.InnerStart); + /// + /// + /// // Insert at the start of a block's content + /// editor.InsertAfter(Query.BraceBlock.First().Start(), "// first line") + /// + /// + public BoundaryQuery Start() => new BoundaryQuery(this, BoundarySide.Start); /// - /// Returns a position query for inserting at the end of block content. + /// Returns a query that selects the closing delimiter (end) of matched blocks. + /// Use with InsertBefore to insert at the end of block content. /// - public InsertionQuery InnerEnd() => new InsertionQuery(this, InsertionPoint.InnerEnd); + /// + /// + /// // Insert at the end of a block's content + /// editor.InsertBefore(Query.BraceBlock.First().End(), "// last line") + /// + /// + public BoundaryQuery End() => new BoundaryQuery(this, BoundarySide.End); +} + +#endregion + +#region Boundary Query + +/// +/// Specifies which boundary of a container to select. +/// +public enum BoundarySide +{ + /// The start/opening boundary of the container. + Start, + /// The end/closing boundary of the container. + End +} + +/// +/// A query that selects the boundary (start or end) of container nodes. +/// For blocks, this returns the opener or closer token. +/// For lists/containers without delimiters, this returns first/last child (or empty for empty containers). +/// +/// +/// This query carries metadata about which container and boundary is being targeted. +/// uses this metadata to compute insertion positions, +/// even for empty containers where returns no results. +/// +public sealed record BoundaryQuery : INodeQuery +{ + /// Gets the underlying container query. + public INodeQuery ContainerQuery { get; } + + /// Gets which boundary (start or end) this query targets. + public BoundarySide Side { get; } + + /// Creates a boundary query for the specified container query and side. + public BoundaryQuery(INodeQuery containerQuery, BoundarySide side) + { + ContainerQuery = containerQuery; + Side = side; + } + + /// + public IEnumerable Select(SyntaxTree tree) => Select(tree.Root); + + /// + public IEnumerable Select(SyntaxNode root) + { + foreach (var container in ContainerQuery.Select(root)) + { + var boundary = GetBoundaryNode(container); + if (boundary != null) + yield return boundary; + } + } + + /// + public bool Matches(SyntaxNode node) + { + // A boundary query matches a node if it's the boundary of a container matched by the inner query + // This is tricky because we need to check if the node is a boundary of its parent + if (node.Parent == null) + return false; + + // Check if parent is a container that matches + if (!ContainerQuery.Matches(node.Parent)) + return false; + + var boundary = GetBoundaryNode(node.Parent); + return boundary != null && ReferenceEquals(boundary.Green, node.Green) && boundary.Position == node.Position; + } + + /// + public bool TryMatch(SyntaxNode startNode, out int consumedCount) + { + if (Matches(startNode)) + { + consumedCount = 1; + return true; + } + consumedCount = 0; + return false; + } + + /// + /// Gets the boundary node for a container. + /// For blocks: returns OpenerNode or CloserNode. + /// For other containers: returns first or last child. + /// + private SyntaxNode? GetBoundaryNode(SyntaxNode container) + { + if (container is SyntaxBlock block) + { + return Side == BoundarySide.Start ? block.OpenerNode : block.CloserNode; + } + + // For non-block containers (lists, syntax nodes), return first/last child + var children = container.Children.ToList(); + if (children.Count == 0) + return null; // Empty container - no boundary node to return + + return Side == BoundarySide.Start ? children[0] : children[^1]; + } + + /// + /// Resolves the containers matched by this boundary query. + /// Used by to compute insertion positions. + /// + internal IEnumerable ResolveContainers(SyntaxTree tree) + { + return ContainerQuery.Select(tree); + } } #endregion diff --git a/TinyTokenizer/Ast/SyntaxEditor.cs b/TinyTokenizer/Ast/SyntaxEditor.cs index f10ec9d..fada12b 100644 --- a/TinyTokenizer/Ast/SyntaxEditor.cs +++ b/TinyTokenizer/Ast/SyntaxEditor.cs @@ -12,7 +12,7 @@ namespace TinyTokenizer.Ast; /// var tree = SyntaxTree.Parse("function foo() { return 1; }"); /// /// tree.CreateEditor() -/// .Insert(Query.BraceBlock.First().InnerStart(), "console.log('enter');") +/// .InsertAfter(Query.BraceBlock.First().Start(), "console.log('enter');") /// .Replace(Query.AnyNumeric.First(), "42") /// .Remove(Query.AnyIdent.WithText("unused")) /// .Commit(); @@ -47,57 +47,69 @@ internal SyntaxEditor(SyntaxTree tree, TokenizerOptions? options = null) #region Insert (Query-based) /// - /// Queues an insertion of text at positions resolved by the query. + /// Queues an insertion of text before all nodes matching the query. + /// For , handles empty containers by using container metadata. /// - /// An insertion query specifying where to insert. + /// A query specifying which nodes to insert before. /// The text to insert (will be parsed into nodes). - public SyntaxEditor Insert(InsertionQuery query, string text) + /// + /// + /// // Insert before the closing brace (at end of block content) + /// editor.InsertBefore(Query.BraceBlock.First().End(), "// last line") + /// + /// + public SyntaxEditor InsertBefore(INodeQuery query, string text) { - var positions = query.ResolvePositions(_tree).ToList(); - - foreach (var pos in positions) + // Handle BoundaryQuery specially for empty container support + if (query is BoundaryQuery boundaryQuery) { - _edits.Add(new InsertEdit(pos, text) { SequenceNumber = _sequenceNumber++ }); + foreach (var container in boundaryQuery.ResolveContainers(_tree)) + { + var pos = CreateBoundaryInsertionPosition(container, boundaryQuery.Side, before: true); + if (pos.HasValue) + _edits.Add(new InsertEdit(pos.Value, text) { SequenceNumber = _sequenceNumber++ }); + } + return this; } - return this; - } - - /// - /// Queues an insertion of text at positions resolved by multiple queries. - /// - public SyntaxEditor Insert(IEnumerable queries, string text) - { - foreach (var query in queries) + // Standard query - insert before each matched node + foreach (var node in query.Select(_tree)) { - Insert(query, text); + InsertBefore(node, text); } return this; } /// - /// Queues an insertion of pre-built nodes at positions resolved by the query. + /// Queues an insertion of text after all nodes matching the query. + /// For , handles empty containers by using container metadata. /// - internal SyntaxEditor Insert(InsertionQuery query, ImmutableArray nodes) + /// A query specifying which nodes to insert after. + /// The text to insert (will be parsed into nodes). + /// + /// + /// // Insert after the opening brace (at start of block content) + /// editor.InsertAfter(Query.BraceBlock.First().Start(), "// first line") + /// + /// + public SyntaxEditor InsertAfter(INodeQuery query, string text) { - var positions = query.ResolvePositions(_tree).ToList(); - - foreach (var pos in positions) + // Handle BoundaryQuery specially for empty container support + if (query is BoundaryQuery boundaryQuery) { - _edits.Add(new InsertNodesEdit(pos, nodes) { SequenceNumber = _sequenceNumber++ }); + foreach (var container in boundaryQuery.ResolveContainers(_tree)) + { + var pos = CreateBoundaryInsertionPosition(container, boundaryQuery.Side, before: false); + if (pos.HasValue) + _edits.Add(new InsertEdit(pos.Value, text) { SequenceNumber = _sequenceNumber++ }); + } + return this; } - return this; - } - - /// - /// Queues an insertion of pre-built nodes at positions resolved by multiple queries. - /// - internal SyntaxEditor Insert(IEnumerable queries, ImmutableArray nodes) - { - foreach (var query in queries) + // Standard query - insert after each matched node + foreach (var node in query.Select(_tree)) { - Insert(query, nodes); + InsertAfter(node, text); } return this; } @@ -114,7 +126,7 @@ internal SyntaxEditor Insert(IEnumerable queries, ImmutableArray /// Thrown if the target node has no parent. public SyntaxEditor InsertBefore(SyntaxNode target, string text) { - var pos = CreateInsertionPosition(target, InsertionPoint.Before); + var pos = CreateInsertionPosition(target, before: true); _edits.Add(new InsertEdit(pos, text) { SequenceNumber = _sequenceNumber++ }); return this; } @@ -139,7 +151,7 @@ public SyntaxEditor InsertBefore(IEnumerable targets, string text) /// Thrown if the target node has no parent. public SyntaxEditor InsertAfter(SyntaxNode target, string text) { - var pos = CreateInsertionPosition(target, InsertionPoint.After); + var pos = CreateInsertionPosition(target, before: false); _edits.Add(new InsertEdit(pos, text) { SequenceNumber = _sequenceNumber++ }); return this; } @@ -186,7 +198,7 @@ public SyntaxEditor InsertBefore(SyntaxNode target, IEnumerable node internal SyntaxEditor InsertBefore(SyntaxNode target, IEnumerable nodesToInsert) { var nodes = nodesToInsert.ToImmutableArray(); - var pos = CreateInsertionPosition(target, InsertionPoint.Before); + var pos = CreateInsertionPosition(target, before: true); _edits.Add(new InsertNodesEdit(pos, nodes) { SequenceNumber = _sequenceNumber++ }); return this; } @@ -200,7 +212,7 @@ public SyntaxEditor InsertBefore(IEnumerable targets, IEnumerable targets, IEnumerable< var nodes = nodesToInsert.ToImmutableArray(); foreach (var target in targets) { - var pos = CreateInsertionPosition(target, InsertionPoint.Before); + var pos = CreateInsertionPosition(target, before: true); _edits.Add(new InsertNodesEdit(pos, nodes) { SequenceNumber = _sequenceNumber++ }); } return this; @@ -251,7 +263,7 @@ public SyntaxEditor InsertAfter(SyntaxNode target, IEnumerable nodes internal SyntaxEditor InsertAfter(SyntaxNode target, IEnumerable nodesToInsert) { var nodes = nodesToInsert.ToImmutableArray(); - var pos = CreateInsertionPosition(target, InsertionPoint.After); + var pos = CreateInsertionPosition(target, before: false); _edits.Add(new InsertNodesEdit(pos, nodes) { SequenceNumber = _sequenceNumber++ }); return this; } @@ -265,7 +277,7 @@ public SyntaxEditor InsertAfter(IEnumerable targets, IEnumerable targets, IEnumerable Leading, ImmutableArray /// Creates an InsertionPosition for inserting before or after a target node. /// /// Thrown if the target node has no parent. - private static InsertionPosition CreateInsertionPosition(SyntaxNode target, InsertionPoint point) + private static InsertionPosition CreateInsertionPosition(SyntaxNode target, bool before) { var parent = target.Parent; if (parent == null) @@ -720,14 +732,97 @@ private static InsertionPosition CreateInsertionPosition(SyntaxNode target, Inse var targetPath = NodePath.FromNode(target); var (targetLeading, targetTrailing) = GetTrivia(target); - return point switch + return before + ? new InsertionPosition(parentPath, childIndex, target.Position, targetPath, targetLeading, targetTrailing) + : new InsertionPosition(parentPath, childIndex + 1, target.EndPosition, targetPath, targetLeading, targetTrailing); + } + + /// + /// Creates an InsertionPosition for a boundary query (Start/End of a container). + /// Handles empty containers by computing position from container metadata. + /// + private static InsertionPosition? CreateBoundaryInsertionPosition(SyntaxNode container, BoundarySide side, bool before) + { + if (container is SyntaxBlock block) + { + // For blocks, the boundary nodes are the opener/closer + // Start + InsertAfter = insert at beginning of content (after opener) + // End + InsertBefore = insert at end of content (before closer) + var blockPath = NodePath.FromNode(block); + + if (side == BoundarySide.Start) + { + // Insert relative to opener + if (before) + { + // InsertBefore(Start) = insert before the opener (before the block content) + var parent = block.Parent; + if (parent == null) return null; + var parentPath = NodePath.FromNode(parent); + return new InsertionPosition(parentPath, block.SiblingIndex, block.Position, null, + ImmutableArray.Empty, ImmutableArray.Empty); + } + else + { + // InsertAfter(Start) = insert at beginning of block content (child index 0) + return new InsertionPosition(blockPath, 0, block.InnerStartPosition, null, + ImmutableArray.Empty, ImmutableArray.Empty); + } + } + else // BoundarySide.End + { + if (before) + { + // InsertBefore(End) = insert at end of block content (after last child) + return new InsertionPosition(blockPath, block.ChildCount, block.InnerEndPosition, null, + ImmutableArray.Empty, ImmutableArray.Empty); + } + else + { + // InsertAfter(End) = insert after the closer (after the block) + var parent = block.Parent; + if (parent == null) return null; + var parentPath = NodePath.FromNode(parent); + return new InsertionPosition(parentPath, block.SiblingIndex + 1, block.EndPosition, null, + ImmutableArray.Empty, ImmutableArray.Empty); + } + } + } + + // For non-block containers (lists, syntax nodes), use first/last child + var children = container.Children.ToList(); + var containerPath = NodePath.FromNode(container); + + if (side == BoundarySide.Start) { - InsertionPoint.Before => new InsertionPosition( - parentPath, childIndex, target.Position, point, targetPath, targetLeading, targetTrailing), - InsertionPoint.After => new InsertionPosition( - parentPath, childIndex + 1, target.EndPosition, point, targetPath, targetLeading, targetTrailing), - _ => throw new ArgumentException($"Unsupported insertion point: {point}", nameof(point)) - }; + if (children.Count == 0) + { + // Empty container - insert at position 0 (child index 0) + return before + ? null // Can't insert before nothing + : new InsertionPosition(containerPath, 0, container.Position, null, + ImmutableArray.Empty, ImmutableArray.Empty); + } + + // Has children - delegate to first child + var first = children[0]; + return CreateInsertionPosition(first, before); + } + else // BoundarySide.End + { + if (children.Count == 0) + { + // Empty container - insert at position 0 (child index 0) + return before + ? new InsertionPosition(containerPath, 0, container.Position, null, + ImmutableArray.Empty, ImmutableArray.Empty) + : null; // Can't insert after nothing + } + + // Has children - delegate to last child + var last = children[^1]; + return CreateInsertionPosition(last, before); + } } /// @@ -1060,4 +1155,15 @@ private static ImmutableArray TransferTrivia( } } +/// +/// Contains all information needed to perform an insertion. +/// +internal readonly record struct InsertionPosition( + NodePath ParentPath, + int ChildIndex, + int Position, + NodePath? TargetPath, + ImmutableArray TargetLeadingTrivia, + ImmutableArray TargetTrailingTrivia); + #endregion