From d2e1e90f948a9ec76696211116cc5e77b9e37d14 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Wed, 4 Dec 2024 21:05:12 +0300 Subject: [PATCH] Compute correct changeset when swapping nodes (#11765) close #11734 The #11428 introduced a special handling of edits removing the node. This logic is also triggered when the GUI swaps two lines leading to computing incorrect changeset. Changelog: - update: correct the logic that determines if the edit removes a line # Important Notes https://github.com/user-attachments/assets/fa84bb09-5f86-4739-b447-e49c49a09a76 --- .../instrument/ChangesetBuilder.scala | 21 ++++++++--- .../test/context/ChangesetBuilderTest.scala | 35 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/ChangesetBuilder.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/ChangesetBuilder.scala index 8f6b45f1d9..587403c874 100644 --- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/ChangesetBuilder.scala +++ b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/ChangesetBuilder.scala @@ -12,6 +12,7 @@ import org.enso.text.editing.model.{IdMap, TextEdit} import org.enso.text.editing.{IndexedSource, TextEditor} import java.util.UUID + import scala.collection.mutable /** The changeset of a module containing the computed list of invalidated @@ -178,6 +179,8 @@ final class ChangesetBuilder[A: TextEditor: IndexedSource]( * @return the set of IR nodes directly affected by the edit */ def invalidated(edits: Seq[TextEdit]): Set[ChangesetBuilder.NodeId] = { + val allEdits = edits.toSet + @scala.annotation.tailrec def go( tree: ChangesetBuilder.Tree, @@ -187,8 +190,9 @@ final class ChangesetBuilder[A: TextEditor: IndexedSource]( ): Set[ChangesetBuilder.NodeId] = { if (edits.isEmpty) ids.toSet else { - val edit = edits.dequeue() - val locationEdit = ChangesetBuilder.toLocationEdit(edit, source) + val edit = edits.dequeue() + val locationEdit = + ChangesetBuilder.toLocationEdit(edit, source, allEdits) var invalidatedSet = ChangesetBuilder.invalidated( tree, @@ -533,17 +537,26 @@ object ChangesetBuilder { * * @param edit the text edit * @param source the source text + * @param edits all applied text edits * @return the edit location in the source text */ private def toLocationEdit[A: IndexedSource]( edit: TextEdit, - source: A + source: A, + edits: Set[TextEdit] ): LocationEdit = { def isSameOffset: Boolean = edit.range.end.character == edit.range.start.character def isAcrossLines: Boolean = edit.range.end.line > edit.range.start.line - val isNodeRemoved = edit.text.isEmpty && isSameOffset && isAcrossLines + def otherEditPositions = + (edits - edit).flatMap(edit => Set(edit.range.start, edit.range.end)) + def noOtherEditsOnTheLine = + !otherEditPositions.exists(p => + p.line == edit.range.start.line || p.line == edit.range.end.line + ) + val isNodeRemoved = + edit.text.isEmpty && isSameOffset && isAcrossLines && noOtherEditsOnTheLine LocationEdit(toLocation(edit, source), edit.text.length, isNodeRemoved) } diff --git a/engine/runtime-instrument-common/src/test/scala/org/enso/compiler/test/context/ChangesetBuilderTest.scala b/engine/runtime-instrument-common/src/test/scala/org/enso/compiler/test/context/ChangesetBuilderTest.scala index 76015824f6..389deeae91 100644 --- a/engine/runtime-instrument-common/src/test/scala/org/enso/compiler/test/context/ChangesetBuilderTest.scala +++ b/engine/runtime-instrument-common/src/test/scala/org/enso/compiler/test/context/ChangesetBuilderTest.scala @@ -242,6 +242,41 @@ class ChangesetBuilderTest ) } + "multiline swap nodes" in { + val code = + """x -> + | y = _.abs + | z = 42 + | y + x""".stripMargin.linesIterator.mkString("\n") + val edits = Seq( + TextEdit(Range(Position(1, 4), Position(2, 4)), ""), + TextEdit(Range(Position(2, 0), Position(2, 0)), " y = z.abs\n") + ) + + val ir = code + .preprocessExpression(freshInlineContext) + .get + .asInstanceOf[Function.Lambda] + + val firstLine = ir.body.children()(0).asInstanceOf[Expression.Binding] + val yName = firstLine.name + val yExpr = firstLine.expression + .asInstanceOf[Function.Lambda] + .body + .asInstanceOf[Application.Prefix] + val yExprFunction = yExpr.function + val yExprFunctionArg = yExpr.arguments(0).value + val secondLine = ir.body.children()(1).asInstanceOf[Expression.Binding] + val zName = secondLine.name + + invalidated(ir, code, edits: _*) should contain theSameElementsAs Seq( + yName.getId, + yExprFunction.getId, + yExprFunctionArg.getId, + zName.getId + ) + } + "multiline insert line 1" in { val code = """x ->