mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
fix(editor): rewrite IDEEditor in Kotlin with Coroutines
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-7
@@ -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}.
|
||||
|
||||
+43
-59
@@ -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() {}
|
||||
|
||||
Reference in New Issue
Block a user