Integrated the Projects API into LSP

This commit is contained in:
Akash Yadav
2022-07-06 17:55:58 +05:30
parent 9810e2784c
commit 26ac5a19f5
135 changed files with 2190 additions and 1810 deletions
+1
View File
@@ -13,6 +13,7 @@
<bytecodeTargetLevel target="1.8">
<module name="AndroidIDE.build-tools.builder-api" target="11" />
<module name="AndroidIDE.build-tools.builder-model" target="11" />
<module name="AndroidIDE.build-tools.eventbus-events.test" target="11" />
<module name="AndroidIDE.build-tools.eventbus-models.main" target="11" />
<module name="AndroidIDE.build-tools.eventbus-models.test" target="11" />
<module name="AndroidIDE.build-tools.project" target="11" />
@@ -83,7 +83,6 @@ import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.ActivityEditorBinding;
import com.itsaky.androidide.databinding.LayoutDiagnosticInfoBinding;
import com.itsaky.androidide.databinding.LayoutSearchProjectBinding;
import com.itsaky.androidide.eventbus.events.project.ProjectInitializedEvent;
import com.itsaky.androidide.fragments.FileTreeFragment;
import com.itsaky.androidide.fragments.LogViewFragment;
import com.itsaky.androidide.fragments.NonEditableEditorFragment;
@@ -98,17 +97,16 @@ import com.itsaky.androidide.interfaces.EditorActivityProvider;
import com.itsaky.androidide.lsp.IDELanguageClientImpl;
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry;
import com.itsaky.androidide.lsp.java.JavaLanguageServer;
import com.itsaky.androidide.lsp.java.models.JavaServerConfiguration;
import com.itsaky.androidide.lsp.java.models.JavaServerSettings;
import com.itsaky.androidide.lsp.models.DiagnosticItem;
import com.itsaky.androidide.lsp.models.InitializeParams;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.lsp.xml.XMLLanguageServer;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.managers.ToolsManager;
import com.itsaky.androidide.models.ApkMetadata;
import com.itsaky.androidide.models.DiagnosticGroup;
import com.itsaky.androidide.models.LogLine;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.models.SaveResult;
import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.projects.ProjectManager;
@@ -135,7 +133,6 @@ import com.itsaky.androidide.views.editor.IDEEditor;
import com.itsaky.inflater.values.ValuesTableFactory;
import com.itsaky.toaster.Toaster;
import org.greenrobot.eventbus.EventBus;
import org.jetbrains.annotations.Contract;
import java.io.File;
@@ -419,9 +416,9 @@ public class EditorActivity extends StudioActivity
return openFile(file, null);
}
public CodeEditorView openFile(File file, com.itsaky.androidide.lsp.models.Range selection) {
public CodeEditorView openFile(File file, com.itsaky.androidide.models.Range selection) {
if (selection == null) {
selection = com.itsaky.androidide.lsp.models.Range.NONE;
selection = com.itsaky.androidide.models.Range.NONE;
}
int index = openFileAndGetIndex(file, selection);
@@ -446,7 +443,7 @@ public class EditorActivity extends StudioActivity
}
@SuppressLint("NotifyDataSetChanged")
public int openFileAndGetIndex(File file, com.itsaky.androidide.lsp.models.Range selection) {
public int openFileAndGetIndex(File file, com.itsaky.androidide.models.Range selection) {
final var openedFileIndex = findIndexOfEditorByFile(file);
if (openedFileIndex != -1) {
@@ -872,8 +869,6 @@ public class EditorActivity extends StudioActivity
ProjectManager.INSTANCE.setupProject(mBuildService.projectProxy);
ProjectManager.INSTANCE.notifyProjectUpdate();
dispatchProjectInitialized();
ThreadUtils.runOnUiThread(
() -> {
if (mBinding == null) {
@@ -892,12 +887,6 @@ public class EditorActivity extends StudioActivity
});
}
protected void dispatchProjectInitialized() {
final var event = new ProjectInitializedEvent();
event.put(Project.class, ProjectManager.INSTANCE.getRootProject());
EventBus.getDefault().post(event);
}
private void initialSetup() {
getApp()
.getPrefManager()
@@ -1083,25 +1072,6 @@ public class EditorActivity extends StudioActivity
ILanguageServerRegistry.getDefault().register(new JavaLanguageServer());
ILanguageServerRegistry.getDefault().register(new XMLLanguageServer());
ProjectManager.INSTANCE.setProjectUpdateNotificationConsumer(
() -> {
try {
final var app = Objects.requireNonNull(ProjectManager.INSTANCE.getApp());
final var server =
ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID);
final var classPaths =
app.getClassPaths().stream()
.filter(Objects::nonNull)
.map(File::toPath)
.collect(Collectors.toSet());
final var sourceDirs =
app.getSourceDirectories().stream().map(File::toPath).collect(Collectors.toSet());
final var configuration = new JavaServerConfiguration(classPaths, sourceDirs);
server.configurationChanged(configuration);
} catch (Throwable err) {
LOG.error("Unable to notify configuration change event", err);
}
});
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_PROJECT_PATH)) {
ProjectManager.INSTANCE.setProjectPath(savedInstanceState.getString(KEY_PROJECT_PATH));
}
@@ -1186,12 +1156,14 @@ public class EditorActivity extends StudioActivity
protected void onStart() {
super.onStart();
mFileActionsHandler.register();
ProjectManager.INSTANCE.register();
}
@Override
protected void onStop() {
super.onStop();
mFileActionsHandler.unregister();
ProjectManager.INSTANCE.unregister();
}
@SuppressWarnings("deprecation")
@@ -16,17 +16,13 @@
*/
package com.itsaky.androidide.language
import com.itsaky.androidide.projects.ProjectManager.findModuleForFile
import com.itsaky.androidide.projects.api.Project
import com.itsaky.androidide.tooling.api.model.IdeGradleProject
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.api.ILanguageServer
import com.itsaky.androidide.lsp.models.CompletionItem
import com.itsaky.androidide.lsp.models.CompletionParams
import com.itsaky.androidide.lsp.models.CompletionResult
import com.itsaky.androidide.lsp.models.FailureType.COMPLETION
import com.itsaky.androidide.lsp.models.LSPFailure
import com.itsaky.androidide.lsp.models.Position
import com.itsaky.androidide.models.Position
import com.itsaky.androidide.utils.ILogger
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException
import io.github.rosemoe.sora.lang.completion.CompletionHelper
import io.github.rosemoe.sora.text.CharPosition
@@ -56,31 +52,15 @@ class CommonCompletionProvider(private val server: ILanguageServer) {
file: Path,
position: CharPosition,
prefixMatcher: Predicate<Char?>
): List<CompletionItem> {
): List<com.itsaky.androidide.lsp.models.CompletionItem> {
val completionResult =
try {
val prefix =
CompletionHelper.computePrefix(content, position) { t: Char -> prefixMatcher.test(t) }
val completer = server.completionProvider
if (completer.canComplete(file)) {
var fileModule: Project? = null
try {
fileModule = findModuleForFile(file.toFile())
} catch (e: Throwable) {
if (e !is InterruptedException) {
// This can occur if the completion was cancelled
LOG.error("Unable to find module for current file", e)
}
}
val params =
CompletionParams(Position(position.line, position.column, position.index), file)
params.content = content
params.prefix = prefix
params.module = fileModule
completer.complete(params)
} else {
CompletionResult.EMPTY
}
val prefix = CompletionHelper.computePrefix(content, position) { prefixMatcher.test(it) }
val params =
CompletionParams(Position(position.line, position.column, position.index), file)
params.content = content
params.prefix = prefix
server.complete(params)
} catch (e: Throwable) {
// Do not log if completion was interrupted or cancelled
if (!(e is InterruptedException || e is CompletionCancelledException)) {
@@ -21,8 +21,8 @@ import androidx.annotation.NonNull;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.lsp.api.ILanguageServer;
import com.itsaky.androidide.lsp.models.FormatCodeParams;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import io.github.rosemoe.sora.lang.Language;
import io.github.rosemoe.sora.text.CharPosition;
@@ -37,6 +37,7 @@ import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.interfaces.EditorActivityProvider;
import com.itsaky.androidide.models.DiagnosticGroup;
import com.itsaky.androidide.models.Location;
import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.utils.DialogUtils;
@@ -49,8 +50,7 @@ import com.itsaky.androidide.lsp.models.CodeActionItem;
import com.itsaky.androidide.lsp.models.Command;
import com.itsaky.androidide.lsp.models.DiagnosticItem;
import com.itsaky.androidide.lsp.models.DiagnosticResult;
import com.itsaky.androidide.lsp.models.Location;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.lsp.models.TextEdit;
import com.itsaky.androidide.lsp.util.DiagnosticUtil;
import com.itsaky.toaster.Toaster;
@@ -17,7 +17,7 @@
**************************************************************************************/
package com.itsaky.androidide.models;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Range;
import java.io.File;
@@ -38,7 +38,7 @@ import static com.itsaky.androidide.lsp.models.HighlightTokenKind.TYPE_PARAMETER
import com.itsaky.androidide.lsp.models.HighlightToken;
import com.itsaky.androidide.lsp.models.HighlightTokenKind;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.models.Position;
import java.util.List;
@@ -19,8 +19,8 @@
**************************************************************************************/
package com.itsaky.androidide.utils;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import java.util.Comparator;
import java.util.List;
@@ -23,8 +23,8 @@ import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import java.io.File;
import java.io.FileFilter;
@@ -47,7 +47,7 @@ import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.TypefaceUtils;
import com.itsaky.inflater.values.ValuesTableFactory;
import com.itsaky.androidide.lsp.api.ILanguageServer;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Range;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
@@ -375,7 +375,7 @@ public class CodeEditorView extends FrameLayout {
}
private void notifySaved() {
binding.editor.didSave();
binding.editor.dispatchDocumentSaveEvent();
if (Objects.requireNonNull(getFile()).getName().endsWith(".xml")) {
ValuesTableFactory.syncWithFile(getFile());
@@ -49,7 +49,7 @@ import com.itsaky.androidide.app.StudioApp
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.lsp.models.Range
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.lsp.xml.XMLLanguageServer
import io.github.rosemoe.sora.event.HandleStateChangeEvent
import io.github.rosemoe.sora.event.ScrollEvent
@@ -275,8 +275,8 @@ open class EditorActionsMenu(val editor: IDEEditor) :
editor
) // For LSP actions, as they cannot access IDEEditor class
data.put(File::class.java, editor.file)
data.put(DiagnosticItem::class.java, getDiagnosticAtCursor())
data.put(Range::class.java, editor.cursorRange)
data.put(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java, getDiagnosticAtCursor())
data.put(com.itsaky.androidide.models.Range::class.java, editor.cursorRange)
data.put(
JavaLanguageServer::class.java,
StudioApp.getInstance().javaLanguageServer as JavaLanguageServer
@@ -290,7 +290,7 @@ open class EditorActionsMenu(val editor: IDEEditor) :
protected open fun getMenu(): Menu = menu
private fun getDiagnosticAtCursor(): DiagnosticItem? {
private fun getDiagnosticAtCursor(): com.itsaky.androidide.lsp.models.DiagnosticItem? {
val start = editor.cursorRange.start
return editor.mLanguageClient?.getDiagnosticAt(editor.file, start.line, start.column)
}
@@ -29,23 +29,19 @@ import androidx.appcompat.app.AppCompatActivity;
import com.blankj.utilcode.util.ThreadUtils;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.eventbus.events.editor.ChangeType;
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent;
import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent;
import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent;
import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent;
import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent;
import com.itsaky.androidide.language.IDELanguage;
import com.itsaky.androidide.lsp.IDELanguageClientImpl;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.lsp.api.ILanguageServer;
import com.itsaky.androidide.lsp.models.ChangeType;
import com.itsaky.androidide.lsp.models.Command;
import com.itsaky.androidide.lsp.models.DefinitionResult;
import com.itsaky.androidide.lsp.models.DiagnosticItem;
import com.itsaky.androidide.lsp.models.DocumentChangeEvent;
import com.itsaky.androidide.lsp.models.DocumentCloseEvent;
import com.itsaky.androidide.lsp.models.DocumentOpenEvent;
import com.itsaky.androidide.lsp.models.DocumentSaveEvent;
import com.itsaky.androidide.lsp.models.ExpandSelectionParams;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.lsp.models.ReferenceParams;
import com.itsaky.androidide.lsp.models.ReferenceResult;
import com.itsaky.androidide.lsp.models.ShowDocumentParams;
@@ -53,8 +49,15 @@ import com.itsaky.androidide.lsp.models.SignatureHelp;
import com.itsaky.androidide.lsp.models.SignatureHelpParams;
import com.itsaky.androidide.lsp.util.DiagnosticUtil;
import com.itsaky.androidide.lsp.util.PathUtils;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.toaster.Toaster;
import org.greenrobot.eventbus.EventBus;
import java.io.File;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -146,13 +149,8 @@ public class IDEEditor extends CodeEditor {
public void setFile(File file) {
this.file = file;
if (file != null && mLanguageServer != null) {
final var documentHandler = mLanguageServer.getDocumentHandler();
if (documentHandler.accepts(file.toPath())) {
final var text = getText().toString();
final var event = new DocumentOpenEvent(file.toPath(), text, mFileVersion = 0);
documentHandler.onFileOpened(event);
}
if (file != null) {
dispatchDocumentOpenEvent();
}
if (file != null) {
@@ -188,32 +186,7 @@ public class IDEEditor extends CodeEditor {
CompletableFuture.runAsync(
() -> {
final var documentHandler = mLanguageServer.getDocumentHandler();
final var file = getFile().toPath();
if (documentHandler.accepts(file)) {
var type = ChangeType.INSERT;
if (event.getAction() == ContentChangeEvent.ACTION_DELETE) {
type = ChangeType.DELETE;
} else if (event.getAction() == ContentChangeEvent.ACTION_SET_NEW_TEXT) {
type = ChangeType.NEW_TEXT;
}
var changeDelta = type == ChangeType.NEW_TEXT ? 0 : event.getChangedText().length();
if (type == ChangeType.DELETE) {
changeDelta = -changeDelta;
}
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));
documentHandler.onContentChange(
new DocumentChangeEvent(
file, getText().toString(), mFileVersion + 1, type, changeDelta, changeRange));
}
dispatchDocumentChangeEvent(event);
checkForSignatureHelp(event);
});
}
@@ -265,6 +238,7 @@ public class IDEEditor extends CodeEditor {
return;
}
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(() -> showSignatureHelp(help));
});
}
@@ -439,7 +413,7 @@ public class IDEEditor extends CodeEditor {
final var params =
new com.itsaky.androidide.lsp.models.DefinitionParams(
getFile().toPath(),
new com.itsaky.androidide.lsp.models.Position(
new com.itsaky.androidide.models.Position(
getCursor().getLeftLine(), getCursor().getLeftColumn()));
return mLanguageServer.findDefinition(params);
@@ -463,6 +437,7 @@ public class IDEEditor extends CodeEditor {
return;
}
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(
() -> {
if (locations.size() == 1) {
@@ -493,6 +468,7 @@ public class IDEEditor extends CodeEditor {
*/
@SuppressWarnings("deprecation")
private void showDefinitionNotFound(final ProgressDialog pd) {
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(
() -> {
StudioApp.getInstance().toast(R.string.msg_no_definition, Toaster.Type.ERROR);
@@ -576,6 +552,7 @@ public class IDEEditor extends CodeEditor {
* @param dialog The dialog to dismiss.
*/
private void dismissOnUiThread(@NonNull final Dialog dialog) {
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(dialog::dismiss);
}
@@ -603,7 +580,7 @@ public class IDEEditor extends CodeEditor {
final var referenceParams =
new ReferenceParams(
getFile().toPath(),
new com.itsaky.androidide.lsp.models.Position(
new com.itsaky.androidide.models.Position(
getCursor().getLeftLine(), getCursor().getLeftColumn()),
true);
return mLanguageServer.findReferences(referenceParams);
@@ -650,6 +627,7 @@ public class IDEEditor extends CodeEditor {
*/
@SuppressWarnings("deprecation")
private void showReferencesNotFound(final ProgressDialog pd) {
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(
() -> {
StudioApp.getInstance().toast(R.string.msg_no_references, Toaster.Type.ERROR);
@@ -657,30 +635,15 @@ public class IDEEditor extends CodeEditor {
});
}
/** If any language server is set, notify the server that the file in this editor was saved. */
public void didSave() {
if (mLanguageServer != null && getFile() != null) {
final var documentHandler = mLanguageServer.getDocumentHandler();
final var file = getFile().toPath();
if (documentHandler.accepts(file)) {
documentHandler.onFileSaved(new DocumentSaveEvent(file));
}
}
}
/** Notify the language server that the file in this editor is about to be closed. */
public void close() {
if (mLanguageServer != null && getFile() != null) {
final var documentHandler = mLanguageServer.getDocumentHandler();
final var file = getFile().toPath();
if (documentHandler.accepts(file)) {
documentHandler.onFileClosed(new DocumentCloseEvent(file));
}
LOG.info("'textDocument/didClose' was sent to the language server.");
} else {
if (getFile() == null) {
LOG.info("No language server is available for this file");
return;
}
dispatchDocumentCloseEvent();
mActionsPopup.unsubscribeEvents();
ensureWindowsDismissed();
}
@@ -707,10 +670,7 @@ public class IDEEditor extends CodeEditor {
return;
}
final var path = getFile().toPath();
if (mLanguageServer != null) {
mLanguageServer.getDocumentHandler().onFileSelected(path);
}
dispatchDocumentSelectedEvent();
}
@SuppressWarnings("unused")
@@ -764,6 +724,7 @@ public class IDEEditor extends CodeEditor {
return;
}
//noinspection ConstantConditions
ThreadUtils.runOnUiThread(() -> setSelection(range));
}));
}
@@ -841,6 +802,74 @@ public class IDEEditor extends CodeEditor {
return new Range(start, end);
}
protected void dispatchDocumentOpenEvent() {
if (getFile() == null) {
return;
}
final var openEvent =
new DocumentOpenEvent(getFile().toPath(), getText().toString(), mFileVersion = 0);
EventBus.getDefault().post(openEvent);
}
protected void dispatchDocumentSelectedEvent() {
if (getFile() == null) {
return;
}
final var selectedEvent = new DocumentSelectedEvent(getFile().toPath());
EventBus.getDefault().post(selectedEvent);
}
protected void dispatchDocumentChangeEvent(final ContentChangeEvent event) {
if (getFile() == null) {
return;
}
final var file = getFile().toPath();
var type = ChangeType.INSERT;
if (event.getAction() == ContentChangeEvent.ACTION_DELETE) {
type = ChangeType.DELETE;
} else if (event.getAction() == ContentChangeEvent.ACTION_SET_NEW_TEXT) {
type = ChangeType.NEW_TEXT;
}
var changeDelta = type == ChangeType.NEW_TEXT ? 0 : event.getChangedText().length();
if (type == ChangeType.DELETE) {
changeDelta = -changeDelta;
}
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 changeEvent =
new DocumentChangeEvent(
file, getText().toString(), mFileVersion + 1, type, changeDelta, changeRange);
EventBus.getDefault().post(changeEvent);
}
protected void dispatchDocumentSaveEvent() {
if (getFile() == null) {
return;
}
final var saveEvent = new DocumentSaveEvent(getFile().toPath());
EventBus.getDefault().post(saveEvent);
}
protected void dispatchDocumentCloseEvent() {
if (getFile() == null) {
return;
}
final var closeEvent = new DocumentCloseEvent(getFile().toPath());
EventBus.getDefault().post(closeEvent);
}
@Override
public IDEEditorSearcher getSearcher() {
return mSearcher;
+27 -19
View File
@@ -14,30 +14,38 @@
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
java {
sourceCompatibility = rootProject.ext.javaSourceVersion
targetCompatibility = rootProject.ext.javaTargetVersion
android {
compileSdkVersion rootProject.ext.compileSdk
buildToolsVersion rootProject.ext.buildTools
defaultConfig {
minSdkVersion rootProject.ext.minSdk
targetSdkVersion rootProject.ext.targetSdk
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile ('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility rootProject.ext.javaSourceVersion
targetCompatibility rootProject.ext.javaTargetVersion
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation common.jkotlin
implementation project (path: ':common')
api project (path: ':build-tools:eventbus')
}
compileKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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/>.
-->
<manifest package="com.itsaky.androidide.eventbus.events"/>
@@ -15,8 +15,9 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.models
package com.itsaky.androidide.eventbus.events.editor
import com.itsaky.androidide.models.Range
import java.net.URI
import java.nio.file.Path
@@ -27,11 +28,17 @@ open class DocumentEvent(var file: Path) {
}
}
/** Dispatched when an editor is opened for the given file. */
data class DocumentOpenEvent(var openedFile: Path, var text: String, var version: Int) :
DocumentEvent(openedFile)
/** Dispatched when the given file is closed. Always dispatched after [DocumentOpenEvent]. */
data class DocumentCloseEvent(var closedFile: Path) : DocumentEvent(closedFile)
/**
* Dispatched when the content of the given opened document changes. The change can be either
* performed by the user or the IDE itself.
*/
data class DocumentChangeEvent(
var changedFile: Path,
var newText: String,
@@ -41,12 +48,13 @@ data class DocumentChangeEvent(
var changeRange: Range
) : DocumentEvent(changedFile)
/** Dispatched when the given document is saved to disk. */
data class DocumentSaveEvent(var savedFile: Path) : DocumentEvent(savedFile)
data class ShowDocumentParams(var file: Path, var selection: Range)
data class ShowDocumentResult(var success: Boolean)
/** Dispatched when the given opened document is selected and it is visible to the user. */
data class DocumentSelectedEvent(var selectedFile: Path) : DocumentEvent(selectedFile)
/** The type of change in a [DocumentChangeEvent]. */
enum class ChangeType {
INSERT,
DELETE,
+3 -1
View File
@@ -31,10 +31,12 @@ android {
dependencies {
api project (path: ':build-tools:eventbus')
api project (path: ':build-tools:eventbus-events')
api project (path: ':build-tools:tooling-api')
implementation project (path: ':build-tools:tooling-api')
implementation project (path: ':common')
implementation google.guava
testImplementation project (path: ':build-tools:tooling-api-testing')
testImplementation tests.junit
testImplementation tests.google_truth
@@ -16,29 +16,43 @@
*/
package com.itsaky.androidide.projects
import com.itsaky.androidide.eventbus.events.EventReceiver
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent
import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent
import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent
import com.itsaky.androidide.eventbus.events.project.ProjectInitializedEvent
import com.itsaky.androidide.projects.api.AndroidModule
import com.itsaky.androidide.projects.api.ModuleProject
import com.itsaky.androidide.projects.api.Project
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.projects.models.ActiveDocument
import com.itsaky.androidide.projects.util.ProjectTransformer
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.utils.ILogger
import java.io.BufferedInputStream
import java.io.BufferedReader
import java.io.File
import java.io.InputStream
import java.io.StringReader
import java.nio.file.Files
import java.nio.file.Path
import java.time.Instant
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode.BACKGROUND
/**
* Manages projects in AndroidIDE.
*
* @author Akash Yadav
*/
object ProjectManager {
object ProjectManager : EventReceiver {
private val log = ILogger.newInstance(javaClass.simpleName)
lateinit var projectPath: String
var rootProject: Project? = null
var app: AndroidModule? = null
var projectUpdateNotificationConsumer: Runnable? = null
fun setupProject(project: IProject) {
val caching = CachingProject(project)
this.rootProject = ProjectTransformer().transform(caching)
@@ -50,17 +64,6 @@ object ProjectManager {
}
}
private fun isInitialized() = rootProject != null
private fun checkInit(): Boolean {
if (isInitialized()) {
return true
}
log.warn("Project is not initialized yet!")
return false
}
fun getProjectDir(): File {
return File(getProjectDirPath())
}
@@ -118,39 +121,247 @@ object ProjectManager {
}
fun notifyProjectUpdate() {
log.debug("Notifying language servers about configuration change...")
if (app == null) {
log.warn("Cannot find application module. Skipping configuration update...")
return
}
// TODO Remove this when Events API has been implemented
if (projectUpdateNotificationConsumer != null) {
projectUpdateNotificationConsumer!!.run()
}
val event = ProjectInitializedEvent()
event.put(Project::class.java, rootProject)
EventBus.getDefault().post(event)
}
fun findModuleForFile(file: File): Project? {
fun findModuleForFile(file: File): ModuleProject? {
if (!checkInit()) {
return null
}
val path = file.canonicalPath
var longestPath = ""
var moduleWithLongestPath: Project? = null
return this.rootProject!!.findModuleForFile(file)
}
for (module in rootProject!!.subModules) {
val moduleDir = module.projectDir.canonicalPath
if (path.startsWith(moduleDir) && longestPath.length < moduleDir.length) {
longestPath = moduleDir
moduleWithLongestPath = module
fun findModuleForFile(file: Path): ModuleProject? {
return findModuleForFile(file.toFile())
}
fun isDocumentActive(file: Path): Boolean {
if (!checkInit()) {
return false
}
for (module in this.rootProject!!.subModules) {
if (module !is ModuleProject) {
continue
}
if (module.isActive(file)) {
return true
}
}
if (longestPath.isEmpty() || moduleWithLongestPath == null) {
return false
}
fun containsSourceFile(file: Path): Boolean {
if (!checkInit()) {
return false
}
for (module in this.rootProject!!.subModules) {
if (module !is ModuleProject) {
continue
}
val source = module.compileJavaSourceClasses.findSource(file)
if (source != null) {
return true
}
}
return false
}
fun getActiveDocument(file: Path): ActiveDocument? {
if (!checkInit()) {
return null
}
return moduleWithLongestPath
for (module in this.rootProject!!.subModules) {
if (module !is ModuleProject) {
continue
}
val document = module.getActiveDocument(file)
if (document != null) {
return document
}
}
return null
}
fun getLastModified(file: Path): Instant {
if (!checkInit()) {
return getLastModifiedFromDisk(file)
}
if (isDocumentActive(file)) {
val project = findModuleForFile(file) ?: return getLastModifiedFromDisk(file)
val source = project.compileJavaSourceClasses.findSource(file)
if (source != null) {
return source.modified
}
}
return getLastModifiedFromDisk(file)
}
fun getDocumentContents(file: Path): String {
if (!checkInit()) {
return getFileContents(file)
}
val document = getActiveDocument(file)
if (document != null) {
return document.content
}
return getFileContents(file)
}
fun getReader(file: Path): BufferedReader {
if (!checkInit()) {
return createFileReader(file)
}
if (isDocumentActive(file)) {
val document = getActiveDocument(file)
if (document != null) {
return BufferedReader(StringReader(document.content))
}
}
return createFileReader(file)
}
fun getInputStream(file: Path): InputStream {
if (!checkInit()) {
return createFileInputStream(file)
}
if (isDocumentActive(file)) {
val document = getActiveDocument(file)
if (document != null) {
BufferedInputStream(document.content.byteInputStream())
}
}
return createFileInputStream(file)
}
//////////////////////////////////////////////////////////////////////////////////////////
////// TODO Subscribe to file creation/deletion/rename events and update source map //////
//////////////////////////////////////////////////////////////////////////////////////////
@Subscribe(threadMode = BACKGROUND)
@Suppress("unused")
fun onDocumentOpen(event: DocumentOpenEvent) {
if (!checkInit()) {
return
}
log.debug("Dispatching document open event to submodules...")
for (subModule in this.rootProject!!.subModules) {
if (subModule !is ModuleProject) {
continue
}
if (subModule.onDocumentOpen(event)) {
log.debug("DocumentOpenEvent consumed by '${subModule.path}'")
break
}
}
}
@Subscribe(threadMode = BACKGROUND)
@Suppress("unused")
fun onDocumentClose(event: DocumentCloseEvent) {
if (!checkInit()) {
return
}
log.debug("Dispatching document close event to submodules...")
for (subModule in this.rootProject!!.subModules) {
if (subModule !is ModuleProject) {
continue
}
if (subModule.onDocumentClose(event)) {
log.debug("DocumentCloseEvent consumed by '${subModule.path}'")
break
}
}
}
@Subscribe(threadMode = BACKGROUND)
@Suppress("unused")
fun onDocumentContentChange(event: DocumentChangeEvent) {
if (!checkInit()) {
return
}
log.debug("Dispatching document change event to submodules...")
for (subModule in this.rootProject!!.subModules) {
if (subModule !is ModuleProject) {
continue
}
if (subModule.onDocumentChanged(event)) {
log.debug("DocumentChangeEvent consumed by '${subModule.path}'")
break
}
}
}
private fun isInitialized() = rootProject != null
private fun checkInit(): Boolean {
if (isInitialized()) {
return true
}
log.warn("Project is not initialized yet!")
return false
}
private fun createFileReader(file: Path): BufferedReader {
return try {
Files.newBufferedReader(file)
} catch (noFile: java.nio.file.NoSuchFileException) {
log.warn("No such file", noFile)
BufferedReader(StringReader(""))
} catch (other: Throwable) {
throw RuntimeException(other)
}
}
private fun createFileInputStream(file: Path): InputStream {
return try {
Files.newInputStream(file)
} catch (noFile: java.nio.file.NoSuchFileException) {
log.warn("No such file", noFile)
"".byteInputStream()
} catch (other: Throwable) {
throw RuntimeException(other)
}
}
private fun getLastModifiedFromDisk(file: Path): Instant {
return Files.getLastModifiedTime(file).toInstant()
}
private fun getFileContents(file: Path): String {
return try {
Files.readAllLines(file).joinToString(separator = "\n")
} catch (noFile: java.nio.file.NoSuchFileException) {
log.warn("No such file", noFile)
return ""
} catch (other: Throwable) {
throw RuntimeException(other)
}
}
}
@@ -17,12 +17,26 @@
package com.itsaky.androidide.projects.api
import com.itsaky.androidide.eventbus.events.EventReceiver
import android.text.TextUtils
import com.google.common.reflect.ClassPath
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent
import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent
import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.projects.models.ActiveDocument
import com.itsaky.androidide.projects.util.ClassTrie
import com.itsaky.androidide.projects.util.DocumentUtils
import com.itsaky.androidide.projects.util.SourceClassTrie
import com.itsaky.androidide.projects.util.SourceClassTrie.SourceNode
import com.itsaky.androidide.tooling.api.model.GradleTask
import com.itsaky.androidide.utils.ILogger
import java.io.File
import java.util.zip.*
import java.io.IOException
import java.net.MalformedURLException
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Path
import java.util.concurrent.*
/**
* A module project. Base class for [AndroidModule] and [JavaModule].
@@ -36,11 +50,10 @@ abstract class ModuleProject(
projectDir: File,
buildDir: File,
buildScript: File,
tasks: List<GradleTask>
tasks: List<GradleTask>,
) :
Project(name, description, path, projectDir, buildDir, buildScript, tasks),
com.itsaky.androidide.tooling.api.model.ModuleProject,
EventReceiver {
com.itsaky.androidide.tooling.api.model.ModuleProject {
private val log = ILogger.newInstance(javaClass.simpleName)
@@ -50,8 +63,14 @@ abstract class ModuleProject(
const val USAGE_RUNTIME = "java-runtime"
}
val compileSourceClasses = ClassTrie()
val compileClasspathClasses = ClassTrie()
@JvmField val compileJavaSourceClasses = SourceClassTrie()
@JvmField val compileClasspathClasses = ClassTrie()
/**
* Map of documents that are open in the editor. Keys here are the canonical paths of the
* documents.
*/
val activeDocuments: MutableMap<Path, ActiveDocument> = ConcurrentHashMap<Path, ActiveDocument>()
/**
* Get the source directories of this module (non-transitive i.e for this module only).
@@ -89,41 +108,172 @@ abstract class ModuleProject(
*/
abstract fun getCompileModuleProjects(): List<ModuleProject>
/** Finds the source files from source directories and indexes them. */
/**
* Called by [com.itsaky.androidide.projects.ProjectManager] when a document is opened.
*
* @param event The event descriptor.
* @return `true` if this module consumed the event. `false` otherwise.
*/
fun onDocumentOpen(event: DocumentOpenEvent): Boolean {
if (!isCacheable(event.openedFile) || !isFromThisModule(event.openedFile)) {
return false
}
log.debug("File ${event.file} opened from module:", path)
activeDocuments[event.openedFile.normalize()] = createDocument(event)
return true
}
/**
* Called by [com.itsaky.androidide.projects.ProjectManager] when a document is changed.
*
* @param event The event descriptor.
* @return `true` if this module consumed the event. `false` otherwise.
*/
fun onDocumentChanged(event: DocumentChangeEvent): Boolean {
if (!isCacheable(event.changedFile) || !isFromThisModule(event.changedFile)) {
return false
}
activeDocuments[event.changedFile.normalize()] = createDocument(event)
return true
}
/**
* Called by [com.itsaky.androidide.projects.ProjectManager] when a document is closed.
*
* @param event The event descriptor.
* @return `true` if this module consumed the event. `false` otherwise.
*/
fun onDocumentClose(event: DocumentCloseEvent): Boolean {
if (!isCacheable(event.closedFile) || !isFromThisModule(event.closedFile)) {
return false
}
activeDocuments.remove(event.closedFile.normalize())
return true
}
fun isActive(file: Path): Boolean {
return this.activeDocuments.containsKey(file.normalize())
}
fun getActiveDocument(file: Path): ActiveDocument? {
return this.activeDocuments[file.normalize()]
}
// TODO implement this as replacement of FileStore.javaSources
/** Finds the source files and classes from source directories and classpaths and indexes them. */
fun indexSourcesAndClasspaths() {
log.debug("Starting to index sources and classpaths for project:", path)
getCompileSourceDirectories().forEach {
val sourceDir = it
val sourceDirLength = sourceDir.absolutePath.length
val sourceDir = it.toPath()
it
.walk()
.filter { file -> file.isFile && file.exists() }
.forEach { file ->
// TODO Multiple classes can be defined in a single file
// Also, their package names might be different
// We need to handle these cases as well
val parentPath = file.parentFile!!.absolutePath
val path = parentPath.substring(sourceDirLength + 1) + "/" + file.nameWithoutExtension
val fullyQualifiedName = path.replace('/', '.')
this.compileSourceClasses.append(fullyQualifiedName)
}
.filter { file -> file.isFile && file.exists() && DocumentUtils.isJavaFile(file.toPath()) }
.map { file -> file.toPath() }
.forEach { file -> this.compileJavaSourceClasses.append(file, sourceDir) }
}
log.debug("Sources indexed for project:", path)
val urls =
getCompileClasspaths().filter { it.exists() }.map { toUrl(it.toPath()) }.toTypedArray()
val loader = URLClassLoader(urls, null)
val scanner: ClassPath
try {
scanner = ClassPath.from(loader)
} catch (e: IOException) {
log.warn("Unable to read classpaths for project:", path)
throw RuntimeException(e)
}
log.debug("Classpaths indexed for project:", path)
var count = 0
scanner.topLevelClasses.forEach {
this.compileClasspathClasses.append(it.name)
count++
}
getCompileClasspaths()
.filter { it.exists() }
.forEach { classpath ->
ZipFile(classpath).use { zip ->
for (entry in zip.entries()) {
if (!entry.name.endsWith(".class")) {
continue
}
val name = entry.name.substringBeforeLast('.').replace('/', '.').replace('$', '.')
if (!compileClasspathClasses.contains(name)) {
compileClasspathClasses.append(name)
} else {
log.warn("Duplicate class '$name'")
}
}
log.debug("Found $count classes in classpath of project '${this.path}'")
}
fun getSourceFilesInDir(dir: Path): List<SourceNode> =
this.compileJavaSourceClasses.getSourceFilesInDir(dir)
fun packageNameOrEmpty(file: Path?): String {
if (file == null) {
return ""
}
val source = this.compileJavaSourceClasses.findSource(file)
if (source != null) {
return source.packageName
}
return ""
}
fun suggestPackageName(file: Path): String {
var dir = file.parent.normalize()
while (dir != null) {
for (sibling in getSourceFilesInDir(dir)) {
if (DocumentUtils.isSameFile(sibling.file, file)) {
continue
}
var packageName: String = packageNameOrEmpty(sibling.file)
if (TextUtils.isEmpty(packageName.trim { it <= ' ' })) {
continue
}
val relativePath = dir.relativize(file.parent)
val relativePackage = relativePath.toString().replace(File.separatorChar, '.')
if (relativePackage.isNotEmpty()) {
packageName = "$packageName.$relativePackage"
}
return packageName
}
dir = dir.parent.normalize()
}
return ""
}
fun listClassesFromSourceDirs(packageName: String): List<SourceNode> {
return compileJavaSourceClasses
.findInPackage(packageName)
.filterIsInstance(SourceNode::class.java)
}
protected open fun isCacheable(file: Path): Boolean {
// For now, we only cache Java files
return DocumentUtils.isJavaFile(file)
}
protected open fun isFromThisModule(file: Path): Boolean {
// TODO This can be probably improved
return file.startsWith(this.projectDir.toPath())
}
protected open fun createDocument(event: DocumentOpenEvent): ActiveDocument {
return ActiveDocument(
file = event.openedFile,
content = event.text,
changeRange = Range.NONE,
version = event.version,
changDelta = 0
)
}
protected open fun createDocument(event: DocumentChangeEvent): ActiveDocument {
return ActiveDocument(
file = event.changedFile,
content = event.newText,
changeRange = event.changeRange,
version = event.version,
changDelta = event.changeDelta
)
}
private fun toUrl(path: Path): URL {
try {
return path.toUri().toURL()
} catch (e: MalformedURLException) {
throw RuntimeException(e)
}
}
}
@@ -23,6 +23,7 @@ import com.itsaky.androidide.tooling.api.IProject.Type
import com.itsaky.androidide.tooling.api.IProject.Type.Gradle
import com.itsaky.androidide.tooling.api.model.GradleTask
import java.io.File
import java.nio.file.Path
import java.util.concurrent.*
/**
@@ -118,4 +119,32 @@ open class Project(
}
return androidModules
}
fun findModuleForFile(file: Path): ModuleProject? {
return findModuleForFile(file.toFile())
}
fun findModuleForFile(file: File): ModuleProject? {
val path = file.path
var longestPath = ""
var moduleWithLongestPath: ModuleProject? = null
for (module in subModules) {
if (module !is ModuleProject) {
continue
}
val moduleDir = module.projectDir.path
if (path.startsWith(moduleDir) && longestPath.length < moduleDir.length) {
longestPath = moduleDir
moduleWithLongestPath = module
}
}
if (longestPath.isEmpty() || moduleWithLongestPath == null) {
return null
}
return moduleWithLongestPath
}
}
@@ -0,0 +1,34 @@
/*
* 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.itsaky.androidide.models.Range
import java.nio.file.Path
/**
* A document that is opened in the editor.
*
* @author Akash Yadav
*/
open class ActiveDocument(
val file: Path,
val content: String,
val changeRange: Range,
var changDelta: Int,
val version: Int
)
@@ -15,9 +15,9 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.java.utils;
package com.itsaky.androidide.projects.util;
import com.itsaky.androidide.lsp.java.FileStore;
import com.itsaky.androidide.projects.ProjectManager;
import java.nio.file.Path;
import java.time.Instant;
@@ -42,7 +42,7 @@ public class Cache<K, V> {
// If key was loaded before file was last modified, it needs to be reloaded
Value value = map.get(key);
Instant modified = FileStore.modified(file);
Instant modified = ProjectManager.INSTANCE.getLastModified(file);
// TODO remove all keys associated with file when file changes
return value.created.isBefore(modified);
}
@@ -24,9 +24,7 @@ import java.util.function.*
*
* @author Akash Yadav
*/
class ClassTrie {
val root = Node()
open class ClassTrie(val root: Node = Node()) {
/**
* Appends the class name entry to this trie.
@@ -34,15 +32,15 @@ class ClassTrie {
* @param name The fully qualified class name.
* @return The added node.
*/
fun append(name: String): Node {
open fun append(name: String): Node {
val segments = segments(name)
var node = root
for (segment in segments) {
for ((index, segment) in segments.withIndex()) {
if (segment.isEmpty() || !segment[0].isJavaIdentifierStart()) {
continue
}
node = node.createChild(segment)
node = createNode(node, segment, segments, index)
}
node.isClass = true
return node
@@ -53,7 +51,7 @@ class ClassTrie {
*
* @param name The fully qualified name of the package segment or class name to remove.
*/
fun remove(name: String) {
open fun remove(name: String) {
remove(name, null)
}
@@ -64,7 +62,7 @@ class ClassTrie {
* @param name The fully qualified name of the package segment or the class.
* @param predicate The predicate which can be used to test which entries should be deleted.
*/
fun remove(name: String, predicate: Predicate<Node>?) {
open fun remove(name: String, predicate: Predicate<Node>?) {
val segments = segments(name)
val condition = predicate ?: Predicate { it.isClass }
var node: Node? = root
@@ -92,7 +90,7 @@ class ClassTrie {
* @param name The fully qualified package or class name.
* @return Whether there is a an entry for the given fully qualified name.
*/
fun contains(name: String): Boolean {
open fun contains(name: String): Boolean {
val segments = segments(name)
var node: Node? = root
for (segment in segments) {
@@ -107,14 +105,13 @@ class ClassTrie {
}
/**
* Finds all class names in the given package.
* Finds all class nodes in the given package.
*
* @param packageName The package name to list classes from.
* @param packageName The package name to list class nodes from.
*/
fun findClassNames(packageName: String): List<String> {
open fun findInPackage(packageName: String): List<Node> {
val segments = segments(packageName)
val classes = mutableListOf<String>()
val curr = StringBuilder()
val classes = mutableListOf<Node>()
var node: Node? = root
for (segement in segments) {
if (node == null || node.children.isEmpty() || !node.children.containsKey(segement)) {
@@ -122,16 +119,7 @@ class ClassTrie {
}
if (node.isClass) {
var name = "$curr.$segement"
if (curr.isEmpty()) {
name = segement
}
classes.add(name)
} else {
if (curr.isNotEmpty()) {
curr.append(".")
}
curr.append(segement)
classes.add(node)
}
node = node.children[segement]
@@ -141,10 +129,29 @@ class ClassTrie {
return classes
}
node.children.values.forEach { addRecursively(it, curr.toString(), classes) }
node.children.values.forEach { addRecursively(it, classes) }
return classes
}
/**
* Finds all class names in the given package.
*
* @param packageName The package name to list classes from.
*/
open fun findClassNames(packageName: String): List<String> {
return findInPackage(packageName).map { it.qualifiedName }
}
/** Returns all class nodes available in this trie. */
fun allClassNodes(): Set<Node> {
return root.allClassNodes()
}
/** Returns all classes available in this trie. */
fun allClassNames(): Set<String> {
return root.allClassNames()
}
fun print() {
root.children.forEach { print(it.value, 0) }
}
@@ -161,32 +168,10 @@ class ClassTrie {
}
}
private fun addRecursively(node: Node, curr: String, classes: MutableList<String>) {
val segement = node.name
var packageName = curr
if (node.isClass) {
var name = "$packageName.$segement"
if (packageName.isEmpty()) {
name = segement
}
classes.add(name)
} else {
packageName =
if (curr.isEmpty()) {
segement
} else {
"$packageName.$segement"
}
}
protected open fun createNode(node: Node, segment: String, segments: List<String>, index: Int) =
node.createChild(segment, segments.subList(0, index + 1).joinToString(separator = "."))
if (node.children.isEmpty()) {
return
}
node.children.values.forEach { addRecursively(it, packageName, classes) }
}
private fun segments(name: String): List<String> {
protected open fun segments(name: String): List<String> {
return if (name.contains('.')) {
name.split(".")
} else {
@@ -194,16 +179,45 @@ class ClassTrie {
}
}
/** A Node can be a package segment or a class name in the package trie. */
class Node(val name: String) {
private fun addRecursively(node: Node, classes: MutableList<Node>) {
if (node.isClass) {
classes.add(node)
}
internal constructor() : this("")
if (node.children.isEmpty()) {
return
}
node.children.values.forEach { addRecursively(it, classes) }
}
/** A Node can be a package segment or a class name in the package trie. */
open class Node(val name: String, val qualifiedName: String) {
internal constructor() : this("", "")
val children: MutableMap<String, Node> = mutableMapOf()
var isClass = false
fun createChild(name: String): Node {
return children.computeIfAbsent(name) { Node(it) }
open fun createChild(name: String, qualifiedName: String): Node {
return children.computeIfAbsent(name) { Node(it, qualifiedName) }
}
open fun allClassNames(): Set<String> {
return this.allClassNodes().map { it.qualifiedName }.toSet()
}
open fun allClassNodes(): Set<Node> {
val all = mutableSetOf<Node>()
if (this.isClass) {
all.add(this)
}
if (children.isNotEmpty()) {
children.values.forEach { all.addAll(it.allClassNodes()) }
}
return all
}
}
}
@@ -0,0 +1,49 @@
/*
* 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.util;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* @author Akash Yadav
*/
public class DocumentUtils {
public static boolean isJavaFile(Path file) {
String name = file.getFileName().toString();
return name.endsWith(".java")
&& Files.exists(file)
&& !Files.isDirectory(file)
&& !name.equals("module-info.java");
}
public static boolean isXmlFile(Path file) {
String name = file.getFileName().toString();
return name.endsWith(".xml") && Files.exists(file) && !Files.isDirectory(file);
}
public static boolean isSameFile(Path first, Path second) {
try {
return Files.isSameFile(first, second);
} catch (IOException e) {
return false;
}
}
}
@@ -41,19 +41,24 @@ class ProjectTransformer {
private val log = ILogger.newInstance(javaClass.simpleName)
fun transform(project: IProject): Project? {
val path = project.projectPath.get()
val root =
Project(
name = project.name.get(),
description = project.description.get() ?: "",
path = path,
projectDir = project.projectDir.get(),
buildDir = project.buildDir.get(),
buildScript = project.buildScript.get(),
tasks = project.tasks.get() ?: listOf()
)
(root.subModules as MutableList).addAll(transform(project.listModules().get(), project))
return root
try {
val path = project.projectPath.get()
val root =
Project(
name = project.name.get(),
description = project.description.get() ?: "",
path = path,
projectDir = project.projectDir.get(),
buildDir = project.buildDir.get(),
buildScript = project.buildScript.get(),
tasks = project.tasks.get() ?: listOf()
)
(root.subModules as MutableList).addAll(transform(project.listModules().get(), project))
return root
} catch (error: Throwable) {
log.error("Unable to transform project", error)
return null
}
}
fun transform(project: com.itsaky.androidide.tooling.api.model.AndroidModule): AndroidModule {
@@ -0,0 +1,145 @@
/*
* 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.util
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Instant
import kotlin.io.path.nameWithoutExtension
/**
* A [ClassTrie] implementation to store information about java source files.
*
* @author Akash Yadav
*/
open class SourceClassTrie(root: SourcePackageNode = SourcePackageNode()) : ClassTrie(root) {
override fun append(name: String): Node {
throw UnsupportedOperationException("Method not supported!")
}
open fun append(path: Path, sourceDir: Path): SourceNode {
val modified = Files.getLastModifiedTime(path).toInstant()
val packageName = StringSearch.packageName(path)
val segments = segments(packageName)
var node: SourcePackageNode = root as SourcePackageNode
var currentPath = sourceDir
for ((index, segment) in segments.withIndex()) {
if (segment.isEmpty() || !segment[0].isJavaIdentifierStart()) {
continue
}
currentPath = currentPath.resolve(segment)
node = createNode(currentPath, node, segment, segments, index)
}
node.isClass = false
val name = path.nameWithoutExtension
val klass = SourceNode(path, packageName, modified, name)
klass.isClass = true
node.children[name] = klass
return klass
}
open fun createNode(dir: Path, node: SourcePackageNode, segment: String, segments: List<String>, index: Int) =
node.createChild(dir, segment, segments.subList(0, index + 1).joinToString(separator = "."))
override fun createNode(node: Node, segment: String, segments: List<String>, index: Int): Node {
throw UnsupportedOperationException("Method not supported")
}
open fun allSources(): List<SourceNode> {
return allClassNodes().filterIsInstance(SourceNode::class.java)
}
fun findSource(file: Path): SourceNode? {
return findSourceInNode(file, root)
}
fun findDirNode(dir: Path, node: Node): SourcePackageNode? {
var dirNode: SourcePackageNode? = null
for (child in node.children.values) {
if (
child !is SourceNode &&
child is SourcePackageNode &&
DocumentUtils.isSameFile(child.dir, dir)
) {
dirNode = child
}
dirNode = dirNode ?: findDirNode(dir, child)
if (dirNode != null) {
break
}
}
return dirNode
}
fun getSourceFilesInDir(dir: Path): List<SourceNode> {
val dirNode = findDirNode(dir, root) ?: return emptyList()
return dirNode.children.values.filterIsInstance(SourceNode::class.java)
}
private fun findSourceInNode(file: Path, node: Node): SourceNode? {
if (node is SourceNode && DocumentUtils.isSameFile(file, node.file)) {
return node
}
for (child in node.children.values) {
if (child is SourceNode && DocumentUtils.isSameFile(file, child.file)) {
return child
}
val childResult = findSourceInNode(file, child)
if (childResult != null) {
return childResult
}
}
return null
}
open class SourceNode(
val file: Path,
val packageName: String,
val modified: Instant,
name: String
) : Node(name, "$packageName.$name") {
override fun createChild(name: String, qualifiedName: String): Node {
throw UnsupportedOperationException("Method not supported!")
}
}
open class SourcePackageNode(val dir: Path, name: String, qualifiedName: String) :
Node(name, qualifiedName) {
internal constructor() : this(Paths.get(""), "", "")
override fun createChild(name: String, qualifiedName: String): Node {
throw UnsupportedOperationException("Method not suported!")
}
open fun createChild(dir: Path, name: String, qualifiedName: String): SourcePackageNode {
return this.children.computeIfAbsent(name) { SourcePackageNode(dir, name, qualifiedName) }
as SourcePackageNode
}
}
}
@@ -1,6 +1,24 @@
package com.itsaky.androidide.lsp.java.utils;
/*
* 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/>.
*/
import com.itsaky.androidide.lsp.java.FileStore;
package com.itsaky.androidide.projects.util;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.projects.models.ActiveDocument;
import java.io.BufferedReader;
import java.io.IOException;
@@ -96,7 +114,7 @@ public class StringSearch {
}
}
int next(String text) {
public int next(String text) {
return next(text.getBytes());
}
@@ -144,7 +162,7 @@ public class StringSearch {
return startsWord(text, offset) && endsWord(text, offset + pattern.length - 1);
}
int nextWord(String text) {
public int nextWord(String text) {
return nextWord(text.getBytes());
}
@@ -181,10 +199,24 @@ public class StringSearch {
private static final ByteBuffer SEARCH_BUFFER = ByteBuffer.allocateDirect(1024 * 1024);
private static String tryGetActiveDocContent(Path file) {
if (ProjectManager.INSTANCE.isDocumentActive(file)) {
final ActiveDocument document = ProjectManager.INSTANCE.getActiveDocument(file);
if (document == null) {
return null;
}
return document.getContent();
}
return null;
}
// TODO cache the progress made by searching shorter queries
public static boolean containsWordMatching(Path java, String query) {
if (FileStore.activeDocuments().contains(java)) {
String text = FileStore.contents(java);
String text = tryGetActiveDocContent(java);
if (text != null) {
return matchesTitleCase(text, query);
}
try (FileChannel channel = FileChannel.open(java)) {
@@ -206,9 +238,9 @@ public class StringSearch {
public static boolean containsWord(Path java, String query) {
StringSearch search = new StringSearch(query);
if (FileStore.activeDocuments().contains(java)) {
byte[] text = FileStore.contents(java).getBytes();
return search.nextWord(text) != -1;
String text = tryGetActiveDocContent(java);
if (text != null) {
return search.nextWord(text.getBytes()) != -1;
}
try (FileChannel channel = FileChannel.open(java)) {
// Read up to 1 MB of data from file
@@ -228,9 +260,9 @@ public class StringSearch {
private static boolean containsString(Path java, String query) {
StringSearch search = new StringSearch(query);
if (FileStore.activeDocuments().contains(java)) {
byte[] text = FileStore.contents(java).getBytes();
return search.next(text) != -1;
String text = tryGetActiveDocContent(java);
if (text != null) {
return search.next(text.getBytes()) != -1;
}
try (FileChannel channel = FileChannel.open(java)) {
// Read up to 1 MB of data from file
@@ -345,7 +377,7 @@ public class StringSearch {
public static String packageName(Path file) {
Pattern packagePattern = Pattern.compile("^package +(.*);");
Pattern startOfClass = Pattern.compile("^[\\w ]*class +\\w+");
try (BufferedReader lines = FileStore.lines(file)) {
try (BufferedReader lines = ProjectManager.INSTANCE.getReader(file)) {
for (String line = lines.readLine(); line != null; line = lines.readLine()) {
if (startOfClass.matcher(line).find()) return "";
Matcher matchPackage = packagePattern.matcher(line);
@@ -368,6 +400,20 @@ public class StringSearch {
return true;
}
/** Convert from line/column (1-based) to offset (0-based) */
public static int offset(String contents, int line, int column) {
line--;
column--;
int cursor = 0;
while (line > 0) {
if (contents.charAt(cursor) == '\n') {
line--;
}
cursor++;
}
return cursor + column;
}
private static class Slice {
private final byte[] target;
private int from, until;
@@ -19,6 +19,7 @@ package com.itsaky.androidide.projects.api
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.projects.util.SourceClassTrie.SourceNode
import com.itsaky.androidide.tooling.api.messages.InitializeProjectMessage
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher
import com.itsaky.androidide.utils.ILogger
@@ -46,6 +47,8 @@ class ModuleProjectTest {
.get()
ProjectManager.setupProject(project)
verifyProjectManagerAPIs()
val root = ProjectManager.rootProject
assertThat(root).isNotNull()
assertThat(root!!.name).isEqualTo("TestApp")
@@ -152,13 +155,38 @@ class ModuleProjectTest {
app.indexSourcesAndClasspaths()
val classNames = app.compileSourceClasses.findClassNames("com.itsaky")
assertThat(classNames).isNotNull()
assertThat(classNames).isNotEmpty()
assertThat(classNames)
val classes = app.compileJavaSourceClasses.findInPackage("com.itsaky")
assertThat(classes).isNotNull()
assertThat(classes).isNotEmpty()
assertThat(classes.map { it.qualifiedName })
.containsExactly(
"com.itsaky.androidide.tooling.test.Main",
"com.itsaky.test.app.MainActivity"
)
assertThat(classes.map { it.isClass }).containsExactly(true, true)
assertThat(classes.map { it::class.java })
.containsExactly(SourceNode::class.java, SourceNode::class.java)
}
private fun verifyProjectManagerAPIs() {
val root = ProjectManager.rootProject
assertThat(root).isNotNull()
assertThat(root!!.path).isEqualTo(":")
val source =
root.projectDir.toPath().resolve("app/src/main/java/com/itsaky/test/app/MainActivity.java")
val moduleForFile = root.findModuleForFile(source)
assertThat(moduleForFile).isNotNull()
assertThat(moduleForFile!!.path).isEqualTo(":app")
assertThat(moduleForFile.projectDir.path).isEqualTo(File(root.projectDir, "app").path)
val sourceNode = moduleForFile.compileJavaSourceClasses.findSource(source)
assertThat(sourceNode).isNotNull()
assertThat(sourceNode!!.isClass).isTrue()
assertThat(sourceNode.name).isEqualTo("MainActivity")
assertThat(sourceNode.packageName).isEqualTo("com.itsaky.test.app")
assertThat(sourceNode.qualifiedName).isEqualTo("com.itsaky.test.app.MainActivity")
assertThat(sourceNode.children).isEmpty()
assertThat(Files.isSameFile(source, sourceNode.file)).isTrue()
}
}
@@ -32,17 +32,19 @@ class ClassTrieTest {
fun testAppend() {
val trie = ClassTrie()
trie.append("java.lang.String")
trie.append("java.lang.String.Inner")
trie.append("java.lang.Object")
trie.append("java.util.List")
trie.append("java.io.File")
trie.append("java.nio.file.Path")
assertThat(trie.findClassNames("java.lang"))
.containsExactly("java.lang.String", "java.lang.Object")
.containsExactly("java.lang.String", "java.lang.String.Inner", "java.lang.Object")
assertThat(trie.findInPackage("java.lang").map { it.isClass }).containsExactly(true, true, true)
assertThat(trie.findClassNames("java.util")).containsExactly("java.util.List")
assertThat(trie.findClassNames("java.nio")).containsExactly("java.nio.file.Path")
}
@Test
fun testRemoval() {
val trie = ClassTrie()
@@ -51,19 +53,30 @@ class ClassTrieTest {
trie.append("java.util.List")
trie.append("java.io.File")
trie.append("java.nio.file.Path")
assertThat(trie.allClassNames())
.containsExactly(
"java.lang.String",
"java.lang.Object",
"java.util.List",
"java.io.File",
"java.nio.file.Path"
)
assertThat(trie.contains("java.io.File")).isTrue()
assertThat(trie.contains("java.nio.file.Path")).isTrue()
trie.remove("java.lang.String")
trie.remove("java.nio.file.Path")
assertThat(trie.allClassNames())
.containsExactly("java.lang.Object", "java.util.List", "java.io.File")
assertThat(trie.contains("java.io.File")).isTrue()
assertThat(trie.contains("java.nio.file.Path")).isFalse()
assertThat(trie.contains("java.lang.String")).isFalse()
assertThat(trie.findClassNames("java.lang"))
.containsExactly("java.lang.Object")
assertThat(trie.findClassNames("java.lang")).containsExactly("java.lang.Object")
assertThat(trie.findClassNames("java.util")).containsExactly("java.util.List")
assertThat(trie.findClassNames("java.nio")).isEmpty()
}
@@ -15,7 +15,7 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.models
package com.itsaky.androidide.models
import java.nio.file.Path
@@ -203,4 +203,3 @@ open class Range(var start: Position, var end: Position) : Comparable<Range> {
}
}
data class ExpandSelectionParams(var file: Path, var selection: Range)
+4 -1
View File
@@ -46,6 +46,7 @@ android {
dependencies {
implementation common.editor
implementation project (':build-tools:fuzzysearch')
implementation project (':build-tools:eventbus-events')
implementation androidx.appcompat
implementation androidx.ktx
@@ -53,7 +54,9 @@ dependencies {
implementation common.utilcode
implementation google.material
compileOnly project (':build-tools:projects')
api project (':build-tools:projects')
api project (':lsp:models')
compileOnly project (':xml-inflater')
compileOnly project (':actions')
compileOnly project (':common')
@@ -20,6 +20,13 @@ package com.itsaky.androidide.lsp.api;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.eventbus.events.project.ProjectInitializedEvent;
import com.itsaky.androidide.projects.api.Project;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
@@ -37,6 +44,9 @@ public class DefaultLanguageServerRegistry extends ILanguageServerRegistry {
@Override
public void register(@NonNull final ILanguageServer server) {
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this);
}
lock.writeLock().lock();
try {
final var old = mRegister.put(server.getServerId(), server);
@@ -64,6 +74,7 @@ public class DefaultLanguageServerRegistry extends ILanguageServerRegistry {
@Override
public void destroy() {
EventBus.getDefault().unregister(this);
lock.readLock().lock();
try {
for (var server : mRegister.values()) {
@@ -91,4 +102,18 @@ public class DefaultLanguageServerRegistry extends ILanguageServerRegistry {
lock.readLock().unlock();
}
}
@Subscribe(threadMode = ThreadMode.BACKGROUND)
@SuppressWarnings("unused")
public void onProjectInitialized(ProjectInitializedEvent event) {
final var project = event.get(Project.class);
if (project == null) {
return;
}
LOG.debug("Dispatching ProjectInitializedEvent to language servers...");
for (final var server : mRegister.values()) {
server.setupWithProject(project);
}
}
}
@@ -39,6 +39,7 @@ import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.models.CompletionItem;
import com.itsaky.androidide.lsp.models.CompletionParams;
import com.itsaky.androidide.lsp.models.CompletionResult;
import com.itsaky.androidide.lsp.models.CompletionsKt;
import com.itsaky.androidide.lsp.models.MatchLevel;
import java.nio.file.Files;
@@ -51,7 +52,7 @@ import java.nio.file.Path;
*/
public interface ICompletionProvider {
int MIN_MATCH_RATIO = 59;
int MIN_MATCH_RATIO = CompletionsKt.MIN_MATCH_RATIO;
default boolean canComplete(Path file) {
return file != null && Files.exists(file) && !Files.isDirectory(file);
@@ -1,85 +0,0 @@
/*
* 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.lsp.api;
import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.models.DocumentChangeEvent;
import com.itsaky.androidide.lsp.models.DocumentCloseEvent;
import com.itsaky.androidide.lsp.models.DocumentOpenEvent;
import com.itsaky.androidide.lsp.models.DocumentSaveEvent;
import java.nio.file.Path;
/**
* Handles documents of a language server implementations.
*
* @author Akash Yadav
*/
public interface IDocumentHandler {
/**
* Check if this document handler can accept the given file or not.
*
* @param file The file to check.
* @return <code>true</code> if the document handler can accept this file, <code>false</code>
* otherwise.
*/
boolean accepts(Path file);
/**
* Notification sent to this handler to notify the file open event. This event tells the document
* handler that the client is now handling the contents of this file.
*
* @param event The document open event data.
*/
void onFileOpened(DocumentOpenEvent event);
/**
* Notify the language server that file's content has been changed. Handler implementations should
* update the contents and last modified timestamp of this file.
*
* @param event The document change event data.
*/
void onContentChange(DocumentChangeEvent event);
/**
* Notify the document handler that the given file's contents were saved. Document handlers can
* optionally run a lint check and provide diagnostics for the file.
*
* @param event The document save event data.
*/
void onFileSaved(DocumentSaveEvent event);
/**
* Notify the document handler that the given file was closed and the client is no longer handling
* this file. Document handlers should remove any caches or data stored about this file and
* optionally clear the diagnostics of this file.
*
* @param event The document close event data.
*/
void onFileClosed(DocumentCloseEvent event);
/**
* Notify the document handler that the user has selected the given file for editing. Usually, the
* given file is visible to user.
*
* @param path The file that was selected.
*/
void onFileSelected(@NonNull Path path);
}
@@ -36,6 +36,8 @@ package com.itsaky.androidide.lsp.api;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.lsp.models.CompletionParams;
import com.itsaky.androidide.lsp.models.CompletionResult;
import com.itsaky.androidide.lsp.models.DefinitionParams;
import com.itsaky.androidide.lsp.models.DefinitionResult;
import com.itsaky.androidide.lsp.models.DiagnosticResult;
@@ -43,12 +45,14 @@ import com.itsaky.androidide.lsp.models.ExpandSelectionParams;
import com.itsaky.androidide.lsp.models.FormatCodeParams;
import com.itsaky.androidide.lsp.models.InitializeParams;
import com.itsaky.androidide.lsp.models.LSPFailure;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.lsp.models.ReferenceParams;
import com.itsaky.androidide.lsp.models.ReferenceResult;
import com.itsaky.androidide.lsp.models.ServerCapabilities;
import com.itsaky.androidide.lsp.models.SignatureHelp;
import com.itsaky.androidide.lsp.models.SignatureHelpParams;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.projects.api.Project;
import java.nio.file.Path;
@@ -131,12 +135,22 @@ public interface ILanguageServer {
void configurationChanged(Object newConfiguration);
/**
* Get the completion provider associated with this language server. Should never be null.
* Setup this language server with the given project. Servers are not expected to keep a reference
* to the provided project. Instead, use {@link ProjectManager#getRootProject()} to obtain the
* project instance.
*
* @param project The initialized project.
*/
void setupWithProject(@NonNull Project project);
/**
* Compute code completions for the given completion params.
*
* @param params The completion params.
* @return The completion provider.
*/
@NonNull
ICompletionProvider getCompletionProvider();
CompletionResult complete(CompletionParams params);
/**
* Find references using the given params.
@@ -195,14 +209,6 @@ public interface ILanguageServer {
return params.getContent();
}
/**
* The document handler associated with this language server instance.
*
* @return The document handler. Must not be null.
*/
@NonNull
IDocumentHandler getDocumentHandler();
/**
* Handle failure caused by LSP
*
@@ -17,11 +17,9 @@
package com.itsaky.androidide.lsp.internal.model
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.models.CompletionParams
import com.itsaky.androidide.lsp.models.CompletionResult
import com.itsaky.androidide.lsp.models.Position
import java.nio.file.Files
import com.itsaky.androidide.projects.util.DocumentUtils
import com.itsaky.androidide.utils.ILogger
import java.nio.file.Paths
/**
@@ -30,14 +28,24 @@ import java.nio.file.Paths
* @author Akash Yadav
*/
class CachedCompletion
private constructor(val params: CompletionParams, val result: CompletionResult) {
private constructor(
val params: com.itsaky.androidide.lsp.models.CompletionParams,
val result: com.itsaky.androidide.lsp.models.CompletionResult
) {
private val log = ILogger.newInstance(javaClass.simpleName)
companion object {
/** Empty cached completion. Could be used to represent "no cache available". */
@JvmField
val EMPTY = cache(CompletionParams(Position.NONE, Paths.get("")), CompletionResult.EMPTY)
val EMPTY =
cache(
com.itsaky.androidide.lsp.models.CompletionParams(
com.itsaky.androidide.models.Position.NONE,
Paths.get("")
),
com.itsaky.androidide.lsp.models.CompletionResult.EMPTY
)
/**
* Creates cached version of the result from the given params and result.
@@ -47,19 +55,21 @@ private constructor(val params: CompletionParams, val result: CompletionResult)
* @param result The result of the completion to cache.
*/
@JvmStatic
fun cache(_params: CompletionParams, result: CompletionResult): CachedCompletion {
fun cache(
_params: com.itsaky.androidide.lsp.models.CompletionParams,
result: com.itsaky.androidide.lsp.models.CompletionResult
): CachedCompletion {
val params =
CompletionParams(_params.position, _params.file).apply {
com.itsaky.androidide.lsp.models.CompletionParams(_params.position, _params.file).apply {
prefix = _params.prefix ?: ""
content = ""
module = null
}
return CachedCompletion(params, result)
}
}
fun canUseCache(params: CompletionParams): Boolean {
fun canUseCache(params: com.itsaky.androidide.lsp.models.CompletionParams): Boolean {
val partial = params.requirePrefix()
val position = this.params.position
val file = this.params.file
@@ -87,7 +97,7 @@ private constructor(val params: CompletionParams, val result: CompletionResult)
return false
}
if (!Files.isSameFile(file, params.file)) {
if (!DocumentUtils.isSameFile(file, params.file)) {
log.info("...no cache available for current file")
return false
}
@@ -20,8 +20,8 @@ package com.itsaky.androidide.lsp.util;
import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.models.DiagnosticItem;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import java.util.Collections;
import java.util.List;
@@ -23,5 +23,5 @@ import com.itsaky.androidide.lsp.models.CompletionResult
/** @author Akash Yadav */
class NoCompletionsProvider : ICompletionProvider {
override fun complete(params: CompletionParams?): CompletionResult = CompletionResult.EMPTY
override fun complete(params: com.itsaky.androidide.lsp.models.CompletionParams?): com.itsaky.androidide.lsp.models.CompletionResult = com.itsaky.androidide.lsp.models.CompletionResult.EMPTY
}
@@ -1,54 +0,0 @@
/*
* 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.lsp.util;
import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.api.IDocumentHandler;
import com.itsaky.androidide.lsp.models.DocumentChangeEvent;
import com.itsaky.androidide.lsp.models.DocumentCloseEvent;
import com.itsaky.androidide.lsp.models.DocumentOpenEvent;
import com.itsaky.androidide.lsp.models.DocumentSaveEvent;
import java.nio.file.Path;
/**
* @author Akash Yadav
*/
public class NoDocumentHandler implements IDocumentHandler {
@Override
public boolean accepts(Path file) {
return false;
}
@Override
public void onFileOpened(DocumentOpenEvent event) {}
@Override
public void onContentChange(DocumentChangeEvent event) {}
@Override
public void onFileSaved(DocumentSaveEvent event) {}
@Override
public void onFileClosed(DocumentCloseEvent event) {}
@Override
public void onFileSelected(@NonNull Path path) {}
}
@@ -17,8 +17,7 @@
package com.itsaky.androidide.lsp.util
import java.io.IOException
import java.nio.file.Files
import com.itsaky.androidide.projects.util.DocumentUtils
import java.nio.file.Path
/** @author Akash Yadav */
@@ -26,11 +25,7 @@ class PathUtils {
companion object {
@JvmStatic
fun isSameFile(first: Path, second: Path): Boolean {
return try {
Files.isSameFile(first, second)
} catch (e: IOException) {
false
}
return DocumentUtils.isSameFile(first, second)
}
}
}
@@ -1,377 +0,0 @@
package com.itsaky.androidide.lsp.java;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.utils.CharSequenceInputStream;
import com.itsaky.androidide.utils.CharSequenceReader;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.lsp.java.utils.StringSearch;
import com.itsaky.androidide.lsp.java.utils.TypeUtils;
import com.itsaky.androidide.lsp.models.DocumentChangeEvent;
import com.itsaky.androidide.lsp.models.DocumentCloseEvent;
import com.itsaky.androidide.lsp.models.DocumentOpenEvent;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.lsp.util.PathUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import javax.lang.model.element.TypeElement;
public class FileStore {
private static final Set<Path> workspaceRoots = new HashSet<>();
private static final Map<Path, VersionedContent> activeDocuments = new HashMap<>();
/** javaSources[file] is the javaSources time of a .java source file. */
// TODO organize by package name for speed of list(...)
private static final TreeMap<Path, Info> javaSources = new TreeMap<>();
private static final ILogger LOG = ILogger.newInstance("FileStore");
public static void shutdown() {
workspaceRoots.clear();
activeDocuments.clear();
javaSources.clear();
}
public static void configurationChanged(Set<Path> newWorkspaceRoots) {
javaSources.clear();
workspaceRoots.clear();
setWorkspaceRoots(newWorkspaceRoots);
}
public static void setWorkspaceRoots(Set<Path> newRoots) {
newRoots.removeIf(it -> !Files.exists(it) || !Files.isReadable(it));
for (Path root : workspaceRoots) {
if (!newRoots.contains(root)) {
workspaceRoots.removeIf(f -> f.startsWith(root));
}
}
for (Path root : newRoots) {
if (!workspaceRoots.contains(root)) {
addFiles(root);
}
}
workspaceRoots.clear();
workspaceRoots.addAll(newRoots);
}
private static void addFiles(Path root) {
if (!Files.isReadable(root) || !Files.isWritable(root)) {
return;
}
try {
Files.walkFileTree(root, new FindJavaSources());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static Collection<Path> all() {
return javaSources.keySet();
}
public static boolean contains(Path file) {
return isJavaFile(file) && javaSources.containsKey(file);
}
public static boolean isJavaFile(@NonNull Path file) {
String name = file.getFileName().toString();
// We hide module-info.java from javac, because when javac sees module-info.java
// it goes into "module mode" and starts looking for classes on the module class path.
// This becomes evident when javac starts recompiling *way too much* on each task,
// because it doesn't realize there are already up-to-date .class files.
// The better solution would be for java-language server to detect the presence of
// module-info.java,
// and go into its own "module mode" where it infers a module source path and a module class
// path.
return name.endsWith(".java") && !Files.isDirectory(file) && !name.equals("module-info.java");
}
public static Instant modified(Path file) {
// If file is open, use last in-memory modification time
if (activeDocuments.containsKey(file)) {
return Objects.requireNonNull(activeDocuments.get(file)).modified;
}
// If we've never checked before, look up modified time on disk
if (!javaSources.containsKey(file)) {
readInfoFromDisk(file);
}
// Look up modified time from cache
return Objects.requireNonNull(javaSources.get(file)).modified;
}
private static void readInfoFromDisk(Path file) {
try {
Instant time = Files.getLastModifiedTime(file).toInstant();
String packageName = StringSearch.packageName(file);
javaSources.put(file, new Info(time, packageName));
} catch (NoSuchFileException e) {
LOG.warn(e);
javaSources.remove(file);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static String suggestedPackageName(Path file) {
// Look in each parent directory of file
for (Path dir = file.getParent(); dir != null; dir = dir.getParent()) {
// Try to find a sibling with a package declaration
for (Path sibling : javaSourcesIn(dir)) {
if (PathUtils.isSameFile(sibling, file)) {
continue;
}
String packageName = packageName(sibling);
if (TextUtils.isEmpty(packageName.trim())) {
continue;
}
Path relativePath = dir.relativize(file.getParent());
String relativePackage = relativePath.toString().replace(File.separatorChar, '.');
if (!relativePackage.isEmpty()) {
packageName = packageName + "." + relativePackage;
}
return packageName;
}
}
return "";
}
private static List<Path> javaSourcesIn(Path dir) {
Map<Path, Info> tail = javaSources.tailMap(dir, false);
List<Path> list = new ArrayList<>();
for (Path file : tail.keySet()) {
if (!file.startsWith(dir)) {
break;
}
list.add(file);
}
return list;
}
public static String packageName(Path file) {
// If we've never checked before, look up package name on disk
if (!javaSources.containsKey(file)) {
readInfoFromDisk(file);
}
// Look up package name from cache
return Objects.requireNonNull(javaSources.get(file)).packageName;
}
public static void externalCreate(Path file) {
readInfoFromDisk(file);
}
public static void externalChange(Path file) {
readInfoFromDisk(file);
}
public static void externalDelete(Path file) {
javaSources.remove(file);
}
public static void open(DocumentOpenEvent params) {
Path document = params.getOpenedFile();
if (!isJavaFile(document)) {
LOG.error("Opened file is not a java file. Ignoring...");
return;
}
activeDocuments.put(
document, new VersionedContent(params.getText(), params.getVersion(), Range.NONE));
}
public static void change(DocumentChangeEvent params) {
final Path document = params.getChangedFile();
if (!isJavaFile(document)) {
LOG.error("Changed file is not a java file. Ignoring...");
return;
}
// We do not support range changes
String newText = params.getNewText();
activeDocuments.put(
document, new VersionedContent(newText, params.getVersion(), params.getChangeRange()));
}
public static void close(DocumentCloseEvent params) {
final Path document = params.getClosedFile();
if (!isJavaFile(document)) {
LOG.error("Closed file is not a java file. Ignoring...");
return;
}
activeDocuments.remove(document);
}
public static Set<Path> activeDocuments() {
return activeDocuments.keySet();
}
@Nullable
public static VersionedContent versionedContent(Path file) {
return activeDocuments.get(file);
}
public static String contents(Path file) {
if (!isJavaFile(file)) {
throw new RuntimeException(file + " is not a java file");
}
if (activeDocuments.containsKey(file)) {
return Objects.requireNonNull(activeDocuments.get(file)).content;
}
try (final BufferedReader reader = Files.newBufferedReader(file)) {
final StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
builder.append("\n");
}
return builder.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static InputStream inputStream(Path file) {
if (activeDocuments.containsKey(file)) {
CharSequence content = Objects.requireNonNull(activeDocuments.get(file)).content;
return new CharSequenceInputStream(content, "UTF-8");
}
try {
return Files.newInputStream(file);
} catch (NoSuchFileException e) {
LOG.warn(e);
byte[] bs = {};
return new ByteArrayInputStream(bs);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static BufferedReader lines(Path file) {
return bufferedReader(file);
}
public static BufferedReader bufferedReader(Path file) {
if (activeDocuments.containsKey(file)) {
CharSequence string = Objects.requireNonNull(activeDocuments.get(file)).content;
return new BufferedReader(new CharSequenceReader(string));
}
try {
return Files.newBufferedReader(file);
} catch (NoSuchFileException e) {
LOG.warn(e);
return new BufferedReader(new StringReader(""));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** Convert from line/column (1-based) to offset (0-based) */
public static int offset(String contents, int line, int column) {
line--;
column--;
int cursor = 0;
while (line > 0) {
if (contents.charAt(cursor) == '\n') {
line--;
}
cursor++;
}
return cursor + column;
}
public static Optional<Path> findDeclaringFile(@NonNull TypeElement el) {
String qualifiedName = el.getQualifiedName().toString();
String packageName = StringSearch.mostName(qualifiedName);
String className = StringSearch.lastName(qualifiedName);
// Fast path: look for text `class Foo` in file Foo.java
for (Path f : list(packageName)) {
if (f.getFileName().toString().equals(className) && TypeUtils.containsType(f, el)) {
return Optional.of(f);
}
}
// Slow path: look for text `class Foo` in any file in package
for (Path f : list(packageName)) {
if (TypeUtils.containsType(f, el)) {
return Optional.of(f);
}
}
return Optional.empty();
}
public static List<Path> list(String packageName) {
List<Path> list = new ArrayList<>();
for (Path file : javaSources.keySet()) {
if (Objects.requireNonNull(javaSources.get(file)).packageName.equals(packageName)) {
list.add(file);
}
}
return list;
}
public static class FindJavaSources extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes _attrs) {
if (isJavaFile(file)) {
readInfoFromDisk(file);
}
return FileVisitResult.CONTINUE;
}
}
private static class Info {
final Instant modified;
final String packageName;
Info(Instant modified, String packageName) {
this.modified = modified;
this.packageName = packageName;
}
}
static class VersionedContent {
final String content;
final int version;
final Range modifiedRange;
final Instant modified = Instant.now();
VersionedContent(String content, int version, final Range modifiedRange) {
this.modifiedRange = modifiedRange;
Objects.requireNonNull(content, "content is null");
this.content = content;
this.version = version;
}
}
}
@@ -0,0 +1,63 @@
/*
* 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.lsp.java;
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService;
import com.itsaky.androidide.projects.api.ModuleProject;
/**
* Provides {@link JavaCompilerService} instances for different {@link ModuleProject}s.
*
* @author Akash Yadav
*/
public class JavaCompilerProvider {
private static JavaCompilerProvider sInstance;
private volatile JavaCompilerService compilerService;
private JavaCompilerProvider() {}
public static JavaCompilerProvider getInstance() {
if (sInstance == null) {
sInstance = new JavaCompilerProvider();
}
return sInstance;
}
public static JavaCompilerService get(ModuleProject module) {
return JavaCompilerProvider.getInstance().forModule(module);
}
public synchronized JavaCompilerService forModule(ModuleProject module) {
// A module instance is set to the compiler only in case the project is initialized or
// this method was called with other mdoule instance.
if (compilerService == null || compilerService.getModule() != module) {
compilerService = new JavaCompilerService(module);
}
return compilerService;
}
public synchronized void destory() {
if (compilerService != null) {
compilerService.destroy();
}
compilerService = null;
}
}
@@ -19,18 +19,16 @@ package com.itsaky.androidide.lsp.java;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.lsp.api.ICompletionProvider;
import com.itsaky.androidide.lsp.api.IDocumentHandler;
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent;
import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent;
import com.itsaky.androidide.javac.services.CancelAbort;
import com.itsaky.androidide.lsp.api.ILanguageClient;
import com.itsaky.androidide.lsp.api.ILanguageServer;
import com.itsaky.androidide.lsp.api.IServerSettings;
import com.itsaky.androidide.lsp.internal.model.CachedCompletion;
import com.itsaky.androidide.lsp.java.actions.JavaCodeActionsMenu;
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService;
import com.itsaky.androidide.lsp.java.models.JavaServerConfiguration;
import com.itsaky.androidide.lsp.java.models.JavaServerSettings;
import com.itsaky.androidide.lsp.java.providers.CodeFormatProvider;
import com.itsaky.androidide.lsp.java.providers.CompletionProvider;
@@ -40,34 +38,38 @@ import com.itsaky.androidide.lsp.java.providers.JavaSelectionProvider;
import com.itsaky.androidide.lsp.java.providers.ReferenceProvider;
import com.itsaky.androidide.lsp.java.providers.SignatureProvider;
import com.itsaky.androidide.lsp.java.utils.AnalyzeTimer;
import com.itsaky.androidide.lsp.models.CompletionParams;
import com.itsaky.androidide.lsp.models.CompletionResult;
import com.itsaky.androidide.lsp.models.DefinitionParams;
import com.itsaky.androidide.lsp.models.DefinitionResult;
import com.itsaky.androidide.lsp.models.DiagnosticResult;
import com.itsaky.androidide.lsp.models.DocumentChangeEvent;
import com.itsaky.androidide.lsp.models.DocumentCloseEvent;
import com.itsaky.androidide.lsp.models.DocumentOpenEvent;
import com.itsaky.androidide.lsp.models.DocumentSaveEvent;
import com.itsaky.androidide.lsp.models.ExpandSelectionParams;
import com.itsaky.androidide.lsp.models.FormatCodeParams;
import com.itsaky.androidide.lsp.models.InitializeParams;
import com.itsaky.androidide.lsp.models.LSPFailure;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.lsp.models.ReferenceParams;
import com.itsaky.androidide.lsp.models.ReferenceResult;
import com.itsaky.androidide.lsp.models.ServerCapabilities;
import com.itsaky.androidide.lsp.models.SignatureHelp;
import com.itsaky.androidide.lsp.models.SignatureHelpParams;
import com.itsaky.androidide.lsp.util.LSPEditorActions;
import com.itsaky.androidide.lsp.util.NoCompletionsProvider;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.projects.api.ModuleProject;
import com.itsaky.androidide.projects.api.Project;
import com.itsaky.androidide.projects.util.DocumentUtils;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.javac.services.CancelAbort;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
public class JavaLanguageServer implements ILanguageServer {
private static final ILogger LOG = ILogger.newInstance("JavaLanguageServer");
public static final String SERVER_ID = "java";
@@ -77,17 +79,12 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
private ILanguageClient client;
private IServerSettings settings;
private Path selectedFile;
private JavaCompilerService compilerService;
private JavaServerConfiguration configuration;
private CachedCompletion cachedCompletion;
private ServerCapabilities capabilities;
private boolean initialized;
private boolean createCompiler;
public JavaLanguageServer() {
this.initialized = false;
this.createCompiler = true;
this.configuration = new JavaServerConfiguration();
this.completionProvider = new CompletionProvider();
this.diagnosticProvider = new JavaDiagnosticProvider(this.completionProvider::isCompleting);
this.cachedCompletion = CachedCompletion.EMPTY;
@@ -117,6 +114,24 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
return settings;
}
protected JavaCompilerService getCompiler(Path file) {
if (!DocumentUtils.isJavaFile(file)) {
return null;
}
final Project root = ProjectManager.INSTANCE.getRootProject();
if (root == null) {
return null;
}
final ModuleProject module = root.findModuleForFile(file);
if (module == null) {
return null;
}
return JavaCompilerProvider.get(module);
}
@Override
public String getServerId() {
return SERVER_ID;
@@ -129,8 +144,6 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
throw new AlreadyInitializedException();
}
FileStore.setWorkspaceRoots(params.getWorkspaceRoots());
capabilities = new ServerCapabilities();
capabilities.setCompletionsAvailable(true);
capabilities.setCodeActionsAvailable(true);
@@ -141,7 +154,9 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
capabilities.setSmartSelectionsEnabled(true);
LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu.class);
if (EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this);
}
initialized = true;
}
@@ -158,13 +173,8 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
@Override
public void shutdown() {
if (compilerService != null) {
compilerService.close();
compilerService = null;
createCompiler = true;
}
FileStore.shutdown();
JavaCompilerProvider.getInstance().destory();
EventBus.getDefault().unregister(this);
timer.shutdown();
initialized = false;
}
@@ -186,101 +196,79 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
}
@Override
public void configurationChanged(Object newConfiguration) {
if (!(newConfiguration instanceof JavaServerConfiguration)) {
LOG.error("Invalid configuration passed to server.", newConfiguration);
LOG.error("Configuration change event will be ignored.");
return;
}
public void configurationChanged(Object newConfiguration) {}
this.configuration = (JavaServerConfiguration) newConfiguration;
LOG.info("Java language server configuration changed.");
LOG.info(
this.configuration.getClassPaths().size(),
"class paths and",
this.configuration.getSourceDirs().size(),
"source directories were provided in the configuration");
// Compiler must be recreated on a configuration change
this.createCompiler = true;
FileStore.configurationChanged(this.configuration.getSourceDirs());
}
@Override
public void setupWithProject(@NonNull final Project project) {}
@NonNull
@Override
public ICompletionProvider getCompletionProvider() {
if (!settings.completionsEnabled()) {
return new NoCompletionsProvider();
public CompletionResult complete(final CompletionParams params) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (compiler == null
|| !settings.completionsEnabled()
|| !this.completionProvider.canComplete(params.getFile())) {
return CompletionResult.EMPTY;
}
return this.completionProvider.reset(
getCompiler(), this.settings, this.cachedCompletion, this::updateCachedCompletion);
this.completionProvider.reset(
compiler, this.settings, this.cachedCompletion, this::updateCachedCompletion);
return this.completionProvider.complete(params);
}
@NonNull
@Override
public ReferenceResult findReferences(@NonNull ReferenceParams params) {
if (!settings.referencesEnabled()) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (!settings.referencesEnabled() || compiler == null) {
return new ReferenceResult(Collections.emptyList());
}
return new ReferenceProvider(getCompiler()).findReferences(params);
return new ReferenceProvider(compiler).findReferences(params);
}
@NonNull
@Override
public DefinitionResult findDefinition(@NonNull DefinitionParams params) {
if (!settings.definitionsEnabled()) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (!settings.definitionsEnabled() || compiler == null) {
return new DefinitionResult(Collections.emptyList());
}
return new DefinitionProvider(getCompiler()).findDefinition(params);
return new DefinitionProvider(compiler).findDefinition(params);
}
@NonNull
@Override
public Range expandSelection(@NonNull ExpandSelectionParams params) {
if (!settings.smartSelectionsEnabled()) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (!settings.smartSelectionsEnabled() || compiler == null) {
return params.getSelection();
}
return new JavaSelectionProvider(getCompiler()).expandSelection(params);
return new JavaSelectionProvider(compiler).expandSelection(params);
}
@NonNull
@Override
public SignatureHelp signatureHelp(@NonNull SignatureHelpParams params) {
if (!settings.signatureHelpEnabled()) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (!settings.signatureHelpEnabled() || compiler == null) {
return new SignatureHelp(Collections.emptyList(), -1, -1);
}
return new SignatureProvider(getCompiler()).signatureHelp(params);
return new SignatureProvider(compiler).signatureHelp(params);
}
@NonNull
@Override
public DiagnosticResult analyze(@NonNull Path file) {
if (!settings.codeAnalysisEnabled()) {
final JavaCompilerService compiler = getCompiler(file);
if (!settings.codeAnalysisEnabled() || compiler == null) {
return DiagnosticResult.NO_UPDATE;
}
return this.diagnosticProvider.analyze(getCompiler(), file);
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public JavaCompilerService getCompiler() {
if (createCompiler) {
LOG.info("Creating new compiler instance...");
compilerService = createCompiler();
createCompiler = false;
}
return compilerService;
}
@NonNull
private JavaCompilerService createCompiler() {
return new JavaCompilerService(configuration.getClassPaths(), Collections.emptySet());
return this.diagnosticProvider.analyze(compiler, file);
}
@NonNull
@@ -289,55 +277,11 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
return new CodeFormatProvider(getSettings()).format(params);
}
@NonNull
@Override
public IDocumentHandler getDocumentHandler() {
return this;
}
private void updateCachedCompletion(CachedCompletion cachedCompletion) {
Objects.requireNonNull(cachedCompletion);
this.cachedCompletion = cachedCompletion;
}
@Override
public boolean accepts(Path file) {
return FileStore.isJavaFile(file);
}
@Override
public void onFileOpened(DocumentOpenEvent event) {
onFileSelected(event.getOpenedFile());
FileStore.open(event);
startOrRestartAnalyzeTimer();
}
@Override
public void onContentChange(@NonNull DocumentChangeEvent event) {
// If a file's content is changed, it is definitely visible to user.
onFileSelected(event.getChangedFile());
FileStore.change(event);
if (compilerService != null) {
compilerService.onContentChanged(event);
}
startOrRestartAnalyzeTimer();
}
@Override
public void onFileSaved(DocumentSaveEvent event) {
// TODO Run a lint check (or a simple compilation)
}
@Override
public void onFileClosed(DocumentCloseEvent event) {
FileStore.close(event);
}
@Override
public void onFileSelected(@NonNull Path path) {
this.selectedFile = path;
}
@Override
public boolean handleFailure(final LSPFailure failure) {
//noinspection SwitchStatementWithTooFewBranches
@@ -348,15 +292,25 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
return true;
}
if (compilerService != null) {
compilerService.close();
}
JavaCompilerProvider.getInstance().destory();
return true;
}
return false;
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@SuppressWarnings("unused")
public void onContentChange(@NonNull DocumentChangeEvent event) {
startOrRestartAnalyzeTimer();
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@SuppressWarnings("unused")
public void onFileSelected(@NonNull DocumentSelectedEvent event) {
this.selectedFile = event.getSelectedFile();
}
private void startOrRestartAnalyzeTimer() {
if (!this.timer.isStarted()) {
this.timer.start();
@@ -22,8 +22,13 @@ import android.graphics.drawable.Drawable
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.lsp.java.FileStore
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.rewrite.Rewrite
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.projects.util.DocumentUtils
import com.itsaky.androidide.utils.ILogger
import io.github.rosemoe.sora.widget.CodeEditor
import java.io.File
import java.nio.file.Path
@@ -41,6 +46,8 @@ abstract class BaseCodeAction : ActionItem {
protected abstract val titleTextRes: Int
private val log = ILogger.newInstance(javaClass.simpleName)
override fun prepare(data: ActionData) {
if (
@@ -55,7 +62,7 @@ abstract class BaseCodeAction : ActionItem {
}
val file = requireFile(data)
visible = FileStore.isJavaFile(file.toPath())
visible = DocumentUtils.isJavaFile(file.toPath())
enabled = visible
}
@@ -77,4 +84,15 @@ abstract class BaseCodeAction : ActionItem {
val method = klass.getDeclaredMethod("newMaterialDialogBuilder", Context::class.java)
return method.invoke(null, data.get(Context::class.java)!!) as MaterialAlertDialogBuilder
}
fun performCodeAction(data: ActionData, result: Rewrite) {
val server = ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID)!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return)
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(compiler, label))
}
}
@@ -20,12 +20,13 @@ package com.itsaky.androidide.lsp.java.actions
import android.content.Context
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationAt
import com.itsaky.androidide.lsp.models.Range
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
import com.itsaky.toaster.Toaster
import com.sun.source.tree.ClassTree
import com.sun.source.tree.Tree.Kind.VARIABLE
@@ -47,7 +48,11 @@ abstract class FieldBasedAction : BaseCodeAction() {
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, Range::class.java, CodeEditor::class.java)) {
if (
!visible ||
!hasRequiredData(data, com.itsaky.androidide.models.Range::class.java, CodeEditor::class.java) ||
ProjectManager.rootProject == null
) {
markInvisible()
return
}
@@ -57,11 +62,11 @@ abstract class FieldBasedAction : BaseCodeAction() {
}
override fun execAction(data: ActionData): Any {
val range = data[Range::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = requirePath(data)
val module = ProjectManager.findModuleForFile(file) ?: return Any()
return server.compiler.compile(file).get { task ->
return JavaCompilerProvider.get(module).compile(file).get { task ->
val triple = findFields(task, file, range)
val type = triple.second
val fields = triple.third
@@ -76,7 +81,7 @@ abstract class FieldBasedAction : BaseCodeAction() {
protected fun findFields(
task: CompileTask,
file: Path,
range: Range
range: com.itsaky.androidide.models.Range
): Triple<FindTypeDeclarationAt, ClassTree, MutableList<VariableTree>> {
// 1-based line and column index
val startLine = range.start.line + 1
@@ -18,16 +18,19 @@ package com.itsaky.androidide.lsp.java.actions.diagnostics
import com.google.common.collect.Iterables.toArray
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.javac.services.util.JavaDiagnosticUtils
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.AddImport
import com.itsaky.androidide.lsp.java.rewrite.Rewrite
import com.itsaky.androidide.javac.services.util.JavaDiagnosticUtils
import com.itsaky.androidide.lsp.models.CodeActionItem
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
import javax.tools.Diagnostic
import javax.tools.JavaFileObject
@@ -44,18 +47,26 @@ class AddImportAction() : BaseCodeAction() {
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, DiagnosticItem::class.java)) {
if (!visible || !hasRequiredData(data, com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data.get(DiagnosticItem::class.java)!!
val diagnostic = data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!
if (diagnosticCode != diagnostic.code || diagnostic.extra !is Diagnostic<*>) {
markInvisible()
return
}
val server = data.get(JavaLanguageServer::class.java)!!
val file = requireFile(data)
val module =
ProjectManager.findModuleForFile(file)
?: run {
markInvisible()
return
}
val compiler = JavaCompilerProvider.get(module)
@Suppress("UNCHECKED_CAST")
val jcDiagnostic =
@@ -67,7 +78,7 @@ class AddImportAction() : BaseCodeAction() {
var found = false
val simpleName = jcDiagnostic.args[1]
for (name in server.compiler.publicTopLevelTypes()) {
for (name in compiler.publicTopLevelTypes()) {
var klass = name
// This will be true in a test environment
@@ -90,14 +101,22 @@ class AddImportAction() : BaseCodeAction() {
@Suppress("UNCHECKED_CAST")
val diagnostic =
JavaDiagnosticUtils.asUnwrapper(
data.get(DiagnosticItem::class.java)!!.extra as Diagnostic<out JavaFileObject>
data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!.extra as Diagnostic<out JavaFileObject>
)!!
val server = data.get(JavaLanguageServer::class.java)!!
val file = requireFile(data)
val module =
ProjectManager.findModuleForFile(file)
?: run {
markInvisible()
return Any()
}
val compiler = JavaCompilerProvider.get(module)
val titles = mutableListOf<String>()
val rewrites = mutableListOf<AddImport>()
val simpleName = diagnostic.d.args[1]
for (name in server.compiler.publicTopLevelTypes()) {
for (name in compiler.publicTopLevelTypes()) {
var klass = name
if (klass.contains('/')) {
klass = klass.replace('/', '.')
@@ -125,17 +144,27 @@ class AddImportAction() : BaseCodeAction() {
return
}
val server = data.get(JavaLanguageServer::class.java)!!
val client = server.client ?: return
val file = requireFile(data)
val actions = mutableListOf<CodeActionItem>()
val module =
ProjectManager.findModuleForFile(file)
?: run {
markInvisible()
return
}
val compiler = JavaCompilerProvider.get(module)
val server =
ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID)
as JavaLanguageServer
val client = server.client ?: return
val actions = mutableListOf<com.itsaky.androidide.lsp.models.CodeActionItem>()
val titles = result.first as List<String>
val rewrites = result.second as List<Rewrite>
for (index in rewrites.indices) {
val name = titles[index]
val rewrite = rewrites[index]
val action = rewrite.asCodeActions(server.compiler, name)
val action = rewrite.asCodeActions(compiler, name)
actions.add(action)
}
@@ -17,14 +17,15 @@
package com.itsaky.androidide.lsp.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.AddException
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
/** @author Akash Yadav */
class AddThrowsAction : BaseCodeAction() {
@@ -38,12 +39,12 @@ class AddThrowsAction : BaseCodeAction() {
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, DiagnosticItem::class.java)) {
if (!visible || !hasRequiredData(data, com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data[DiagnosticItem::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
@@ -51,10 +52,11 @@ class AddThrowsAction : BaseCodeAction() {
}
override fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return Any())
val file = requirePath(data)
return server.compiler.compile(file).get { task ->
return compiler.compile(file).get { task ->
val needsThrow = CodeActionUtils.findMethod(task, diagnostic.range)
val exceptionName = CodeActionUtils.extractExceptionName(diagnostic.message)
return@get AddException(
@@ -72,10 +74,6 @@ class AddThrowsAction : BaseCodeAction() {
return
}
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(server.compiler, label))
performCodeAction(data, result)
}
}
@@ -17,14 +17,15 @@
package com.itsaky.androidide.lsp.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.CreateMissingMethod
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
/** @author Akash Yadav */
class CreateMissingMethodAction : BaseCodeAction() {
@@ -38,12 +39,12 @@ class CreateMissingMethodAction : BaseCodeAction() {
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, DiagnosticItem::class.java)) {
if (!visible || !hasRequiredData(data, com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data[DiagnosticItem::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
@@ -51,10 +52,11 @@ class CreateMissingMethodAction : BaseCodeAction() {
}
override fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return Any())
val file = requirePath(data)
return server.compiler.compile(file).get {
return compiler.compile(file).get {
CreateMissingMethod(file, findPosition(it, diagnostic.range.start))
}
}
@@ -64,11 +66,7 @@ class CreateMissingMethodAction : BaseCodeAction() {
log.warn("Unable to create missing method")
return
}
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(server.compiler, label))
performCodeAction(data, result)
}
}
@@ -17,14 +17,15 @@
package com.itsaky.androidide.lsp.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.ConvertFieldToBlock
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
/** @author Akash Yadav */
class FieldToBlockAction : BaseCodeAction() {
@@ -43,12 +44,12 @@ class FieldToBlockAction : BaseCodeAction() {
return
}
if (!hasRequiredData(data, DiagnosticItem::class.java)) {
if (!hasRequiredData(data, com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data.get(DiagnosticItem::class.java)!!
val diagnostic = data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
@@ -56,11 +57,12 @@ class FieldToBlockAction : BaseCodeAction() {
}
override fun execAction(data: ActionData): Any {
val server = data[JavaLanguageServer::class.java]!!
val diagnostic = data[DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return Any())
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val file = requirePath(data)
return server.compiler.compile(file).get {
return compiler.compile(file).get {
ConvertFieldToBlock(file, findPosition(it, diagnostic.range.start))
}
}
@@ -70,11 +72,7 @@ class FieldToBlockAction : BaseCodeAction() {
log.warn("Unable to convert field to block")
return
}
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(server.compiler, label))
performCodeAction(data, result)
}
}
@@ -17,15 +17,13 @@
package com.itsaky.androidide.lsp.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.javac.services.util.JavaDiagnosticUtils
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.ImplementAbstractMethods
import com.itsaky.androidide.javac.services.util.JavaDiagnosticUtils
import com.itsaky.androidide.lsp.models.DiagnosticItem
import java.io.File
import com.itsaky.androidide.utils.ILogger
import javax.tools.Diagnostic
import javax.tools.JavaFileObject
@@ -46,12 +44,12 @@ class ImplementAbstractMethodsAction : BaseCodeAction() {
return
}
if (!hasRequiredData(data, DiagnosticItem::class.java)) {
if (!hasRequiredData(data, com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data.get(DiagnosticItem::class.java)!!
val diagnostic = data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!
if (diagnosticCode != diagnostic.code || diagnostic.extra !is Diagnostic<*>) {
markInvisible()
return
@@ -71,7 +69,7 @@ class ImplementAbstractMethodsAction : BaseCodeAction() {
override fun execAction(data: ActionData): Any {
val diagnostic =
JavaDiagnosticUtils.asJCDiagnostic(
data.get(DiagnosticItem::class.java)!!.extra as Diagnostic<out JavaFileObject>
data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!.extra as Diagnostic<out JavaFileObject>
)
return ImplementAbstractMethods(diagnostic!!)
}
@@ -81,17 +79,7 @@ class ImplementAbstractMethodsAction : BaseCodeAction() {
log.warn("Unable to perform action. Invalid result from execAction(..)")
return
}
val server = data.get(JavaLanguageServer::class.java)!!
val client = server.client
if (client == null) {
log.error("No client set to java language server")
return
}
client.performCodeAction(
data.get(File::class.java)!!,
result.asCodeActions(server.compiler, label)
)
performCodeAction(data, result)
}
}
@@ -17,14 +17,15 @@
package com.itsaky.androidide.lsp.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.RemoveClass
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
/** @author Akash Yadav */
class RemoveClassAction : BaseCodeAction() {
@@ -38,12 +39,12 @@ class RemoveClassAction : BaseCodeAction() {
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, DiagnosticItem::class.java)) {
if (!visible || !hasRequiredData(data, com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data[DiagnosticItem::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
@@ -51,11 +52,12 @@ class RemoveClassAction : BaseCodeAction() {
}
override fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return Any())
val file = requirePath(data)
return server.compiler.compile(file).get {
return compiler.compile(file).get {
RemoveClass(file, findPosition(it, diagnostic.range.start))
}
}
@@ -65,11 +67,7 @@ class RemoveClassAction : BaseCodeAction() {
log.warn("Unable to remove class")
return
}
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(server.compiler!!, label))
performCodeAction(data, result)
}
}
@@ -17,14 +17,15 @@
package com.itsaky.androidide.lsp.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.RemoveMethod
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findMethod
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
/** @author Akash Yadav */
class RemoveMethodAction : BaseCodeAction() {
@@ -38,12 +39,12 @@ class RemoveMethodAction : BaseCodeAction() {
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, DiagnosticItem::class.java)) {
if (!visible || !hasRequiredData(data, com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data[DiagnosticItem::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
@@ -51,11 +52,12 @@ class RemoveMethodAction : BaseCodeAction() {
}
override fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return Any())
val file = requirePath(data)
return server.compiler.compile(file).get {
return compiler.compile(file).get {
val unusedMethod = findMethod(it, diagnostic.range)
RemoveMethod(
unusedMethod.className,
@@ -70,11 +72,7 @@ class RemoveMethodAction : BaseCodeAction() {
log.warn("Unable to remove method")
return
}
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(server.compiler!!, label))
performCodeAction(data, result)
}
}
@@ -17,14 +17,15 @@
package com.itsaky.androidide.lsp.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.RemoveException
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
/** @author Akash Yadav */
class RemoveUnusedThrowsAction : BaseCodeAction() {
@@ -38,12 +39,12 @@ class RemoveUnusedThrowsAction : BaseCodeAction() {
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, DiagnosticItem::class.java)) {
if (!visible || !hasRequiredData(data, com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data[DiagnosticItem::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
@@ -51,10 +52,11 @@ class RemoveUnusedThrowsAction : BaseCodeAction() {
}
override fun execAction(data: ActionData): Any {
val d = data[DiagnosticItem::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val d = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return Any())
val file = requirePath(data)
return server.compiler.compile(file).get { task ->
return compiler.compile(file).get { task ->
val notThrown = CodeActionUtils.extractNotThrownExceptionName(d.message)
val methodWithExtraThrow = CodeActionUtils.findMethod(task, d.range)
@@ -72,11 +74,7 @@ class RemoveUnusedThrowsAction : BaseCodeAction() {
log.warn("Unable to remove unused throws")
return
}
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(server.compiler, label))
performCodeAction(data, result)
}
}
@@ -17,14 +17,15 @@
package com.itsaky.androidide.lsp.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.AddSuppressWarningAnnotation
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
/** @author Akash Yadav */
class SuppressUncheckedWarningAction : BaseCodeAction() {
@@ -38,12 +39,12 @@ class SuppressUncheckedWarningAction : BaseCodeAction() {
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, DiagnosticItem::class.java)) {
if (!visible || !hasRequiredData(data, com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data[DiagnosticItem::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
@@ -51,10 +52,11 @@ class SuppressUncheckedWarningAction : BaseCodeAction() {
}
override fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return Any())
val file = requirePath(data)
return server.compiler.compile(file).get { task ->
return compiler.compile(file).get { task ->
val warnedMethod = CodeActionUtils.findMethod(task, diagnostic.range)
return@get AddSuppressWarningAnnotation(
warnedMethod.className,
@@ -70,10 +72,6 @@ class SuppressUncheckedWarningAction : BaseCodeAction() {
return
}
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(server.compiler, label))
performCodeAction(data, result)
}
}
@@ -17,15 +17,15 @@
package com.itsaky.androidide.lsp.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.ConvertVariableToStatement
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
import com.itsaky.androidide.lsp.models.DiagnosticItem
import java.io.File
import com.itsaky.androidide.projects.ProjectManager
/** @author Akash Yadav */
class VariableToStatementAction : BaseCodeAction() {
@@ -44,12 +44,12 @@ class VariableToStatementAction : BaseCodeAction() {
return
}
if (!hasRequiredData(data, DiagnosticItem::class.java)) {
if (!hasRequiredData(data, com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data.get(DiagnosticItem::class.java)!!
val diagnostic = data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
@@ -60,9 +60,9 @@ class VariableToStatementAction : BaseCodeAction() {
}
override fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val compiler = server.compiler!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return Any())
val path = requirePath(data)
return compiler.compile(path).get {
@@ -75,10 +75,7 @@ class VariableToStatementAction : BaseCodeAction() {
log.warn("Unable to convert variable to statement")
return
}
val file = data[File::class.java]
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
client.performCodeAction(file, result.asCodeActions(server.compiler, label))
performCodeAction(data, result)
}
}
@@ -24,14 +24,15 @@ import com.github.javaparser.ast.Modifier
import com.github.javaparser.ast.body.ConstructorDeclaration
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.StopWatch
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.R.string
import com.itsaky.androidide.lsp.java.actions.FieldBasedAction
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.utils.EditHelper
import com.itsaky.androidide.lsp.models.Range
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.toaster.Toaster.Type.ERROR
import com.sun.source.tree.ClassTree
import com.sun.source.tree.VariableTree
@@ -69,11 +70,12 @@ class GenerateConstructorAction : FieldBasedAction() {
}
private fun generateConstructor(data: ActionData, selected: MutableSet<String>) {
val server = data[JavaLanguageServer::class.java]!!
val range = data[Range::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return)
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = requirePath(data)
server.compiler.compile(file).run { task ->
compiler.compile(file).run { task ->
val triple = findFields(task, file, range)
val typeFinder = triple.first
val type = triple.second
@@ -17,14 +17,15 @@
package com.itsaky.androidide.lsp.java.actions.generators
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.GenerateRecordConstructor
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.ProjectManager
/** @author Akash Yadav */
class GenerateMissingConstructorAction : BaseCodeAction() {
@@ -37,12 +38,12 @@ class GenerateMissingConstructorAction : BaseCodeAction() {
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, DiagnosticItem::class.java)) {
if (!visible || !hasRequiredData(data, com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data[DiagnosticItem::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
@@ -50,10 +51,11 @@ class GenerateMissingConstructorAction : BaseCodeAction() {
}
override fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return Any())
val file = requirePath(data)
return server.compiler.compile(file).get { task ->
return compiler.compile(file).get { task ->
val needsConstructor =
CodeActionUtils.findClassNeedingConstructor(task, diagnostic.range) ?: return@get false
return@get GenerateRecordConstructor(needsConstructor)
@@ -65,11 +67,7 @@ class GenerateMissingConstructorAction : BaseCodeAction() {
log.warn("Unable to generate constructor")
return
}
val server = data[JavaLanguageServer::class.java]!!
val client = server.client!!
val file = requireFile(data)
client.performCodeAction(file, result.asCodeActions(server.compiler, label))
performCodeAction(data, result)
}
}
@@ -24,15 +24,16 @@ import com.github.javaparser.ast.expr.SimpleName
import com.github.javaparser.ast.type.VoidType
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.FieldBasedAction
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.utils.EditHelper
import com.itsaky.androidide.lsp.java.utils.JavaParserUtils
import com.itsaky.androidide.lsp.java.utils.TypeUtils.toType
import com.itsaky.androidide.lsp.models.Range
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.toaster.Toaster
import com.sun.source.tree.ClassTree
import com.sun.source.util.TreePath
@@ -79,11 +80,12 @@ class GenerateSettersAndGettersAction : FieldBasedAction() {
}
private fun generateForFields(data: ActionData, names: MutableSet<String>) {
val server = data[JavaLanguageServer::class.java]!!
val range = data[Range::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return)
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = requirePath(data)
server.compiler.compile(file).run { task ->
compiler.compile(file).run { task ->
val triple = findFields(task, file, range)
val typeFinder = triple.first
val type = triple.second
@@ -24,14 +24,15 @@ import com.github.javaparser.ast.body.MethodDeclaration
import com.github.javaparser.ast.stmt.ReturnStmt
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.R.string
import com.itsaky.androidide.lsp.java.actions.FieldBasedAction
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.utils.EditHelper
import com.itsaky.androidide.lsp.models.Range
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.toaster.Toaster
import com.itsaky.toaster.Toaster.Type.ERROR
import com.sun.source.tree.ClassTree
@@ -79,11 +80,12 @@ class GenerateToStringMethodAction : FieldBasedAction() {
}
private fun generateToString(data: ActionData, selected: MutableSet<String>) {
val server = data[JavaLanguageServer::class.java]!!
val range = data[Range::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return)
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = requirePath(data)
server.compiler.compile(file).run { task ->
compiler.compile(file).run { task ->
val triple = findFields(task, file, range)
val typeFinder = triple.first
val type = triple.second
@@ -20,8 +20,7 @@ import android.content.Context
import com.blankj.utilcode.util.ThreadUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseCodeAction
import com.itsaky.androidide.lsp.java.compiler.CompileTask
@@ -33,8 +32,10 @@ import com.itsaky.androidide.lsp.java.utils.FindHelper
import com.itsaky.androidide.lsp.java.utils.JavaParserUtils
import com.itsaky.androidide.lsp.java.utils.MethodPtr
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationAt
import com.itsaky.androidide.lsp.models.Position
import com.itsaky.androidide.lsp.models.Range
import com.itsaky.androidide.models.Position
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
import com.itsaky.toaster.Toaster
import com.sun.source.tree.MethodTree
import com.sun.source.util.Trees
@@ -64,7 +65,7 @@ class OverrideSuperclassMethodsAction : BaseCodeAction() {
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !hasRequiredData(data, Range::class.java, CodeEditor::class.java)) {
if (!visible || !hasRequiredData(data, com.itsaky.androidide.models.Range::class.java, CodeEditor::class.java)) {
markInvisible()
return
}
@@ -74,11 +75,12 @@ class OverrideSuperclassMethodsAction : BaseCodeAction() {
}
override fun execAction(data: ActionData): Any {
val range = data[Range::class.java]!!
val server = data[JavaLanguageServer::class.java]!!
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return Any())
val file = requirePath(data)
return server.compiler.compile(file).get { task ->
return compiler.compile(file).get { task ->
// 1-based line and column index
val startLine = range.start.line + 1
val startColumn = range.start.column + 1
@@ -190,10 +192,11 @@ class OverrideSuperclassMethodsAction : BaseCodeAction() {
}
private fun overrideMethods(data: ActionData, checkedMethods: MutableList<MethodPtr>) {
val server = data[JavaLanguageServer::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return)
val file = requirePath(data)
server.compiler.compile(file).run { task ->
compiler.compile(file).run { task ->
val types = task.task.types
val trees = Trees.instance(task.task)
val sb = StringBuilder()
@@ -217,7 +220,7 @@ class OverrideSuperclassMethodsAction : BaseCodeAction() {
val thisDeclaredType = thisClass.asType() as DeclaredType
val executableType = types.asMemberOf(thisDeclaredType, superMethod) as ExecutableType
val source = findSource(server.compiler, task, superMethod)
val source = findSource(compiler, task, superMethod)
val method =
if (source != null) {
JavaParserUtils.printMethod(superMethod, executableType, source)
@@ -253,9 +256,10 @@ class OverrideSuperclassMethodsAction : BaseCodeAction() {
data: ActionData,
sb: StringBuilder,
imports: MutableSet<String>,
position: Position,
position: com.itsaky.androidide.models.Position,
) {
val server = data[JavaLanguageServer::class.java]!!
val compiler =
JavaCompilerProvider.get(ProjectManager.findModuleForFile(requireFile(data)) ?: return)
val editor = data[CodeEditor::class.java]!!
val file = requirePath(data)
val text = editor.text
@@ -266,7 +270,7 @@ class OverrideSuperclassMethodsAction : BaseCodeAction() {
for (name in imports) {
val rewrite = AddImport(file, name)
val edits = rewrite.rewrite(server.compiler)[file]
val edits = rewrite.rewrite(compiler)[file]
if (edits == null || edits.isEmpty()) {
continue
}
@@ -21,14 +21,16 @@ import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import com.itsaky.androidide.javac.services.compiler.ReusableCompiler;
import com.itsaky.androidide.javac.services.partial.DiagnosticListenerImpl;
import com.itsaky.androidide.lsp.java.parser.Parser;
import com.itsaky.androidide.lsp.java.visitors.MethodRangeScanner;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.projects.api.ModuleProject;
import com.itsaky.androidide.projects.util.SourceClassTrie;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.StopWatch;
import com.itsaky.androidide.lsp.java.FileStore;
import com.itsaky.androidide.lsp.java.parser.Parser;
import com.itsaky.androidide.javac.services.partial.DiagnosticListenerImpl;
import com.itsaky.androidide.lsp.java.visitors.MethodRangeScanner;
import com.itsaky.androidide.lsp.models.Range;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.api.JavacTaskImpl;
@@ -180,7 +182,7 @@ public class CompileBatch implements AutoCloseable {
private String errorText(javax.tools.Diagnostic<? extends javax.tools.JavaFileObject> err) {
Path file = Paths.get(err.getSource().toUri());
CharSequence contents = FileStore.contents(file);
CharSequence contents = ProjectManager.INSTANCE.getDocumentContents(file);
int begin = (int) err.getStartPosition();
int end = (int) err.getEndPosition();
return substring(contents, begin, end);
@@ -210,11 +212,30 @@ public class CompileBatch implements AutoCloseable {
private String packageName(javax.tools.Diagnostic<? extends javax.tools.JavaFileObject> err) {
Path file = Paths.get(err.getSource().toUri());
return FileStore.packageName(file);
ModuleProject module = ProjectManager.INSTANCE.findModuleForFile(file);
if (module == null) {
return "";
}
SourceClassTrie.SourceNode node = module.compileJavaSourceClasses.findSource(file);
if (node == null) {
return "";
}
return node.getPackageName();
}
private Path findPackagePrivateClass(String packageName, String className) {
for (Path file : FileStore.list(packageName)) {
final List<SourceClassTrie.SourceNode> classes =
parent.module != null
? parent.module.listClassesFromSourceDirs(packageName)
: Collections.emptyList();
if (classes.isEmpty()) {
return FILE_NOT_FOUND;
}
for (SourceClassTrie.SourceNode node : classes) {
final Path file = node.getFile();
Parser parse = Parser.parseFile(file);
for (Name candidate : parse.packagePrivateClasses()) {
if (candidate.contentEquals(className)) {
@@ -34,14 +34,10 @@ import javax.tools.JavaFileObject;
public interface CompilerProvider {
Path NOT_FOUND = Paths.get("");
Set<String> imports();
List<String> publicTopLevelTypes();
List<String> packagePrivateTopLevelTypes(String packageName);
Iterable<Path> search(String query);
Optional<JavaFileObject> findAnywhere(String className);
Path findTypeDeclaration(String className);
@@ -22,22 +22,24 @@ import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import com.itsaky.androidide.javac.services.compiler.ReusableCompiler;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.StopWatch;
import com.itsaky.androidide.lsp.java.FileStore;
import com.itsaky.androidide.lsp.java.models.CompilationRequest;
import com.itsaky.androidide.lsp.java.parser.ParseTask;
import com.itsaky.androidide.lsp.java.parser.Parser;
import com.itsaky.androidide.javac.services.partial.CompilationInfo;
import com.itsaky.androidide.javac.services.partial.PartialReparser;
import com.itsaky.androidide.javac.services.partial.PartialReparserImpl;
import com.itsaky.androidide.lsp.java.utils.Cache;
import com.itsaky.androidide.lsp.java.models.CompilationRequest;
import com.itsaky.androidide.lsp.java.parser.ParseTask;
import com.itsaky.androidide.lsp.java.parser.Parser;
import com.itsaky.androidide.lsp.java.utils.Extractors;
import com.itsaky.androidide.lsp.java.utils.ScanClassPath;
import com.itsaky.androidide.lsp.java.utils.StringSearch;
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarations;
import com.itsaky.androidide.lsp.models.DocumentChangeEvent;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.projects.api.ModuleProject;
import com.itsaky.androidide.projects.models.ActiveDocument;
import com.itsaky.androidide.projects.util.Cache;
import com.itsaky.androidide.projects.util.SourceClassTrie;
import com.itsaky.androidide.projects.util.StringSearch;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.StopWatch;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.util.SourcePositions;
@@ -53,15 +55,14 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
@@ -74,33 +75,36 @@ public class JavaCompilerService implements CompilerProvider {
private static final ILogger LOG = ILogger.newInstance("JavaCompilerService");
protected final ScanClassPath classPathScanner = new ScanClassPath();
protected final Set<Path> classPath, docPath;
protected final Set<Path> classPath;
protected final Set<String> jdkClasses = classPathScanner.jdkTopLevelClasses(), classPathClasses;
protected final List<Diagnostic<? extends JavaFileObject>> diagnostics = new ArrayList<>();
protected final Map<JavaFileObject, Long> cachedModified = new HashMap<>();
protected final Cache<Void, List<String>> cacheFileImports = new Cache<>();
protected final SynchronizedTask synchronizedTask = new SynchronizedTask();
protected final SourceFileManager fileManager;
protected final ModuleProject module;
public ReusableCompiler compiler = new ReusableCompiler();
private CompileBatch cachedCompile;
private int changeDelta = 0;
public JavaCompilerService(Set<Path> classPath, Set<Path> docPath) {
this.classPath = Collections.unmodifiableSet(classPath);
this.docPath = Collections.unmodifiableSet(docPath);
// The module project must not be null
// It is marked as nullable just to work with tests
public JavaCompilerService(@Nullable ModuleProject module) {
this.module = module;
if (module == null) {
this.classPath = Collections.unmodifiableSet(Collections.emptySet());
} else {
this.classPath =
Collections.unmodifiableSet(
module.getCompileClasspaths().stream().map(File::toPath).collect(Collectors.toSet()));
}
this.classPathClasses = classPathScanner.classPathTopLevelClasses(classPath);
this.fileManager = new SourceFileManager();
this.fileManager = new SourceFileManager(module);
}
@Override
public Set<String> imports() {
HashSet<String> all = new HashSet<>();
for (Path f : FileStore.all()) {
all.addAll(readImports(f));
}
return all;
public ModuleProject getModule() {
return module;
}
private List<String> readImports(Path file) {
@@ -110,11 +114,15 @@ public class JavaCompilerService implements CompilerProvider {
return cacheFileImports.get(file, null);
}
private String packageNameOrEmpty(Path file) {
return module != null ? module.packageNameOrEmpty(file) : "";
}
private void loadImports(Path file) {
List<String> list = new ArrayList<>();
Pattern importClass = Pattern.compile("^import +([\\w\\.]+\\.\\w+);");
Pattern importStar = Pattern.compile("^import +([\\w\\.]+\\.\\*);");
try (BufferedReader lines = FileStore.lines(file)) {
Pattern importClass = Pattern.compile("^import +([\\w.]+\\.\\w+);");
Pattern importStar = Pattern.compile("^import +([\\w.]+\\.\\*);");
try (BufferedReader lines = ProjectManager.INSTANCE.getReader(file)) {
for (String line = lines.readLine(); line != null; line = lines.readLine()) {
// If we reach a class declaration, stop looking for imports
// TODO This could be a little more specific
@@ -141,17 +149,10 @@ public class JavaCompilerService implements CompilerProvider {
@Override
public List<String> publicTopLevelTypes() {
List<String> all = new ArrayList<>();
for (Path file : FileStore.all()) {
String fileName = file.getFileName().toString();
if (!fileName.endsWith(".java")) {
continue;
}
String className = fileName.substring(0, fileName.length() - ".java".length());
String packageName = FileStore.packageName(file);
if (!packageName.isEmpty()) {
className = packageName + "." + className;
}
all.add(className);
List<SourceClassTrie.SourceNode> sourceClasses =
module != null ? module.compileJavaSourceClasses.allSources() : Collections.emptyList();
for (SourceClassTrie.SourceNode node : sourceClasses) {
all.add(node.getQualifiedName());
}
all.addAll(classPathClasses);
all.addAll(jdkClasses);
@@ -163,12 +164,6 @@ public class JavaCompilerService implements CompilerProvider {
return Collections.emptyList();
}
@Override
public Iterable<Path> search(String query) {
Predicate<Path> test = f -> StringSearch.containsWordMatching(f, query);
return () -> FileStore.all().stream().filter(test).iterator();
}
@Override
public Optional<JavaFileObject> findAnywhere(String className) {
Path fromSource = findTypeDeclaration(className);
@@ -189,9 +184,12 @@ public class JavaCompilerService implements CompilerProvider {
// optimization.
String packageName = Extractors.packageName(className);
String simpleName = Extractors.simpleName(className);
for (Path f : FileStore.list(packageName)) {
if (containsWord(f, simpleName) && containsType(f, className)) {
return f;
List<SourceClassTrie.SourceNode> classes =
module != null ? module.listClassesFromSourceDirs(packageName) : Collections.emptyList();
for (SourceClassTrie.SourceNode node : classes) {
final Path path = node.getFile();
if (containsWord(path, simpleName) && containsType(path, className)) {
return path;
}
}
return NOT_FOUND;
@@ -202,11 +200,14 @@ public class JavaCompilerService implements CompilerProvider {
String packageName = Extractors.packageName(className);
String simpleName = Extractors.simpleName(className);
List<Path> candidates = new ArrayList<>();
for (Path f : FileStore.all()) {
if (containsWord(f, packageName)
&& containsImport(f, className)
&& containsWord(f, simpleName)) {
candidates.add(f);
List<SourceClassTrie.SourceNode> sourceNodes =
module != null ? module.compileJavaSourceClasses.allSources() : Collections.emptyList();
for (SourceClassTrie.SourceNode node : sourceNodes) {
final Path path = node.getFile();
if (containsWord(path, packageName)
&& containsImport(path, className)
&& containsWord(path, simpleName)) {
candidates.add(path);
}
}
@@ -222,7 +223,7 @@ public class JavaCompilerService implements CompilerProvider {
private boolean containsImport(Path file, String className) {
String packageName = Extractors.packageName(className);
if (FileStore.packageName(file).equals(packageName)) {
if (packageNameOrEmpty(file).equals(packageName)) {
return true;
}
String star = packageName + ".*";
@@ -237,9 +238,12 @@ public class JavaCompilerService implements CompilerProvider {
@Override
public Path[] findMemberReferences(String className, String memberName) {
List<Path> candidates = new ArrayList<>();
for (Path f : FileStore.all()) {
if (containsWord(f, memberName)) {
candidates.add(f);
List<SourceClassTrie.SourceNode> sourceNodes =
module != null ? module.compileJavaSourceClasses.allSources() : Collections.emptyList();
for (SourceClassTrie.SourceNode node : sourceNodes) {
final Path path = node.getFile();
if (containsWord(path, memberName)) {
candidates.add(path);
}
}
return candidates.toArray(new Path[0]);
@@ -285,7 +289,8 @@ public class JavaCompilerService implements CompilerProvider {
return true;
}
if (f.getLastModified() != cachedModified.get(f)) {
final Long modified = cachedModified.get(f);
if (modified != null && f.getLastModified() != modified) {
return true;
}
}
@@ -338,6 +343,13 @@ public class JavaCompilerService implements CompilerProvider {
final MethodTree methodTree = (MethodTree) currentMethod.second.getLeaf();
LOG.debug("Trying to reparse method:", methodTree.getName());
final int changeDelta;
final ActiveDocument document = ProjectManager.INSTANCE.getActiveDocument(file.toPath());
if (document != null) {
changeDelta = document.getChangDelta();
} else {
changeDelta = 0;
}
final CompilationInfo info =
new CompilationInfo(
cachedCompile.task, cachedCompile.diagnosticListener, cachedCompile.roots.get(0));
@@ -345,7 +357,7 @@ public class JavaCompilerService implements CompilerProvider {
final SourcePositions sourcePositions = Trees.instance(cachedCompile.task).getSourcePositions();
final int start = (int) sourcePositions.getStartPosition(info.cu, methodTree.getBody());
final int end =
(int) sourcePositions.getEndPosition(info.cu, methodTree.getBody()) + this.changeDelta;
(int) sourcePositions.getEndPosition(info.cu, methodTree.getBody()) + changeDelta;
watch.lapFromLast("Found start and end positions of current method");
final PartialReparser reparser = new PartialReparserImpl();
@@ -354,7 +366,7 @@ public class JavaCompilerService implements CompilerProvider {
"Cannot reparse. Invalid change delta. end:",
end,
"changeDelta:",
this.changeDelta,
changeDelta,
"content.length:",
request.partialRequest.contents.length());
recompile(request);
@@ -375,7 +387,10 @@ public class JavaCompilerService implements CompilerProvider {
LOG.info("Successfully reparsed method", methodTree.getName());
updateModificationCache(request);
cachedCompile.updatePositions(info.cu, true);
this.changeDelta = 0;
if (document != null) {
document.setChangDelta(0);
}
}
@Nullable
@@ -405,7 +420,6 @@ public class JavaCompilerService implements CompilerProvider {
close();
cachedCompile = performCompilation(request.sources);
updateModificationCache(request);
this.changeDelta = 0;
}
private void updateModificationCache(final CompilationRequest request) {
@@ -440,10 +454,6 @@ public class JavaCompilerService implements CompilerProvider {
return new CompileBatch(this, moreSources);
}
public void onContentChanged(@NonNull DocumentChangeEvent event) {
this.changeDelta += event.getChangeDelta();
}
public synchronized void close() {
if (cachedCompile != null && !cachedCompile.closed) {
cachedCompile.close();
@@ -17,8 +17,9 @@
package com.itsaky.androidide.lsp.java.compiler;
import com.itsaky.androidide.lsp.java.FileStore;
import com.itsaky.androidide.lsp.java.utils.StringSearch;
import com.itsaky.androidide.projects.api.ModuleProject;
import com.itsaky.androidide.projects.util.SourceClassTrie;
import com.itsaky.androidide.projects.util.StringSearch;
import com.sun.tools.javac.api.JavacTool;
import java.io.File;
@@ -26,6 +27,8 @@ import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Stream;
@@ -42,9 +45,19 @@ import javax.tools.StandardLocation;
public class SourceFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
private static final Logger LOG = Logger.getLogger("main");
private ModuleProject module;
public SourceFileManager() {
this(null);
}
public SourceFileManager(final ModuleProject module) {
super(createDelegateFileManager());
this.module = module;
}
public void setModule(final ModuleProject module) {
this.module = module;
}
private static StandardJavaFileManager createDelegateFileManager() {
@@ -57,29 +70,36 @@ public class SourceFileManager extends ForwardingJavaFileManager<StandardJavaFil
LOG.warning(error.getMessage(null));
}
private String packageNameOrEmpty(Path file) {
return this.module != null ? module.packageNameOrEmpty(file) : "";
}
@Override
public Iterable<JavaFileObject> list(
Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse)
throws IOException {
if (location == StandardLocation.SOURCE_PATH) {
if (this.module == null) {
return Collections.emptyList();
}
Stream<JavaFileObject> stream =
FileStore.list(packageName).stream().map(this::asJavaFileObject);
module.listClassesFromSourceDirs(packageName).stream().map(this::asJavaFileObject);
return stream::iterator;
} else {
return super.list(location, packageName, kinds, recurse);
}
}
private JavaFileObject asJavaFileObject(Path file) {
private JavaFileObject asJavaFileObject(SourceClassTrie.SourceNode node) {
// TODO erase method bodies of files that are not open
return new SourceFileObject(file);
return new SourceFileObject(node.getFile());
}
@Override
public String inferBinaryName(Location location, JavaFileObject file) {
if (location == StandardLocation.SOURCE_PATH) {
SourceFileObject source = (SourceFileObject) file;
String packageName = FileStore.packageName(source.path);
String packageName = packageNameOrEmpty(source.path);
String className = removeExtension(source.path.getFileName().toString());
if (!packageName.isEmpty()) className = packageName + "." + className;
return className;
@@ -105,9 +125,12 @@ public class SourceFileManager extends ForwardingJavaFileManager<StandardJavaFil
if (location == StandardLocation.SOURCE_PATH) {
String packageName = StringSearch.mostName(className);
String simpleClassName = StringSearch.lastName(className);
for (Path f : FileStore.list(packageName)) {
if (f.getFileName().toString().equals(simpleClassName + kind.extension)) {
return new SourceFileObject(f);
List<SourceClassTrie.SourceNode> classes =
module != null ? module.listClassesFromSourceDirs(packageName) : Collections.emptyList();
for (SourceClassTrie.SourceNode node : classes) {
final Path path = node.getFile();
if (path.getFileName().toString().equals(simpleClassName + kind.extension)) {
return new SourceFileObject(path);
}
}
// Fall through to disk in case we have .jar or .zip files on the source path
@@ -128,7 +151,7 @@ public class SourceFileManager extends ForwardingJavaFileManager<StandardJavaFil
public boolean contains(Location location, FileObject file) throws IOException {
if (location == StandardLocation.SOURCE_PATH) {
SourceFileObject source = (SourceFileObject) file;
return FileStore.contains(source.path);
return module.compileJavaSourceClasses.findSource(source.path) != null;
} else {
return super.contains(location, file);
}
@@ -17,8 +17,9 @@
package com.itsaky.androidide.lsp.java.compiler;
import com.itsaky.androidide.lsp.java.FileStore;
import com.itsaky.androidide.lsp.util.PathUtils;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.projects.util.DocumentUtils;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
@@ -47,7 +48,7 @@ public class SourceFileObject implements JavaFileObject {
}
public SourceFileObject(Path path, String contents, Instant modified) {
if (!FileStore.isJavaFile(path)) throw new RuntimeException(path + " is not a java source");
if (!DocumentUtils.isJavaFile(path)) throw new RuntimeException(path + " is not a java source");
this.path = path;
this.contents = contents;
this.modified = modified;
@@ -116,7 +117,7 @@ public class SourceFileObject implements JavaFileObject {
byte[] bytes = contents.getBytes();
return new ByteArrayInputStream(bytes);
}
return FileStore.inputStream(path);
return ProjectManager.INSTANCE.getInputStream(path);
}
@Override
@@ -129,7 +130,7 @@ public class SourceFileObject implements JavaFileObject {
if (contents != null) {
return new StringReader(contents);
}
return FileStore.bufferedReader(path);
return ProjectManager.INSTANCE.getReader(path);
}
@Override
@@ -137,7 +138,7 @@ public class SourceFileObject implements JavaFileObject {
if (contents != null) {
return contents;
}
return FileStore.contents(path);
return ProjectManager.INSTANCE.getDocumentContents(this.path);
}
@Override
@@ -150,7 +151,7 @@ public class SourceFileObject implements JavaFileObject {
if (contents != null) {
return modified.toEpochMilli();
}
return FileStore.modified(path).toEpochMilli();
return ProjectManager.INSTANCE.getLastModified(this.path).toEpochMilli();
}
@Override
@@ -36,10 +36,10 @@ class ClassImportEditHandler(val imports: Set<String>, file: Path) : IJavaEditHa
override fun performEdits(
compiler: JavaCompilerService,
editor: CodeEditor,
completionItem: CompletionItem
completionItem: com.itsaky.androidide.lsp.models.CompletionItem
) {
val className = completionItem.data!!.className
val edits = EditHelper.addImportIfNeeded(compiler, file, imports, className)
RewriteHelper.performEdits(edits, editor)
com.itsaky.androidide.lsp.util.RewriteHelper.performEdits(edits, editor)
}
}
@@ -17,11 +17,11 @@
package com.itsaky.androidide.lsp.java.edits
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
import com.itsaky.androidide.lsp.edits.IEditHandler
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
import com.itsaky.androidide.lsp.models.CompletionItem
import com.itsaky.androidide.projects.ProjectManager
import io.github.rosemoe.sora.widget.CodeEditor
import java.nio.file.Path
@@ -32,11 +32,10 @@ import java.nio.file.Path
*/
abstract class IJavaEditHandler(protected val file: Path) : IEditHandler {
override fun performEdits(editor: CodeEditor, completionItem: CompletionItem) {
val languageServer =
ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID)
if (languageServer != null && languageServer is JavaLanguageServer) {
performEdits(languageServer.compiler, editor, completionItem)
override fun performEdits(editor: CodeEditor, completionItem: com.itsaky.androidide.lsp.models.CompletionItem) {
val compiler = JavaCompilerProvider.get(ProjectManager.findModuleForFile(file) ?: return)
if (compiler != null) {
performEdits(compiler, editor, completionItem)
}
}
@@ -51,6 +50,6 @@ abstract class IJavaEditHandler(protected val file: Path) : IEditHandler {
abstract fun performEdits(
compiler: JavaCompilerService,
editor: CodeEditor,
completionItem: CompletionItem
completionItem: com.itsaky.androidide.lsp.models.CompletionItem
)
}
@@ -44,9 +44,9 @@ class MultipleClassImportEditHandler(
override fun performEdits(
compiler: JavaCompilerService,
editor: CodeEditor,
completionItem: CompletionItem
completionItem: com.itsaky.androidide.lsp.models.CompletionItem
) {
val edits = mutableListOf<TextEdit>()
val edits = mutableListOf<com.itsaky.androidide.lsp.models.TextEdit>()
for (className in classes) {
try {
edits.addAll(EditHelper.addImportIfNeeded(compiler, file, imported, className))
@@ -54,6 +54,6 @@ class MultipleClassImportEditHandler(
log.error("Unable to compute edits to perform import for class:", className)
}
}
RewriteHelper.performEdits(edits, editor)
com.itsaky.androidide.lsp.util.RewriteHelper.performEdits(edits, editor)
}
}
@@ -17,52 +17,36 @@
package com.itsaky.androidide.lsp.java.parser;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.lsp.java.FileStore;
import com.itsaky.androidide.lsp.java.compiler.SourceFileManager;
import com.itsaky.androidide.lsp.java.compiler.SourceFileObject;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.lsp.util.StringUtils;
import com.sun.source.tree.BlockTree;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.utils.ILogger;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ErroneousTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.LineMap;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.SwitchTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreeScanner;
import com.sun.source.util.Trees;
import com.sun.tools.javac.api.JavacTool;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
@@ -97,6 +81,7 @@ public class Parser {
/** Create a task that compiles a single file */
private static JavacTask singleFileTask(JavaFileObject file) {
FILE_MANAGER.setModule(ProjectManager.INSTANCE.findModuleForFile(Paths.get(file.toUri())));
return (JavacTask)
COMPILER.getTask(
null,
@@ -250,60 +235,6 @@ public class Parser {
return "";
}
static String describeTree(Tree leaf) {
if (leaf instanceof MethodTree) {
MethodTree method = (MethodTree) leaf;
StringJoiner params = new StringJoiner(", ");
for (VariableTree p : method.getParameters()) {
params.add(p.getType() + " " + p.getName());
}
return method.getName() + "(" + params + ")";
}
if (leaf instanceof ClassTree) {
ClassTree cls = (ClassTree) leaf;
return "class " + cls.getSimpleName();
}
if (leaf instanceof BlockTree) {
BlockTree block = (BlockTree) leaf;
return String.format(Locale.ROOT, "{ ...%d lines... }", block.getStatements().size());
}
return leaf.toString();
}
static Optional<Path> declaringFile(Element e) {
// Find top-level type surrounding `to`
LOG.info(String.format("...looking up declaring file of `%s`...", e));
Optional<TypeElement> top = topLevelDeclaration(e);
if (!top.isPresent()) {
LOG.warn("...no top-level type!");
return Optional.empty();
}
// Find file by looking at package and class name
LOG.info(String.format("...top-level type is %s", top.get()));
Optional<Path> file = FileStore.findDeclaringFile(top.get());
if (!file.isPresent()) {
LOG.info("...couldn't find declaring file for type");
return Optional.empty();
}
return file;
}
private static Optional<TypeElement> topLevelDeclaration(Element e) {
if (e == null) {
return Optional.empty();
}
Element parent = e;
TypeElement result = null;
while (parent.getEnclosingElement() != null) {
if (parent instanceof TypeElement) {
result = (TypeElement) parent;
}
parent = parent.getEnclosingElement();
}
return Optional.ofNullable(result);
}
public Set<Name> packagePrivateClasses() {
Set<Name> result = new HashSet<>();
for (Tree t : root.getTypeDecls()) {
@@ -317,160 +248,4 @@ public class Parser {
}
return result;
}
public List<String> accessibleClasses(String partialName, String fromPackage) {
String toPackage = Objects.toString(root.getPackageName(), "");
boolean samePackage = fromPackage.equals(toPackage) || toPackage.isEmpty();
List<String> result = new ArrayList<String>();
for (Tree t : root.getTypeDecls()) {
if (!(t instanceof ClassTree)) {
continue;
}
ClassTree cls = (ClassTree) t;
// If class is not accessible, skip it
boolean isPublic = cls.getModifiers().getFlags().contains(Modifier.PUBLIC);
if (!samePackage && !isPublic) {
continue;
}
// If class doesn't match partialName, skip it
String name = cls.getSimpleName().toString();
if (!StringUtils.matchesPartialName(name, partialName)) {
continue;
}
if (root.getPackageName() != null) {
name = root.getPackageName() + "." + name;
}
result.add(name);
}
return result;
}
String prune(long cursor) {
SourcePositions pos = Trees.instance(task).getSourcePositions();
StringBuilder buffer = new StringBuilder(contents);
long[] cursors = {cursor};
return prune(root, pos, buffer, cursors, true);
}
private static String prune(
CompilationUnitTree root,
SourcePositions pos,
StringBuilder buffer,
long[] offsets,
boolean eraseAfterCursor) {
class Scan extends TreeScanner<Void, Void> {
boolean erasedAfterCursor = !eraseAfterCursor;
@Override
public Void visitImport(ImportTree node, Void __) {
// Erase 'static' keyword so autocomplete works better
if (containsCursor(node) && node.isStatic()) {
int start = (int) pos.getStartPosition(root, node);
start = buffer.indexOf("static", start);
int end = start + "static".length();
erase(buffer, start, end);
}
return super.visitImport(node, null);
}
@Override
public Void visitBlock(BlockTree node, Void __) {
if (containsCursor(node)) {
super.visitBlock(node, null);
// When we find the deepest block that includes the cursor
if (!erasedAfterCursor) {
long start = lastCursorIn(node);
long end = pos.getEndPosition(root, node);
if (end >= buffer.length()) {
end = buffer.length() - 1;
}
// Find the next line
while (start < end && buffer.charAt((int) start) != '\n') start++;
// Find the end of the block
while (end > start && buffer.charAt((int) end) != '}') end--;
// Erase from next line to end of block
erase(buffer, start, end - 1);
erasedAfterCursor = true;
}
} else if (!node.getStatements().isEmpty()) {
StatementTree first = node.getStatements().get(0);
StatementTree last = node.getStatements().get(node.getStatements().size() - 1);
long start = pos.getStartPosition(root, first);
long end = pos.getEndPosition(root, last);
if (end >= buffer.length()) {
end = buffer.length() - 1;
}
erase(buffer, start, end);
}
return null;
}
long lastCursorIn(Tree node) {
long start = pos.getStartPosition(root, node);
long end = pos.getEndPosition(root, node);
long last = -1;
for (long cursor : offsets) {
if (start <= cursor && cursor <= end) {
last = cursor;
}
}
if (last == -1) {
throw new RuntimeException(
String.format(
"No cursor in %s is between %d and %d", Arrays.toString(offsets), start, end));
}
return last;
}
@Override
public Void visitSwitch(SwitchTree node, Void __) {
if (containsCursor(node)) {
// Prevent the enclosing block from erasing the closing } of the switch
erasedAfterCursor = true;
}
return super.visitSwitch(node, null);
}
@Override
public Void visitErroneous(ErroneousTree node, Void nothing) {
return super.scan(node.getErrorTrees(), nothing);
}
boolean anyContainsCursor(Collection<? extends Tree> nodes) {
for (Tree n : nodes) {
if (containsCursor(n)) {
return true;
}
}
return false;
}
boolean containsCursor(Tree node) {
long start = pos.getStartPosition(root, node);
long end = pos.getEndPosition(root, node);
for (long cursor : offsets) {
if (start <= cursor && cursor <= end) {
return true;
}
}
return false;
}
}
new Scan().scan(root, null);
return buffer.toString();
}
private static void erase(StringBuilder buffer, long start, long end) {
for (int i = (int) start; i < end; i++) {
switch (buffer.charAt(i)) {
case '\r':
case '\n':
break;
default:
buffer.setCharAt(i, ' ');
}
}
}
}
@@ -25,7 +25,7 @@ import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.lsp.api.IServerSettings;
import com.itsaky.androidide.lsp.java.models.JavaServerSettings;
import com.itsaky.androidide.lsp.models.FormatCodeParams;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Range;
import java.util.Collection;
@@ -19,6 +19,7 @@ package com.itsaky.androidide.lsp.java.providers;
import androidx.annotation.NonNull;
import com.itsaky.androidide.models.Location;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.lsp.java.compiler.CompileTask;
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
@@ -28,7 +29,6 @@ import com.itsaky.androidide.lsp.java.utils.FindHelper;
import com.itsaky.androidide.lsp.java.utils.NavigationHelper;
import com.itsaky.androidide.lsp.models.DefinitionParams;
import com.itsaky.androidide.lsp.models.DefinitionResult;
import com.itsaky.androidide.lsp.models.Location;
import com.itsaky.androidide.lsp.util.PathUtils;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
@@ -16,17 +16,17 @@
*/
package com.itsaky.androidide.lsp.java.providers
import com.itsaky.androidide.lsp.java.FileStore
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.models.DiagnosticCode.EMPTY_BLOCK
import com.itsaky.androidide.lsp.java.models.DiagnosticCode.UNUSED_THROWS
import com.itsaky.androidide.lsp.java.visitors.DiagnosticVisitor
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.lsp.models.DiagnosticSeverity
import com.itsaky.androidide.lsp.models.DiagnosticSeverity.WARNING
import com.itsaky.androidide.lsp.models.Position
import com.itsaky.androidide.lsp.models.Range
import com.itsaky.androidide.lsp.util.PathUtils.Companion.isSameFile
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.projects.ProjectManager
import com.sun.source.tree.BlockTree
import com.sun.source.tree.ClassTree
import com.sun.source.tree.CompilationUnitTree
@@ -61,7 +61,10 @@ object DiagnosticsProvider {
* @return The list of diagnostics retrieved from the task. Never null.
*/
@JvmStatic
fun findDiagnostics(task: CompileTask, file: Path?): List<DiagnosticItem> {
fun findDiagnostics(
task: CompileTask,
file: Path?
): List<DiagnosticItem> {
val result = mutableListOf<DiagnosticItem>()
var root: CompilationUnitTree? = null
for (tree in task.roots) {
@@ -102,7 +105,11 @@ object DiagnosticsProvider {
}
}
private fun warnEmptyBlock(task: CompileTask, path: TreePath, name: String): DiagnosticItem {
private fun warnEmptyBlock(
task: CompileTask,
path: TreePath,
name: String
): DiagnosticItem {
val trees = Trees.instance(task.task)
val thisTree = path.leaf
val code = EMPTY_BLOCK
@@ -118,10 +125,11 @@ object DiagnosticsProvider {
message = "'$name' statement has empty body",
severity = WARNING,
range =
Range(getPosition(start, lines), getPosition(end, lines)).apply {
this.start.index = start.toInt()
this.end.index = end.toInt()
}
Range(getPosition(start, lines), getPosition(end, lines))
.apply {
this.start.index = start.toInt()
this.end.index = end.toInt()
}
)
}
@@ -141,7 +149,11 @@ object DiagnosticsProvider {
}
}
private fun warnNotThrown(task: CompileTask, name: String?, path: TreePath): DiagnosticItem {
private fun warnNotThrown(
task: CompileTask,
name: String?,
path: TreePath
): DiagnosticItem {
val trees = Trees.instance(task.task)
val pos = trees.sourcePositions
val root = path.compilationUnit
@@ -151,17 +163,21 @@ object DiagnosticsProvider {
return DiagnosticItem(
message = String.format("'%s' is not thrown in the body of the method", name),
range =
Range(getPosition(start, lines), getPosition(end, lines)).apply {
this.start.index = start.toInt()
this.end.index = end.toInt()
},
code = DiagnosticCode.UNUSED_THROWS.id,
Range(getPosition(start, lines), getPosition(end, lines))
.apply {
this.start.index = start.toInt()
this.end.index = end.toInt()
},
code = UNUSED_THROWS.id,
severity = DiagnosticSeverity.INFO,
source = ""
)
}
private fun warnUnused(task: CompileTask, unusedEl: Element): DiagnosticItem {
private fun warnUnused(
task: CompileTask,
unusedEl: Element
): DiagnosticItem {
val trees = Trees.instance(task.task)
val path = trees.getPath(unusedEl) ?: throw RuntimeException("$unusedEl has no path")
val root = path.compilationUnit
@@ -176,7 +192,7 @@ object DiagnosticsProvider {
}
}
val file = Paths.get(root.sourceFile.toUri())
val contents = FileStore.contents(file)
val contents = ProjectManager.getDocumentContents(file)
var name = unusedEl.simpleName
if (name.contentEquals("<init>")) {
name = unusedEl.enclosingElement.simpleName
@@ -240,10 +256,11 @@ object DiagnosticsProvider {
code = code,
severity = severity,
range =
Range(getPosition(start, root.lineMap), getPosition(end, root.lineMap)).apply {
this.start.index = start.toInt()
this.end.index = end.toInt()
},
Range(getPosition(start, root.lineMap), getPosition(end, root.lineMap))
.apply {
this.start.index = start.toInt()
this.end.index = end.toInt()
},
source = ""
)
}
@@ -275,19 +292,22 @@ object DiagnosticsProvider {
return Range(start, end)
}
private fun getPosition(position: Long, lines: LineMap): Position {
private fun getPosition(position: Long, lines: LineMap): com.itsaky.androidide.models.Position {
// decrement the numbers
// to convert 1-based indexes to 0-based
val line = (lines.getLineNumber(position) - 1).toInt()
val column = (lines.getColumnNumber(position) - 1).toInt()
return Position(line, column)
return com.itsaky.androidide.models.Position(line, column)
}
private fun severityFor(kind: Diagnostic.Kind): DiagnosticSeverity {
private fun severityFor(
kind: Diagnostic.Kind
): DiagnosticSeverity {
return when (kind) {
Diagnostic.Kind.ERROR -> DiagnosticSeverity.ERROR
Diagnostic.Kind.WARNING,
Diagnostic.Kind.MANDATORY_WARNING -> DiagnosticSeverity.WARNING
Diagnostic.Kind.MANDATORY_WARNING ->
WARNING
Diagnostic.Kind.NOTE -> DiagnosticSeverity.INFO
Diagnostic.Kind.OTHER -> DiagnosticSeverity.HINT
else -> DiagnosticSeverity.HINT
@@ -16,18 +16,16 @@
*/
package com.itsaky.androidide.lsp.java.providers
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.lsp.java.CompilationCancellationException.Companion.isCancelled
import com.itsaky.androidide.lsp.java.FileStore
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
import com.itsaky.androidide.lsp.java.providers.DiagnosticsProvider.findDiagnostics
import com.itsaky.androidide.lsp.models.DiagnosticResult
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
import java.nio.file.Path
import java.time.Instant
import java.util.concurrent.atomic.*
import java.util.function.*
import com.itsaky.androidide.javac.services.CancelAbort
/**
* Code analyzer for java source code.
@@ -38,19 +36,22 @@ class JavaDiagnosticProvider(private val completionChecker: Supplier<Boolean>) {
private val log = ILogger.newInstance(javaClass.simpleName)
private val analyzeTimestamps = mutableMapOf<Path, Instant>()
private var cachedDiagnostics = DiagnosticResult.NO_UPDATE
private var cachedDiagnostics = com.itsaky.androidide.lsp.models.DiagnosticResult.NO_UPDATE
private var analyzing = AtomicBoolean(false)
fun analyze(compiler: JavaCompilerService, file: Path): DiagnosticResult {
fun analyze(
compiler: JavaCompilerService,
file: Path
): com.itsaky.androidide.lsp.models.DiagnosticResult {
log.debug("Analyzing:", file)
if (completionChecker.get()) {
log.warn("Completion is in progress. Skipping analyze...")
return DiagnosticResult.NO_UPDATE
return com.itsaky.androidide.lsp.models.DiagnosticResult.NO_UPDATE
}
val modifiedAt = FileStore.modified(file)
val modifiedAt = ProjectManager.getLastModified(file)
val analyzedAt = analyzeTimestamps[file]
if (analyzedAt?.isAfter(modifiedAt) == true) {
@@ -60,7 +61,7 @@ class JavaDiagnosticProvider(private val completionChecker: Supplier<Boolean>) {
if (analyzing.get()) {
log.warn("Another analyze process is in progress...")
return DiagnosticResult.NO_UPDATE
return com.itsaky.androidide.lsp.models.DiagnosticResult.NO_UPDATE
}
analyzing.set(true)
@@ -76,7 +77,7 @@ class JavaDiagnosticProvider(private val completionChecker: Supplier<Boolean>) {
log.warn("Unable to analyze file", err)
compiler.destroy()
DiagnosticResult.NO_UPDATE
com.itsaky.androidide.lsp.models.DiagnosticResult.NO_UPDATE
}
.also {
cachedDiagnostics = it
@@ -85,13 +86,16 @@ class JavaDiagnosticProvider(private val completionChecker: Supplier<Boolean>) {
}
}
private fun doAnalyze(file: Path, task: CompileTask): DiagnosticResult {
private fun doAnalyze(
file: Path,
task: CompileTask
): com.itsaky.androidide.lsp.models.DiagnosticResult {
return if (!isTaskValid(task)) {
// Do not use Collections.emptyList ()
// The returned list is accessed and the list returned by Collections.emptyList()
// throws exception when trying to access.
cachedDiagnostics
} else DiagnosticResult(file, findDiagnostics(task, file))
} else com.itsaky.androidide.lsp.models.DiagnosticResult(file, findDiagnostics(task, file))
}
companion object {
@@ -23,7 +23,7 @@ import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
import com.itsaky.androidide.lsp.java.visitors.FindBiggerRange;
import com.itsaky.androidide.lsp.models.ExpandSelectionParams;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Range;
import com.sun.source.tree.CompilationUnitTree;
/**
@@ -25,9 +25,9 @@ import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
import com.itsaky.androidide.lsp.java.utils.FindHelper;
import com.itsaky.androidide.lsp.java.utils.NavigationHelper;
import com.itsaky.androidide.lsp.java.visitors.FindReferences;
import com.itsaky.androidide.lsp.models.Location;
import com.itsaky.androidide.lsp.models.ReferenceParams;
import com.itsaky.androidide.lsp.models.ReferenceResult;
import com.itsaky.androidide.models.Location;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.TreePath;
@@ -49,8 +49,8 @@ class ClassNamesCompletionProvider(
path: TreePath,
partial: String,
endsWithParen: Boolean,
): CompletionResult {
val list = mutableListOf<CompletionItem>()
): com.itsaky.androidide.lsp.models.CompletionResult {
val list = mutableListOf<com.itsaky.androidide.lsp.models.CompletionItem>()
val packageName = Objects.toString(root.packageName, "")
val uniques: MutableSet<String> = HashSet()
val previousSize: Int = list.size
@@ -108,6 +108,6 @@ class ClassNamesCompletionProvider(
log.info("...found " + (list.size - previousSize) + " class names")
return CompletionResult(list)
return com.itsaky.androidide.lsp.models.CompletionResult(list)
}
}
@@ -22,7 +22,6 @@ import com.itsaky.androidide.lsp.api.IServerSettings
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider
import com.itsaky.androidide.lsp.java.edits.ClassImportEditHandler
import com.itsaky.androidide.lsp.java.utils.EditHelper
import com.itsaky.androidide.lsp.models.Command
import com.itsaky.androidide.lsp.models.CompletionData
import com.itsaky.androidide.lsp.models.CompletionItem
@@ -83,7 +82,7 @@ abstract class IJavaCompletionProvider(
path: TreePath,
partial: String,
endsWithParen: Boolean
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
val root = task.root(completingFile)
filePackage = root.`package`.packageName.toString()
fileImports = root.imports.map { it.qualifiedIdentifier.toString() }.toSet()
@@ -105,10 +104,10 @@ abstract class IJavaCompletionProvider(
path: TreePath,
partial: String,
endsWithParen: Boolean,
): CompletionResult
): com.itsaky.androidide.lsp.models.CompletionResult
protected open fun matchLevel(candidate: CharSequence, partial: CharSequence): MatchLevel {
return CompletionItem.matchLevel(candidate.toString(), partial.toString())
protected open fun matchLevel(candidate: CharSequence, partial: CharSequence): com.itsaky.androidide.lsp.models.MatchLevel {
return com.itsaky.androidide.lsp.models.CompletionItem.matchLevel(candidate.toString(), partial.toString())
}
protected open fun putMethod(
@@ -126,13 +125,13 @@ abstract class IJavaCompletionProvider(
keyword: String,
partialName: CharSequence,
matchRatio: Int,
): CompletionItem {
val item = CompletionItem()
): com.itsaky.androidide.lsp.models.CompletionItem {
val item = com.itsaky.androidide.lsp.models.CompletionItem()
item.setLabel(keyword)
item.kind = KEYWORD
item.detail = "keyword"
item.sortText = keyword
item.matchLevel = CompletionItem.matchLevel(keyword, partialName.toString())
item.matchLevel = com.itsaky.androidide.lsp.models.CompletionItem.matchLevel(keyword, partialName.toString())
return item
}
@@ -140,12 +139,12 @@ abstract class IJavaCompletionProvider(
task: CompileTask,
overloads: List<ExecutableElement>,
addParens: Boolean,
matchLevel: MatchLevel,
): CompletionItem {
matchLevel: com.itsaky.androidide.lsp.models.MatchLevel,
): com.itsaky.androidide.lsp.models.CompletionItem {
val first = overloads[0]
val item = CompletionItem()
val item = com.itsaky.androidide.lsp.models.CompletionItem()
item.setLabel(first.simpleName.toString())
item.kind = CompletionItemKind.METHOD
item.kind = com.itsaky.androidide.lsp.models.CompletionItemKind.METHOD
item.detail = first.returnType.toString() + " " + first
item.sortText = item.label.toString()
item.matchLevel = matchLevel
@@ -156,7 +155,7 @@ abstract class IJavaCompletionProvider(
item.insertText = first.simpleName.toString() + "()$0"
} else {
item.insertText = first.simpleName.toString() + "($0)"
item.command = Command("Trigger Parameter Hints", Command.TRIGGER_PARAMETER_HINTS)
item.command = com.itsaky.androidide.lsp.models.Command("Trigger Parameter Hints", com.itsaky.androidide.lsp.models.Command.TRIGGER_PARAMETER_HINTS)
}
item.insertTextFormat = SNIPPET // Snippet
}
@@ -166,10 +165,10 @@ abstract class IJavaCompletionProvider(
protected open fun item(
task: CompileTask,
element: Element,
matchLevel: MatchLevel
): CompletionItem {
matchLevel: com.itsaky.androidide.lsp.models.MatchLevel
): com.itsaky.androidide.lsp.models.CompletionItem {
if (element.kind == METHOD) throw RuntimeException("method")
val item = CompletionItem()
val item = com.itsaky.androidide.lsp.models.CompletionItem()
item.setLabel(element.simpleName.toString())
item.kind = kind(element)
item.detail = element.toString()
@@ -182,8 +181,8 @@ abstract class IJavaCompletionProvider(
protected open fun classItem(
className: String,
partialName: String,
matchLevel: MatchLevel
): CompletionItem {
matchLevel: com.itsaky.androidide.lsp.models.MatchLevel
): com.itsaky.androidide.lsp.models.CompletionItem {
return classItem(emptySet(), null, className, partialName, matchLevel)
}
@@ -192,15 +191,15 @@ abstract class IJavaCompletionProvider(
file: Path?,
className: String,
partialName: String,
matchLevel: MatchLevel,
): CompletionItem {
val item = CompletionItem()
matchLevel: com.itsaky.androidide.lsp.models.MatchLevel,
): com.itsaky.androidide.lsp.models.CompletionItem {
val item = com.itsaky.androidide.lsp.models.CompletionItem()
item.setLabel(simpleName(className).toString())
item.kind = CompletionItemKind.CLASS
item.kind = com.itsaky.androidide.lsp.models.CompletionItemKind.CLASS
item.detail = className
item.sortText = item.label.toString()
item.matchLevel = matchLevel
val data = CompletionData()
val data = com.itsaky.androidide.lsp.models.CompletionData()
data.className = className
item.data = data
item.additionalEditHandler = ClassImportEditHandler(imports, file!!)
@@ -215,40 +214,40 @@ abstract class IJavaCompletionProvider(
protected open fun packageItem(
name: String,
partialName: String,
matchLevel: MatchLevel
): CompletionItem =
CompletionItem().apply {
matchLevel: com.itsaky.androidide.lsp.models.MatchLevel
): com.itsaky.androidide.lsp.models.CompletionItem =
com.itsaky.androidide.lsp.models.CompletionItem().apply {
setLabel(name)
this.kind = MODULE
this.sortText = name
this.matchLevel = matchLevel
}
protected open fun kind(e: Element): CompletionItemKind {
protected open fun kind(e: Element): com.itsaky.androidide.lsp.models.CompletionItemKind {
return when (e.kind) {
ANNOTATION_TYPE -> CompletionItemKind.ANNOTATION_TYPE
CLASS -> CompletionItemKind.CLASS
CONSTRUCTOR -> CompletionItemKind.CONSTRUCTOR
ENUM -> CompletionItemKind.ENUM
ANNOTATION_TYPE -> com.itsaky.androidide.lsp.models.CompletionItemKind.ANNOTATION_TYPE
CLASS -> com.itsaky.androidide.lsp.models.CompletionItemKind.CLASS
CONSTRUCTOR -> com.itsaky.androidide.lsp.models.CompletionItemKind.CONSTRUCTOR
ENUM -> com.itsaky.androidide.lsp.models.CompletionItemKind.ENUM
ENUM_CONSTANT -> ENUM_MEMBER
EXCEPTION_PARAMETER,
PARAMETER, -> PROPERTY
FIELD -> CompletionItemKind.FIELD
FIELD -> com.itsaky.androidide.lsp.models.CompletionItemKind.FIELD
STATIC_INIT,
INSTANCE_INIT, -> FUNCTION
INTERFACE -> CompletionItemKind.INTERFACE
INTERFACE -> com.itsaky.androidide.lsp.models.CompletionItemKind.INTERFACE
LOCAL_VARIABLE,
RESOURCE_VARIABLE, -> VARIABLE
METHOD -> CompletionItemKind.METHOD
METHOD -> com.itsaky.androidide.lsp.models.CompletionItemKind.METHOD
PACKAGE -> MODULE
TYPE_PARAMETER -> CompletionItemKind.TYPE_PARAMETER
TYPE_PARAMETER -> com.itsaky.androidide.lsp.models.CompletionItemKind.TYPE_PARAMETER
OTHER -> NONE
else -> NONE
}
}
protected open fun data(task: CompileTask, element: Element, overloads: Int): CompletionData? {
val data = CompletionData()
protected open fun data(task: CompileTask, element: Element, overloads: Int): com.itsaky.androidide.lsp.models.CompletionData? {
val data = com.itsaky.androidide.lsp.models.CompletionData()
when {
element is TypeElement -> data.className = element.qualifiedName.toString()
element.kind == FIELD -> {
@@ -38,8 +38,8 @@ class IdentifierCompletionProvider(
path: TreePath,
partial: String,
endsWithParen: Boolean,
): CompletionResult {
val list = mutableListOf<CompletionItem>()
): com.itsaky.androidide.lsp.models.CompletionResult {
val list = mutableListOf<com.itsaky.androidide.lsp.models.CompletionItem>()
val scopeMembers =
ScopeCompletionProvider(completingFile, cursor, compiler, settings)
@@ -57,7 +57,7 @@ class IdentifierCompletionProvider(
.complete(task, path, partial, endsWithParen)
list.addAll(staticImports.items)
if (CompletionResult.TRIM_TO_MAX && list.size < CompletionResult.MAX_ITEMS) {
if (com.itsaky.androidide.lsp.models.CompletionResult.TRIM_TO_MAX && list.size < com.itsaky.androidide.lsp.models.CompletionResult.MAX_ITEMS) {
val allLower: Boolean = settings.shouldMatchAllLowerCase()
if (allLower || partial.isNotEmpty() && Character.isUpperCase(partial[0])) {
val classNames =
@@ -78,6 +78,6 @@ class IdentifierCompletionProvider(
.complete(task, path, partial, endsWithParen)
list.addAll(keywords.items)
return CompletionResult(list)
return com.itsaky.androidide.lsp.models.CompletionResult(list)
}
}
@@ -46,11 +46,11 @@ class ImportCompletionProvider(
path: TreePath,
partial: String,
endsWithParen: Boolean,
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
log.info("...complete import")
val names: MutableSet<String> = HashSet()
val list = mutableListOf<CompletionItem>()
val list = mutableListOf<com.itsaky.androidide.lsp.models.CompletionItem>()
for (className in compiler.publicTopLevelTypes()) {
val matchLevel = matchLevel(className, partial)
if (matchLevel == NO_MATCH) {
@@ -79,6 +79,6 @@ class ImportCompletionProvider(
}
}
return CompletionResult(list)
return com.itsaky.androidide.lsp.models.CompletionResult(list)
}
}
@@ -47,7 +47,7 @@ class KeywordCompletionProvider(
path: TreePath,
partial: String,
endsWithParen: Boolean,
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
val level: Tree = findKeywordLevel(path)
var keywords = arrayOf<String>()
when (level) {
@@ -56,7 +56,7 @@ class KeywordCompletionProvider(
is MethodTree -> keywords = METHOD_BODY_KEYWORDS
}
val list = mutableListOf<CompletionItem>()
val list = mutableListOf<com.itsaky.androidide.lsp.models.CompletionItem>()
for (k in keywords) {
val matchLevel = matchLevel(k, partial)
if (matchLevel == NO_MATCH) {
@@ -66,7 +66,7 @@ class KeywordCompletionProvider(
list.add(keyword(k, partial, 100))
}
return CompletionResult(list)
return com.itsaky.androidide.lsp.models.CompletionResult(list)
}
private fun findKeywordLevel(treePath: TreePath): Tree {
@@ -54,7 +54,7 @@ class MemberReferenceCompletionProvider(
path: TreePath,
partial: String,
endsWithParen: Boolean,
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
val trees = Trees.instance(task.task)
val select = path.leaf as MemberReferenceTree
log.info("...complete methods of " + select.qualifierExpression)
@@ -67,20 +67,20 @@ class MemberReferenceCompletionProvider(
is ArrayType -> completeArrayMemberReference(isStatic, partial)
is TypeVariable -> completeTypeVariableMemberReference(task, scope, type, isStatic, partial)
is DeclaredType -> completeDeclaredTypeMemberReference(task, scope, type, isStatic, partial)
else -> CompletionResult.EMPTY
else -> com.itsaky.androidide.lsp.models.CompletionResult.EMPTY
}
}
private fun completeArrayMemberReference(
isStatic: Boolean,
partialName: CharSequence,
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
return if (isStatic) {
val list = mutableListOf<CompletionItem>()
val list = mutableListOf<com.itsaky.androidide.lsp.models.CompletionItem>()
list.add(keyword("new", partialName, 100))
CompletionResult(list)
com.itsaky.androidide.lsp.models.CompletionResult(list)
} else {
CompletionResult.EMPTY
com.itsaky.androidide.lsp.models.CompletionResult.EMPTY
}
}
@@ -90,7 +90,7 @@ class MemberReferenceCompletionProvider(
type: TypeVariable,
isStatic: Boolean,
partial: String,
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
return when (type.upperBound) {
is DeclaredType ->
completeDeclaredTypeMemberReference(
@@ -108,7 +108,7 @@ class MemberReferenceCompletionProvider(
isStatic,
partial
)
else -> CompletionResult.EMPTY
else -> com.itsaky.androidide.lsp.models.CompletionResult.EMPTY
}
}
@@ -118,12 +118,12 @@ class MemberReferenceCompletionProvider(
type: DeclaredType,
isStatic: Boolean,
partial: String,
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
val trees = Trees.instance(task.task)
val typeElement = type.asElement() as TypeElement
val list: MutableList<CompletionItem> = ArrayList()
val list: MutableList<com.itsaky.androidide.lsp.models.CompletionItem> = ArrayList()
val methods: MutableMap<String, MutableList<ExecutableElement>> = mutableMapOf()
val matchLevels: MutableMap<String, MatchLevel> = HashMap()
val matchLevels: MutableMap<String, com.itsaky.androidide.lsp.models.MatchLevel> = HashMap()
for (member in task.task.elements.getAllMembers(typeElement)) {
val matchLevel = matchLevel(member.simpleName, partial)
if (matchLevel == NO_MATCH) {
@@ -163,6 +163,6 @@ class MemberReferenceCompletionProvider(
list.add(keyword("new", partial, 100))
}
return CompletionResult(list)
return com.itsaky.androidide.lsp.models.CompletionResult(list)
}
}
@@ -56,7 +56,7 @@ class MemberSelectCompletionProvider(
path: TreePath,
partial: String,
endsWithParen: Boolean,
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
val trees = Trees.instance(task.task)
val select = path.leaf as MemberSelectTree
log.info("...complete members of " + select.expression)
@@ -70,20 +70,20 @@ class MemberSelectCompletionProvider(
completeTypeVariableMemberSelect(task, scope, type, isStatic, partial, endsWithParen)
is DeclaredType ->
completeDeclaredTypeMemberSelect(task, scope, type, isStatic, partial, endsWithParen)
else -> CompletionResult.EMPTY
else -> com.itsaky.androidide.lsp.models.CompletionResult.EMPTY
}
}
private fun completeArrayMemberSelect(
isStatic: Boolean,
partialName: CharSequence
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
return if (isStatic) {
CompletionResult.EMPTY
com.itsaky.androidide.lsp.models.CompletionResult.EMPTY
} else {
val list = mutableListOf<CompletionItem>()
val list = mutableListOf<com.itsaky.androidide.lsp.models.CompletionItem>()
list.add(keyword("length", partialName, 100))
CompletionResult(list)
com.itsaky.androidide.lsp.models.CompletionResult(list)
}
}
@@ -94,7 +94,7 @@ class MemberSelectCompletionProvider(
isStatic: Boolean,
partial: String,
endsWithParen: Boolean,
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
return when (type.upperBound) {
is DeclaredType ->
completeDeclaredTypeMemberSelect(
@@ -114,7 +114,7 @@ class MemberSelectCompletionProvider(
partial,
endsWithParen
)
else -> CompletionResult.EMPTY
else -> com.itsaky.androidide.lsp.models.CompletionResult.EMPTY
}
}
@@ -125,12 +125,12 @@ class MemberSelectCompletionProvider(
isStatic: Boolean,
partialName: String,
endsWithParen: Boolean,
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
val trees = Trees.instance(task.task)
val typeElement = type.asElement() as TypeElement
val list: MutableList<CompletionItem> = ArrayList()
val list: MutableList<com.itsaky.androidide.lsp.models.CompletionItem> = ArrayList()
val methods = mutableMapOf<String, MutableList<ExecutableElement>>()
val matchLevels: MutableMap<String, MatchLevel> = mutableMapOf()
val matchLevels: MutableMap<String, com.itsaky.androidide.lsp.models.MatchLevel> = mutableMapOf()
for (member in task.task.elements.getAllMembers(typeElement)) {
if (member.kind == CONSTRUCTOR) {
continue
@@ -173,7 +173,7 @@ class MemberSelectCompletionProvider(
list.add(keyword("super", partialName, 100))
}
return CompletionResult(list)
return com.itsaky.androidide.lsp.models.CompletionResult(list)
}
private fun isEnclosingClass(type: DeclaredType, start: Scope): Boolean {
@@ -18,7 +18,6 @@
package com.itsaky.androidide.lsp.java.providers.completion
import com.itsaky.androidide.lsp.api.IServerSettings
import com.itsaky.androidide.lsp.java.FileStore
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider
import com.itsaky.androidide.lsp.java.edits.MultipleClassImportEditHandler
@@ -29,12 +28,9 @@ import com.itsaky.androidide.lsp.java.utils.FindHelper
import com.itsaky.androidide.lsp.java.utils.JavaPoetUtils.Companion.buildMethod
import com.itsaky.androidide.lsp.java.utils.JavaPoetUtils.Companion.print
import com.itsaky.androidide.lsp.java.utils.ScopeHelper
import com.itsaky.androidide.lsp.models.CompletionItem
import com.itsaky.androidide.lsp.models.CompletionItemKind
import com.itsaky.androidide.lsp.models.CompletionResult
import com.itsaky.androidide.lsp.models.InsertTextFormat.SNIPPET
import com.itsaky.androidide.lsp.models.MatchLevel
import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH
import com.itsaky.androidide.projects.ProjectManager
import com.squareup.javapoet.MethodSpec.Builder
import com.sun.source.tree.ClassTree
import com.sun.source.tree.MethodTree
@@ -70,9 +66,9 @@ class ScopeCompletionProvider(
path: TreePath,
partial: String,
endsWithParen: Boolean,
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
val trees = Trees.instance(task.task)
val list: MutableList<CompletionItem> = ArrayList()
val list: MutableList<com.itsaky.androidide.lsp.models.CompletionItem> = ArrayList()
val scope = trees.getScope(path)
val filter =
Predicate<CharSequence?> {
@@ -107,7 +103,7 @@ class ScopeCompletionProvider(
log.info("...found " + list.size + " scope members")
return CompletionResult(list)
return com.itsaky.androidide.lsp.models.CompletionResult(list)
}
/**
@@ -124,8 +120,8 @@ class ScopeCompletionProvider(
parentPath: TreePath,
method: ExecutableElement,
endsWithParen: Boolean,
matchLevel: MatchLevel,
): CompletionItem {
matchLevel: com.itsaky.androidide.lsp.models.MatchLevel,
): com.itsaky.androidide.lsp.models.CompletionItem {
if (parentPath.leaf.kind != CLASS) {
// Can only override if the cursor is directly in a class declaration
return method(task, listOf(method), !endsWithParen, matchLevel)
@@ -154,7 +150,8 @@ class ScopeCompletionProvider(
// Print the method details and the annotations
// Print the method details and the annotations
val indent = EditHelper.indent(FileStore.contents(completingFile), cursor.toInt())
val indent =
EditHelper.indent(ProjectManager.getDocumentContents(completingFile), cursor.toInt())
val builder: Builder
try {
builder = buildMethod(method, types, type)
@@ -168,9 +165,9 @@ class ScopeCompletionProvider(
var insertText = print(methodSpec, imports, false)
insertText = insertText.replace("\n", "\n${repeatSpaces(indent)}")
val item = CompletionItem()
val item = com.itsaky.androidide.lsp.models.CompletionItem()
item.setLabel(methodSpec.name)
item.kind = CompletionItemKind.METHOD
item.kind = com.itsaky.androidide.lsp.models.CompletionItemKind.METHOD
item.detail = method.returnType.toString() + " " + method
item.sortText = item.label.toString()
item.insertText = insertText
@@ -182,10 +179,11 @@ class ScopeCompletionProvider(
}
imports.removeIf { "java.lang." == it || fileImports.contains(it) || filePackage == it }
item.additionalEditHandler = MultipleClassImportEditHandler(imports, fileImports, completingFile)
item.additionalEditHandler =
MultipleClassImportEditHandler(imports, fileImports, completingFile)
return item
}
private fun findSource(
compiler: CompilerProvider,
task: CompileTask,
@@ -55,11 +55,11 @@ class StaticImportCompletionProvider(
path: TreePath,
partial: String,
endsWithParen: Boolean,
): CompletionResult {
val list = mutableListOf<CompletionItem>()
): com.itsaky.androidide.lsp.models.CompletionResult {
val list = mutableListOf<com.itsaky.androidide.lsp.models.CompletionItem>()
val trees = Trees.instance(task.task)
val methods = mutableMapOf<String, MutableList<ExecutableElement>>()
val matchRatios: MutableMap<String, MatchLevel> = mutableMapOf()
val matchRatios: MutableMap<String, com.itsaky.androidide.lsp.models.MatchLevel> = mutableMapOf()
val previousSize: Int = list.size
outer@ for (i in root.imports) {
if (!i.isStatic) {
@@ -111,7 +111,7 @@ class StaticImportCompletionProvider(
log.info("...found " + (list.size - previousSize) + " static imports")
return CompletionResult(list)
return com.itsaky.androidide.lsp.models.CompletionResult(list)
}
private fun importMatchesPartial(staticImport: Name, partial: String): Boolean {
@@ -49,7 +49,7 @@ class SwitchConstantCompletionProvider(
path: TreePath,
partial: String,
endsWithParen: Boolean,
): CompletionResult {
): com.itsaky.androidide.lsp.models.CompletionResult {
val switchTree = path.leaf as SwitchTree
val exprPath = TreePath(path, switchTree.expression)
val type = Trees.instance(task.task).getTypeMirror(exprPath)
@@ -73,7 +73,7 @@ class SwitchConstantCompletionProvider(
log.info("...complete constants of type $type")
val list: MutableList<CompletionItem> = ArrayList()
val list: MutableList<com.itsaky.androidide.lsp.models.CompletionItem> = ArrayList()
for (member in task.task.elements.getAllMembers(element)) {
if (member.kind != ENUM_CONSTANT) {
continue
@@ -87,7 +87,7 @@ class SwitchConstantCompletionProvider(
list.add(item(task, member, matchLevel))
}
return CompletionResult(list)
return com.itsaky.androidide.lsp.models.CompletionResult(list)
}
private fun completeIdentifier(
@@ -17,13 +17,13 @@
package com.itsaky.androidide.lsp.java.providers.completion
import com.itsaky.androidide.lsp.java.FileStore
import com.itsaky.androidide.lsp.java.parser.ParseTask
import com.itsaky.androidide.lsp.models.CompletionItem
import com.itsaky.androidide.lsp.models.CompletionItemKind.SNIPPET
import com.itsaky.androidide.lsp.models.CompletionResult
import com.itsaky.androidide.lsp.models.InsertTextFormat
import com.itsaky.androidide.lsp.models.MatchLevel.CASE_INSENSITIVE_EQUAL
import com.itsaky.androidide.projects.ProjectManager
import com.sun.source.tree.CompilationUnitTree
import com.sun.source.tree.Tree.Kind.ERRONEOUS
import java.nio.file.Path
@@ -36,7 +36,10 @@ class TopLevelSnippetsProvider {
if (!hasTypeDeclaration(task.root)) {
result.add(classSnippet(file))
if (task.root.getPackage() == null) {
result.add(packageSnippet(file))
val packageSnippet = packageSnippet(file)
if (packageSnippet != null) {
result.add(packageSnippet)
}
}
}
}
@@ -56,8 +59,9 @@ class TopLevelSnippetsProvider {
return snippetItem("class $name", "class $name {\n $0\n}")
}
private fun packageSnippet(file: Path): CompletionItem {
val name = FileStore.suggestedPackageName(file)
private fun packageSnippet(file: Path): CompletionItem? {
val module = ProjectManager.findModuleForFile(file) ?: return null
val name = module.suggestPackageName(file)
return snippetItem("package $name", "package $name;\n\n")
}
@@ -22,8 +22,8 @@ import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
import com.itsaky.androidide.lsp.java.utils.FindHelper;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.lsp.models.TextEdit;
import com.sun.source.tree.LineMap;
import com.sun.source.tree.MethodTree;
@@ -22,8 +22,8 @@ import androidx.annotation.RestrictTo;
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
import com.itsaky.androidide.lsp.java.parser.ParseTask;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.lsp.models.TextEdit;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.Tree;
@@ -23,8 +23,8 @@ import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
import com.itsaky.androidide.lsp.java.utils.EditHelper;
import com.itsaky.androidide.lsp.java.utils.FindHelper;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.lsp.models.TextEdit;
import com.sun.source.tree.LineMap;
import com.sun.source.tree.MethodTree;
@@ -24,8 +24,8 @@ import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
import com.itsaky.androidide.lsp.java.parser.ParseTask;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.lsp.models.TextEdit;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.LineMap;
@@ -22,8 +22,8 @@ import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
import com.itsaky.androidide.lsp.java.parser.ParseTask;
import com.itsaky.androidide.lsp.java.visitors.FindVariableAtCursor;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.lsp.models.TextEdit;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.LineMap;
@@ -25,8 +25,8 @@ import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
import com.itsaky.androidide.lsp.java.utils.EditHelper;
import com.itsaky.androidide.lsp.java.visitors.FindMethodCallAt;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.lsp.models.TextEdit;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
@@ -24,8 +24,8 @@ import com.itsaky.androidide.lsp.java.compiler.CompileTask;
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
import com.itsaky.androidide.lsp.java.utils.EditHelper;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.lsp.models.TextEdit;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
@@ -29,8 +29,8 @@ import com.itsaky.androidide.lsp.java.visitors.FindAnonymousTypeDeclaration;
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationAt;
import com.itsaky.androidide.lsp.models.CodeActionItem;
import com.itsaky.androidide.lsp.models.Command;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.lsp.models.TextEdit;
import com.squareup.javapoet.MethodSpec;
import com.sun.source.tree.ClassTree;
@@ -20,8 +20,8 @@ package com.itsaky.androidide.lsp.java.rewrite;
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
import com.itsaky.androidide.lsp.java.utils.FindHelper;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.lsp.models.TextEdit;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
@@ -30,8 +30,8 @@ import com.itsaky.androidide.lsp.java.rewrite.Rewrite;
import com.itsaky.androidide.lsp.java.visitors.FindMethodDeclarationAt;
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationAt;
import com.itsaky.androidide.lsp.models.CodeActionItem;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.LineMap;
@@ -21,8 +21,8 @@ import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
import com.itsaky.androidide.lsp.java.rewrite.AddImport;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.lsp.models.TextEdit;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
@@ -20,9 +20,9 @@ package com.itsaky.androidide.lsp.java.utils;
import com.itsaky.androidide.lsp.java.compiler.CompileTask;
import com.itsaky.androidide.lsp.java.parser.ParseTask;
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationNamed;
import com.itsaky.androidide.lsp.models.Location;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Location;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.sun.source.tree.ArrayTypeTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
@@ -16,8 +16,8 @@
*/
package com.itsaky.androidide.lsp.java.utils;
import static com.itsaky.androidide.lsp.java.utils.StringSearch.containsClass;
import static com.itsaky.androidide.lsp.java.utils.StringSearch.containsInterface;
import static com.itsaky.androidide.projects.util.StringSearch.containsClass;
import static com.itsaky.androidide.projects.util.StringSearch.containsInterface;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.NodeList;
@@ -21,8 +21,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.lsp.models.Position;
import com.itsaky.androidide.lsp.models.Range;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.LineMap;
import com.sun.source.tree.MethodTree;

Some files were not shown because too many files have changed in this diff Show More