feat: add patch ability to ActiveDocument

This commit is contained in:
Akash Yadav
2023-03-29 11:55:25 +05:30
parent 273e61daa8
commit 51155f9456
6 changed files with 306 additions and 206 deletions
@@ -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<SignatureHelp> future =
CompletableFuture.supplyAsync(
() ->
languageServer.signatureHelp(
new SignatureHelpParams(
getFile().toPath(),
getCursorLSPPosition())));
final CompletableFuture<SignatureHelp> 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<DefinitionResult> 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<DefinitionResult> 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<ReferenceResult> future,
final ReferenceResult result,
final Throwable error) {
if (result == null
|| languageClient == null
|| future.isCancelled()
|| future.isCompletedExceptionally()) {
private void onFindReferencesResult(final ProgressDialog pd,
final CompletableFuture<ReferenceResult> 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<Range> 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<Range> 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());
}
@@ -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,
@@ -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
)
}
@@ -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()
}
@@ -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())
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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")
}
}