fix(editor): migrate IDELanguage to Kotlin

This commit is contained in:
Akash Yadav
2023-11-15 09:48:12 +05:30
parent 80cdc48795
commit 67b1388779
16 changed files with 232 additions and 175 deletions
@@ -16,6 +16,7 @@
*/
package com.itsaky.androidide.editor.language
import com.itsaky.androidide.editor.language.utils.CompletionHelper
import com.itsaky.androidide.lsp.api.ILanguageServer
import com.itsaky.androidide.lsp.models.CompletionItem
import com.itsaky.androidide.lsp.models.CompletionParams
@@ -27,11 +28,9 @@ import com.itsaky.androidide.models.Position
import com.itsaky.androidide.progress.ProcessCancelledException
import com.itsaky.androidide.utils.ILogger
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException
import io.github.rosemoe.sora.lang.completion.CompletionHelper
import io.github.rosemoe.sora.text.CharPosition
import io.github.rosemoe.sora.text.ContentReference
import java.nio.file.Path
import java.util.function.Predicate
/**
* Common implementation of completion provider which requests completions to provided language
@@ -39,10 +38,15 @@ import java.util.function.Predicate
*
* @author Akash Yadav
*/
class CommonCompletionProvider(private val server: ILanguageServer,
private val cancelChecker: CompletionCancelChecker) {
internal class CommonCompletionProvider(
private val server: ILanguageServer,
private val cancelChecker: CompletionCancelChecker
) {
private val log = ILogger.newInstance(javaClass.simpleName)
companion object {
private val log = ILogger.newInstance("CommonCompletionProvider")
}
/**
* Computes completion items using the provided language server instance.
@@ -53,18 +57,19 @@ class CommonCompletionProvider(private val server: ILanguageServer,
* @return The computed completion items. May return an empty list if the there was an error
* computing the completion items.
*/
fun complete(
content: ContentReference?,
inline fun complete(
content: ContentReference,
file: Path,
position: CharPosition,
prefixMatcher: Predicate<Char?>
prefixMatcher: (Char) -> Boolean
): List<CompletionItem> {
val completionResult =
try {
setupLookupForCompletion(file)
val prefix = CompletionHelper.computePrefix(content, position) { prefixMatcher.test(it) }
val prefix = CompletionHelper.computePrefix(content, position, prefixMatcher)
val params =
CompletionParams(Position(position.line, position.column, position.index), file, cancelChecker)
CompletionParams(Position(position.line, position.column, position.index), file,
cancelChecker)
params.content = content
params.prefix = prefix
server.complete(params)
@@ -1,134 +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.editor.language;
import android.os.Bundle;
import androidx.annotation.NonNull;
import com.itsaky.androidide.editor.api.IEditor;
import com.itsaky.androidide.editor.ui.IDECompletionPublisher;
import com.itsaky.androidide.lookup.Lookup;
import com.itsaky.androidide.lsp.api.ILanguageServer;
import com.itsaky.androidide.preferences.internal.EditorPreferencesKt;
import com.itsaky.androidide.progress.ICancelChecker;
import com.itsaky.androidide.utils.ILogger;
import io.github.rosemoe.sora.lang.Language;
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
import io.github.rosemoe.sora.lang.format.Formatter;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.ContentReference;
import java.nio.file.Paths;
import java.util.ArrayList;
/**
* Base class for language implementations in the IDE.
*
* @author Akash Yadav
*/
public abstract class IDELanguage implements Language {
private static final ILogger LOG = ILogger.newInstance("IDELanguage");
private Formatter formatter;
@Override
public void requireAutoComplete(
@NonNull final ContentReference content,
@NonNull final CharPosition position,
@NonNull final CompletionPublisher publisher,
@NonNull final Bundle extraArguments)
throws CompletionCancelledException {
try {
final var cancelChecker = new CompletionCancelChecker(publisher);
Lookup.getDefault().register(ICancelChecker.class, cancelChecker);
doComplete(content, position, publisher, cancelChecker, extraArguments);
} finally {
Lookup.getDefault().unregister(ICancelChecker.class);
}
}
private void doComplete(
final @NonNull ContentReference content,
final @NonNull CharPosition position,
final @NonNull CompletionPublisher publisher,
final @NonNull CompletionCancelChecker cancelChecker,
final @NonNull Bundle extraArguments) {
final var server = getLanguageServer();
if (server == null) {
LOG.warn("Cannot provide completions. No language server available.");
return;
}
final var path = extraArguments.getString(IEditor.KEY_FILE, null);
if (path == null) {
LOG.warn("Cannot provide completions. No file provided.");
return;
}
final var completionProvider = new CommonCompletionProvider(server, cancelChecker);
final var file = Paths.get(path);
final var completionItems =
completionProvider.complete(content, file, position, this::checkIsCompletionChar);
publisher.setUpdateThreshold(1);
((IDECompletionPublisher) publisher).addLSPItems(completionItems);
}
protected ILanguageServer getLanguageServer() {
return null;
}
/**
* Check if the given character is a completion character.
*
* @param c The character to check.
* @return <code>true</code> if the character is completion char, <code>false</code> otherwise.
*/
protected boolean checkIsCompletionChar(char c) {
return false;
}
@Override
public boolean useTab() {
return !EditorPreferencesKt.getUseSoftTab();
}
@NonNull
@Override
public Formatter getFormatter() {
if (formatter != null) {
return formatter;
}
return formatter = new LSPFormatter(getLanguageServer());
}
public int getTabSize() {
return EditorPreferencesKt.getTabSize();
}
@Override
public int getIndentAdvance(@NonNull final ContentReference content, final int line,
final int column) {
return getIndentAdvance(content.getLine(line).substring(0, column));
}
public int getIndentAdvance(@NonNull String line) {
return 0;
}
}
@@ -0,0 +1,124 @@
/*
* 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.language
import android.os.Bundle
import com.itsaky.androidide.editor.api.IEditor
import com.itsaky.androidide.editor.ui.IDECompletionPublisher
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.api.ILanguageServer
import com.itsaky.androidide.preferences.internal.tabSize
import com.itsaky.androidide.preferences.internal.useSoftTab
import com.itsaky.androidide.progress.ICancelChecker
import com.itsaky.androidide.utils.ILogger
import io.github.rosemoe.sora.lang.Language
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException
import io.github.rosemoe.sora.lang.completion.CompletionPublisher
import io.github.rosemoe.sora.lang.format.Formatter
import io.github.rosemoe.sora.text.CharPosition
import io.github.rosemoe.sora.text.ContentReference
import java.nio.file.Paths
/**
* Base class for language implementations in the IDE.
*
* @author Akash Yadav
*/
abstract class IDELanguage : Language {
private var formatter: Formatter? = null
protected open val languageServer: ILanguageServer?
get() = null
open fun getTabSize(): Int {
return tabSize
}
@Throws(CompletionCancelledException::class)
override fun requireAutoComplete(
content: ContentReference,
position: CharPosition,
publisher: CompletionPublisher,
extraArguments: Bundle
) {
try {
val cancelChecker = CompletionCancelChecker(publisher)
Lookup.getDefault().register(ICancelChecker::class.java, cancelChecker)
doComplete(content, position, publisher, cancelChecker, extraArguments)
} finally {
Lookup.getDefault().unregister(
ICancelChecker::class.java)
}
}
private fun doComplete(
content: ContentReference,
position: CharPosition,
publisher: CompletionPublisher,
cancelChecker: CompletionCancelChecker,
extraArguments: Bundle
) {
val server = languageServer
if (server == null) {
LOG.warn("Cannot provide completions. No language server available.")
return
}
val path = extraArguments.getString(IEditor.KEY_FILE, null)
if (path == null) {
LOG.warn("Cannot provide completions. No file provided.")
return
}
val completionProvider = CommonCompletionProvider(server, cancelChecker)
val file = Paths.get(path)
val completionItems = completionProvider.complete(content, file, position) { checkIsCompletionChar(it) }
publisher.setUpdateThreshold(1)
(publisher as IDECompletionPublisher).addLSPItems(completionItems)
}
/**
* Check if the given character is a completion character.
*
* @param c The character to check.
* @return `true` if the character is completion char, `false` otherwise.
*/
protected open fun checkIsCompletionChar(c: Char): Boolean {
return false
}
override fun useTab(): Boolean {
return !useSoftTab
}
override fun getFormatter(): Formatter {
return formatter ?: LSPFormatter(languageServer).also { formatter = it }
}
override fun getIndentAdvance(content: ContentReference, line: Int,
column: Int): Int {
return getIndentAdvance(content.getLine(line).substring(0, column))
}
open fun getIndentAdvance(line: String): Int {
return 0
}
companion object {
private val LOG = ILogger.newInstance("IDELanguage")
}
}
@@ -26,6 +26,7 @@ import com.itsaky.androidide.lsp.api.ILanguageServer
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.treesitter.java.TSLanguageJava
import io.github.rosemoe.sora.lang.Language.INTERRUPTION_LEVEL_SLIGHT
import io.github.rosemoe.sora.util.MyCharacter
import io.github.rosemoe.sora.widget.SymbolPairMatch
@@ -45,9 +46,8 @@ class JavaLanguage(context: Context) :
val FACTORY = Factory { JavaLanguage(it) }
}
override fun getLanguageServer(): ILanguageServer? {
return ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID)
}
override val languageServer: ILanguageServer?
get() = ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID)
override fun checkIsCompletionChar(c: Char): Boolean {
return MyCharacter.isJavaIdentifierPart(c) || c == '.'
@@ -20,6 +20,7 @@ package com.itsaky.androidide.editor.language.treesitter
import android.content.Context
import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguage.Factory
import com.itsaky.androidide.treesitter.json.TSLanguageJson
import io.github.rosemoe.sora.lang.Language.INTERRUPTION_LEVEL_STRONG
/**
* [TreeSitterLanguage] implementation for JSON files.
@@ -28,10 +29,13 @@ import com.itsaky.androidide.treesitter.json.TSLanguageJson
*/
class JsonLanguage(context: Context) :
TreeSitterLanguage(context, TSLanguageJson.getInstance(), TS_TYPE) {
companion object {
const val TS_TYPE = "json"
@JvmField val FACTORY = Factory { JsonLanguage(it) }
@JvmField
val FACTORY = Factory { JsonLanguage(it) }
}
override fun getInterruptionLevel(): Int {
@@ -18,7 +18,9 @@
package com.itsaky.androidide.editor.language.treesitter
import android.content.Context
import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguage.Factory
import com.itsaky.androidide.treesitter.kotlin.TSLanguageKotlin
import io.github.rosemoe.sora.lang.Language.INTERRUPTION_LEVEL_STRONG
/**
* [TreeSitterLanguage] implementation for Kotlin.
@@ -29,6 +31,7 @@ open class KotlinLanguage(context: Context) :
TreeSitterLanguage(context, TSLanguageKotlin.getInstance(), TS_TYPE_KT) {
companion object {
val FACTORY = Factory { KotlinLanguage(it) }
const val TS_TYPE_KT = "kt"
const val TS_TYPE_KTS = "kts"
@@ -27,6 +27,7 @@ import com.itsaky.androidide.editor.language.treesitter.internal.TSLanguageRegis
interface TSLanguageRegistry {
companion object {
@JvmStatic
val instance by lazy { TSLanguageRegistryImpl() }
}
@@ -33,6 +33,7 @@ import com.itsaky.androidide.treesitter.TSQueryCursor
import com.itsaky.androidide.treesitter.TSQueryMatch
import com.itsaky.androidide.utils.ILogger
import io.github.rosemoe.sora.editor.ts.TsTheme
import io.github.rosemoe.sora.lang.Language.INTERRUPTION_LEVEL_STRONG
import io.github.rosemoe.sora.lang.analysis.AnalyzeManager
import io.github.rosemoe.sora.text.ContentReference
import io.github.rosemoe.sora.text.TextUtils
@@ -78,7 +79,7 @@ abstract class TreeSitterLanguage(context: Context, lang: TSLanguage, type: Stri
}
open fun finalizeIndent(indent: Int): Int {
return indent * tabSize
return indent * getTabSize()
}
override fun getAnalyzeManager(): AnalyzeManager {
@@ -106,7 +107,7 @@ abstract class TreeSitterLanguage(context: Context, lang: TSLanguage, type: Stri
content.toString(),
line,
column,
decrementBy = TextUtils.countLeadingSpaceCount(content.getLine(line), tabSize)
decrementBy = TextUtils.countLeadingSpaceCount(content.getLine(line), getTabSize())
)
}
@@ -30,7 +30,7 @@ import java.io.Closeable
*/
class TreeSitterLanguageSpec
@JvmOverloads constructor(val spec: TsLanguageSpec,
indentsQueryScm: String = ""
indentsQueryScm: String = ""
) : Closeable {
// <editor-fold desc="Proxy properties">
@@ -38,7 +38,8 @@ class TreeSitterLanguageSpec
get() = spec.language
// </editor-fold>
val indentsQuery: TSQuery? = if (indentsQueryScm.isBlank()) TSQuery.EMPTY else TSQuery.create(language, indentsQueryScm)
val indentsQuery: TSQuery? = if (indentsQueryScm.isBlank()) TSQuery.EMPTY else TSQuery.create(
language, indentsQueryScm)
.let { if (it.canAccess()) it else null }
init {
@@ -23,6 +23,7 @@ import com.itsaky.androidide.lsp.api.ILanguageServer
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
import com.itsaky.androidide.lsp.xml.XMLLanguageServer
import com.itsaky.androidide.treesitter.xml.TSLanguageXml
import io.github.rosemoe.sora.lang.Language.INTERRUPTION_LEVEL_STRONG
import io.github.rosemoe.sora.util.MyCharacter
/**
@@ -33,6 +34,9 @@ import io.github.rosemoe.sora.util.MyCharacter
class XMLLanguage(context: Context) :
TreeSitterLanguage(context, lang = TSLanguageXml.getInstance(), type = TS_TYPE) {
override val languageServer: ILanguageServer?
get() = ILanguageServerRegistry.getDefault().getServer(XMLLanguageServer.SERVER_ID)
companion object {
const val TS_TYPE = "xml"
@@ -41,10 +45,6 @@ class XMLLanguage(context: Context) :
val FACTORY = Factory { XMLLanguage(it) }
}
override fun getLanguageServer(): ILanguageServer? {
return ILanguageServerRegistry.getDefault().getServer(XMLLanguageServer.SERVER_ID)
}
override fun checkIsCompletionChar(c: Char): Boolean {
return MyCharacter.isJavaIdentifierPart(c) || c == '<' || c == '/'
}
@@ -47,7 +47,7 @@ class TSLanguageRegistryImpl : TSLanguageRegistry {
fileType: String
): TreeSitterLanguage.Factory<T> {
return (registry[fileType] ?: throw TSLanguageRegistry.NotRegisteredException(fileType))
as TreeSitterLanguage.Factory<T>
as TreeSitterLanguage.Factory<T>
}
override fun destroy() {
@@ -42,14 +42,14 @@ object AnyOfPredicate : TreeSitterPredicate() {
override fun canHandle(steps: List<TsClientPredicateStep>): Boolean {
return steps.size > 4 &&
steps.let {
it[0].predicateType == TSQueryPredicateStep.Type.String &&
it[1].predicateType == TSQueryPredicateStep.Type.Capture &&
it[it.lastIndex].predicateType == TSQueryPredicateStep.Type.Done &&
it.subList(2, it.lastIndex - 1).all { step ->
step.predicateType == TSQueryPredicateStep.Type.String
}
}
steps.let {
it[0].predicateType == TSQueryPredicateStep.Type.String &&
it[1].predicateType == TSQueryPredicateStep.Type.Capture &&
it[it.lastIndex].predicateType == TSQueryPredicateStep.Type.Done &&
it.subList(2, it.lastIndex - 1).all { step ->
step.predicateType == TSQueryPredicateStep.Type.String
}
}
}
override fun doPredicateInternal(
@@ -43,12 +43,12 @@ object EqualPredicate : TreeSitterPredicate() {
override fun canHandle(steps: List<TsClientPredicateStep>): Boolean {
return steps.size == 4 &&
steps[0].predicateType == TSQueryPredicateStep.Type.String &&
steps[1].predicateType == TSQueryPredicateStep.Type.Capture &&
steps[2].predicateType.let {
it == TSQueryPredicateStep.Type.Capture || it == TSQueryPredicateStep.Type.String
} &&
steps[3].predicateType == TSQueryPredicateStep.Type.Done
steps[0].predicateType == TSQueryPredicateStep.Type.String &&
steps[1].predicateType == TSQueryPredicateStep.Type.Capture &&
steps[2].predicateType.let {
it == TSQueryPredicateStep.Type.Capture || it == TSQueryPredicateStep.Type.String
} &&
steps[3].predicateType == TSQueryPredicateStep.Type.Done
}
override fun doPredicateInternal(
@@ -63,7 +63,7 @@ object EqualPredicate : TreeSitterPredicate() {
predicateSteps[2].let {
check(
it.predicateType == TSQueryPredicateStep.Type.String ||
it.predicateType == TSQueryPredicateStep.Type.Capture
it.predicateType == TSQueryPredicateStep.Type.Capture
) {
"Second predicate step of #eq? predicate must be a string or a capture"
}
@@ -28,7 +28,8 @@ import io.github.rosemoe.sora.editor.ts.predicate.TsSyntheticCaptureContainer
*
* @author Akash Yadav
*/
open class InvertingPredicate(override val name: String, private val predicate: TreeSitterPredicate) :
open class InvertingPredicate(override val name: String,
private val predicate: TreeSitterPredicate) :
TreeSitterPredicate() {
override fun canHandle(steps: List<TsClientPredicateStep>): Boolean {
@@ -70,8 +70,8 @@ abstract class TreeSitterPredicate : TsPredicate {
if (
predicateSteps.isEmpty() ||
predicateSteps[0].content != "${name}?" ||
!canHandle(predicateSteps)
predicateSteps[0].content != "${name}?" ||
!canHandle(predicateSteps)
) {
return PredicateResult.UNHANDLED
}
@@ -0,0 +1,51 @@
/*
* 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.language.utils
import io.github.rosemoe.sora.text.CharPosition
import io.github.rosemoe.sora.text.ContentReference
/**
* Utility functions for completion providers.
*
* @author Akash Yadav
*/
object CompletionHelper {
/**
* Searches backward on the line, with the given checker to check chars.
* Returns the longest text that matches the requirement.
*
* This is a variant of [CompletionHelper.computePrefix][io.github.rosemoe.sora.lang.completion.CompletionHelper.computePrefix]
* which inlines the predicate for better performance.
*/
inline fun computePrefix(
ref: ContentReference, pos: CharPosition,
checker: (Char) -> Boolean
): String {
var begin = pos.column
val line = ref.getLine(pos.line)
while (begin > 0) {
if (!checker(line[begin - 1])) {
break
}
begin--
}
return line.substring(begin, pos.column)
}
}