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
This commit is contained in:
Dmitry Bushev 2024-12-04 21:05:12 +03:00 committed by GitHub
parent 48a87cefb8
commit d2e1e90f94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 52 additions and 4 deletions

View File

@ -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)
}

View File

@ -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 ->