fix(editor): rewrite IDEEditor in Kotlin with Coroutines

This commit is contained in:
Akash Yadav
2023-09-30 15:08:28 +05:30
parent 47491af765
commit 230ccfa536
15 changed files with 1426 additions and 1651 deletions
@@ -224,7 +224,7 @@ public class CodeEditorView extends LinearLayout {
}
protected void onInputTypePrefChanged() {
binding.editor.setInputType(IDEEditor.createInputFlags());
binding.editor.setInputType(IDEEditor.Companion.createInputTypeFlags());
}
protected void onPrintingFlagsPrefChanged() {
@@ -439,6 +439,6 @@ public class CodeEditorView extends LinearLayout {
return;
}
editor.updateFile(file);
editor.setFile(file);
}
}
@@ -104,15 +104,19 @@ fun Activity.flashInfo(@StringRes msg: Int) {
}
@JvmOverloads
fun Activity.flashProgress(configure: Consumer<Flashbar.Builder>? = null, action: Consumer<Flashbar>) {
fun <R> Activity.flashProgress(
configure: (Flashbar.Builder.() -> Unit)? = null,
action: (Flashbar) -> R?
) : R? {
val builder = flashbarBuilder(gravity = TOP, duration = DURATION_INDEFINITE)
.showProgress(Flashbar.ProgressPosition.LEFT)
configure?.accept(builder)
configure?.invoke(builder)
val flashbar = builder.build()
flashbar.show()
action.accept(flashbar)
return action(flashbar)
}
fun Flashbar.Builder.showOnUiThread() {
@@ -19,6 +19,7 @@ package com.itsaky.androidide.editor.api;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
@@ -38,6 +39,7 @@ public interface IEditor {
*
* @return The file instance.
*/
@Nullable
File getFile();
/**
@@ -59,7 +61,11 @@ public interface IEditor {
*
* @param range The range to select.
*/
void setSelection(@NonNull Range range);
default void setSelection(@NonNull Range range) {
setSelection(range.getStart(), range.getEnd());
}
void setSelection(@NonNull Position start, @NonNull Position end);
/**
* Get the cursor's selection range in the form of {@link Range}.
@@ -27,6 +27,7 @@ import io.github.rosemoe.sora.widget.base.EditorPopupWindow
*/
abstract class AbstractPopupWindow(editor: CodeEditor, features: Int) :
EditorPopupWindow(editor, features) {
override fun show() {
(editor as? IDEEditor)?.ensureWindowsDismissed()
super.show()
@@ -79,7 +79,7 @@ class EditorSearchLayout(context: Context, val editor: IDEEditor) : FrameLayout(
1 -> SearchOptions(ignoreCase, it.isChecked)
else -> searchOptions
}
editor.searcher?.updateSearchOptions(searchOptions)
editor.searcher.updateSearchOptions(searchOptions)
true
} else false
@@ -115,7 +115,7 @@ class EditorSearchLayout(context: Context, val editor: IDEEditor) : FrameLayout(
findInFileBinding.searchInput.removeTextChangedListener(this.searchInputTextWatcher)
findInFileBinding.root.visibility = GONE
this.searchInputTextWatcher = null
this.editor.searcher.onClose()
searcher.onClose()
}
if (!searcher.hasQuery()) {
return
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,890 @@
/*
* 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.editor.ui
import android.app.Activity
import android.content.Context
import android.graphics.Rect
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import androidx.annotation.StringRes
import androidx.annotation.UiThread
import com.blankj.utilcode.util.FileUtils
import com.itsaky.androidide.editor.R.string
import com.itsaky.androidide.editor.adapters.CompletionListAdapter
import com.itsaky.androidide.editor.api.IEditor
import com.itsaky.androidide.editor.api.ILspEditor
import com.itsaky.androidide.editor.language.IDELanguage
import com.itsaky.androidide.editor.language.cpp.CppLanguage
import com.itsaky.androidide.editor.language.groovy.GroovyLanguage
import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguage
import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguageProvider
import com.itsaky.androidide.editor.schemes.IDEColorScheme
import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider.readScheme
import com.itsaky.androidide.editor.snippets.AbstractSnippetVariableResolver
import com.itsaky.androidide.editor.snippets.FileVariableResolver
import com.itsaky.androidide.editor.snippets.WorkspaceVariableResolver
import com.itsaky.androidide.eventbus.events.editor.ChangeType
import com.itsaky.androidide.eventbus.events.editor.ColorSchemeInvalidatedEvent
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.flashbar.Flashbar
import com.itsaky.androidide.lsp.api.ILanguageClient
import com.itsaky.androidide.lsp.api.ILanguageServer
import com.itsaky.androidide.lsp.models.Command
import com.itsaky.androidide.lsp.models.DefinitionParams
import com.itsaky.androidide.lsp.models.DefinitionResult
import com.itsaky.androidide.lsp.models.ExpandSelectionParams
import com.itsaky.androidide.lsp.models.ReferenceParams
import com.itsaky.androidide.lsp.models.ReferenceResult
import com.itsaky.androidide.lsp.models.ShowDocumentParams
import com.itsaky.androidide.lsp.models.SignatureHelp
import com.itsaky.androidide.lsp.models.SignatureHelpParams
import com.itsaky.androidide.models.Position
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.preferences.internal.tabSize
import com.itsaky.androidide.preferences.internal.visiblePasswordFlag
import com.itsaky.androidide.progress.ICancelChecker
import com.itsaky.androidide.projects.FileManager.onDocumentClose
import com.itsaky.androidide.projects.FileManager.onDocumentContentChange
import com.itsaky.androidide.projects.FileManager.onDocumentOpen
import com.itsaky.androidide.syntax.colorschemes.DynamicColorScheme
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE
import com.itsaky.androidide.utils.DocumentUtils
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashProgress
import io.github.rosemoe.sora.event.ContentChangeEvent
import io.github.rosemoe.sora.event.SelectionChangeEvent
import io.github.rosemoe.sora.lang.EmptyLanguage
import io.github.rosemoe.sora.lang.Language
import io.github.rosemoe.sora.widget.CodeEditor
import io.github.rosemoe.sora.widget.EditorSearcher
import io.github.rosemoe.sora.widget.IDEEditorSearcher
import io.github.rosemoe.sora.widget.component.EditorAutoCompletion
import io.github.rosemoe.sora.widget.component.EditorBuiltinComponent
import io.github.rosemoe.sora.widget.component.EditorTextActionWindow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.io.File
import java.util.function.Consumer
/**
* [CodeEditor] implementation for the IDE.
*
* @author Akash Yadav
*/
open class IDEEditor @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : CodeEditor(context, attrs, defStyleAttr, defStyleRes), IEditor, ILspEditor {
private var _actionsMenu: EditorActionsMenu? = null
private var _signatureHelpWindow: SignatureHelpWindow? = null
private var _diagnosticWindow: DiagnosticWindow? = null
private var file: File? = null
private var fileVersion = 0
private var isModified = false
private val selectionChangeHandler = Handler(Looper.getMainLooper())
private var selectionChangeRunner: Runnable? = Runnable {
val languageClient = languageClient ?: return@Runnable
val cursor = this.cursor ?: return@Runnable
if (cursor.isSelected || _signatureHelpWindow?.isShowing == true) {
return@Runnable
}
diagnosticWindow.showDiagnostic(
languageClient.getDiagnosticAt(file, cursor.leftLine, cursor.leftColumn)
)
}
protected val editorScope = CoroutineScope(Dispatchers.Default)
var languageServer: ILanguageServer? = null
private set
var languageClient: ILanguageClient? = null
private set
/**
* Whether the cursor position change animation is enabled for the editor.
*/
var isEnsurePosAnimEnabled = true
/**
* The text searcher for the editor.
*/
lateinit var searcher: IDEEditorSearcher
/**
* The signature help window for the editor.
*/
val signatureHelpWindow: SignatureHelpWindow
get() {
return _signatureHelpWindow ?: SignatureHelpWindow(this)
.also { _signatureHelpWindow = it }
}
/**
* The diagnostic window for the editor.
*/
val diagnosticWindow: DiagnosticWindow
get() {
return _diagnosticWindow ?: DiagnosticWindow(this)
.also { _diagnosticWindow = it }
}
companion object {
private const val SELECTION_CHANGE_DELAY = 500L
private val log = ILogger.newInstance("IDEEditor")
/**
* Create input type flags for the editor.
*/
fun createInputTypeFlags(): Int {
var flags = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE or
EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS
if (visiblePasswordFlag) {
flags = flags or EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
}
return flags
}
}
init {
initEditor()
}
override fun getFile(): File? {
return this.file
}
/**
* Set the file for this editor.
*/
fun setFile(file: File?) {
this.file = file
file?.also {
dispatchDocumentOpenEvent()
}
}
override fun isModified(): Boolean {
return this.isModified
}
override fun setSelection(position: Position) {
setSelection(position.line, position.column)
}
override fun setSelection(start: Position, end: Position) {
if (!isValidPosition(start, true) || !isValidPosition(end, true)) {
log.warn("Invalid selection range: start=$start end=$end")
return
}
setSelectionRegion(start.line, start.column, end.line, end.column)
}
override fun getCursorLSPRange(): Range {
val end = cursor.right().let {
Position(line = it.line, column = it.column, index = it.index)
}
return Range(cursorLSPPosition, end)
}
override fun getCursorLSPPosition(): Position {
return cursor.left().let {
Position(line = it.line, column = it.column, index = it.index)
}
}
override fun validateRange(range: Range) {
val start = range.start
val end = range.end
val text = text
val lineCount = text.lineCount
start.line = 0.coerceAtLeast(start.line).coerceAtMost(lineCount - 1)
start.column = 0.coerceAtLeast(start.column).coerceAtMost(text.getColumnCount(start.line))
end.line = 0.coerceAtLeast(end.line).coerceAtMost(lineCount - 1)
end.column = 0.coerceAtLeast(end.column).coerceAtMost(text.getColumnCount(end.line))
}
override fun isValidRange(range: Range?, allowColumnEqual: Boolean): Boolean {
if (range == null) {
return false
}
val start = range.start
val end = range.end
return isValidPosition(start, allowColumnEqual)
// make sure start position is before end position
&& isValidPosition(end, allowColumnEqual) && start < end
}
override fun isValidPosition(position: Position?, allowColumnEqual: Boolean): Boolean {
return if (position == null) {
false
} else isValidLine(position.line) &&
isValidColumn(position.line, position.column, allowColumnEqual)
}
override fun isValidLine(line: Int): Boolean {
return line >= 0 && line < text.lineCount
}
override fun isValidColumn(line: Int, column: Int, allowEqual: Boolean): Boolean {
val columnCount = text.getColumnCount(line)
return column >= 0 && (column < columnCount || allowEqual && column == columnCount)
}
override fun append(text: CharSequence?): Int {
val content = getText()
if (lineCount <= 0) {
return 0
}
val line = lineCount - 1
var col = content.getColumnCount(line)
if (col < 0) {
col = 0
}
content.insert(line, col, text)
return line
}
@UiThread
override fun replaceContent(newContent: CharSequence?) {
val lastLine = text.lineCount - 1
val lastColumn = text.getColumnCount(lastLine)
text.replace(0, 0, lastLine, lastColumn, newContent ?: "")
}
override fun goToEnd() {
val line = text.lineCount - 1
setSelection(line, 0)
}
override fun setLanguageServer(server: ILanguageServer?) {
this.languageServer = server
server?.also {
this.languageClient = it.client
snippetController.apply {
fileVariableResolver = FileVariableResolver(this@IDEEditor)
workspaceVariableResolver = WorkspaceVariableResolver()
}
}
}
override fun setLanguageClient(client: ILanguageClient?) {
this.languageClient = client
}
override fun executeCommand(command: Command?) {
if (command == null) {
log.warn("Cannot execute command in editor. Command is null.")
return
}
log.info(String.format("Executing command '%s' for completion item.", command.title))
when (command.command) {
Command.TRIGGER_COMPLETION -> {
val completion = getComponent(EditorAutoCompletion::class.java)
completion.requireCompletion()
}
Command.TRIGGER_PARAMETER_HINTS -> signatureHelp()
Command.FORMAT_CODE -> formatCodeAsync()
}
}
override fun signatureHelp() {
val languageServer = this.languageServer ?: return
val file = this.file ?: return
this.languageClient ?: return
editorScope.launch {
val params = SignatureHelpParams(file.toPath(), cursorLSPPosition)
val help = languageServer.signatureHelp(params)
withContext(Dispatchers.Main) {
showSignatureHelp(help)
}
}
}
override fun showSignatureHelp(help: SignatureHelp?) {
signatureHelpWindow.setupAndDisplay(help)
}
override fun findDefinition() {
val languageServer = this.languageServer ?: return
val file = file ?: return
launchAsyncWithProgress(string.msg_finding_definition) { flashbar, cancelChecker ->
val params = DefinitionParams(file.toPath(), cursorLSPPosition)
val result = languageServer.findDefinition(params, cancelChecker)
onFindDefinitionResult(flashbar, result)
}
}
override fun findReferences() {
val languageServer = this.languageServer ?: return
val file = file ?: return
launchAsyncWithProgress(string.msg_finding_references) { flashbar, cancelChecker ->
delay(3000L)
val params = ReferenceParams(file.toPath(), cursorLSPPosition, true)
val result = languageServer.findReferences(params, cancelChecker)
onFindReferencesResult(flashbar, result)
}
}
override fun expandSelection() {
val languageServer = this.languageServer ?: return
val file = file ?: return
launchAsyncWithProgress(string.please_wait) { flashbar, _ ->
val params = ExpandSelectionParams(file.toPath(), cursorLSPRange)
val result = languageServer.expandSelection(params)
withContext(Dispatchers.Main) {
flashbar.dismiss()
setSelection(result)
}
}
}
override fun ensureWindowsDismissed() {
if (_diagnosticWindow?.isShowing == true) {
_diagnosticWindow?.dismiss()
}
if (_signatureHelpWindow?.isShowing == true) {
_signatureHelpWindow?.dismiss()
}
if (_actionsMenu?.isShowing == true) {
_actionsMenu?.dismiss()
}
}
// not overridable!
final override fun <T : EditorBuiltinComponent?> replaceComponent(
clazz: Class<T>,
replacement: T & Any
) {
super.replaceComponent(clazz, replacement)
}
// not overridable
final override fun <T : EditorBuiltinComponent?> getComponent(clazz: Class<T>): T & Any {
return super.getComponent(clazz)
}
override fun release() {
ensureWindowsDismissed()
super.release()
snippetController.apply {
(fileVariableResolver as? AbstractSnippetVariableResolver?)?.close()
(workspaceVariableResolver as? AbstractSnippetVariableResolver?)?.close()
fileVariableResolver = null
workspaceVariableResolver = null
}
_actionsMenu?.destroy()
_actionsMenu = null
languageServer = null
languageClient = null
if (EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().unregister(this)
}
try {
if (editorScope.isActive) {
editorScope.cancel("Editor is releasing resources.")
}
} catch (e: Exception) {
log.error("Failed to close coroutine dispatcher", e)
}
}
override fun getSearcher(): EditorSearcher {
return this.searcher
}
override fun getExtraArguments(): Bundle {
return super.getExtraArguments().apply {
putString(IEditor.KEY_FILE, file?.absolutePath)
}
}
override fun ensurePositionVisible(line: Int, column: Int, noAnimation: Boolean) {
super.ensurePositionVisible(line, column, !isEnsurePosAnimEnabled || noAnimation)
}
override fun getTabWidth(): Int {
return tabSize
}
override fun beginSearchMode() {
throw UnsupportedOperationException(
"Search ActionMode is not supported. Use CodeEditorView.beginSearch() instead.")
}
override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
if (!gainFocus) {
ensureWindowsDismissed()
}
}
/**
* Analyze the opened file and publish the diagnostics result.
*/
open fun analyze() {
if (editorLanguage !is IDELanguage) {
return
}
val languageServer = languageServer ?: return
val file = file ?: return
editorScope.launch {
val result = languageServer.analyze(file.toPath())
languageClient?.publishDiagnostics(result)
}
}
/**
* Mark this editor as NOT modified.
*/
open fun markUnmodified() {
this.isModified = false
}
/**
* Mark this editor as modified.
*/
open fun markModified() {
this.isModified = true
}
/**
* Notify the language server that the file in this editor is about to be closed.
*/
open fun notifyClose() {
file ?: run {
log.info("Cannot notify language server. File is null.")
return
}
dispatchDocumentCloseEvent()
_actionsMenu?.unsubscribeEvents()
selectionChangeRunner?.also {
selectionChangeHandler.removeCallbacks(it)
}
selectionChangeRunner = null
ensureWindowsDismissed()
}
/**
* Called when this editor is selected and visible to the user.
*/
open fun onEditorSelected() {
file ?: return
dispatchDocumentSelectedEvent()
}
/**
* Dispatches the [DocumentSaveEvent] for this editor.
*/
open fun dispatchDocumentSaveEvent() {
if (getFile() == null) {
return
}
val saveEvent = DocumentSaveEvent(getFile()!!.toPath())
EventBus.getDefault().post(saveEvent)
}
/**
* Called when the color scheme has been invalidated. This usually happens when the user reloads
* the color schemes.
*/
@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN_ORDERED)
open fun onColorSchemeInvalidated(event: ColorSchemeInvalidatedEvent?) {
val file = getFile() ?: return
setupLanguage(file)
}
/**
* Setup the editor language for the given [file].
*
* This applies a proper [Language] and the color scheme to the editor.
*/
open fun setupLanguage(file: File?) {
if (file == null) {
return
}
val language = createLanguage(file)
val extension = file.extension
if (language is TreeSitterLanguage) {
readScheme(context, extension,
Consumer { scheme: SchemeAndroidIDE? ->
applyTreeSitterLang(language, extension, scheme)
})
} else {
setEditorLanguage(language)
}
}
/**
* Applies the given [TreeSitterLanguage] and the [color scheme][scheme] for the given [file type][type].
*/
open fun applyTreeSitterLang(language: TreeSitterLanguage?, type: String,
scheme: SchemeAndroidIDE?
) {
applyTreeSitterLang(language as Language?, type, scheme)
}
private fun applyTreeSitterLang(
language: Language?,
type: String,
scheme: SchemeAndroidIDE?
) {
var finalScheme = if (scheme != null) {
scheme
} else {
log.error("Failed to read current color scheme")
SchemeAndroidIDE.newInstance(context)
}
if (finalScheme is IDEColorScheme &&
finalScheme.getLanguageScheme(type) == null
) {
log.warn("Color scheme does not support file type '$type'")
finalScheme = SchemeAndroidIDE.newInstance(context)
}
if (finalScheme is DynamicColorScheme) {
finalScheme.apply(context)
}
colorScheme = finalScheme!!
setEditorLanguage(language)
}
private fun createLanguage(file: File): Language {
if (!file.isFile) {
return EmptyLanguage()
}
val tsLang = TreeSitterLanguageProvider.forFile(file, context)
if (tsLang != null) {
return tsLang
}
return when (FileUtils.getFileExtension(file)) {
"gradle" -> GroovyLanguage()
"c", "h", "cc", "cpp", "cxx" -> CppLanguage()
else -> EmptyLanguage()
}
}
/**
* Update the file for this editor. This does not dispatch the [DocumentOpenEvent].
*/
open fun updateFile(file: File?) {
this.file = file
}
/**
* Initialize the editor.
*/
protected open fun initEditor() {
_actionsMenu = EditorActionsMenu(this).also {
it.init()
}
searcher = IDEEditorSearcher(this)
colorScheme = SchemeAndroidIDE.newInstance(context)
inputType = createInputTypeFlags()
val window = EditorCompletionWindow(this)
window.setAdapter(CompletionListAdapter())
replaceComponent(EditorAutoCompletion::class.java, window)
getComponent(EditorTextActionWindow::class.java).isEnabled = false
subscribeEvent(ContentChangeEvent::class.java) { event, _ ->
file ?: return@subscribeEvent
editorScope.launch {
dispatchDocumentChangeEvent(event)
checkForSignatureHelp(event)
}
}
subscribeEvent(SelectionChangeEvent::class.java) { _, _ ->
if (_diagnosticWindow?.isShowing == true) {
_diagnosticWindow?.dismiss()
}
selectionChangeRunner?.also {
selectionChangeHandler.removeCallbacks(it)
selectionChangeHandler.postDelayed(it, SELECTION_CHANGE_DELAY)
}
}
EventBus.getDefault().register(this)
}
protected open fun launchAsyncWithProgress(
message: Int,
action: suspend CoroutineScope.(flashbar: Flashbar, cancelChecker: ICancelChecker) -> Unit
): Job {
val cancelChecker = object : ICancelChecker.Default() {
var job: Job? = null
override fun cancel() {
job?.cancel("Cancelled by user")
job = null
super.cancel()
}
}
return (context as Activity).flashProgress({
configureFlashbar(this, string.msg_finding_definition, cancelChecker)
}) { flashbar ->
return@flashProgress editorScope.launch {
cancelChecker.job = coroutineContext[Job]
action(flashbar, cancelChecker)
}
}!!
}
protected open suspend fun onFindDefinitionResult(
flashbar: Flashbar,
result: DefinitionResult?,
) = withContext(Dispatchers.Main) {
flashbar.dismiss()
val languageClient = languageClient ?: run {
log.error("No language client found to handle the definitions result")
return@withContext
}
if (result == null) {
log.error("Invalid definitions result from language server (null)")
flashError(string.msg_no_definition)
return@withContext
}
val locations = result.locations
if (locations.isEmpty()) {
log.error("No definitions found")
flashError(string.msg_no_definition)
return@withContext
}
if (locations.size != 1) {
languageClient.showLocations(locations)
return@withContext
}
val (file1, range) = locations[0]
if (DocumentUtils.isSameFile(file1, getFile()!!.toPath())) {
setSelection(range)
return@withContext
}
languageClient.showDocument(
ShowDocumentParams(file1, range)
)
}
protected open suspend fun onFindReferencesResult(
flashbar: Flashbar,
result: ReferenceResult?
) = withContext(Dispatchers.Main) {
flashbar.dismiss()
flashbar.dismiss()
val languageClient = languageClient ?: run {
log.error("No language client found to handle the references result")
return@withContext
}
if (result == null) {
log.error("Invalid references result from language server (null)")
flashError(string.msg_no_references)
return@withContext
}
val locations = result.locations
if (locations.isEmpty()) {
log.error("No references found")
flashError(string.msg_no_references)
return@withContext
}
if (result.locations.size == 1) {
val (file, range) = result.locations[0]
if (DocumentUtils.isSameFile(file, getFile()!!.toPath())) {
setSelection(range)
return@withContext
}
}
languageClient.showLocations(result.locations)
}
protected open fun dispatchDocumentOpenEvent() {
val file = this.file ?: return
this.fileVersion = 0
val openEvent = DocumentOpenEvent(
file.toPath(),
text.toString(),
fileVersion
)
// Notify FileManager first
onDocumentOpen(openEvent)
EventBus.getDefault().post(openEvent)
}
protected open fun dispatchDocumentChangeEvent(event: ContentChangeEvent) {
val file = file?.toPath() ?: return
var type = ChangeType.INSERT
if (event.action == ContentChangeEvent.ACTION_DELETE) {
type = ChangeType.DELETE
} else if (event.action == ContentChangeEvent.ACTION_SET_NEW_TEXT) {
type = ChangeType.NEW_TEXT
}
var changeDelta = if (type == ChangeType.NEW_TEXT) 0 else event.changedText.length
if (type == ChangeType.DELETE) {
changeDelta = -changeDelta
}
val start = event.changeStart
val end = event.changeEnd
val changeRange = Range(Position(start.line, start.column, start.index),
Position(end.line, end.column, end.index))
val changedText = event.changedText.toString()
val changeEvent = DocumentChangeEvent(file, changedText, text.toString(),
++fileVersion, type, changeDelta, changeRange)
// Notify FileManager first
onDocumentContentChange(changeEvent)
EventBus.getDefault().post(changeEvent)
}
protected open fun dispatchDocumentSelectedEvent() {
val file = file ?: return
val selectedEvent = DocumentSelectedEvent(file.toPath())
EventBus.getDefault().post(selectedEvent)
}
protected open fun dispatchDocumentCloseEvent() {
val file = file ?: return
val closeEvent = DocumentCloseEvent(file.toPath(), cursorLSPRange)
// Notify FileManager first
onDocumentClose(closeEvent)
EventBus.getDefault().post(closeEvent)
}
/**
* Checks if the content change event should trigger signature help. Signature help trigger
* characters are :
*
*
* * `'('` (parentheses)
* * `','` (comma)
*
*
* @param event The content change event.
*/
private fun checkForSignatureHelp(event: ContentChangeEvent) {
if (languageServer == null) {
return
}
val changeLength = event.changedText.length
if (event.action != ContentChangeEvent.ACTION_INSERT || changeLength in 1..2) {
// changeLength will be 2 as '(' and ')' are inserted at the same time
return
}
val ch = event.changedText[0]
if (ch == '(' || ch == ',') {
signatureHelp()
}
}
private fun configureFlashbar(
builder: Flashbar.Builder,
@StringRes message: Int,
cancelChecker: ICancelChecker
) {
builder.message(message)
.negativeActionText(android.R.string.cancel)
.negativeActionTapListener { bar: Flashbar ->
cancelChecker.cancel()
bar.dismiss()
}
}
private suspend fun Flashbar.dismissOnUiThread() {
withContext(Dispatchers.Main) {
dismiss()
}
}
}
@@ -19,20 +19,16 @@ 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.GradleProject;
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.Objects;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
/**
* Thread-safe implementation of {@link ILanguageServerRegistry}.
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* This file is part of AndroidIDE.
*
@@ -31,83 +30,75 @@
* 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;
package com.itsaky.androidide.lsp.api
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.lsp.models.CodeFormatResult;
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.ExpandSelectionParams;
import com.itsaky.androidide.lsp.models.FormatCodeParams;
import com.itsaky.androidide.lsp.models.LSPFailure;
import com.itsaky.androidide.lsp.models.ReferenceParams;
import com.itsaky.androidide.lsp.models.ReferenceResult;
import com.itsaky.androidide.lsp.models.SignatureHelp;
import com.itsaky.androidide.lsp.models.SignatureHelpParams;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.progress.ICancelChecker;
import com.itsaky.androidide.projects.api.Project;
import java.nio.file.Path;
import java.util.Collections;
import com.itsaky.androidide.lsp.models.CodeFormatResult
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.ExpandSelectionParams
import com.itsaky.androidide.lsp.models.FormatCodeParams
import com.itsaky.androidide.lsp.models.LSPFailure
import com.itsaky.androidide.lsp.models.ReferenceParams
import com.itsaky.androidide.lsp.models.ReferenceResult
import com.itsaky.androidide.lsp.models.SignatureHelp
import com.itsaky.androidide.lsp.models.SignatureHelpParams
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.progress.ICancelChecker
import com.itsaky.androidide.projects.api.Project
import java.nio.file.Path
/**
* A language server provides API for providing functions related to a specific file type.
*
* @author Akash Yadav
*/
public interface ILanguageServer {
interface ILanguageServer {
/**
* Get the unique language server ID.
*
* @return The server ID.
*/
String getServerId();
val serverId: String?
/**
* Called by client to notify the server to shutdown. Language servers must release all the
* resources in use.
*
* <p>After this is called, clients must re-initialize the server.
*
* After this is called, clients must re-initialize the server.
*/
void shutdown();
fun shutdown()
/**
* Set the client to whom notifications and events must be sent.
*
* @param client The client to set.
*/
void connectClient(@Nullable ILanguageClient client);
fun connectClient(client: ILanguageClient?)
/**
* Get the instance of the language client connected to this server.
*
* @return The language client.
*/
@Nullable
ILanguageClient getClient();
val client: ILanguageClient?
/**
* Apply settings to the language server. Its up to the language server how it applies these
* settings to the language service providers.
*
* @param settings The new settings to use. Pass {@code null} to use default settings.
* @param settings The new settings to use. Pass `null` to use default settings.
*/
void applySettings(@Nullable IServerSettings settings);
fun applySettings(settings: IServerSettings?)
/**
* Setup this language server with the given project. Servers are not expected to keep a reference
* to the provided project. Instead, use
* {@link com.itsaky.androidide.projects.IProjectManager#getRootProject getRootProject()} to
* [getRootProject()][com.itsaky.androidide.projects.IProjectManager.rootProject] to
* obtain the project instance.
*
* @param project The initialized project.
*/
void setupWithProject(@NonNull Project project);
fun setupWithProject(project: Project)
/**
* Compute code completions for the given completion params.
@@ -116,8 +107,7 @@ public interface ILanguageServer {
* @param cancelChecker
* @return The completion provider.
*/
@NonNull
CompletionResult complete(CompletionParams params, ICancelChecker cancelChecker);
fun complete(params: CompletionParams?, cancelChecker: ICancelChecker?): CompletionResult
/**
* Find references using the given params.
@@ -126,8 +116,7 @@ public interface ILanguageServer {
* @param cancelChecker
* @return The result of the computation.
*/
@NonNull
ReferenceResult findReferences(@NonNull ReferenceParams params, ICancelChecker cancelChecker);
suspend fun findReferences(params: ReferenceParams, cancelChecker: ICancelChecker?): ReferenceResult
/**
* Find definition using the given params.
@@ -136,8 +125,7 @@ public interface ILanguageServer {
* @param cancelChecker
* @return The result of the computation.
*/
@NonNull
DefinitionResult findDefinition(@NonNull DefinitionParams params, ICancelChecker cancelChecker);
suspend fun findDefinition(params: DefinitionParams, cancelChecker: ICancelChecker?): DefinitionResult
/**
* Request the server to provide an expanded selection range for the current selection.
@@ -145,8 +133,7 @@ public interface ILanguageServer {
* @param params The params for computing the expanded selection range.
* @return The expanded range or same selection range if computation was failed.
*/
@NonNull
Range expandSelection(@NonNull ExpandSelectionParams params);
suspend fun expandSelection(params: ExpandSelectionParams): Range
/**
* Compute signature help with the given params.
@@ -154,18 +141,16 @@ public interface ILanguageServer {
* @param params The params to compute signature help.
* @return The signature help.
*/
@NonNull
SignatureHelp signatureHelp(@NonNull SignatureHelpParams params);
suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp
/**
* Analyze the given file and provide diagnostics from the analyze result.
*
* @param file The file to analyze.
* @return The diagnostic result. Points to {@link DiagnosticResult#NO_UPDATE} if no diagnotic
* @return The diagnostic result. Points to [DiagnosticResult.NO_UPDATE] if no diagnotic
* items are available.
*/
@NonNull
DiagnosticResult analyze(@NonNull Path file);
suspend fun analyze(file: Path): DiagnosticResult
/**
* Format the given source code input.
@@ -173,18 +158,17 @@ public interface ILanguageServer {
* @param params The code formatting parameters.
* @return The formatted source.
*/
@NonNull
default CodeFormatResult formatCode(FormatCodeParams params) {
return new CodeFormatResult(false, Collections.emptyList());
fun formatCode(params: FormatCodeParams?): CodeFormatResult {
return CodeFormatResult(false, mutableListOf())
}
/**
* Handle failure caused by LSP
*
* @param failure {@link LSPFailure} describing the failure.
* @return <code>true</code> if the failure was handled. <code>false</code> otherwise.
* @param failure [LSPFailure] describing the failure.
* @return `true` if the failure was handled. `false` otherwise.
*/
default boolean handleFailure(LSPFailure failure) {
return false;
fun handleFailure(failure: LSPFailure?): Boolean {
return false
}
}
}
@@ -1,378 +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.java;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
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.DocumentSelectedEvent;
import com.itsaky.androidide.javac.services.fs.CacheFSInfoSingleton;
import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider;
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.compiler.SourceFileManager;
import com.itsaky.androidide.lsp.java.models.JavaServerSettings;
import com.itsaky.androidide.lsp.java.providers.CodeFormatProvider;
import com.itsaky.androidide.lsp.java.providers.CompletionProvider;
import com.itsaky.androidide.lsp.java.providers.DefinitionProvider;
import com.itsaky.androidide.lsp.java.providers.JavaDiagnosticProvider;
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.providers.snippet.JavaSnippetRepository;
import com.itsaky.androidide.lsp.java.utils.AnalyzeTimer;
import com.itsaky.androidide.lsp.java.utils.CancelChecker;
import com.itsaky.androidide.lsp.models.CodeFormatResult;
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.ExpandSelectionParams;
import com.itsaky.androidide.lsp.models.FormatCodeParams;
import com.itsaky.androidide.lsp.models.LSPFailure;
import com.itsaky.androidide.lsp.models.ReferenceParams;
import com.itsaky.androidide.lsp.models.ReferenceResult;
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.models.Range;
import com.itsaky.androidide.progress.ICancelChecker;
import com.itsaky.androidide.projects.FileManager;
import com.itsaky.androidide.projects.IProjectManager;
import com.itsaky.androidide.projects.api.GradleProject;
import com.itsaky.androidide.projects.api.ModuleProject;
import com.itsaky.androidide.projects.api.Project;
import com.itsaky.androidide.utils.DocumentUtils;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.VMUtils;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
public class JavaLanguageServer implements ILanguageServer {
public static final String SERVER_ID = "java";
private final CompletionProvider completionProvider;
private final JavaDiagnosticProvider diagnosticProvider;
private ILanguageClient client;
private IServerSettings settings;
private Path selectedFile;
private final AnalyzeTimer timer = new AnalyzeTimer(this::analyzeSelected);
private CachedCompletion cachedCompletion;
private static final ILogger LOG = ILogger.newInstance("JavaLanguageServer");
public JavaLanguageServer() {
this.completionProvider = new CompletionProvider();
this.diagnosticProvider = new JavaDiagnosticProvider();
this.cachedCompletion = CachedCompletion.EMPTY;
applySettings(getSettings());
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this);
}
JavaSnippetRepository.INSTANCE.init();
}
public IServerSettings getSettings() {
if (settings == null) {
settings = JavaServerSettings.getInstance();
}
return settings;
}
@Override
public String getServerId() {
return SERVER_ID;
}
@Override
public void shutdown() {
JavaCompilerProvider.getInstance().destroy();
SourceFileManager.clearCache();
CacheFSInfoSingleton.INSTANCE.clearCache();
CachingJarFileSystemProvider.INSTANCE.clearCache();
EventBus.getDefault().unregister(this);
timer.cancel();
}
@Override
public void connectClient(@Nullable ILanguageClient client) {
this.client = client;
}
@Nullable
@Override
public ILanguageClient getClient() {
return this.client;
}
@Override
public void applySettings(@Nullable IServerSettings settings) {
this.settings = settings;
}
@Override
public void setupWithProject(@NonNull final Project project) {
LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu.INSTANCE);
// Once we have project initialized
// Destory the NO_MODULE_COMPILER instance
JavaCompilerService.NO_MODULE_COMPILER.destroy();
// Clear cached file managers
SourceFileManager.clearCache();
// Clear cached JAR file system for R.jar
// Using the cached instance will result in completions not being updated for updated resources
// TODO Clearing caches for JAR files ending with '/R.jar' is probably not a good idea
// Maybe this could be improved by using data from the AndroidModule project model
CachingJarFileSystemProvider.INSTANCE.clearCachesForPaths(path -> path.endsWith("/R.jar"));
// Clear cached module-specific compilers
JavaCompilerProvider.getInstance().destroy();
// Cache classpath locations
for (final GradleProject subModule : project.getSubProjects()) {
if (!(subModule instanceof ModuleProject)) {
continue;
}
SourceFileManager.forModule(((ModuleProject) subModule));
}
startOrRestartAnalyzeTimer();
}
@NonNull
@Override
public CompletionResult complete(final CompletionParams params, ICancelChecker cancelChecker) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (compiler == null
|| !settings.completionsEnabled()
|| !this.completionProvider.canComplete(params.getFile())) {
return CompletionResult.EMPTY;
}
if (diagnosticProvider.isAnalyzing()) {
LOG.warn("Cancelling source code analysis due to completion request");
diagnosticProvider.cancel();
}
this.completionProvider.reset(
compiler, this.settings, this.cachedCompletion, this::updateCachedCompletion);
return this.completionProvider.complete(params);
}
@NonNull
@Override
public ReferenceResult findReferences(@NonNull ReferenceParams params,
ICancelChecker cancelChecker) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (!settings.referencesEnabled() || compiler == null) {
return new ReferenceResult(Collections.emptyList());
}
return new ReferenceProvider(compiler, cancelChecker).findReferences(params);
}
@NonNull
@Override
public DefinitionResult findDefinition(@NonNull DefinitionParams params,
ICancelChecker cancelChecker) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (!settings.definitionsEnabled() || compiler == null) {
return new DefinitionResult(Collections.emptyList());
}
return new DefinitionProvider(compiler, getSettings(), cancelChecker).findDefinition(params);
}
@NonNull
@Override
public Range expandSelection(@NonNull ExpandSelectionParams params) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (!settings.smartSelectionsEnabled() || compiler == null) {
return params.getSelection();
}
return new JavaSelectionProvider(compiler).expandSelection(params);
}
@NonNull
@Override
public SignatureHelp signatureHelp(@NonNull SignatureHelpParams params) {
final JavaCompilerService compiler = getCompiler(params.getFile());
if (!settings.signatureHelpEnabled() || compiler == null) {
return new SignatureHelp(Collections.emptyList(), -1, -1);
}
return new SignatureProvider(compiler).signatureHelp(params);
}
@NonNull
@Override
public DiagnosticResult analyze(@NonNull Path file) {
if (!settings.diagnosticsEnabled() || !DocumentUtils.isJavaFile(file)) {
return DiagnosticResult.NO_UPDATE;
}
final JavaCompilerService compiler = getCompiler(file);
if (!settings.codeAnalysisEnabled() || compiler == null) {
return DiagnosticResult.NO_UPDATE;
}
return this.diagnosticProvider.analyze(file);
}
@NonNull
@Override
public CodeFormatResult formatCode(FormatCodeParams params) {
return new CodeFormatProvider(getSettings()).format(params);
}
@Override
public boolean handleFailure(final LSPFailure failure) {
//noinspection SwitchStatementWithTooFewBranches
switch (failure.getType()) {
case COMPLETION:
if (CancelChecker.isCancelled(failure.getError())) {
return true;
}
JavaCompilerProvider.getInstance().destroy();
return true;
}
return false;
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public JavaCompilerService getCompiler(Path file) {
if (!DocumentUtils.isJavaFile(file)) {
return JavaCompilerService.NO_MODULE_COMPILER;
}
final var root = IProjectManager.getInstance().getRootProject();
if (root == null) {
return JavaCompilerService.NO_MODULE_COMPILER;
}
final ModuleProject module = root.findModuleForFile(file);
if (module == null) {
return JavaCompilerService.NO_MODULE_COMPILER;
}
return JavaCompilerProvider.get(module);
}
private void updateCachedCompletion(CachedCompletion cachedCompletion) {
Objects.requireNonNull(cachedCompletion);
this.cachedCompletion = cachedCompletion;
}
private void startOrRestartAnalyzeTimer() {
if (VMUtils.isJvm()) {
return;
}
if (!this.timer.isStarted()) {
this.timer.start();
} else {
this.timer.restart();
}
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@SuppressWarnings("unused")
public void onContentChange(@NonNull DocumentChangeEvent event) {
if (!DocumentUtils.isJavaFile(event.getChangedFile())) {
return;
}
// TODO Find an alternative to efficiently update changeDelta in JavaCompilerService instance
JavaCompilerService.NO_MODULE_COMPILER.onDocumentChange(event);
final ModuleProject module = IProjectManager.getInstance()
.findModuleForFile(event.getChangedFile());
if (module != null) {
final JavaCompilerService compiler = JavaCompilerProvider.get(module);
compiler.onDocumentChange(event);
}
startOrRestartAnalyzeTimer();
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@SuppressWarnings("unused")
public void onFileSelected(@NonNull DocumentSelectedEvent event) {
this.selectedFile = event.getSelectedFile();
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@SuppressWarnings("unused")
public void onFileOpened(@NonNull DocumentOpenEvent event) {
this.selectedFile = event.getOpenedFile();
startOrRestartAnalyzeTimer();
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@SuppressWarnings("unused")
public void onFileClosed(@NonNull DocumentCloseEvent event) {
if (this.diagnosticProvider != null) {
this.diagnosticProvider.clearTimestamp(event.getClosedFile());
}
if (FileManager.INSTANCE.getActiveDocumentCount() == 0) {
this.selectedFile = null;
this.timer.cancel();
}
}
private void analyzeSelected() {
if (this.selectedFile == null) {
return;
}
CompletableFuture.supplyAsync(() -> analyze(selectedFile))
.whenComplete(
((diagnostics, throwable) -> {
if (client != null) {
client.publishDiagnostics(diagnostics);
}
}));
}
}
@@ -0,0 +1,322 @@
/*
* 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 androidx.annotation.RestrictTo
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.DocumentSelectedEvent
import com.itsaky.androidide.javac.services.fs.CacheFSInfoSingleton
import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider.clearCache
import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider.clearCachesForPaths
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.compiler.SourceFileManager
import com.itsaky.androidide.lsp.java.models.JavaServerSettings
import com.itsaky.androidide.lsp.java.providers.CodeFormatProvider
import com.itsaky.androidide.lsp.java.providers.CompletionProvider
import com.itsaky.androidide.lsp.java.providers.DefinitionProvider
import com.itsaky.androidide.lsp.java.providers.JavaDiagnosticProvider
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.providers.snippet.JavaSnippetRepository.init
import com.itsaky.androidide.lsp.java.utils.AnalyzeTimer
import com.itsaky.androidide.lsp.java.utils.CancelChecker.Companion.isCancelled
import com.itsaky.androidide.lsp.models.CodeFormatResult
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.ExpandSelectionParams
import com.itsaky.androidide.lsp.models.FailureType
import com.itsaky.androidide.lsp.models.FormatCodeParams
import com.itsaky.androidide.lsp.models.LSPFailure
import com.itsaky.androidide.lsp.models.ReferenceParams
import com.itsaky.androidide.lsp.models.ReferenceResult
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.models.Range
import com.itsaky.androidide.progress.ICancelChecker
import com.itsaky.androidide.projects.FileManager.getActiveDocumentCount
import com.itsaky.androidide.projects.IProjectManager.Companion.getInstance
import com.itsaky.androidide.projects.api.ModuleProject
import com.itsaky.androidide.projects.api.Project
import com.itsaky.androidide.utils.DocumentUtils
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.VMUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.nio.file.Path
import java.util.Objects
import java.util.concurrent.CompletableFuture
class JavaLanguageServer : ILanguageServer {
private val completionProvider: CompletionProvider = CompletionProvider()
private val diagnosticProvider: JavaDiagnosticProvider?
override var client: ILanguageClient? = null
private set
private var _settings: IServerSettings? = null
private var selectedFile: Path? = null
private val timer = AnalyzeTimer { analyzeSelected() }
private var cachedCompletion: CachedCompletion
val settings: IServerSettings
get() {
return _settings ?: JavaServerSettings.getInstance()
.also { _settings = it }
}
override val serverId: String = SERVER_ID
companion object {
const val SERVER_ID = "ide.lsp.java"
private val LOG = ILogger.newInstance("JavaLanguageServer")
}
init {
diagnosticProvider = JavaDiagnosticProvider()
cachedCompletion = CachedCompletion.EMPTY
applySettings(JavaServerSettings.getInstance())
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this)
}
init()
}
override fun shutdown() {
JavaCompilerProvider.getInstance().destroy()
SourceFileManager.clearCache()
CacheFSInfoSingleton.clearCache()
clearCache()
EventBus.getDefault().unregister(this)
timer.cancel()
}
override fun connectClient(client: ILanguageClient?) {
this.client = client
}
override fun applySettings(settings: IServerSettings?) {
this._settings = settings
}
override fun setupWithProject(project: Project) {
LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu)
// Once we have project initialized
// Destory the NO_MODULE_COMPILER instance
JavaCompilerService.NO_MODULE_COMPILER.destroy()
// Clear cached file managers
SourceFileManager.clearCache()
// Clear cached JAR file system for R.jar
// Using the cached instance will result in completions not being updated for updated resources
// TODO Clearing caches for JAR files ending with '/R.jar' is probably not a good idea
// Maybe this could be improved by using data from the AndroidModule project model
clearCachesForPaths { path: String -> path.endsWith("/R.jar") }
// Clear cached module-specific compilers
JavaCompilerProvider.getInstance().destroy()
// Cache classpath locations
for (subModule in project.subProjects) {
if (subModule !is ModuleProject) {
continue
}
SourceFileManager.forModule(subModule)
}
startOrRestartAnalyzeTimer()
}
override fun complete(params: CompletionParams?,
cancelChecker: ICancelChecker?): CompletionResult {
val compiler = getCompiler(params!!.file)
if (!settings.completionsEnabled() || !completionProvider.canComplete(params.file)
) {
return CompletionResult.EMPTY
}
if (diagnosticProvider!!.isAnalyzing()) {
LOG.warn("Cancelling source code analysis due to completion request")
diagnosticProvider.cancel()
}
completionProvider.reset(
compiler, settings, cachedCompletion) { cachedCompletion: CachedCompletion ->
updateCachedCompletion(cachedCompletion)
}
return completionProvider.complete(params)
}
override suspend fun findReferences(params: ReferenceParams,
cancelChecker: ICancelChecker?): ReferenceResult {
val compiler = getCompiler(params.file)
return if (!settings.referencesEnabled()) {
ReferenceResult(emptyList())
} else ReferenceProvider(compiler, cancelChecker).findReferences(params)
}
override suspend fun findDefinition(params: DefinitionParams,
cancelChecker: ICancelChecker?): DefinitionResult {
val compiler = getCompiler(params.file)
return if (!settings.definitionsEnabled()) {
DefinitionResult(emptyList())
} else DefinitionProvider(compiler, settings, cancelChecker).findDefinition(params)
}
override suspend fun expandSelection(params: ExpandSelectionParams): Range {
val compiler = getCompiler(params.file)
return if (!settings.smartSelectionsEnabled()) {
params.selection
} else JavaSelectionProvider(compiler).expandSelection(params)
}
override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp {
val compiler = getCompiler(params.file)
return if (!settings.signatureHelpEnabled()) {
SignatureHelp(emptyList(), -1, -1)
} else SignatureProvider(compiler).signatureHelp(params)
}
override suspend fun analyze(file: Path): DiagnosticResult {
if (!settings.diagnosticsEnabled() || !DocumentUtils.isJavaFile(file)) {
return DiagnosticResult.NO_UPDATE
}
return if (!settings.codeAnalysisEnabled()) {
DiagnosticResult.NO_UPDATE
} else diagnosticProvider!!.analyze(file)
}
override fun formatCode(params: FormatCodeParams?): CodeFormatResult {
return CodeFormatProvider(settings).format(params)
}
override fun handleFailure(failure: LSPFailure?): Boolean {
return when (failure!!.type) {
FailureType.COMPLETION -> {
if (isCancelled(failure.error)) {
return true
}
JavaCompilerProvider.getInstance().destroy()
true
}
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun getCompiler(file: Path?): JavaCompilerService {
if (!DocumentUtils.isJavaFile(file)) {
return JavaCompilerService.NO_MODULE_COMPILER
}
val root = getInstance().rootProject
?: return JavaCompilerService.NO_MODULE_COMPILER
val module = root.findModuleForFile(file!!) ?: return JavaCompilerService.NO_MODULE_COMPILER
return JavaCompilerProvider.get(module)
}
private fun updateCachedCompletion(cachedCompletion: CachedCompletion) {
Objects.requireNonNull(cachedCompletion)
this.cachedCompletion = cachedCompletion
}
private fun startOrRestartAnalyzeTimer() {
if (VMUtils.isJvm()) {
return
}
if (!timer.isStarted) {
timer.start()
} else {
timer.restart()
}
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@Suppress("unused")
fun onContentChange(event: DocumentChangeEvent) {
if (!DocumentUtils.isJavaFile(event.changedFile)) {
return
}
// TODO Find an alternative to efficiently update changeDelta in JavaCompilerService instance
JavaCompilerService.NO_MODULE_COMPILER.onDocumentChange(event)
val module = getInstance()
.findModuleForFile(event.changedFile)
if (module != null) {
val compiler = JavaCompilerProvider.get(module)
compiler.onDocumentChange(event)
}
startOrRestartAnalyzeTimer()
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@Suppress("unused")
fun onFileSelected(event: DocumentSelectedEvent) {
selectedFile = event.selectedFile
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@Suppress("unused")
fun onFileOpened(event: DocumentOpenEvent) {
selectedFile = event.openedFile
startOrRestartAnalyzeTimer()
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@Suppress("unused")
fun onFileClosed(event: DocumentCloseEvent) {
diagnosticProvider?.clearTimestamp(event.closedFile)
if (getActiveDocumentCount() == 0) {
selectedFile = null
timer.cancel()
}
}
private fun analyzeSelected() {
if (selectedFile == null || client == null) {
return
}
CoroutineScope(Dispatchers.Default).launch {
val result = analyze(selectedFile!!)
withContext(Dispatchers.Main) {
client?.publishDiagnostics(result)
}
}
}
}
@@ -29,7 +29,7 @@ import org.junit.Ignore
object JavaLSPTest : LSPTest() {
val server by lazy {
ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID)
ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.serverId)
as JavaLanguageServer
}
@@ -43,7 +43,7 @@ object JavaLSPTest : LSPTest() {
ILanguageServerRegistry.getDefault().register(JavaLanguageServer())
}
override fun getServerId() = JavaLanguageServer.SERVER_ID
override fun getServerId() = JavaLanguageServer.serverId
fun getCompiler(): JavaCompilerService {
return JavaCompilerProvider.get(findAppModule()!!)
@@ -1,174 +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.xml;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent;
import com.itsaky.androidide.lsp.api.ICompletionProvider;
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.models.CodeFormatResult;
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.ExpandSelectionParams;
import com.itsaky.androidide.lsp.models.FormatCodeParams;
import com.itsaky.androidide.lsp.models.ReferenceParams;
import com.itsaky.androidide.lsp.models.ReferenceResult;
import com.itsaky.androidide.lsp.models.SignatureHelp;
import com.itsaky.androidide.lsp.models.SignatureHelpParams;
import com.itsaky.androidide.lsp.util.NoCompletionsProvider;
import com.itsaky.androidide.lsp.xml.models.XMLServerSettings;
import com.itsaky.androidide.lsp.xml.providers.AdvancedEditProvider;
import com.itsaky.androidide.lsp.xml.providers.CodeFormatProvider;
import com.itsaky.androidide.lsp.xml.providers.XmlCompletionProvider;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.progress.ICancelChecker;
import com.itsaky.androidide.projects.api.Project;
import com.itsaky.androidide.utils.DocumentUtils;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.Collections;
/**
* Language server implementation for XML files.
*
* @author Akash Yadav
*/
public class XMLLanguageServer implements ILanguageServer {
public static final String SERVER_ID = "xml";
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
private ILanguageClient client;
private IServerSettings settings;
public XMLLanguageServer() {
EventBus.getDefault().register(this);
}
@Override
public String getServerId() {
return SERVER_ID;
}
@Override
public void shutdown() {
if (EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().unregister(this);
}
}
@Override
public void connectClient(ILanguageClient client) {
this.client = client;
}
@Nullable
@Override
public ILanguageClient getClient() {
return this.client;
}
@Override
public void applySettings(IServerSettings settings) {
this.settings = settings;
}
@Override
public void setupWithProject(@NonNull final Project project) {}
@NonNull
@Override
public CompletionResult complete(final CompletionParams params, ICancelChecker cancelChecker) {
final ICompletionProvider completionProvider;
if (!getSettings().completionsEnabled()) {
completionProvider = new NoCompletionsProvider();
} else {
completionProvider = new XmlCompletionProvider(this.getSettings());
}
return completionProvider.complete(params);
}
@NonNull
public IServerSettings getSettings() {
if (settings == null) {
settings = XMLServerSettings.INSTANCE;
}
return settings;
}
@NonNull
@Override
public ReferenceResult findReferences(@NonNull ReferenceParams params,
ICancelChecker cancelChecker) {
return new ReferenceResult(Collections.emptyList());
}
@NonNull
@Override
public DefinitionResult findDefinition(@NonNull DefinitionParams params,
ICancelChecker cancelChecker) {
return new DefinitionResult(Collections.emptyList());
}
@NonNull
@Override
public Range expandSelection(@NonNull ExpandSelectionParams params) {
return params.getSelection();
}
@NonNull
@Override
public SignatureHelp signatureHelp(@NonNull SignatureHelpParams params) {
return new SignatureHelp(Collections.emptyList(), -1, -1);
}
@NonNull
@Override
public DiagnosticResult analyze(@NonNull Path file) {
return DiagnosticResult.NO_UPDATE;
}
@NonNull
@Override
public CodeFormatResult formatCode(FormatCodeParams params) {
return new CodeFormatProvider().format(params);
}
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void onDocumentChange(DocumentChangeEvent event) {
if (!DocumentUtils.isXmlFile(event.getChangedFile())) {
return;
}
AdvancedEditProvider.INSTANCE.onContentChange(event);
}
}
@@ -0,0 +1,146 @@
/*
* 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.xml
import androidx.annotation.RestrictTo
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent
import com.itsaky.androidide.lsp.api.ICompletionProvider
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.models.CodeFormatResult
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.ExpandSelectionParams
import com.itsaky.androidide.lsp.models.FormatCodeParams
import com.itsaky.androidide.lsp.models.LSPFailure
import com.itsaky.androidide.lsp.models.ReferenceParams
import com.itsaky.androidide.lsp.models.ReferenceResult
import com.itsaky.androidide.lsp.models.SignatureHelp
import com.itsaky.androidide.lsp.models.SignatureHelpParams
import com.itsaky.androidide.lsp.util.NoCompletionsProvider
import com.itsaky.androidide.lsp.xml.models.XMLServerSettings
import com.itsaky.androidide.lsp.xml.providers.AdvancedEditProvider.onContentChange
import com.itsaky.androidide.lsp.xml.providers.CodeFormatProvider
import com.itsaky.androidide.lsp.xml.providers.XmlCompletionProvider
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.progress.ICancelChecker
import com.itsaky.androidide.projects.api.Project
import com.itsaky.androidide.utils.DocumentUtils
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.nio.file.Path
/**
* Language server implementation for XML files.
*
* @author Akash Yadav
*/
class XMLLanguageServer : ILanguageServer {
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override var client: ILanguageClient? = null
private set
private var settings: IServerSettings? = null
override val serverId: String = SERVER_ID
init {
EventBus.getDefault().register(this)
}
override fun shutdown() {
if (EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().unregister(this)
}
}
override fun connectClient(client: ILanguageClient?) {
this.client = client
}
override fun applySettings(settings: IServerSettings?) {
this.settings = settings
}
override fun setupWithProject(project: Project) {}
override fun complete(params: CompletionParams?,
cancelChecker: ICancelChecker?): CompletionResult {
val completionProvider: ICompletionProvider
completionProvider = if (!getSettings().completionsEnabled()) {
NoCompletionsProvider()
} else {
XmlCompletionProvider(getSettings())
}
return completionProvider.complete(params)
}
fun getSettings(): IServerSettings {
if (settings == null) {
settings = XMLServerSettings
}
return settings!!
}
override suspend fun findReferences(params: ReferenceParams,
cancelChecker: ICancelChecker?): ReferenceResult {
return ReferenceResult(emptyList())
}
override suspend fun findDefinition(params: DefinitionParams,
cancelChecker: ICancelChecker?): DefinitionResult {
return DefinitionResult(emptyList())
}
override suspend fun expandSelection(params: ExpandSelectionParams): Range {
return params.selection
}
override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp {
return SignatureHelp(emptyList(), -1, -1)
}
override suspend fun analyze(file: Path): DiagnosticResult {
return DiagnosticResult.NO_UPDATE
}
override fun formatCode(params: FormatCodeParams?): CodeFormatResult {
return CodeFormatProvider().format(params)
}
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onDocumentChange(event: DocumentChangeEvent) {
if (!DocumentUtils.isXmlFile(event.changedFile)) {
return
}
onContentChange(event)
}
override fun handleFailure(failure: LSPFailure?): Boolean {
return super<ILanguageServer>.handleFailure(failure)
}
companion object {
const val SERVER_ID = "ide.lsp.xml"
}
}
@@ -79,7 +79,7 @@ object XMLLSPTest : LSPTest() {
}
override fun getServerId(): String {
return XMLLanguageServer.SERVER_ID
return XMLLanguageServer.serverId
}
override fun test() {}