From 51155f9456a278ec552389b521e28aee96c7bb38 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 29 Mar 2023 11:55:25 +0530 Subject: [PATCH] feat: add patch ability to ActiveDocument --- .../androidide/editor/ui/IDEEditor.java | 286 ++++++++---------- .../eventbus/events/editor/DocumentEvents.kt | 2 +- .../itsaky/androidide/projects/FileManager.kt | 31 +- .../projects/models/ActiveDocument.kt | 53 ++-- .../projects/models/ActiveJavaDocument.kt | 11 +- .../projects/models/ActiveDocumentTest.kt | 129 ++++++++ 6 files changed, 306 insertions(+), 206 deletions(-) create mode 100644 subprojects/projects/src/test/java/com/itsaky/androidide/projects/models/ActiveDocumentTest.kt diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java index 1988be639..b7d73ea08 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java @@ -110,8 +110,8 @@ public class IDEEditor extends CodeEditor implements IEditor, ILspEditor { final var cursor = getCursor(); // we do not use getSignatureHelpWindow() to avoid initializing the window unnecessarily - if (languageClient == null || cursor == null || cursor.isSelected() - || (signatureHelpWindow != null && signatureHelpWindow.isShowing())) { + if (languageClient == null || cursor == null || cursor.isSelected() || + (signatureHelpWindow != null && signatureHelpWindow.isShowing())) { return; } @@ -198,9 +198,9 @@ public class IDEEditor extends CodeEditor implements IEditor, ILspEditor { } } -public void updateFile(File file) { + public void updateFile(File file) { this.file = file; -} + } @NonNull @Override @@ -216,8 +216,8 @@ public void updateFile(File file) { return; } - final var openEvent = - new DocumentOpenEvent(getFile().toPath(), getText().toString(), fileVersion = 0); + final var openEvent = new DocumentOpenEvent(getFile().toPath(), getText().toString(), + fileVersion = 0); EventBus.getDefault().post(openEvent); } @@ -233,27 +233,20 @@ public void updateFile(File file) { @Override public void signatureHelp() { if (languageServer != null && getFile() != null) { - final CompletableFuture future = - CompletableFuture.supplyAsync( - () -> - languageServer.signatureHelp( - new SignatureHelpParams( - getFile().toPath(), - getCursorLSPPosition()))); + final CompletableFuture future = CompletableFuture.supplyAsync( + () -> languageServer.signatureHelp( + new SignatureHelpParams(getFile().toPath(), getCursorLSPPosition()))); - future.whenComplete( - (help, error) -> { - if (help == null - || languageClient == null - || future.isCancelled() - || future.isCompletedExceptionally()) { - LOG.error("An error occurred while finding signature help", error); - return; - } + future.whenComplete((help, error) -> { + if (help == null || languageClient == null || future.isCancelled() || + future.isCompletedExceptionally()) { + LOG.error("An error occurred while finding signature help", error); + return; + } - //noinspection ConstantConditions - ThreadUtils.runOnUiThread(() -> showSignatureHelp(help)); - }); + //noinspection ConstantConditions + ThreadUtils.runOnUiThread(() -> showSignatureHelp(help)); + }); } } @@ -298,11 +291,8 @@ public void updateFile(File file) { @Override public void setSelection(@NonNull Range range) { if (isValidPosition(range.getStart(), true) && isValidPosition(range.getEnd(), true)) { - setSelectionRegion( - range.getStart().getLine(), - range.getStart().getColumn(), - range.getEnd().getLine(), - range.getEnd().getColumn()); + setSelectionRegion(range.getStart().getLine(), range.getStart().getColumn(), + range.getEnd().getLine(), range.getEnd().getColumn()); } else { LOG.warn("Selection range is invalid", range); } @@ -366,9 +356,8 @@ public void updateFile(File file) { final var start = range.getStart(); final var end = range.getEnd(); - return isValidPosition(start, allowColumnEqual) - && isValidPosition(end, allowColumnEqual) - && start.compareTo(end) < 0; // make sure start position is before end position + return isValidPosition(start, allowColumnEqual) && isValidPosition(end, allowColumnEqual) && + start.compareTo(end) < 0; // make sure start position is before end position } /** @@ -383,8 +372,8 @@ public void updateFile(File file) { return false; } - return isValidLine(position.getLine()) - && isValidColumn(position.getLine(), position.getColumn(), allowColumnEqual); + return isValidLine(position.getLine()) && + isValidColumn(position.getLine(), position.getColumn(), allowColumnEqual); } /** @@ -484,59 +473,50 @@ public void updateFile(File file) { return; } - final ProgressDialog pd = - ProgressDialog.show( - getContext(), null, getContext().getString(string.msg_finding_definition)); + final ProgressDialog pd = ProgressDialog.show(getContext(), null, + getContext().getString(string.msg_finding_definition)); try { - final CompletableFuture future = - CompletableFuture.supplyAsync( - () -> { - final var params = - new com.itsaky.androidide.lsp.models.DefinitionParams( - getFile().toPath(), - new com.itsaky.androidide.models.Position( - getCursor().getLeftLine(), getCursor().getLeftColumn())); + final CompletableFuture future = CompletableFuture.supplyAsync(() -> { + final var params = new com.itsaky.androidide.lsp.models.DefinitionParams(getFile().toPath(), + new com.itsaky.androidide.models.Position(getCursor().getLeftLine(), + getCursor().getLeftColumn())); - return languageServer.findDefinition(params); - }); + return languageServer.findDefinition(params); + }); - future.whenComplete( - (result, error) -> { - if (result == null - || languageClient == null - || future.isCancelled() - || future.isCompletedExceptionally()) { - LOG.error("An error occurred while finding definition", error); - showDefinitionNotFound(pd); - return; + future.whenComplete((result, error) -> { + if (result == null || languageClient == null || future.isCancelled() || + future.isCompletedExceptionally()) { + LOG.error("An error occurred while finding definition", error); + showDefinitionNotFound(pd); + return; + } + + final var locations = result.getLocations(); + if (locations.size() == 0) { + LOG.error("No definitions found", "Size:", locations.size()); + showDefinitionNotFound(pd); + return; + } + + //noinspection ConstantConditions + ThreadUtils.runOnUiThread(() -> { + if (locations.size() == 1) { + var location = locations.get(0); + if (DocumentUtils.isSameFile(location.getFile(), getFile().toPath())) { + setSelection(location.getRange()); + return; + } + languageClient.showDocument( + new ShowDocumentParams(location.getFile(), location.getRange())); + } else { + languageClient.showLocations(locations); } - - final var locations = result.getLocations(); - if (locations.size() == 0) { - LOG.error("No definitions found", "Size:", locations.size()); - showDefinitionNotFound(pd); - return; - } - - //noinspection ConstantConditions - ThreadUtils.runOnUiThread( - () -> { - if (locations.size() == 1) { - var location = locations.get(0); - if (DocumentUtils.isSameFile(location.getFile(), getFile().toPath())) { - setSelection(location.getRange()); - return; - } - languageClient.showDocument( - new ShowDocumentParams(location.getFile(), location.getRange())); - } else { - languageClient.showLocations(locations); - } - }); - - dismissOnUiThread(pd); }); + + dismissOnUiThread(pd); + }); } catch (Throwable th) { LOG.error("An error occurred while finding definition", th); showDefinitionNotFound(pd); @@ -551,11 +531,10 @@ public void updateFile(File file) { @SuppressWarnings("deprecation") private void showDefinitionNotFound(final ProgressDialog pd) { //noinspection ConstantConditions - ThreadUtils.runOnUiThread( - () -> { - FlashbarUtilsKt.flashError(string.msg_no_definition); - pd.dismiss(); - }); + ThreadUtils.runOnUiThread(() -> { + FlashbarUtilsKt.flashError(string.msg_no_definition); + pd.dismiss(); + }); } /** @@ -581,22 +560,16 @@ public void updateFile(File file) { return; } - @SuppressWarnings("deprecation") final ProgressDialog pd = - ProgressDialog.show( - getContext(), null, getContext().getString(string.msg_finding_references)); + @SuppressWarnings("deprecation") final ProgressDialog pd = ProgressDialog.show(getContext(), + null, getContext().getString(string.msg_finding_references)); try { - final var future = - CompletableFuture.supplyAsync( - () -> { - final var referenceParams = - new ReferenceParams( - getFile().toPath(), - new com.itsaky.androidide.models.Position( - getCursor().getLeftLine(), getCursor().getLeftColumn()), - true); - return languageServer.findReferences(referenceParams); - }); + final var future = CompletableFuture.supplyAsync(() -> { + final var referenceParams = new ReferenceParams(getFile().toPath(), + new com.itsaky.androidide.models.Position(getCursor().getLeftLine(), + getCursor().getLeftColumn()), true); + return languageServer.findReferences(referenceParams); + }); future.whenComplete((result, error) -> onFindReferencesResult(pd, future, result, error)); } catch (Throwable th) { @@ -606,15 +579,12 @@ public void updateFile(File file) { } @SuppressWarnings("deprecation") - private void onFindReferencesResult( - final ProgressDialog pd, - final CompletableFuture future, - final ReferenceResult result, - final Throwable error) { - if (result == null - || languageClient == null - || future.isCancelled() - || future.isCompletedExceptionally()) { + private void onFindReferencesResult(final ProgressDialog pd, + final CompletableFuture future, + final ReferenceResult result, final Throwable error + ) { + if (result == null || languageClient == null || future.isCancelled() || + future.isCompletedExceptionally()) { LOG.error("An error occurred while finding references", error); showReferencesNotFound(pd); return; @@ -647,11 +617,10 @@ public void updateFile(File file) { @SuppressWarnings("deprecation") private void showReferencesNotFound(final ProgressDialog pd) { //noinspection ConstantConditions - ThreadUtils.runOnUiThread( - () -> { - FlashbarUtilsKt.flashError(string.msg_no_references); - pd.dismiss(); - }); + ThreadUtils.runOnUiThread(() -> { + FlashbarUtilsKt.flashError(string.msg_no_references); + pd.dismiss(); + }); } /** @@ -666,32 +635,29 @@ public void updateFile(File file) { } //noinspection deprecation - final var pd = - ProgressDialog.show( - getContext(), null, getContext().getString(string.please_wait), true, false); - final CompletableFuture future = - CompletableFuture.supplyAsync( - () -> - languageServer.expandSelection( - new ExpandSelectionParams(getFile().toPath(), getCursorLSPRange()))); + final var pd = ProgressDialog.show(getContext(), null, + getContext().getString(string.please_wait), true, false); + final CompletableFuture future = CompletableFuture.supplyAsync( + () -> languageServer.expandSelection( + new ExpandSelectionParams(getFile().toPath(), getCursorLSPRange()))); - future.whenComplete( - ((range, throwable) -> { - pd.dismiss(); + future.whenComplete(((range, throwable) -> { + pd.dismiss(); - if (throwable != null) { - LOG.error("Error computing expanded selection range", throwable); - return; - } + if (throwable != null) { + LOG.error("Error computing expanded selection range", throwable); + return; + } - //noinspection ConstantConditions - ThreadUtils.runOnUiThread(() -> setSelection(range)); - })); + //noinspection ConstantConditions + ThreadUtils.runOnUiThread(() -> setSelection(range)); + })); } @Override - protected void onFocusChanged( - final boolean gainFocus, final int direction, @Nullable final Rect previouslyFocusedRect) { + protected void onFocusChanged(final boolean gainFocus, final int direction, + @Nullable final Rect previouslyFocusedRect + ) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); if (!gainFocus) { ensureWindowsDismissed(); @@ -727,11 +693,10 @@ public void updateFile(File file) { return; } - CompletableFuture.runAsync( - () -> { - dispatchDocumentChangeEvent(event); - checkForSignatureHelp(event); - }); + CompletableFuture.runAsync(() -> { + dispatchDocumentChangeEvent(event); + checkForSignatureHelp(event); + }); } /** @@ -750,9 +715,8 @@ public void updateFile(File file) { return; } final var changeLength = event.getChangedText().length(); - if (event.getAction() != ContentChangeEvent.ACTION_INSERT - || changeLength > 0 - && changeLength <= 2) { // changeLength will be 2 as '(' and ')' are inserted at the same time + if (event.getAction() != ContentChangeEvent.ACTION_INSERT || changeLength > 0 && + changeLength <= 2) { // changeLength will be 2 as '(' and ')' are inserted at the same time return; } @@ -782,22 +746,18 @@ public void updateFile(File file) { final var start = event.getChangeStart(); final var end = event.getChangeEnd(); - final var changeRange = - new Range( - new Position(start.line, start.column, start.index), - new Position(end.line, end.column, end.index)); + final var changeRange = new Range(new Position(start.line, start.column, start.index), + new Position(end.line, end.column, end.index)); - final var changeEvent = - new DocumentChangeEvent( - file, getText().toString(), fileVersion + 1, type, changeDelta, changeRange); + final var changedText = event.getChangedText().toString(); + final var changeEvent = new DocumentChangeEvent(file, changedText, ++fileVersion, type, + changeDelta, changeRange); EventBus.getDefault().post(changeEvent); } public static int createInputFlags() { - var flags = - EditorInfo.TYPE_CLASS_TEXT - | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE - | EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS; + var flags = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE | + EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS; if (getVisiblePasswordFlag()) { flags |= EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; } @@ -816,12 +776,11 @@ public void updateFile(File file) { public void analyze() { if (languageServer != null && getFile() != null && getEditorLanguage() instanceof IDELanguage) { CompletableFuture.supplyAsync(() -> languageServer.analyze(getFile().toPath())) - .whenComplete( - (diagnostics, throwable) -> { - if (languageClient != null) { - languageClient.publishDiagnostics(diagnostics); - } - }); + .whenComplete((diagnostics, throwable) -> { + if (languageClient != null) { + languageClient.publishDiagnostics(diagnostics); + } + }); } } @@ -966,14 +925,15 @@ public void updateFile(File file) { } private void applyTreeSitterLang(final Language language, final String extension, - SchemeAndroidIDE scheme) { + SchemeAndroidIDE scheme + ) { if (scheme == null) { LOG.error("Failed to read current color scheme"); scheme = SchemeAndroidIDE.newInstance(getContext()); } - if (scheme instanceof IDEColorScheme - && ((IDEColorScheme) scheme).getLanguageScheme(extension) == null) { + if (scheme instanceof IDEColorScheme && + ((IDEColorScheme) scheme).getLanguageScheme(extension) == null) { LOG.warn("Color scheme does not support file type '" + extension + "'"); scheme = SchemeAndroidIDE.newInstance(getContext()); } diff --git a/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/DocumentEvents.kt b/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/DocumentEvents.kt index c14b1c902..f70d619bf 100644 --- a/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/DocumentEvents.kt +++ b/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/DocumentEvents.kt @@ -44,7 +44,7 @@ constructor(var closedFile: Path, val selectionRange: Range = Range.NONE) : */ data class DocumentChangeEvent( var changedFile: Path, - var newText: String, + var changedText: String, var version: Int, var changeType: ChangeType, var changeDelta: Int, diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/FileManager.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/FileManager.kt index 76a0fb88f..f21c25f09 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/FileManager.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/FileManager.kt @@ -23,7 +23,6 @@ import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent import com.itsaky.androidide.eventbus.events.file.FileRenameEvent -import com.itsaky.androidide.models.Range import com.itsaky.androidide.progress.ProcessCancelledException import com.itsaky.androidide.progress.ProgressManager import com.itsaky.androidide.projects.models.ActiveDocument @@ -112,13 +111,25 @@ object FileManager : EventReceiver { @Subscribe(threadMode = BACKGROUND) @Suppress("unused") fun onDocumentContentChange(event: DocumentChangeEvent) { - activeDocuments[event.changedFile.normalize()] = createDocument(event) + val document = activeDocuments[event.changedFile.normalize()] + + if (document == null) { + // create document if not already created + // this should not happen under normal circumstances + activeDocuments[event.changedFile.normalize()] = createDocument(event) + log.warn("Document change event received before open event for file ${event.changedFile}") + return + } + + // document is already open + // patch the changes + document.patch(event) } @Subscribe(threadMode = BACKGROUND) @Suppress("unused") fun onDocumentClose(event: DocumentCloseEvent) { - activeDocuments.remove(event.closedFile.normalize()) + activeDocuments.remove(event.closedFile.normalize())?.close() } @Subscribe(threadMode = BACKGROUND) @@ -132,28 +143,24 @@ object FileManager : EventReceiver { @Subscribe(threadMode = BACKGROUND) fun onFileDeleted(event: FileDeletionEvent) { // If the file was an active document, remove the document cache - activeDocuments.remove(event.file.toPath().normalize()) + activeDocuments.remove(event.file.toPath().normalize())?.close() } private fun createDocument(event: DocumentOpenEvent): ActiveDocument { return ActiveDocument( file = event.openedFile, - content = event.text, - changeRange = Range.NONE, version = event.version, - changDelta = 0, - modified = Instant.now() + modified = Instant.now(), + content = event.text ) } private fun createDocument(event: DocumentChangeEvent): ActiveDocument { return ActiveDocument( file = event.changedFile, - content = event.newText, - changeRange = event.changeRange, version = event.version, - changDelta = event.changeDelta, - modified = Instant.now() + modified = Instant.now(), + content = event.changedText ) } diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveDocument.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveDocument.kt index b1412ff74..6ff235699 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveDocument.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveDocument.kt @@ -17,10 +17,12 @@ package com.itsaky.androidide.projects.models -import com.itsaky.androidide.models.Range -import com.itsaky.androidide.utils.DocumentUtils +import com.itsaky.androidide.eventbus.events.editor.ChangeType +import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent +import com.itsaky.androidide.utils.ILogger import java.io.BufferedInputStream import java.io.BufferedReader +import java.io.Closeable import java.nio.file.Path import java.time.Instant @@ -31,32 +33,41 @@ import java.time.Instant */ open class ActiveDocument( val file: Path, - val content: String, - val changeRange: Range, - var changDelta: Int, val version: Int, - val modified: Instant -) { + val modified: Instant, + content: String = "" +) : Closeable { - companion object { + private val _content = StringBuilder(content) + private val log = ILogger.newInstance("ActiveDocument") - @JvmStatic - fun create( - file: Path, - content: String, - changeRange: Range, - changDelta: Int, - version: Int, - modified: Instant - ): ActiveDocument { - if (DocumentUtils.isJavaFile(file)) { - return ActiveJavaDocument(file, content, changeRange, changDelta, version, modified) + val content: String + get() = _content.toString() + + fun patch(event: DocumentChangeEvent) { + val text = event.changedText + val start = event.changeRange.start.requireIndex() + val end = event.changeRange.end.requireIndex() + + log.debug("----------------------") + log.logThis() + log.debug(event) + log.debug("----------------------") + + when (event.changeType) { + ChangeType.DELETE -> _content.delete(start, end) + ChangeType.INSERT -> _content.insert(start, text) + else -> { + _content.clear() + _content.append(text) } - - return ActiveDocument(file, content, changeRange, changDelta, version, modified) } } + override fun close() { + _content.clear() + } + fun inputStream(): BufferedInputStream { return content.byteInputStream().buffered() } diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveJavaDocument.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveJavaDocument.kt index a36043429..02e4f4bfd 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveJavaDocument.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveJavaDocument.kt @@ -17,7 +17,6 @@ package com.itsaky.androidide.projects.models -import com.itsaky.androidide.models.Range import com.itsaky.androidide.projects.util.StringSearch import java.nio.file.Path import java.time.Instant @@ -27,14 +26,8 @@ import java.time.Instant * * @author Akash Yadav */ -class ActiveJavaDocument( - file: Path, - content: String, - changeRange: Range, - changDelta: Int, - version: Int, - modified: Instant -) : ActiveDocument(file, content, changeRange, changDelta, version, modified) { +class ActiveJavaDocument(file: Path, content: String, version: Int, modified: Instant) : + ActiveDocument(file = file, version = version, modified = modified, content = content) { val packageName: String = StringSearch.packageName(reader()) } diff --git a/subprojects/projects/src/test/java/com/itsaky/androidide/projects/models/ActiveDocumentTest.kt b/subprojects/projects/src/test/java/com/itsaky/androidide/projects/models/ActiveDocumentTest.kt new file mode 100644 index 000000000..9db8f2e27 --- /dev/null +++ b/subprojects/projects/src/test/java/com/itsaky/androidide/projects/models/ActiveDocumentTest.kt @@ -0,0 +1,129 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.projects.models + +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.eventbus.events.editor.ChangeType +import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent +import com.itsaky.androidide.models.Position +import com.itsaky.androidide.models.Range +import java.nio.file.Paths +import java.time.Instant +import org.junit.Test + +/** @author Akash Yadav */ +class ActiveDocumentTest { + + @Test + fun `test new text set patch`() { + val file = Paths.get("/") + val doc = ActiveDocument(file, 0, Instant.now(), "this is a string") + doc.patch( + DocumentChangeEvent( + file, + "this is a new text", + 2, + ChangeType.NEW_TEXT, + -1, + Range.pointRange(Position(0, doc.content.length).apply { index = column }) + ) + ) + + assertThat(doc.content).isEqualTo("this is a new text") + } + + @Test + fun `test single line insert patch`() { + val file = Paths.get("/") + val doc = ActiveDocument(file, 0, Instant.now(), "this is a string") + doc.patch( + DocumentChangeEvent( + file, + " added", + 2, + ChangeType.INSERT, + -1, + Range.pointRange(Position(0, doc.content.length).apply { index = column }) + ) + ) + + assertThat(doc.content).isEqualTo("this is a string added") + } + + @Test + fun `test single line delete patch`() { + val file = Paths.get("/") + val content = "this is a string added" + val doc = ActiveDocument(file, 0, Instant.now(), content) + doc.patch( + DocumentChangeEvent( + file, + " added", + 2, + ChangeType.DELETE, + -1, + Range( + Position(0, content.length - " added".length).apply { index = column }, + Position(0, content.length).apply { index = column } + ) + ) + ) + + assertThat(doc.content).isEqualTo("this is a string") + } + + @Test + fun `test multi line insert patch`() { + val file = Paths.get("/") + val doc = ActiveDocument(file, 0, Instant.now(), "this is an string") + doc.patch( + DocumentChangeEvent( + file, + "added\nand added\nand added ", + 2, + ChangeType.INSERT, + -1, + Range.pointRange(Position(0, doc.content.length - 6).apply { index = column }) + ) + ) + + assertThat(doc.content).isEqualTo("this is an added\nand added\nand added string") + } + + @Test + fun `test multi line delete patch`() { + val file = Paths.get("/") + val content = "this is an added\nand added\nand added string" + val doc = ActiveDocument(file, 0, Instant.now(), content) + doc.patch( + DocumentChangeEvent( + file, + " added", + 2, + ChangeType.DELETE, + -1, + Range( + Position(0, 9).apply { index = column }, + Position(0, content.length - 7).apply { index = column } + ) + ) + ) + + assertThat(doc.content).isEqualTo("this is a string") + } +}