diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 81dd6d74b..cfb7b3815 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -17,6 +17,11 @@ jobs: build: name: Build debug APK runs-on: ubuntu-latest + env: + SIGNING_STORE_BIN: ${{ secrets.SIGNING_STORE_BIN }} + SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} + SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} + SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} steps: - name: Cancel Previous Runs uses: styfle/cancel-workflow-action@0.13.0 diff --git a/.gitignore b/.gitignore index ecd80c7fb..f5d8f7022 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,7 @@ lint/generated/ lint/outputs/ lint/tmp/ # lint/reports/ + +# Local Signing Config +signing.properties +signing.keystore \ No newline at end of file diff --git a/README.es.md b/README.es.md index 75e15856a..55ffd804f 100644 --- a/README.es.md +++ b/README.es.md @@ -54,7 +54,7 @@ Consulte las [combinaciones de teclas admitidas](./keybindings.md).
GeneralAppearance -ProblemIndicator +AutoCompletion
## Para empezar diff --git a/README.jp.md b/README.jp.md index 0b849c7e3..e0dc09e49 100644 --- a/README.jp.md +++ b/README.jp.md @@ -51,7 +51,7 @@ sora-editor は効率的な Android コードエディターです
GeneralAppearance -ProblemIndicator +AutoCompletion
## 始めに diff --git a/README.zh-cn.md b/README.zh-cn.md index ae434f106..b315570a3 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -48,7 +48,7 @@ sora-editor是一款高效的安卓代码编辑器
GeneralAppearance -ProblemIndicator +AutoCompletion
## 讨论 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ba369ca29..5f446dddf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,43 +27,53 @@ plugins { } android { + namespace = "io.github.rosemoe.sora.app" + defaultConfig { applicationId = "io.github.rosemoe.sora.app" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - versionCode = Versions.versionCode - versionName = Versions.versionName + "-" + System.currentTimeMillis() } + signingConfigs { - create("general") { - storeFile = file("../debug.jks") - storePassword = "114514" - keyAlias = "debug" - keyPassword = "114514" - enableV1Signing = true - enableV2Signing = true - } + AppSigning.getAppSigningConfigOptional(project) + .onSuccess { + create("general") { + storeFile = it.storeFile + storePassword = it.storePassword + keyAlias = it.keyAlias + keyPassword = it.keyPassword + + enableV1Signing = true + enableV2Signing = true + } + + buildTypes.forEach { buildType -> + buildType.signingConfig = signingConfigs.getByName("general") + } + }.onFailure { + logger.error("Failed to get signing config. Signing configuration is left as is.") + } } - buildTypes { - release { - isMinifyEnabled = false - signingConfig = signingConfigs.getByName("general") - proguardFiles("proguard-rules.pro") - } - debug { + + for (buildType in buildTypes) { + buildType.apply { isMinifyEnabled = false - signingConfig = signingConfigs.getByName("general") proguardFiles("proguard-rules.pro") } } + compileOptions { isCoreLibraryDesugaringEnabled = true } + androidResources { additionalParameters.add("--warn-manifest-validation") } + buildFeatures { viewBinding = true } + packaging { resources.pickFirsts.addAll( arrayOf( @@ -75,7 +85,6 @@ android { ) ) } - namespace = "io.github.rosemoe.sora.app" } dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ac033326..48338e8a1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,7 @@ android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:name=".EditorApplication" android:theme="@style/AppTheme"> diff --git a/app/src/main/assets/samples/sample.txt b/app/src/main/assets/samples/sample.txt index 32811859f..fbf1b0106 100644 --- a/app/src/main/assets/samples/sample.txt +++ b/app/src/main/assets/samples/sample.txt @@ -1,25 +1,5 @@ /* - * sora-editor - the awesome code editor for Android - * https://github.com/Rosemoe/sora-editor - * Copyright (C) 2020-2022 Rosemoe - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 - * USA - * - * Please contact Rosemoe by email 2073412493@qq.com if you need - * additional information or have any questions + * Sample Java source file. */ package io.github.rosemoe.sora.util; diff --git a/app/src/main/java/io/github/rosemoe/sora/app/BaseEditorActivity.java b/app/src/main/java/io/github/rosemoe/sora/app/BaseEditorActivity.java index 2fa4c827c..621ae8e50 100644 --- a/app/src/main/java/io/github/rosemoe/sora/app/BaseEditorActivity.java +++ b/app/src/main/java/io/github/rosemoe/sora/app/BaseEditorActivity.java @@ -47,7 +47,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { setContentView(binding.getRoot()); setSupportActionBar(binding.activityToolbar); - UtilsKt.applyEdgeToEdgeForViews(binding.toolbarContainer, binding.getRoot()); + UtilsKt.applyEdgeToEdge(this, binding.toolbarContainer, binding.getRoot()); var supportActionBar = getSupportActionBar(); if (supportActionBar != null) { diff --git a/app/src/main/java/io/github/rosemoe/sora/app/EditorApplication.kt b/app/src/main/java/io/github/rosemoe/sora/app/EditorApplication.kt new file mode 100644 index 000000000..6f6a577ab --- /dev/null +++ b/app/src/main/java/io/github/rosemoe/sora/app/EditorApplication.kt @@ -0,0 +1,37 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2026 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.app + +import android.app.Application +import com.google.android.material.color.DynamicColors + +class EditorApplication : Application() { + + override fun onCreate() { + super.onCreate() + DynamicColors.applyToActivitiesIfAvailable(this) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt b/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt index 28b422cdb..42280fa50 100644 --- a/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt +++ b/app/src/main/java/io/github/rosemoe/sora/app/MainActivity.kt @@ -23,13 +23,13 @@ ******************************************************************************/ package io.github.rosemoe.sora.app -import android.app.AlertDialog import android.content.DialogInterface import android.content.res.Configuration import android.graphics.Typeface import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.PersistableBundle import android.text.Editable import android.text.TextWatcher import android.util.Log @@ -41,7 +41,10 @@ import android.widget.PopupMenu import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts.GetContent import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat import androidx.lifecycle.lifecycleScope +import androidx.savedstate.write +import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.github.dingyi222666.monarch.languages.JavaLanguage import io.github.dingyi222666.monarch.languages.KotlinLanguage import io.github.dingyi222666.monarch.languages.PythonLanguage @@ -165,7 +168,7 @@ class MainActivity : AppCompatActivity() { setContentView(binding.root) setSupportActionBar(binding.activityToolbar) - applyEdgeToEdgeForViews(binding.toolbarContainer, binding.root) + applyEdgeToEdge(this, binding.toolbarContainer, binding.root) val typeface = Typeface.createFromAsset(assets, "JetBrainsMono-Regular.ttf") @@ -284,8 +287,37 @@ class MainActivity : AppCompatActivity() { ) editor.setEditorLanguage(language) - // Open assets file - openAssetsFile("samples/sample.txt") + val savedText = savedInstanceState?.getString("text") + if (savedText != null) { + val textSize = savedInstanceState.getFloat("font.size") + if (textSize > 0f) { + editor.textSizePx = textSize + } + editor.setText(savedText) + val left = savedInstanceState.getInt("position.left").coerceIn(0, editor.text.length) + val right = savedInstanceState.getInt("position.right").coerceIn(0, editor.text.length) + val leftPos = editor.text.indexer.getCharPosition(left.coerceAtMost(right)) + val rightPos = editor.text.indexer.getCharPosition(right.coerceAtLeast(left)) + editor.setSelectionRegion( + leftPos.line, + leftPos.column, + rightPos.line, + rightPos.column, + false + ) + editor.scroller.startScroll( + savedInstanceState.getInt("scroll.x"), + savedInstanceState.getInt("scroll.y"), + 0, + 0, + 0 + ) + editor.scroller.abortAnimation() + editor.postInvalidate() + } else { + // Open assets file + openAssetsFile("samples/sample.txt") + } updatePositionText() updateBtnState() @@ -556,10 +588,9 @@ class MainActivity : AppCompatActivity() { if ("big_sample" !in name) { binding.editor.inlayHints = InlayHintsContainer().also { - it.add(TextInlayHint(28, 0, "unit:")) - it.add(TextInlayHint(28, 7, "open")) - it.add(TextInlayHint(28, 22, "^class")) - it.add(ColorInlayHint(30, 30, ConstColor("#f44336"))) + it.add(ColorInlayHint(10, 30, ConstColor("#f44336"))) + it.add(TextInlayHint(29, 7, "^DigitTens")) + it.add(TextInlayHint(100, 1, "^Numbers")) } } } @@ -623,9 +654,16 @@ class MainActivity : AppCompatActivity() { binding.positionDisplay.text = text } - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - switchThemeIfRequired(this, binding.editor) + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.write { + putString("text", binding.editor.text.toString()) + putFloat("font.size", binding.editor.textSizePx) + putInt("position.left", binding.editor.cursor.left) + putInt("position.right", binding.editor.cursor.right) + putInt("scroll.x", binding.editor.offsetX) + putInt("scroll.y", binding.editor.offsetY) + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -647,13 +685,13 @@ class MainActivity : AppCompatActivity() { R.id.open_test_activity -> startActivity() R.id.open_lsp_activity -> { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - AlertDialog.Builder(this) + MaterialAlertDialogBuilder(this) .setTitle(getString(R.string.not_supported)) .setMessage(getString(R.string.dialog_api_warning_msg)) .setPositiveButton(android.R.string.ok, null) .show() } else { - AlertDialog.Builder(this) + MaterialAlertDialogBuilder(this) .setTitle(R.string.dialog_lsp_entry_title) .setMessage(R.string.dialog_lsp_entry_msg) .setPositiveButton(R.string.choice_yes) { _, _ -> @@ -698,7 +736,7 @@ class MainActivity : AppCompatActivity() { getString(R.string.center), getString(R.string.bottom) ) - AlertDialog.Builder(this) + MaterialAlertDialogBuilder(this) .setTitle(R.string.fixed) .setSingleChoiceItems(themes, -1) { dialog: DialogInterface, which: Int -> editor.lnPanelPositionMode = LineInfoPanelPositionMode.FOLLOW @@ -781,7 +819,7 @@ class MainActivity : AppCompatActivity() { "Ubuntu-Regular.ttf", "Roboto-Regular.ttf" ) - AlertDialog.Builder(this) + MaterialAlertDialogBuilder(this) .setTitle(android.R.string.dialog_alert_title) .setSingleChoiceItems(fonts, -1) { dialog: DialogInterface, which: Int -> if (which in assetsPaths.indices) { @@ -807,7 +845,7 @@ class MainActivity : AppCompatActivity() { getString(R.string.bottom_left), getString(R.string.bottom_right) ) - AlertDialog.Builder(this) + MaterialAlertDialogBuilder(this) .setTitle(R.string.fixed) .setSingleChoiceItems(themes, -1) { dialog: DialogInterface, which: Int -> editor.lnPanelPositionMode = LineInfoPanelPositionMode.FIXED @@ -905,7 +943,7 @@ class MainActivity : AppCompatActivity() { "Monarch TypeScript" to "source.typescript" ) - AlertDialog.Builder(this) + MaterialAlertDialogBuilder(this) .setTitle(R.string.switch_language) .setSingleChoiceItems(languageOptions, -1) { dialog: DialogInterface, which: Int -> when (val selected = languageOptions[which]) { @@ -991,7 +1029,7 @@ class MainActivity : AppCompatActivity() { "Solarized(Dark) for TM(VSCode)", "TM theme from file" ) - AlertDialog.Builder(this) + MaterialAlertDialogBuilder(this) .setTitle(R.string.color_scheme) .setSingleChoiceItems(themes, -1) { dialog: DialogInterface, which: Int -> when (which) { diff --git a/app/src/main/java/io/github/rosemoe/sora/app/Utils.kt b/app/src/main/java/io/github/rosemoe/sora/app/Utils.kt index cffea4320..ca1c68219 100644 --- a/app/src/main/java/io/github/rosemoe/sora/app/Utils.kt +++ b/app/src/main/java/io/github/rosemoe/sora/app/Utils.kt @@ -28,11 +28,12 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.content.res.Configuration -import android.os.Build -import android.os.Build.VERSION.SDK_INT import android.view.View +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import io.github.rosemoe.sora.langs.monarch.MonarchColorScheme import io.github.rosemoe.sora.langs.textmate.TextMateColorScheme import io.github.rosemoe.sora.langs.textmate.registry.ThemeRegistry @@ -40,8 +41,11 @@ import io.github.rosemoe.sora.widget.CodeEditor import io.github.rosemoe.sora.widget.schemes.EditorColorScheme import io.github.rosemoe.sora.widget.schemes.SchemeDarcula +fun Context.isNightMode(): Boolean = + (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + fun switchThemeIfRequired(context: Context, editor: CodeEditor) { - if ((context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES) { + if (context.isNightMode()) { if (editor.colorScheme is TextMateColorScheme) { ThemeRegistry.getInstance().setTheme("darcula") } else if (editor.colorScheme is MonarchColorScheme) { @@ -66,16 +70,16 @@ inline fun Context.startActivity() { } /** - * Adjust the top padding of view to the height of status bar due to edge-to-edge since API 35 + * Enable edge-to-edge and apply paddings */ -fun applyEdgeToEdgeForViews(paddingView: View, rootView: View) { - if (SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - ViewCompat.setOnApplyWindowInsetsListener(paddingView) { v, insets -> - val statusBar = insets.getInsets(WindowInsetsCompat.Type.statusBars()) - v.setPadding(0, statusBar.top, 0, 0) - val ime = insets.getInsets(WindowInsetsCompat.Type.ime()) - rootView.setPadding(0, 0, 0, ime.bottom) - insets - } +fun applyEdgeToEdge(activity: AppCompatActivity, topPaddingView: View, rootView: View) { + activity.enableEdgeToEdge() + ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets -> + val systemWindowInsets = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime() + ) + topPaddingView.updatePadding(top = systemWindowInsets.top) + rootView.updatePadding(bottom = systemWindowInsets.bottom) + insets } } diff --git a/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspLanguageServerService.kt b/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspLanguageServerService.kt index 672af9cf0..3d59f9ec4 100644 --- a/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspLanguageServerService.kt +++ b/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspLanguageServerService.kt @@ -32,16 +32,23 @@ import android.os.IBinder import android.util.Log import com.tang.vscode.LuaLanguageClient import com.tang.vscode.LuaLanguageServer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.eclipse.lsp4j.jsonrpc.Launcher import java.util.concurrent.Future -import kotlin.concurrent.thread class LspLanguageServerService : Service() { - - private lateinit var future: Future private lateinit var socket: LocalServerSocket - private lateinit var socketClient: LocalSocket + + private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + private var acceptJob: Job? = null companion object { private const val TAG = "LanguageServer" @@ -51,60 +58,65 @@ class LspLanguageServerService : Service() { return null } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - // Only used in test - thread { + if (!::socket.isInitialized) { socket = LocalServerSocket("lua-lsp") + } - Log.d(TAG, "Starting socket on address ${socket.localSocketAddress}") - - socketClient = socket.accept() + if (acceptJob == null) { + acceptJob = serviceScope.launch { + Log.d(TAG, "Starting accept loop on address ${socket.localSocketAddress.namespace}") + while (true) { + try { + val socketClient = socket.accept() + Log.d(TAG, "Accepted client $socketClient") + launch { handleClient(socketClient) } + } catch (e: Exception) { + Log.d(TAG, "Error accepting connection", e) + break + } + } + } + } - runCatching { + return START_STICKY + } - val server = LuaLanguageServer(); + private suspend fun handleClient(socketClient: LocalSocket) { + var future: Future? = null - val inputStream = socketClient.inputStream - val outputStream = socketClient.outputStream + val server = LuaLanguageServer() - val launcher = Launcher.createLauncher( - server, LuaLanguageClient::class.java, - inputStream, outputStream - ) + runCatching { - val remoteProxy = launcher.remoteProxy + val inputStream = socketClient.inputStream + val outputStream = socketClient.outputStream - server.connect(remoteProxy); + val launcher = Launcher.createLauncher( + server, LuaLanguageClient::class.java, + inputStream, outputStream + ) - future = launcher.startListening() + server.connect(launcher.remoteProxy) - // Blocking call - future.get() + future = launcher.startListening() - /* XMLServerLauncher.launch( - socketClient.getInputStream(), - socketClient.getOutputStream() - ).get()*/ - }.onFailure { - Log.d(TAG, "Unexpected exception is thrown in the Language Server Thread.", it) + // Suspend until the session ends, without blocking the dispatcher thread + withContext(Dispatchers.IO) { + future?.get() } - - socketClient.close() - - socket.close() + }.onFailure { + Log.d(TAG, "Unexpected exception in Language Server client thread.", it) } - return START_STICKY + Log.d(TAG, "Closed client $socketClient") + future?.cancel(true) + runCatching { socketClient.close() } } override fun onDestroy() { - future.cancel(true) - socket.close() - socketClient.close() + serviceScope.cancel() + runCatching { socket.close() } super.onDestroy() } - - } - diff --git a/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspTestActivity.kt b/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspTestActivity.kt index 65ae47abc..e8cd1bc52 100644 --- a/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspTestActivity.kt +++ b/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspTestActivity.kt @@ -28,6 +28,7 @@ import android.content.Intent import android.content.res.Configuration import android.graphics.Typeface import android.os.Bundle +import android.util.Log import android.view.Menu import android.view.MenuItem import android.widget.Toast @@ -43,13 +44,13 @@ import io.github.rosemoe.sora.langs.textmate.registry.ThemeRegistry import io.github.rosemoe.sora.langs.textmate.registry.dsl.languages import io.github.rosemoe.sora.langs.textmate.registry.model.ThemeModel import io.github.rosemoe.sora.langs.textmate.registry.provider.AssetsFileResolver -import io.github.rosemoe.sora.lsp.client.connection.LocalSocketStreamConnectionProvider -import io.github.rosemoe.sora.lsp.client.languageserver.serverdefinition.CustomLanguageServerDefinition +import io.github.rosemoe.sora.lsp.client.languageserver.ServerStatus import io.github.rosemoe.sora.lsp.client.languageserver.serverdefinition.languageServerDefinition import io.github.rosemoe.sora.lsp.client.languageserver.wrapper.EventHandler import io.github.rosemoe.sora.lsp.editor.LspEditor +import io.github.rosemoe.sora.lsp.editor.LspEditorEventListener +import io.github.rosemoe.sora.lsp.editor.LspEditorStatus import io.github.rosemoe.sora.lsp.editor.LspProject -import io.github.rosemoe.sora.lsp.editor.diagnostics.DiagnosticsContainer import io.github.rosemoe.sora.lsp.editor.text.MarkdownCodeHighlighterRegistry import io.github.rosemoe.sora.lsp.editor.text.withEditorHighlighter import io.github.rosemoe.sora.lsp.events.EventType @@ -60,9 +61,9 @@ import io.github.rosemoe.sora.widget.component.EditorAutoCompletion import io.github.rosemoe.sora.widget.component.EditorTextActionWindow import io.github.rosemoe.sora.widget.getComponent import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.eclipse.lsp4j.DiagnosticCapabilities import org.eclipse.lsp4j.DiagnosticRegistrationOptions import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams import org.eclipse.lsp4j.InitializeResult @@ -71,7 +72,6 @@ import org.eclipse.lsp4j.WorkspaceFolder import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent import org.eclipse.lsp4j.jsonrpc.messages.Either import org.eclipse.lsp4j.services.LanguageServer -import org.eclipse.tm4e.core.internal.theme.Theme import org.eclipse.tm4e.core.registry.IThemeSource import java.io.FileOutputStream import java.lang.ref.WeakReference @@ -81,6 +81,9 @@ class LspTestActivity : BaseEditorActivity() { private lateinit var lspEditor: LspEditor private lateinit var lspProject: LspProject + private val projectPath by lazy(LazyThreadSafetyMode.NONE) { + externalCacheDir?.resolve("testProject")?.absolutePath ?: "" + } private lateinit var rootMenu: Menu @@ -151,7 +154,6 @@ class LspTestActivity : BaseEditorActivity() { editor.editable = false } - val projectPath = externalCacheDir?.resolve("testProject")?.absolutePath ?: "" startService( Intent(this@LspTestActivity, LspLanguageServerService::class.java) @@ -192,20 +194,27 @@ class LspTestActivity : BaseEditorActivity() { var connected: Boolean - // delay(Timeout[Timeouts.INIT].toLong()) //wait for server start - - try { - lspEditor.connectWithTimeout() - - lspEditor.requestManager?.didChangeWorkspaceFolders( + lspEditor.eventListener = LspEditorEventListener { _, new, _ -> + if (new != LspEditorStatus.CONNECTED) { + return@LspEditorEventListener + } + lspEditor.requestManager.didChangeWorkspaceFolders( DidChangeWorkspaceFoldersParams().apply { this.event = WorkspaceFoldersChangeEvent().apply { added = - listOf(WorkspaceFolder("file://$projectPath/std/Lua53", "MyLuaProject")) + listOf( + WorkspaceFolder( + "file://$projectPath/std/Lua53", + "MyLuaProject" + ) + ) } } ) + } + try { + lspEditor.connectWithTimeout() connected = true } catch (e: Exception) { @@ -303,6 +312,18 @@ class LspTestActivity : BaseEditorActivity() { } else { editor.formatCodeAsync() } + } else if (id == R.id.restart_server) { + lifecycleScope.launch(Dispatchers.Main) { + val languageServerWrapper = + lspProject.getLanguageServerWrapper("lua", "lua-lsp") ?: return@launch + toast("Restarting language server...") + languageServerWrapper.restartAndReconnect() + if (languageServerWrapper.status == ServerStatus.INITIALIZED) { + toast("Initialized language server") + } else { + toast("Unable to connect language server") + } + } } return super.onOptionsItemSelected(item) } @@ -321,17 +342,21 @@ class LspTestActivity : BaseEditorActivity() { class EventListener( - private val activityRef: WeakReference + private val activityRef: WeakReference, ) : EventHandler.EventListener { override fun initialize(server: LanguageServer?, result: InitializeResult) { - activityRef.get()?.apply { + val activity = activityRef.get() ?: return + activity.apply { runOnUiThread { rootMenu.findItem(R.id.code_format).isEnabled = result.capabilities.documentFormattingProvider != null } } } - } + override fun onStatusChange(newStatus: ServerStatus, oldStatus: ServerStatus) { + Log.d("LSP_TEST_ACTIVITY", "New status: $newStatus; Old status: $oldStatus") + } + } } diff --git a/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspTestJavaActivity.java b/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspTestJavaActivity.java index fa89152eb..f8f1215f9 100644 --- a/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspTestJavaActivity.java +++ b/app/src/main/java/io/github/rosemoe/sora/app/lsp/LspTestJavaActivity.java @@ -70,6 +70,7 @@ import io.github.rosemoe.sora.langs.textmate.registry.model.ThemeModel; import io.github.rosemoe.sora.langs.textmate.registry.provider.AssetsFileResolver; import io.github.rosemoe.sora.lsp.client.connection.LocalSocketStreamConnectionProvider; +import io.github.rosemoe.sora.lsp.client.languageserver.ServerStatus; import io.github.rosemoe.sora.lsp.client.languageserver.serverdefinition.CustomLanguageServerDefinition; import io.github.rosemoe.sora.lsp.client.languageserver.wrapper.EventHandler; import io.github.rosemoe.sora.lsp.editor.LspEditor; @@ -362,8 +363,6 @@ public EventListener(LspTestJavaActivity activity) { @Override public void initialize(@Nullable LanguageServer server, @NonNull InitializeResult result) { - - var activity = ref.get(); if (activity == null) { @@ -378,6 +377,10 @@ public void initialize(@Nullable LanguageServer server, @NonNull InitializeResul item.setEnabled(isEnabled); }); + } + + @Override + public void onStatusChange(@NonNull ServerStatus newStatus, @NonNull ServerStatus oldStatus) { } diff --git a/app/src/main/res/layout/activity_editor.xml b/app/src/main/res/layout/activity_editor.xml index ba49ae4c9..11a4dec83 100644 --- a/app/src/main/res/layout/activity_editor.xml +++ b/app/src/main/res/layout/activity_editor.xml @@ -28,21 +28,18 @@ android:layout_height="match_parent" android:orientation="vertical"> - - + - - - - + diff --git a/app/src/main/res/menu/menu_lsp.xml b/app/src/main/res/menu/menu_lsp.xml index 7bb6581b8..fdf340226 100644 --- a/app/src/main/res/menu/menu_lsp.xml +++ b/app/src/main/res/menu/menu_lsp.xml @@ -27,4 +27,8 @@ android:id="@+id/code_format" android:title="@string/format" android:visible="true"/> + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 000000000..8bf845d8b --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,143 @@ + + #AAC7FF + #0A305F + #284777 + #D6E3FF + #BEC6DC + #283141 + #3E4759 + #DAE2F9 + #DDBCE0 + #3F2844 + #573E5C + #FAD8FD + #FFB4AB + #690005 + #93000A + #FFDAD6 + #111318 + #E2E2E9 + #111318 + #E2E2E9 + #44474E + #C4C6D0 + #8E9099 + #44474E + #000000 + #E2E2E9 + #2E3036 + #415F91 + #D6E3FF + #001B3E + #AAC7FF + #284777 + #DAE2F9 + #131C2B + #BEC6DC + #3E4759 + #FAD8FD + #28132E + #DDBCE0 + #573E5C + #111318 + #37393E + #0C0E13 + #191C20 + #1D2024 + #282A2F + #33353A + #CDDDFF + #002551 + #7491C7 + #000000 + #D4DCF2 + #1D2636 + #8891A5 + #000000 + #F3D2F7 + #331D39 + #A487A9 + #000000 + #FFD2CC + #540003 + #FF5449 + #000000 + #111318 + #E2E2E9 + #111318 + #FFFFFF + #44474E + #DADCE6 + #AFB2BB + #8E9099 + #000000 + #E2E2E9 + #282A2F + #294878 + #D6E3FF + #00112B + #AAC7FF + #133665 + #DAE2F9 + #081121 + #BEC6DC + #2E3647 + #FAD8FD + #1D0823 + #DDBCE0 + #452E4A + #111318 + #43444A + #06070C + #1B1E22 + #26282D + #313238 + #3C3E43 + #EBF0FF + #000000 + #A6C3FC + #000B20 + #EBF0FF + #000000 + #BAC3D8 + #030B1A + #FFE9FF + #000000 + #D8B8DC + #16041D + #FFECE9 + #000000 + #FFAEA4 + #220001 + #111318 + #E2E2E9 + #111318 + #FFFFFF + #44474E + #FFFFFF + #EEEFF9 + #C0C2CC + #000000 + #E2E2E9 + #000000 + #294878 + #D6E3FF + #000000 + #AAC7FF + #00112B + #DAE2F9 + #000000 + #BEC6DC + #081121 + #FAD8FD + #000000 + #DDBCE0 + #1D0823 + #111318 + #4E5056 + #000000 + #1D2024 + #2E3036 + #393B41 + #45474C + diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 8d47687a8..000000000 --- a/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night/theme_overlays.xml b/app/src/main/res/values-night/theme_overlays.xml new file mode 100644 index 000000000..d0ad798ac --- /dev/null +++ b/app/src/main/res/values-night/theme_overlays.xml @@ -0,0 +1,153 @@ + + + + + + + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 000000000..be8186ca2 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,75 @@ + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a63ca5998..80722bbdd 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -24,12 +24,146 @@ - #3F51B5 - #303F9F - #EC407A - - @color/indigo_500 - @color/indigo_700 - @color/pink_400 + #415F91 + #FFFFFF + #D6E3FF + #284777 + #565F71 + #FFFFFF + #DAE2F9 + #3E4759 + #705575 + #FFFFFF + #FAD8FD + #573E5C + #BA1A1A + #FFFFFF + #FFDAD6 + #93000A + #F9F9FF + #191C20 + #F9F9FF + #191C20 + #E0E2EC + #44474E + #74777F + #C4C6D0 + #000000 + #2E3036 + #F0F0F7 + #AAC7FF + #D6E3FF + #001B3E + #AAC7FF + #284777 + #DAE2F9 + #131C2B + #BEC6DC + #3E4759 + #FAD8FD + #28132E + #DDBCE0 + #573E5C + #D9D9E0 + #F9F9FF + #FFFFFF + #F3F3FA + #EDEDF4 + #E7E8EE + #E2E2E9 + #133665 + #FFFFFF + #506DA0 + #FFFFFF + #2E3647 + #FFFFFF + #646D80 + #FFFFFF + #452E4A + #FFFFFF + #7F6484 + #FFFFFF + #740006 + #FFFFFF + #CF2C27 + #FFFFFF + #F9F9FF + #191C20 + #F9F9FF + #0F1116 + #E0E2EC + #33363E + #4F525A + #6A6D75 + #000000 + #2E3036 + #F0F0F7 + #AAC7FF + #506DA0 + #FFFFFF + #375586 + #FFFFFF + #646D80 + #FFFFFF + #4C5567 + #FFFFFF + #7F6484 + #FFFFFF + #654C6B + #FFFFFF + #C5C6CD + #F9F9FF + #FFFFFF + #F3F3FA + #E7E8EE + #DCDCE3 + #D1D1D8 + #032B5B + #FFFFFF + #2A497A + #FFFFFF + #232C3D + #FFFFFF + #41495B + #FFFFFF + #3A2440 + #FFFFFF + #59405E + #FFFFFF + #600004 + #FFFFFF + #98000A + #FFFFFF + #F9F9FF + #191C20 + #F9F9FF + #000000 + #E0E2EC + #000000 + #292C33 + #464951 + #000000 + #2E3036 + #FFFFFF + #AAC7FF + #2A497A + #FFFFFF + #0E3262 + #FFFFFF + #41495B + #FFFFFF + #2A3344 + #FFFFFF + #59405E + #FFFFFF + #412A47 + #FFFFFF + #B8B8BF + #F9F9FF + #FFFFFF + #F0F0F7 + #E2E2E9 + #D3D4DB + #C5C6CD \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ed76c3f6..a17d26550 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,7 +64,7 @@ Use ICU library Open new activity Completion Window Animation - Open lsp activity + Open LSP activity Not supported LSP works only on Android O or above Line Info Panel Position @@ -94,4 +94,5 @@ Do you want to open LspActivity written in Kotlin? Yes No + Restart server diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml deleted file mode 100644 index 47687e199..000000000 --- a/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/values/theme_overlays.xml b/app/src/main/res/values/theme_overlays.xml new file mode 100644 index 000000000..f45f55f12 --- /dev/null +++ b/app/src/main/res/values/theme_overlays.xml @@ -0,0 +1,153 @@ + + + + + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..c3b429a2b --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,75 @@ + + + + + + diff --git a/build-logic/convention/src/main/kotlin/AppSigning.kt b/build-logic/convention/src/main/kotlin/AppSigning.kt new file mode 100644 index 000000000..527789a55 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AppSigning.kt @@ -0,0 +1,95 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2026 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +import org.gradle.api.Project +import java.io.File +import java.util.Base64 +import java.util.Properties + +/** + * Signing config for application + */ +object AppSigning { + + data class AppSigningConfig( + val storeFile: File, + val storePassword: String, + val keyAlias: String, + val keyPassword: String + ) + + fun getAppSigningConfigOptional(project: Project): Result = runCatching { + getAppSigningConfig(project) + }.onFailure { + val message = when (it) { + is MissingEnvVarException -> "App signing config not correctly configured" + else -> "Error when generating app signing config" + } + project.logger.error(message, it) + } + + fun getAppSigningConfig(project: Project): AppSigningConfig { + val properties = Properties().also { + val file = project.rootProject.file("signing.properties") + if (file.exists()) { + file.reader().use { rd -> + it.load(rd) + } + } + } + val storeFile = project.rootProject.file("signing.keystore") + + val storeBin = getEnvOrProp(properties, "SIGNING_STORE_BIN") + val storePassword = getEnvOrProp(properties, "SIGNING_STORE_PASSWORD") + val keyAlias = getEnvOrProp(properties, "SIGNING_KEY_ALIAS") + val keyPassword = getEnvOrProp(properties, "SIGNING_KEY_PASSWORD") + + val keystoreData = Base64.getDecoder().decode(storeBin) + storeFile.parentFile.mkdirs() + storeFile.createNewFile() + storeFile.writeBytes(keystoreData) + + return AppSigningConfig( + storeFile = storeFile, + storePassword = storePassword, + keyAlias = keyAlias, + keyPassword = keyPassword + ) + } + + private fun getEnvOrProp(properties: Properties, key: String): String { + var value: String? = System.getenv(key) + if (value.isNullOrBlank()) { + value = properties[key] as? String? + } + + if (value.isNullOrBlank()) { + throw MissingEnvVarException("`$key` is not set in environment variables or properties") + } + return value + } + + class MissingEnvVarException(msg: String) : Exception(msg) + +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/Versions.kt b/build-logic/convention/src/main/kotlin/Versions.kt index 65f4e2ade..544bd4e57 100644 --- a/build-logic/convention/src/main/kotlin/Versions.kt +++ b/build-logic/convention/src/main/kotlin/Versions.kt @@ -27,7 +27,13 @@ object Versions { private const val version = "0.24.4" const val versionCode = 93 - val versionName by lazy { + val appVersionName by lazy { + if (CI.isCiBuild) { + "$version-${CI.commitHash}" + } else "$version-${System.currentTimeMillis()}" + } + + val artifactVersion by lazy { if (CI.isCiBuild) { "$version-SNAPSHOT" } else version diff --git a/build.gradle.kts b/build.gradle.kts index 630c38074..dbb9b950c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,7 +62,7 @@ fun Project.configureAndroidAndKotlin() { defaultConfig { targetSdk = Versions.targetSdkVersion versionCode = Versions.versionCode - versionName = Versions.versionName + versionName = Versions.appVersionName } } @@ -76,7 +76,7 @@ fun Project.configureAndroidAndKotlin() { subprojects { group = "io.github.rosemoe" - version = Versions.versionName + version = Versions.artifactVersion plugins.withId("com.android.application") { configureAndroidAndKotlin() @@ -87,8 +87,6 @@ subprojects { plugins.withId("com.vanniktech.maven.publish.base") { configure { - group = "io.github.rosemoe" - version = Versions.versionName pomFromGradleProperties() publishToMavenCentral() signAllPublications() diff --git a/debug.jks b/debug.jks deleted file mode 100644 index ce0047dfd..000000000 Binary files a/debug.jks and /dev/null differ diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/CustomConnectProvider.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/CustomConnectProvider.kt index b1115f116..a607faef4 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/CustomConnectProvider.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/CustomConnectProvider.kt @@ -37,11 +37,13 @@ import java.io.OutputStream class CustomConnectProvider(private val streamProvider: StreamProvider) : StreamConnectionProvider { private lateinit var _inputStream: InputStream private lateinit var _outputStream: OutputStream + private var closed = true override fun start() { val streams = streamProvider.getStreams() _inputStream = streams.first _outputStream = streams.second + closed = false } override val inputStream: InputStream @@ -50,12 +52,17 @@ class CustomConnectProvider(private val streamProvider: StreamProvider) : Stream override val outputStream: OutputStream get() = _outputStream + override val isClosed: Boolean + get() = closed + override fun close() { try { inputStream.close() outputStream.close() } catch (e: IOException) { // ignore + } finally { + closed = true } } diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/LocalSocketStreamConnectionProvider.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/LocalSocketStreamConnectionProvider.kt index 88524979a..31d67e296 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/LocalSocketStreamConnectionProvider.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/LocalSocketStreamConnectionProvider.kt @@ -52,11 +52,14 @@ class LocalSocketStreamConnectionProvider( override val outputStream: OutputStream get() = socket.getOutputStream() + override val isClosed: Boolean + get() = !::socket.isInitialized || socket.isClosed + override fun close() { try { - socket.close() + socket.shutdownOutput() } catch (e: Exception) { e.printStackTrace() } } -} \ No newline at end of file +} diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/SocketStreamConnectionProvider.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/SocketStreamConnectionProvider.kt index 6885f078d..947247cee 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/SocketStreamConnectionProvider.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/SocketStreamConnectionProvider.kt @@ -30,7 +30,6 @@ import java.io.InputStream import java.io.OutputStream import java.net.InetSocketAddress import java.net.Socket -import java.net.SocketAddress /** @@ -56,6 +55,9 @@ class SocketStreamConnectionProvider( override val outputStream: OutputStream get() = socket.getOutputStream() + override val isClosed: Boolean + get() = !::socket.isInitialized || socket.isClosed + override fun close() { try { socket.close() @@ -63,4 +65,4 @@ class SocketStreamConnectionProvider( e.printStackTrace() } } -} \ No newline at end of file +} diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/StreamConnectionProvider.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/StreamConnectionProvider.kt index 75739fc02..fa643bbea 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/StreamConnectionProvider.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/connection/StreamConnectionProvider.kt @@ -49,8 +49,13 @@ interface StreamConnectionProvider : Closeable { */ val outputStream: OutputStream + /** + * Whether this provider has been closed. + */ + val isClosed: Boolean + /** * Close the connection. */ override fun close() -} \ No newline at end of file +} diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/ServerStatus.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/ServerStatus.kt index 4c7fe5165..e3983f4d7 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/ServerStatus.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/ServerStatus.kt @@ -27,10 +27,19 @@ package io.github.rosemoe.sora.lsp.client.languageserver /** * An enum representing a server status */ -enum class ServerStatus { - STOPPED, - STARTING, - STARTED, - INITIALIZED, - STOPPING +sealed interface ServerStatus { + object IDLE: ServerStatus + object STARTING : ServerStatus + object STARTED : ServerStatus + object INITIALIZED : ServerStatus + data class STOPPING(val reason: ShutdownReason) : ServerStatus + data class STOPPED(val reason: ShutdownReason) : ServerStatus +} + +enum class ShutdownReason { + MANUAL, + RESTART, + CRASH, + TIMEOUT, + UNUSED } \ No newline at end of file diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/requestmanager/AggregatedRequestManager.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/requestmanager/AggregatedRequestManager.kt index b63a50af5..1032ac952 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/requestmanager/AggregatedRequestManager.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/requestmanager/AggregatedRequestManager.kt @@ -1,8 +1,6 @@ package io.github.rosemoe.sora.lsp.client.languageserver.requestmanager import io.github.rosemoe.sora.lsp.client.languageserver.wrapper.LanguageServerWrapper -import io.github.rosemoe.sora.lsp.requests.Timeout -import io.github.rosemoe.sora.lsp.requests.Timeouts import org.eclipse.lsp4j.ApplyWorkspaceEditParams import org.eclipse.lsp4j.ApplyWorkspaceEditResponse import org.eclipse.lsp4j.CodeAction @@ -32,7 +30,6 @@ import org.eclipse.lsp4j.DocumentLink import org.eclipse.lsp4j.DocumentLinkParams import org.eclipse.lsp4j.DocumentDiagnosticParams import org.eclipse.lsp4j.DocumentDiagnosticReport -import org.eclipse.lsp4j.DocumentDiagnosticReportKind import org.eclipse.lsp4j.DocumentOnTypeFormattingParams import org.eclipse.lsp4j.DocumentRangeFormattingParams import org.eclipse.lsp4j.DocumentSymbol @@ -66,7 +63,6 @@ import org.eclipse.lsp4j.ReferenceParams import org.eclipse.lsp4j.RegistrationParams import org.eclipse.lsp4j.RenameParams import org.eclipse.lsp4j.ServerCapabilities -import org.eclipse.lsp4j.SetTraceParams import org.eclipse.lsp4j.ShowMessageRequestParams import org.eclipse.lsp4j.SignatureHelp import org.eclipse.lsp4j.SignatureHelpParams @@ -86,7 +82,6 @@ import org.eclipse.lsp4j.services.WorkspaceService import io.github.rosemoe.sora.lsp.utils.merge import java.util.LinkedHashMap import java.util.concurrent.CompletableFuture -import java.util.concurrent.TimeUnit class AggregatedRequestManager( sessions: Set @@ -99,12 +94,15 @@ class AggregatedRequestManager( var activeManagers: List = sessionEntries.mapNotNull { it.requestManager } private set - internal fun updateSessions(newSessions: Set) { sessionEntries = newSessions activeManagers = sessionEntries.mapNotNull { it.requestManager } } + fun getSessions(): Set { + return sessionEntries + } + override val capabilities: ServerCapabilities? get() = mergeCapabilities() @@ -472,12 +470,11 @@ class AggregatedRequestManager( return firstFuture { resolveCodeAction(unresolved) } } - override fun getTextDocumentService(): TextDocumentService? { - // Don't support text document service. - return null + override fun getTextDocumentService(): TextDocumentService { + return this } - override fun getWorkspaceService(): WorkspaceService? { - return null + override fun getWorkspaceService(): WorkspaceService { + return this } } diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/serverdefinition/LanguageServerDefinition.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/serverdefinition/LanguageServerDefinition.kt index b1dd8433a..cdfa856bd 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/serverdefinition/LanguageServerDefinition.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/serverdefinition/LanguageServerDefinition.kt @@ -62,10 +62,10 @@ abstract class LanguageServerDefinition { @Throws(IOException::class) fun start(workingDir: String): Pair { var streamConnectionProvider = streamConnectionProviders[workingDir] - return if (streamConnectionProvider != null) { + return if (streamConnectionProvider != null && !streamConnectionProvider.isClosed) { streamConnectionProvider.inputStream to streamConnectionProvider.outputStream } else { - streamConnectionProvider = createConnectionProvider(workingDir) + streamConnectionProvider = streamConnectionProvider ?: createConnectionProvider(workingDir) streamConnectionProvider.start() streamConnectionProviders[workingDir] = streamConnectionProvider streamConnectionProvider.inputStream to streamConnectionProvider.outputStream @@ -73,7 +73,7 @@ abstract class LanguageServerDefinition { } open fun callExitForLanguageServer(): Boolean { - return false + return true } /** diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/wrapper/EventHandler.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/wrapper/EventHandler.kt index 5775b4ce8..f7d9beea1 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/wrapper/EventHandler.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/wrapper/EventHandler.kt @@ -26,16 +26,15 @@ package io.github.rosemoe.sora.lsp.client.languageserver.wrapper import io.github.rosemoe.sora.lsp.client.languageserver.ServerInitializeListener import io.github.rosemoe.sora.lsp.client.languageserver.ServerStatus +import io.github.rosemoe.sora.lsp.events.AsyncEventListener import org.eclipse.lsp4j.InitializeResult import org.eclipse.lsp4j.MessageParams import org.eclipse.lsp4j.jsonrpc.MessageConsumer import org.eclipse.lsp4j.jsonrpc.messages.Message -import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage import org.eclipse.lsp4j.services.LanguageServer import java.util.function.BooleanSupplier import java.util.function.Function - /** * A language server and client event handler. */ @@ -53,18 +52,15 @@ class EventHandler internal constructor( } } - fun setLanguageServer(languageServer: LanguageServer) { this.languageServer = languageServer } interface EventListener : ServerInitializeListener { - override fun initialize(server: LanguageServer?, result: InitializeResult) { - // do nothing - } - + override fun initialize(server: LanguageServer?, result: InitializeResult) {} fun onStatusChange(newStatus: ServerStatus, oldStatus: ServerStatus) {} fun onHandlerException(exception: Exception) {} + fun onEventException(eventListener: AsyncEventListener, exception: Exception) {} fun onShowMessage(messageParams: MessageParams?) {} fun onLogMessage(messageParams: MessageParams?) {} diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/wrapper/LanguageServerWrapper.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/wrapper/LanguageServerWrapper.kt index cb3aa9a29..3f7cce510 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/wrapper/LanguageServerWrapper.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/client/languageserver/wrapper/LanguageServerWrapper.kt @@ -29,11 +29,13 @@ import androidx.annotation.WorkerThread import io.github.rosemoe.sora.lsp.client.DefaultLanguageClient import io.github.rosemoe.sora.lsp.client.ServerWrapperBaseClientContext import io.github.rosemoe.sora.lsp.client.languageserver.ServerStatus +import io.github.rosemoe.sora.lsp.client.languageserver.ShutdownReason import io.github.rosemoe.sora.lsp.client.languageserver.requestmanager.DefaultRequestManager import io.github.rosemoe.sora.lsp.client.languageserver.requestmanager.RequestManager import io.github.rosemoe.sora.lsp.client.languageserver.serverdefinition.LanguageServerDefinition import io.github.rosemoe.sora.lsp.editor.LspEditor import io.github.rosemoe.sora.lsp.editor.LspProject +import io.github.rosemoe.sora.lsp.events.AsyncEventListener import io.github.rosemoe.sora.lsp.requests.Timeout import io.github.rosemoe.sora.lsp.requests.Timeouts import io.github.rosemoe.sora.lsp.utils.LSPException @@ -81,6 +83,7 @@ import java.io.OutputStream import java.lang.ref.WeakReference import java.net.URI import java.util.Collections +import java.util.Locale import java.util.WeakHashMap import java.util.concurrent.CompletableFuture import java.util.concurrent.Executors @@ -108,16 +111,11 @@ class LanguageServerWrapper( private var launcherFuture: Future<*>? = null private var initializeFuture: CompletableFuture? = null private var capabilitiesAlreadyRequested = false - private var crashCount = 0 - - @Volatile - private var alreadyShownTimeout = false - @Volatile - private var alreadyShownCrash = false + private var crashCount = 0 @Volatile - var status = ServerStatus.STOPPED + var status: ServerStatus = ServerStatus.IDLE private set(value) { if (field == value) return serverDefinition.eventListener.onStatusChange(value, field) @@ -130,8 +128,12 @@ class LanguageServerWrapper( private var eventHandler: EventHandler? = null + fun reportEventException(eventListener: AsyncEventListener, exception: Exception) { + eventHandler?.listener?.onEventException(eventListener, exception) + } + /** - * Warning: this is a long running operation + * Warning: this is a long-running operation * * @return the languageServer capabilities, or null if initialization job didn't complete */ @@ -152,25 +154,26 @@ class LanguageServerWrapper( if (capabilitiesAlreadyRequested) 0L else Timeout[Timeouts.INIT].toLong(), TimeUnit.MILLISECONDS ) - - } catch (e: TimeoutException) { + } catch (_: TimeoutException) { val msg = String.format( + Locale.getDefault(), "%s \n is not initialized after %d seconds", serverDefinition.toString(), Timeout[Timeouts.INIT] / 1000 ) - Log.w( - TAG, - msg - ) + Log.w(TAG, msg) serverDefinition.eventListener.onHandlerException(LSPException(msg)) - stop(false) + stop(false, ShutdownReason.TIMEOUT) return null } catch (e: Exception) { Log.w(TAG, "Error while waiting for initialization", e) - serverDefinition.eventListener - .onHandlerException(LSPException("Error while waiting for initialization", e)) - stop(false) + serverDefinition.eventListener.onHandlerException( + LSPException( + "Error while waiting for initialization", + e + ) + ) + stop(false, ShutdownReason.CRASH) return null } @@ -181,12 +184,8 @@ class LanguageServerWrapper( return effectiveCapabilities ?: initializeResult?.capabilities } - - /** - * Starts the LanguageServer - */ - fun start() { - start(false) + private fun isOffline(): Boolean { + return status is ServerStatus.STOPPED || status is ServerStatus.IDLE } /** @@ -195,98 +194,91 @@ class LanguageServerWrapper( * @param throwException Whether to throw a startup failure exception */ @WorkerThread - fun start(throwException: Boolean) { - if (status != ServerStatus.STOPPED || alreadyShownCrash || alreadyShownTimeout) { - return - } - - val projectRootPath = project.projectUri.path - status = ServerStatus.STARTING - - try { - val streams: Pair = - serverDefinition.start(projectRootPath) - - val (inputStream, outputStream) = streams - - val initParams = getInitParams() - // using for lsp - val executorService = Executors.newCachedThreadPool() + fun start(throwException: Boolean = false) { + synchronized(this) { + if (!isOffline()) return - val wrapperRef = WeakReference(this@LanguageServerWrapper) - eventHandler = EventHandler( - serverDefinition.eventListener - ) { wrapperRef.get()?.status != ServerStatus.STOPPED } + val projectRootPath = project.projectUri.path + status = ServerStatus.STARTING - client = - DefaultLanguageClient(ServerWrapperBaseClientContext(this@LanguageServerWrapper)) + try { + val streams: Pair = + serverDefinition.start(projectRootPath) - val launcher = LSPLauncher - .createClientLauncher( - client, inputStream, outputStream, executorService, - eventHandler - ) - - val connectedLanguageServer = launcher.remoteProxy - - languageServer = connectedLanguageServer - - launcherFuture = launcher.startListening() - - eventHandler?.setLanguageServer(connectedLanguageServer) - - initializeFuture = - connectedLanguageServer.initialize(initParams) - .thenApply { res -> - initializeResult = res - Log.i( - TAG, - "Got initializeResult for $serverDefinition ; $projectRootPath" - ) - - val fallbackCapabilities = serverDefinition.expectedCapabilities() - val mergedCapabilities = - mergeCapabilities(res.capabilities, fallbackCapabilities) - effectiveCapabilities = mergedCapabilities - res.capabilities = mergedCapabilities + val (inputStream, outputStream) = streams - requestManager = DefaultRequestManager( - this@LanguageServerWrapper, - requireNotNull(languageServer), - requireNotNull(client), - mergedCapabilities - ) + val initParams = getInitParams() + // using for lsp + val executorService = Executors.newCachedThreadPool() - eventHandler?.listener?.initialize( - connectedLanguageServer, - InitializeResult(mergedCapabilities) - ) + val wrapperRef = WeakReference(this@LanguageServerWrapper) + eventHandler = EventHandler( + serverDefinition.eventListener + ) { wrapperRef.get()?.isOffline()?.not() == true } - status = ServerStatus.STARTED - // send the initialized message since some language servers depends on this message - (requestManager as DefaultRequestManager).initialized(InitializedParams()) - status = ServerStatus.INITIALIZED + client = + DefaultLanguageClient(ServerWrapperBaseClientContext(this@LanguageServerWrapper)) - return@thenApply res - } + val launcher = LSPLauncher + .createClientLauncher( + client, inputStream, outputStream, executorService, + eventHandler + ) - } catch (e: IOException) { - Log.w( - TAG, - "Failed to start $serverDefinition ; $projectRootPath", e - ) - serverDefinition.eventListener.onHandlerException( - LSPException( - "Failed to start " + - serverDefinition + " ; " + projectRootPath, e + val connectedLanguageServer = launcher.remoteProxy + + languageServer = connectedLanguageServer + + launcherFuture = launcher.startListening() + + eventHandler?.setLanguageServer(connectedLanguageServer) + + initializeFuture = + connectedLanguageServer.initialize(initParams) + .thenApply { res -> + initializeResult = res + Log.i( + TAG, + "Got initializeResult for $serverDefinition ; $projectRootPath" + ) + + val fallbackCapabilities = serverDefinition.expectedCapabilities() + val mergedCapabilities = + mergeCapabilities(res.capabilities, fallbackCapabilities) + effectiveCapabilities = mergedCapabilities + res.capabilities = mergedCapabilities + + requestManager = DefaultRequestManager( + this@LanguageServerWrapper, + requireNotNull(languageServer), + requireNotNull(client), + mergedCapabilities + ) + + eventHandler?.listener?.initialize( + connectedLanguageServer, + InitializeResult(mergedCapabilities) + ) + + status = ServerStatus.STARTED + // send the initialized message since some language servers depends on this message + (requestManager as DefaultRequestManager).initialized(InitializedParams()) + status = ServerStatus.INITIALIZED + + return@thenApply res + } + + } catch (e: IOException) { + Log.w(TAG, "Failed to start $serverDefinition ; $projectRootPath", e) + serverDefinition.eventListener.onHandlerException( + LSPException("Failed to start $serverDefinition ; $projectRootPath", e) ) - ) - if (throwException) { - throw RuntimeException(e) + if (throwException) { + throw RuntimeException(e) + } + stop(true, ShutdownReason.CRASH) } - stop(true) } - } /* @@ -294,59 +286,40 @@ class LanguageServerWrapper( * (otherwise the response might not be delivered correctly to the client). * Only if the exit flag is true, particular server instance will exit. */ - fun stop(exit: Boolean) { - if (status == ServerStatus.STOPPED || status == ServerStatus.STOPPING) { - return - } - status = ServerStatus.STOPPING - initializeFuture?.cancel(true) - try { - val shutdown = languageServer?.shutdown() - - shutdown?.get(Timeout[Timeouts.SHUTDOWN].toLong(), TimeUnit.MILLISECONDS) - - if (exit && serverDefinition.callExitForLanguageServer()) { - languageServer?.exit() + fun stop(exit: Boolean, reason: ShutdownReason = ShutdownReason.MANUAL) { + synchronized(this) { + if (isOffline() || status is ServerStatus.STOPPING) { + return } - - } catch (e: java.lang.Exception) { - // most likely closed externally. - Log.w( - TAG, - "exception occured while trying to shut down", - e - ) - } finally { - launcherFuture?.cancel(true) - serverDefinition.stop(project.projectUri.path) - for (ed in connectedEditors) { - disconnect(ed) + status = ServerStatus.STOPPING(reason) + initializeFuture?.cancel(true) + try { + val shutdown = languageServer?.shutdown() + + shutdown?.get(Timeout[Timeouts.SHUTDOWN].toLong(), TimeUnit.MILLISECONDS) + + if (exit && serverDefinition.callExitForLanguageServer()) { + languageServer?.exit() + } + + } catch (e: java.lang.Exception) { + // most likely closed externally. + Log.w(TAG, "Exception occurred while trying to shut down", e) + } finally { + launcherFuture?.cancel(true) + serverDefinition.stop(project.projectUri.path) + for (ed in connectedEditors.toList()) { + disconnect(ed) + ed.onWrapperStopped(this) + } + launcherFuture = null + capabilitiesAlreadyRequested = false + initializeResult = null + initializeFuture = null + languageServer = null + eventHandler = null + status = ServerStatus.STOPPED(reason) } - launcherFuture = null - capabilitiesAlreadyRequested = false - initializeResult = null - initializeFuture = null - languageServer = null - eventHandler = null - status = ServerStatus.STOPPED - } - } - - - /** - * Disconnects an editor from the LanguageServer - * - * @param editor The editor - */ - @WorkerThread - fun disconnect(editor: LspEditor) { - if (!connectedEditors.contains(editor)) { - return - } - - connectedEditors.remove(editor) - if (connectedEditors.isEmpty()) { - stop(false) } } @@ -414,7 +387,7 @@ class LanguageServerWrapper( crashCount += 1 if (crashCount <= 3) { commonCoroutineScope.launch { - reconnect() + restartAndReconnect() } } else { serverDefinition.eventListener.onHandlerException( @@ -427,7 +400,7 @@ class LanguageServerWrapper( ) ) ) - alreadyShownCrash = true + stop(false, ShutdownReason.CRASH) crashCount = 0 } } @@ -504,18 +477,28 @@ class LanguageServerWrapper( for (ed in readyToConnect) { connect(ed) } - } catch (e: Exception) { + Log.w(TAG, e) + } + } - Log.w( - TAG, - e - ) + /** + * Disconnects an editor from the LanguageServer + * + * @param editor The editor + */ + @WorkerThread + fun disconnect(editor: LspEditor) { + if (!connectedEditors.contains(editor)) { + return } + connectedEditors.remove(editor) + if (connectedEditors.isEmpty()) { + stop(false, ShutdownReason.UNUSED) + } } - /** * @return the LanguageServer */ @@ -529,37 +512,24 @@ class LanguageServerWrapper( return languageServer } - - private fun reconnect() { - // Need to copy by value since connected editors gets cleared during 'stop()' invocation. - stop(false) - for (editor in connectedEditors) { - connect(editor) - } - } - - - /** - * Is the language server in a state where it can be restartable. Normally language server is - * restartable if it has timeout or has a startup error. - */ - val isRestartable = - status == ServerStatus.STOPPED && (alreadyShownTimeout || alreadyShownCrash) - - /** * Reset language server wrapper state so it can be started again if it was failed earlier. */ + @WorkerThread fun restart() { - if (isRestartable) { - alreadyShownCrash = false - alreadyShownTimeout = false - } else { - stop(false) - } + stop(false, ShutdownReason.RESTART) start() } + @WorkerThread + fun restartAndReconnect() { + val editorsSnapshot = connectedEditors.toList() + restart() + for (editor in editorsSnapshot) { + editor.connectWithTimeoutBlocking() + } + } + private fun mergeCapabilities( primary: ServerCapabilities, fallback: ServerCapabilities? @@ -572,5 +542,4 @@ class LanguageServerWrapper( merged.override(fallback) return merged } - } diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditor.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditor.kt index 06d1f0ae3..02525c10c 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditor.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditor.kt @@ -26,20 +26,9 @@ package io.github.rosemoe.sora.lsp.editor import androidx.annotation.WorkerThread import io.github.rosemoe.sora.annotations.Experimental -import io.github.rosemoe.sora.event.ContentChangeEvent -import io.github.rosemoe.sora.event.Event -import io.github.rosemoe.sora.event.HoverEvent -import io.github.rosemoe.sora.event.ScrollEvent -import io.github.rosemoe.sora.event.SelectionChangeEvent -import io.github.rosemoe.sora.event.SubscriptionReceipt import io.github.rosemoe.sora.lang.Language import io.github.rosemoe.sora.lsp.client.languageserver.requestmanager.RequestManager -import io.github.rosemoe.sora.lsp.client.languageserver.serverdefinition.LanguageServerDefinition import io.github.rosemoe.sora.lsp.client.languageserver.wrapper.LanguageServerWrapper -import io.github.rosemoe.sora.lsp.editor.event.LspEditorContentChangeEvent -import io.github.rosemoe.sora.lsp.editor.event.LspEditorHoverEvent -import io.github.rosemoe.sora.lsp.editor.event.LspEditorScrollEvent -import io.github.rosemoe.sora.lsp.editor.event.LspEditorSelectionChangeEvent import io.github.rosemoe.sora.lsp.editor.format.LspFormatter import io.github.rosemoe.sora.lsp.events.EventType import io.github.rosemoe.sora.lsp.events.diagnostics.publishDiagnostics @@ -52,11 +41,9 @@ import io.github.rosemoe.sora.lsp.utils.FileUri import io.github.rosemoe.sora.lsp.utils.clearVersions import io.github.rosemoe.sora.text.CharPosition import io.github.rosemoe.sora.widget.CodeEditor -import io.github.rosemoe.sora.widget.subscribeEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.future.future -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.eclipse.lsp4j.CodeAction @@ -64,7 +51,6 @@ import org.eclipse.lsp4j.ColorInformation import org.eclipse.lsp4j.Command import org.eclipse.lsp4j.Diagnostic import org.eclipse.lsp4j.DocumentHighlight -import org.eclipse.lsp4j.DocumentHighlightKind import org.eclipse.lsp4j.Hover import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.SignatureHelp @@ -77,9 +63,6 @@ class LspEditor( val project: LspProject, val uri: FileUri, ) { - - private val serverDefinition: LanguageServerDefinition - private val delegate = LspEditorDelegate(this) private val uiDelegate = LspEditorUIDelegate(this) @@ -87,8 +70,6 @@ class LspEditor( private var currentLanguage: LspLanguage? = null - private var subscriptionReceipts: MutableList> = mutableListOf() - @Volatile private var isClosed = false @@ -117,34 +98,8 @@ class LspEditor( uiDelegate.detachEditor() _currentEditor = WeakReference(currentEditor) - clearSubscriptions() - currentEditor.setEditorLanguage(currentLanguage) uiDelegate.attachEditor(currentEditor) - - if (isEnableInlayHint) { - coroutineScope.launch { - this@LspEditor.requestInlayHint(CharPosition(0, 0)) - this@LspEditor.requestDocumentColor() - } - } - - clearSubscriptions() - subscriptionReceipts = - mutableListOf( - currentEditor.subscribeEvent( - LspEditorContentChangeEvent(this) - ), - currentEditor.subscribeEvent( - LspEditorSelectionChangeEvent(this) - ), - currentEditor.subscribeEvent( - LspEditorHoverEvent(this) - ), - currentEditor.subscribeEvent( - LspEditorScrollEvent(this) - ) - ) } get() { return _currentEditor.get() @@ -166,9 +121,19 @@ class LspEditor( } } - var isConnected = false - private set - + var eventListener: LspEditorEventListener = LspEditorEventListener.DEFAULT + + var status: LspEditorStatus = LspEditorStatus.IDLE + private set(value) { + if (field == value) return + val old = field + field = value + eventListener.onStatusChanged(this, value, old) + } + + val isConnected: Boolean + get() = status == LspEditorStatus.CONNECTED + val languageServerWrapper: LanguageServerWrapper get() = delegate.getPrimaryWrapper() ?: throw IllegalStateException("No language server wrapper for extension $fileExt") @@ -209,12 +174,6 @@ class LspEditor( get() = uiDelegate.isEnableInlayHint set(value) { uiDelegate.isEnableInlayHint = value - if (value) { - coroutineScope.launch { - this@LspEditor.requestInlayHint(CharPosition(0, 0)) - this@LspEditor.requestDocumentColor() - } - } } val hoverWindow @@ -233,9 +192,6 @@ class LspEditor( get() = delegate.aggregatedRequestManager.activeManagers init { - serverDefinition = project.getServerDefinition(fileExt) - ?: project.getServerDefinitions(fileExt).firstOrNull() - ?: throw Exception("No server definition for extension $fileExt") currentLanguage = LspLanguage(this) } @@ -250,14 +206,17 @@ class LspEditor( @Throws(TimeoutException::class) suspend fun connect(throwException: Boolean = true): Boolean = withContext(Dispatchers.IO) { + status = LspEditorStatus.CONNECTING eventManager.init() runCatching { - // Delegate handles multi-server coordination and returns merged capabilities. + // Delegate handles multiserver coordination and returns merged capabilities. val capabilities = delegate.connectAll() ?: throw TimeoutException("Unable to connect language server") openDocument() + editor?.let { uiDelegate.attachEditor(it) } + currentLanguage?.let { language -> if (capabilities.documentFormattingProvider?.left != false || capabilities.documentFormattingProvider?.right != null) { language.formatter = LspFormatter(language) @@ -268,12 +227,13 @@ class LspEditor( requestInlayHint(CharPosition(0, 0)) } - isConnected = true + status = LspEditorStatus.CONNECTED }.onFailure { if (throwException) { + status = LspEditorStatus.DISCONNECTED throw it } - isConnected = false + status = LspEditorStatus.DISCONNECTED }.isSuccess } @@ -328,21 +288,25 @@ class LspEditor( @WorkerThread @Throws(RuntimeException::class) fun disconnect() { + uiDelegate.detachEditor() runCatching { coroutineScope.future { eventManager.emitAsync(EventType.documentClose) }.get() - delegate.disconnectAll() - - isConnected = false + status = LspEditorStatus.DISCONNECTED }.onFailure { - isConnected = false + status = LspEditorStatus.DISCONNECTED delegate.disconnectAll() throw it } } + internal fun onWrapperStopped(wrapper: LanguageServerWrapper) { + uiDelegate.clearWrapperState() + delegate.onWrapperDisconnected(wrapper) + } + /** * Notify the language server to open the document */ @@ -420,25 +384,14 @@ class LspEditor( return false } - private fun clearSubscriptions() { - val iterator = subscriptionReceipts.iterator() - - while (iterator.hasNext()) { - iterator.next().unsubscribe() - iterator.remove() - } - } - @WorkerThread fun dispose() { - clearSubscriptions() synchronized(disposeLock) { if (isClosed) { return // throw IllegalStateException("Editor is already closed") } disconnect() - uiDelegate.detachEditor() _currentEditor.clear() clearVersions { it == this.uri diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorDelegate.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorDelegate.kt index e2ad2d250..73658cadf 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorDelegate.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorDelegate.kt @@ -39,30 +39,31 @@ internal class LspEditorDelegate(private val editor: LspEditor) { } } + fun onWrapperDisconnected(wrapper: LanguageServerWrapper) { + sessionInfos.removeAll { it.wrapper === wrapper } + val remaining = sessionInfos + .filter { it.wrapper.status == ServerStatus.INITIALIZED } + .map { it.wrapper } + .toSet() + aggregatedRequestManager.updateSessions(remaining) + } @WorkerThread fun connectAll(): ServerCapabilities? { refreshSessions() var lastCapabilities: ServerCapabilities? = null - for (info in sessionInfos) { if (info.wrapper.status == ServerStatus.INITIALIZED) { continue } - runCatching { - info.wrapper.start() - val capabilities = info.wrapper.getServerCapabilities() - - if (capabilities != null) { - info.wrapper.connect(editor) - lastCapabilities = capabilities - } - }.onFailure { e -> - info.wrapper.crashed(e as? Exception ?: RuntimeException(e)) - android.util.Log.w("LspEditorDelegate", - "Failed to connect to ${info.definition.name}: ${e.message}", e) + info.wrapper.start() + val capabilities = info.wrapper.getServerCapabilities() + + if (capabilities != null) { + info.wrapper.connect(editor) + lastCapabilities = capabilities } } diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorEventListener.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorEventListener.kt new file mode 100644 index 000000000..0b7bfe5eb --- /dev/null +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorEventListener.kt @@ -0,0 +1,36 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2026 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.lsp.editor + +/** + * Listener for [LspEditor] status changes. + */ +fun interface LspEditorEventListener { + fun onStatusChanged(editor: LspEditor, newStatus: LspEditorStatus, oldStatus: LspEditorStatus) + + companion object { + val DEFAULT: LspEditorEventListener = LspEditorEventListener { _, _, _ -> } + } +} diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorStatus.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorStatus.kt new file mode 100644 index 000000000..114dd6820 --- /dev/null +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorStatus.kt @@ -0,0 +1,35 @@ +/******************************************************************************* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2026 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + ******************************************************************************/ + +package io.github.rosemoe.sora.lsp.editor + +/** + * Status of an [LspEditor]. + */ +enum class LspEditorStatus { + IDLE, + CONNECTING, + CONNECTED, + DISCONNECTED +} diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorUIDelegate.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorUIDelegate.kt index 0de300e8b..d44d8c2c0 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorUIDelegate.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspEditorUIDelegate.kt @@ -1,5 +1,11 @@ package io.github.rosemoe.sora.lsp.editor +import io.github.rosemoe.sora.event.ContentChangeEvent +import io.github.rosemoe.sora.event.Event +import io.github.rosemoe.sora.event.HoverEvent +import io.github.rosemoe.sora.event.ScrollEvent +import io.github.rosemoe.sora.event.SelectionChangeEvent +import io.github.rosemoe.sora.event.SubscriptionReceipt import io.github.rosemoe.sora.graphics.inlayHint.ColorInlayHintRenderer import io.github.rosemoe.sora.graphics.inlayHint.TextInlayHintRenderer import io.github.rosemoe.sora.lang.styling.HighlightTextContainer @@ -7,14 +13,21 @@ import io.github.rosemoe.sora.lang.styling.color.EditorColor import io.github.rosemoe.sora.lang.styling.inlayHint.InlayHintsContainer import io.github.rosemoe.sora.lsp.editor.codeaction.CodeActionWindow import io.github.rosemoe.sora.lsp.editor.diagnostics.LspDiagnosticTooltipLayout +import io.github.rosemoe.sora.lsp.editor.event.LspEditorContentChangeEvent +import io.github.rosemoe.sora.lsp.editor.event.LspEditorHoverEvent +import io.github.rosemoe.sora.lsp.editor.event.LspEditorScrollEvent +import io.github.rosemoe.sora.lsp.editor.event.LspEditorSelectionChangeEvent import io.github.rosemoe.sora.lsp.editor.hover.HoverWindow import io.github.rosemoe.sora.lsp.editor.signature.SignatureHelpWindow +import io.github.rosemoe.sora.text.CharPosition import io.github.rosemoe.sora.widget.CodeEditor import io.github.rosemoe.sora.widget.component.DefaultDiagnosticTooltipLayout import io.github.rosemoe.sora.widget.component.EditorAutoCompletion import io.github.rosemoe.sora.widget.component.EditorDiagnosticTooltipWindow import io.github.rosemoe.sora.widget.getComponent import io.github.rosemoe.sora.widget.schemes.EditorColorScheme +import io.github.rosemoe.sora.widget.subscribeEvent +import kotlinx.coroutines.launch import org.eclipse.lsp4j.ColorInformation import org.eclipse.lsp4j.CodeAction import org.eclipse.lsp4j.Command @@ -32,6 +45,7 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { private var currentEditorRef: WeakReference = WeakReference(null as CodeEditor?) private var hoverWindowRef: WeakReference = WeakReference(null as HoverWindow?) private var signatureHelpWindowRef: WeakReference = WeakReference(null as SignatureHelpWindow?) + private var subscriptionReceipts: MutableList> = mutableListOf() private var codeActionWindowRef: WeakReference = WeakReference(null as CodeActionWindow?) private var cachedInlayHints: List? = null @@ -83,6 +97,10 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { TextInlayHintRenderer.DefaultInstance, ColorInlayHintRenderer.DefaultInstance ) + editor.coroutineScope.launch { + editor.requestInlayHint(CharPosition(0, 0)) + editor.requestDocumentColor() + } } else { resetInlinePresentations() } @@ -108,6 +126,8 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { get() = codeActionWindowRef.get() fun attachEditor(codeEditor: CodeEditor) { + clearSubscriptions() + currentEditorRef.clear() hoverWindowRef.clear() signatureHelpWindowRef.clear() @@ -128,6 +148,10 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { TextInlayHintRenderer.DefaultInstance, ColorInlayHintRenderer.DefaultInstance ) + editor.coroutineScope.launch { + editor.requestInlayHint(CharPosition(0, 0)) + editor.requestDocumentColor() + } } codeActionWindowRef = WeakReference(CodeActionWindow(editor, codeEditor)) @@ -136,9 +160,33 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { if (diagnosticWindow.layout is DefaultDiagnosticTooltipLayout) { diagnosticWindow.layout = LspDiagnosticTooltipLayout() } + + subscriptionReceipts = mutableListOf( + codeEditor.subscribeEvent( + LspEditorContentChangeEvent(editor) + ), + codeEditor.subscribeEvent( + LspEditorSelectionChangeEvent(editor) + ), + codeEditor.subscribeEvent( + LspEditorHoverEvent(editor) + ), + codeEditor.subscribeEvent( + LspEditorScrollEvent(editor) + ) + ) + } + + fun clearWrapperState() { + hoverWindow?.dismiss() + signatureHelpWindow?.dismiss() + codeActionWindow?.dismiss() + resetInlinePresentations() } fun detachEditor() { + clearSubscriptions() + hoverWindow?.setEnabled(false) hoverWindowRef.clear() @@ -153,6 +201,14 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { currentEditorRef.clear() } + fun clearSubscriptions() { + val iterator = subscriptionReceipts.iterator() + while (iterator.hasNext()) { + iterator.next().unsubscribe() + iterator.remove() + } + } + fun showSignatureHelp(signatureHelp: SignatureHelp?) { val window = signatureHelpWindow ?: return val editorInstance = currentEditorRef.get() ?: return @@ -274,6 +330,9 @@ internal class LspEditorUIDelegate(private val editor: LspEditor) { if (it.inlayHints != null) { it.post { it.inlayHints = null } } + if (it.highlightTexts != null) { + it.post { it.highlightTexts = null } + } } } } diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspProject.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspProject.kt index 7e41f98f1..72775c433 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspProject.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/editor/LspProject.kt @@ -24,11 +24,12 @@ package io.github.rosemoe.sora.lsp.editor +import io.github.rosemoe.sora.lsp.client.languageserver.ShutdownReason import io.github.rosemoe.sora.lsp.client.languageserver.serverdefinition.LanguageServerDefinition import io.github.rosemoe.sora.lsp.client.languageserver.wrapper.LanguageServerWrapper import io.github.rosemoe.sora.lsp.editor.diagnostics.DiagnosticsContainer import io.github.rosemoe.sora.lsp.events.EventEmitter -import io.github.rosemoe.sora.lsp.events.code.CodeActionEventEvent +import io.github.rosemoe.sora.lsp.events.code.CodeActionEvent import io.github.rosemoe.sora.lsp.events.color.DocumentColorEvent import io.github.rosemoe.sora.lsp.events.completion.CompletionEvent import io.github.rosemoe.sora.lsp.events.diagnostics.PublishDiagnosticsEvent @@ -142,13 +143,17 @@ class LspProject( editors.clear() } - internal fun getLanguageServerWrapper(ext: String, name: String): LanguageServerWrapper? { + fun getLanguageServerWrapper(ext: String, name: String): LanguageServerWrapper? { return wrappers[ServerKey(ext, name)] } internal fun getOrCreateLanguageServerWrapper(ext: String, name: String = ext): LanguageServerWrapper { val key = ServerKey(ext, name) - return wrappers[key] ?: createLanguageServerWrapper(ext, name) + return wrappers.computeIfAbsent(key) { + val definition = getServerDefinition(ext, name) + ?: throw IllegalArgumentException("No server definition for extension $ext with name $name") + LanguageServerWrapper(definition, this) + } } internal fun createLanguageServerWrapper(ext: String, name: String): LanguageServerWrapper { @@ -159,14 +164,14 @@ class LspProject( return wrapper } - internal fun getLanguageServerWrappers(ext: String): List { + fun getLanguageServerWrappers(ext: String): List { return wrappers.entries.filter { it.key.ext == ext }.map { it.value } } fun dispose() { closeAllEditors() wrappers.values.forEach { - it.stop(true) + it.stop(true, ShutdownReason.UNUSED) } wrappers.clear() definitions.clear() @@ -187,7 +192,7 @@ class LspProject( ::ApplyEditsEvent, ::CompletionEvent, ::PublishDiagnosticsEvent, ::FullFormattingEvent, ::RangeFormattingEvent, ::QueryDocumentDiagnosticsEvent, - ::DocumentOpenEvent, ::HoverEvent, ::CodeActionEventEvent, + ::DocumentOpenEvent, ::HoverEvent, ::CodeActionEvent, ::WorkSpaceApplyEditEvent, ::WorkSpaceExecuteCommand, ::InlayHintEvent, ::DocumentHighlightEvent, ::DocumentColorEvent diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/EventEmitter.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/EventEmitter.kt index 478a3995a..dec7a69b1 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/EventEmitter.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/EventEmitter.kt @@ -24,7 +24,9 @@ package io.github.rosemoe.sora.lsp.events +import android.util.Log import androidx.annotation.WorkerThread +import io.github.rosemoe.sora.lsp.editor.LspEditor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -169,8 +171,23 @@ abstract class AsyncEventListener : EventListener { throw IllegalStateException("This listener is async, please use handleAsync instead") } + protected abstract suspend fun doHandleAsync(context: EventContext) + override suspend fun handleAsync(context: EventContext) { - super.handleAsync(context) + try { + doHandleAsync(context) + } catch(e: Exception) { + onException(context, e) + } + } + + protected open fun onException(context: EventContext, exception: Exception) { + exception.printStackTrace() + Log.e("LSP client", "Event $eventName failed", exception) + val editor = context.getOrNull("lsp-editor") ?: return + editor.requestManager.getSessions().forEach { + it.reportEventException(this, exception) + } } } diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/code/CodeActionEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/code/CodeActionEvent.kt index 96db241b8..c5a1f451a 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/code/CodeActionEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/code/CodeActionEvent.kt @@ -24,7 +24,6 @@ package io.github.rosemoe.sora.lsp.events.code -import android.util.Log import io.github.rosemoe.sora.lsp.editor.LspEditor import io.github.rosemoe.sora.lsp.events.AsyncEventListener import io.github.rosemoe.sora.lsp.events.EventContext @@ -45,18 +44,18 @@ import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.jsonrpc.messages.Either import java.util.concurrent.CompletableFuture -class CodeActionEventEvent : AsyncEventListener() { +class CodeActionEvent : AsyncEventListener() { override val eventName: String = EventType.codeAction var future: CompletableFuture? = null override val isAsync = true - override suspend fun handleAsync(context: EventContext) = withContext(Dispatchers.IO) { + override suspend fun doHandleAsync(context: EventContext) = withContext(Dispatchers.IO) { val editor = context.get("lsp-editor") val range = context.getByClass() ?: return@withContext - val requestManager = editor.requestManager ?: return@withContext + val requestManager = editor.requestManager val diagnostics = editor.diagnosticsContainer.findDiagnostics(editor.uri, range) @@ -68,29 +67,22 @@ class CodeActionEventEvent : AsyncEventListener() { val future = requestManager.codeAction(codeActionParams) ?: return@withContext - this@CodeActionEventEvent.future = future.thenAccept { } + this@CodeActionEvent.future = future.thenAccept { } - try { - var codeActions: List>? = null + var codeActions: List>? = null - withTimeout(Timeout[Timeouts.CODEACTION].toLong()) { - codeActions = - future.await() ?: emptyList() - } - - editor.showCodeActions(range, codeActions) - } catch (exception: Exception) { - // throw? - exception.printStackTrace() - Log.e("LSP client", "show code action timeout", exception) + withTimeout(Timeout[Timeouts.CODEACTION].toLong()) { + codeActions = + future.await() ?: emptyList() } + + editor.showCodeActions(range, codeActions) } override fun dispose() { future?.cancel(true) future = null } - } val EventType.codeAction: String diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/color/DocumentColor.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/color/DocumentColor.kt index bfbb4b483..6de4b7558 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/color/DocumentColor.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/color/DocumentColor.kt @@ -24,7 +24,6 @@ package io.github.rosemoe.sora.lsp.events.color -import android.util.Log import io.github.rosemoe.sora.annotations.Experimental import io.github.rosemoe.sora.lsp.editor.LspEditor import io.github.rosemoe.sora.lsp.events.AsyncEventListener @@ -76,7 +75,6 @@ class DocumentColorEvent : AsyncEventListener() { onBufferOverflow = BufferOverflow.DROP_OLDEST ) - coroutineScope.launch(Dispatchers.Main) { flow .debounce(50) @@ -89,7 +87,7 @@ class DocumentColorEvent : AsyncEventListener() { } } - override suspend fun handleAsync(context: EventContext) { + override suspend fun doHandleAsync(context: EventContext) { val editor = context.get("lsp-editor") val uri = editor.uri @@ -101,7 +99,7 @@ class DocumentColorEvent : AsyncEventListener() { withContext(Dispatchers.IO) { val editor = request.editor - val requestManager = editor.requestManager ?: return@withContext + val requestManager = editor.requestManager val future = requestManager.documentColor(DocumentColorParams(request.uri.createTextDocumentIdentifier())) @@ -109,25 +107,19 @@ class DocumentColorEvent : AsyncEventListener() { this@DocumentColorEvent.future = future.thenAccept { } - try { - val documentColors: List? - - withTimeout(Timeout[Timeouts.DOC_HIGHLIGHT].toLong()) { - documentColors = - future.await() - } + val documentColors: List? - if (documentColors == null || documentColors.isEmpty()) { - editor.showDocumentColors(null) - return@withContext - } + withTimeout(Timeout[Timeouts.DOC_HIGHLIGHT].toLong()) { + documentColors = + future.await() + } - editor.showDocumentColors(documentColors) - } catch (exception: Exception) { - // throw? - exception.printStackTrace() - Log.e("LSP client", "show document color timeout", exception) + if (documentColors.isNullOrEmpty()) { + editor.showDocumentColors(null) + return@withContext } + + editor.showDocumentColors(documentColors) } override fun dispose() { diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/completion/CompletionEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/completion/CompletionEvent.kt index f64dd13b8..d467d5214 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/completion/CompletionEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/completion/CompletionEvent.kt @@ -39,13 +39,12 @@ import org.eclipse.lsp4j.CompletionContext import org.eclipse.lsp4j.CompletionItem import java.util.concurrent.CompletableFuture - class CompletionEvent : AsyncEventListener() { override val eventName = EventType.completion private var future: CompletableFuture>? = null - override suspend fun handleAsync(context: EventContext) { + override suspend fun doHandleAsync(context: EventContext) { val editor = context.get("lsp-editor") val position = context.getByClass() ?: return @@ -74,14 +73,17 @@ class CompletionEvent : AsyncEventListener() { this.future = future - try { - context.put("completion-items", future.await()) - } catch (e: Exception) { - if (e !is TimeoutCancellationException) { - Logger.instance(this.javaClass.name) - .e("Request completion failed", e) - throw e - } + context.put("completion-items", future.await()) + } + + override fun onException(context: EventContext, exception: Exception) { + val editor = context.getOrNull("lsp-editor") + editor?.requestManager?.getSessions()?.forEach { + it.reportEventException(this, exception) + } + if (exception !is TimeoutCancellationException) { + Logger.instance(this.javaClass.name).e("Request completion failed", exception) + throw exception } } @@ -89,7 +91,6 @@ class CompletionEvent : AsyncEventListener() { future?.cancel(true) future = null } - } val EventType.completion: String diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/diagnostics/PublishDiagnosticsEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/diagnostics/PublishDiagnosticsEvent.kt index 0d4a116b0..e74aa0869 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/diagnostics/PublishDiagnosticsEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/diagnostics/PublishDiagnosticsEvent.kt @@ -62,8 +62,6 @@ class PublishDiagnosticsEvent : EventListener { originEditor.diagnostics = diagnosticsContainer } } - - } val EventType.publishDiagnostics: String diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/diagnostics/QueryDocumentDiagnosticsEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/diagnostics/QueryDocumentDiagnosticsEvent.kt index d318fe4c9..387f6c68d 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/diagnostics/QueryDocumentDiagnosticsEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/diagnostics/QueryDocumentDiagnosticsEvent.kt @@ -40,7 +40,7 @@ class QueryDocumentDiagnosticsEvent : AsyncEventListener() { var future: CompletableFuture? = null - override suspend fun handleAsync(context: EventContext) { + override suspend fun doHandleAsync(context: EventContext) { val editor = context.get("lsp-editor") val requestManager = editor.requestManager @@ -66,8 +66,6 @@ class QueryDocumentDiagnosticsEvent : AsyncEventListener() { future?.cancel(true) future = null } - - } val EventType.queryDocumentDiagnostics: String diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentChangeEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentChangeEvent.kt index 21794bfd6..2edc93c6c 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentChangeEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentChangeEvent.kt @@ -45,7 +45,7 @@ class DocumentChangeEvent : AsyncEventListener() { var future: CompletableFuture? = null - override suspend fun handleAsync(context: EventContext) { + override suspend fun doHandleAsync(context: EventContext) { val editor = context.get("lsp-editor") val event = context.getByClass() ?: return diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentCloseEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentCloseEvent.kt index 8787fe916..c9b6900ec 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentCloseEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentCloseEvent.kt @@ -32,13 +32,12 @@ import io.github.rosemoe.sora.lsp.utils.createDidCloseTextDocumentParams import kotlinx.coroutines.future.await import java.util.concurrent.CompletableFuture - class DocumentCloseEvent : AsyncEventListener() { override val eventName = EventType.documentClose var future: CompletableFuture? = null - override suspend fun handleAsync(context: EventContext) { + override suspend fun doHandleAsync(context: EventContext) { val editor = context.get("lsp-editor") val params = editor.uri.createDidCloseTextDocumentParams() @@ -58,8 +57,6 @@ class DocumentCloseEvent : AsyncEventListener() { future?.cancel(true) future = null } - - } val EventType.documentClose: String diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentOpenEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentOpenEvent.kt index 37e3279b7..5c0fbd157 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentOpenEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentOpenEvent.kt @@ -37,7 +37,7 @@ class DocumentOpenEvent : AsyncEventListener() { var future: CompletableFuture? = null - override suspend fun handleAsync(context: EventContext) { + override suspend fun doHandleAsync(context: EventContext) { val editor = context.get("lsp-editor") val params = editor.createDidOpenTextDocumentParams() diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentSaveEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentSaveEvent.kt index 6c30b96a1..8a22a022c 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentSaveEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/document/DocumentSaveEvent.kt @@ -38,12 +38,12 @@ class DocumentSaveEvent : AsyncEventListener() { var future: CompletableFuture? = null - override suspend fun handleAsync(context: EventContext) { + override suspend fun doHandleAsync(context: EventContext) { val editor = context.get("lsp-editor") val params = editor.createDidSaveTextDocumentParams() - editor.requestManager?.let { requestManager -> + editor.requestManager.let { requestManager -> future = CompletableFuture.runAsync { requestManager.didSave( params @@ -58,7 +58,6 @@ class DocumentSaveEvent : AsyncEventListener() { future?.cancel(true) future = null } - } val EventType.documentSave: String diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/format/FullFormattingEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/format/FullFormattingEvent.kt index 7b38bb5de..87e9ecbf2 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/format/FullFormattingEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/format/FullFormattingEvent.kt @@ -48,12 +48,12 @@ import org.eclipse.lsp4j.TextEdit class FullFormattingEvent : AsyncEventListener() { override val eventName = EventType.fullFormatting - override suspend fun handleAsync(context: EventContext) { + override suspend fun doHandleAsync(context: EventContext) { val editor = context.get("lsp-editor") val content = context.getByClass() ?: return - val requestManager = editor.requestManager ?: return + val requestManager = editor.requestManager val formattingParams = DocumentFormattingParams() @@ -64,25 +64,27 @@ class FullFormattingEvent : AsyncEventListener() { val formattingFuture = requestManager.formatting(formattingParams) ?: return - try { - val textEditList: List + val textEditList: List - withTimeout(Timeout[Timeouts.FORMATTING].toLong()) { - textEditList = formattingFuture.await() ?: listOf() - } + withTimeout(Timeout[Timeouts.FORMATTING].toLong()) { + textEditList = formattingFuture.await() ?: listOf() + } - withContext(Dispatchers.Main) { - editor.eventManager.emit(EventType.applyEdits) { - put("edits", textEditList) - put("content", content) - } + withContext(Dispatchers.Main) { + editor.eventManager.emit(EventType.applyEdits) { + put("edits", textEditList) + put("content", content) } - - } catch (exception: Exception) { - throw LSPException("Formatting code timeout", exception) } } + override fun onException(context: EventContext, exception: Exception) { + val editor = context.getOrNull("lsp-editor") + editor?.requestManager?.getSessions()?.forEach { + it.reportEventException(this, exception) + } + throw LSPException("Formatting code timeout", exception) + } } val EventType.fullFormatting: String diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/format/RangeFormattingEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/format/RangeFormattingEvent.kt index 4525b9c70..f0cce5ae2 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/format/RangeFormattingEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/format/RangeFormattingEvent.kt @@ -45,17 +45,16 @@ import org.eclipse.lsp4j.DocumentRangeFormattingParams import org.eclipse.lsp4j.FormattingOptions import org.eclipse.lsp4j.TextEdit - class RangeFormattingEvent : AsyncEventListener() { override val eventName = EventType.rangeFormatting - override suspend fun handleAsync(context: EventContext) { + override suspend fun doHandleAsync(context: EventContext) { val editor = context.get("lsp-editor") val textRange = context.get("range") val content = context.get("text") - val requestManager = editor.requestManager ?: return + val requestManager = editor.requestManager val formattingParams = DocumentRangeFormattingParams() @@ -69,25 +68,27 @@ class RangeFormattingEvent : AsyncEventListener() { val formattingFuture = requestManager.rangeFormatting(formattingParams) ?: return - try { - val textEditList: List + val textEditList: List - withTimeout(Timeout[Timeouts.FORMATTING].toLong()) { - textEditList = formattingFuture.await() ?: listOf() - } + withTimeout(Timeout[Timeouts.FORMATTING].toLong()) { + textEditList = formattingFuture.await() ?: listOf() + } - withContext(Dispatchers.Main) { - editor.eventManager.emit(EventType.applyEdits) { - put("edits", textEditList) - put("content", content) - } + withContext(Dispatchers.Main) { + editor.eventManager.emit(EventType.applyEdits) { + put("edits", textEditList) + put("content", content) } - - } catch (exception: Exception) { - throw LSPException("Formatting code timeout", exception) } } + override fun onException(context: EventContext, exception: Exception) { + val editor = context.getOrNull("lsp-editor") + editor?.requestManager?.getSessions()?.forEach { + it.reportEventException(this, exception) + } + throw LSPException("Formatting code timeout", exception) + } } val EventType.rangeFormatting: String diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/highlight/DocumentHighlightEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/highlight/DocumentHighlightEvent.kt index c1257b468..f70289475 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/highlight/DocumentHighlightEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/highlight/DocumentHighlightEvent.kt @@ -24,7 +24,6 @@ package io.github.rosemoe.sora.lsp.events.highlight -import android.util.Log import io.github.rosemoe.sora.lsp.editor.LspEditor import io.github.rosemoe.sora.lsp.events.AsyncEventListener import io.github.rosemoe.sora.lsp.events.EventContext @@ -54,11 +53,11 @@ class DocumentHighlightEvent : AsyncEventListener() { val selectionStart: CharPosition, ) - override suspend fun handleAsync(context: EventContext) = withContext(Dispatchers.IO) { + override suspend fun doHandleAsync(context: EventContext) = withContext(Dispatchers.IO) { val editor = context.get("lsp-editor") val request = context.getByClass() ?: return@withContext - val requestManager = editor.requestManager ?: return@withContext + val requestManager = editor.requestManager val params = DocumentHighlightParams( editor.uri.createTextDocumentIdentifier(), @@ -69,18 +68,13 @@ class DocumentHighlightEvent : AsyncEventListener() { this@DocumentHighlightEvent.future = future.thenAccept { } - try { - val documentHighlights: List? + val documentHighlights: List? - withTimeout(Timeout[Timeouts.DOC_HIGHLIGHT].toLong()) { - documentHighlights = future.await() - } - - editor.showDocumentHighlight(documentHighlights) - } catch (exception: Exception) { - exception.printStackTrace() - Log.e("LSP client", "show document highlight timeout", exception) + withTimeout(Timeout[Timeouts.DOC_HIGHLIGHT].toLong()) { + documentHighlights = future.await() } + + editor.showDocumentHighlight(documentHighlights) } override fun dispose() { diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/hover/HoverEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/hover/HoverEvent.kt index 94ad70612..eacd67dad 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/hover/HoverEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/hover/HoverEvent.kt @@ -24,7 +24,6 @@ package io.github.rosemoe.sora.lsp.events.hover -import android.util.Log import io.github.rosemoe.sora.lsp.editor.LspEditor import io.github.rosemoe.sora.lsp.events.AsyncEventListener import io.github.rosemoe.sora.lsp.events.EventContext @@ -50,11 +49,11 @@ class HoverEvent : AsyncEventListener() { override val isAsync = true - override suspend fun handleAsync(context: EventContext) = withContext(Dispatchers.IO) { + override suspend fun doHandleAsync(context: EventContext) = withContext(Dispatchers.IO) { val editor = context.get("lsp-editor") val position = context.getByClass() ?: return@withContext - val requestManager = editor.requestManager ?: return@withContext + val requestManager = editor.requestManager val hoverParams = HoverParams( editor.uri.createTextDocumentIdentifier(), @@ -65,28 +64,19 @@ class HoverEvent : AsyncEventListener() { this@HoverEvent.future = future.thenAccept { } + val hover: Hover? - try { - val hover: Hover? - - withTimeout(Timeout[Timeouts.HOVER].toLong()) { - hover = - future.await() - } - - editor.showHover(hover) - } catch (exception: Exception) { - // throw? - exception.printStackTrace() - Log.e("LSP client", "show hover timeout", exception) + withTimeout(Timeout[Timeouts.HOVER].toLong()) { + hover = future.await() } + + editor.showHover(hover) } override fun dispose() { future?.cancel(true) future = null } - } val EventType.hover: String diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/inlayhint/InlayHintEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/inlayhint/InlayHintEvent.kt index 55f0a43ee..3d5cf2ed6 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/inlayhint/InlayHintEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/inlayhint/InlayHintEvent.kt @@ -24,7 +24,6 @@ package io.github.rosemoe.sora.lsp.events.inlayhint -import android.util.Log import io.github.rosemoe.sora.annotations.Experimental import io.github.rosemoe.sora.lsp.editor.LspEditor import io.github.rosemoe.sora.lsp.events.AsyncEventListener @@ -80,7 +79,6 @@ class InlayHintEvent : AsyncEventListener() { onBufferOverflow = BufferOverflow.DROP_OLDEST ) - coroutineScope.launch(Dispatchers.Main) { flow .debounce(50) @@ -93,7 +91,7 @@ class InlayHintEvent : AsyncEventListener() { } } - override suspend fun handleAsync(context: EventContext) { + override suspend fun doHandleAsync(context: EventContext) { val editor = context.get("lsp-editor") val position = context.getByClass() ?: return @@ -109,7 +107,7 @@ class InlayHintEvent : AsyncEventListener() { val position = request.position val content = editor.editor?.text ?: return@withContext - val requestManager = editor.requestManager ?: return@withContext + val requestManager = editor.requestManager // Request over 500 lines for current window @@ -133,25 +131,18 @@ class InlayHintEvent : AsyncEventListener() { this@InlayHintEvent.future = future.thenAccept { } + val inlayHints: List? - try { - val inlayHints: List? - - withTimeout(Timeout[Timeouts.INLAY_HINT].toLong()) { - inlayHints = future.await() - } - - if (inlayHints == null || inlayHints.isEmpty()) { - editor.showInlayHints(null) - return@withContext - } + withTimeout(Timeout[Timeouts.INLAY_HINT].toLong()) { + inlayHints = future.await() + } - editor.showInlayHints(inlayHints) - } catch (exception: Exception) { - // throw? - exception.printStackTrace() - Log.e("LSP client", "show inlay hint timeout", exception) + if (inlayHints.isNullOrEmpty()) { + editor.showInlayHints(null) + return@withContext } + + editor.showInlayHints(inlayHints) } override fun dispose() { diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/signature/SignatureHelpEvent.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/signature/SignatureHelpEvent.kt index 8d2fa687c..88fd7aaf5 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/signature/SignatureHelpEvent.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/signature/SignatureHelpEvent.kt @@ -24,7 +24,6 @@ package io.github.rosemoe.sora.lsp.events.signature -import android.util.Log import io.github.rosemoe.sora.lsp.editor.LspEditor import io.github.rosemoe.sora.lsp.events.AsyncEventListener import io.github.rosemoe.sora.lsp.events.EventContext @@ -50,11 +49,11 @@ class SignatureHelpEvent : AsyncEventListener() { override val isAsync = true - override suspend fun handleAsync(context: EventContext) = withContext(Dispatchers.IO) { + override suspend fun doHandleAsync(context: EventContext) = withContext(Dispatchers.IO) { val editor = context.get("lsp-editor") val position = context.getByClass() ?: return@withContext - val requestManager = editor.requestManager ?: return@withContext + val requestManager = editor.requestManager val signatureHelpParams = SignatureHelpParams( editor.uri.createTextDocumentIdentifier(), @@ -65,21 +64,14 @@ class SignatureHelpEvent : AsyncEventListener() { this@SignatureHelpEvent.future = future.thenAccept { } - try { - val signatureHelp: SignatureHelp? + val signatureHelp: SignatureHelp? - withTimeout(Timeout[Timeouts.SIGNATURE].toLong()) { - signatureHelp = - future.await() - } - - editor.showSignatureHelp(signatureHelp) - - } catch (exception: Exception) { - // throw? - exception.printStackTrace() - Log.e("LSP client", "show signatureHelp timeout", exception) + withTimeout(Timeout[Timeouts.SIGNATURE].toLong()) { + signatureHelp = + future.await() } + + editor.showSignatureHelp(signatureHelp) } override fun dispose() { diff --git a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/workspace/WorkSpaceExecuteCommand.kt b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/workspace/WorkSpaceExecuteCommand.kt index 0dc4e80ab..7f5337517 100644 --- a/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/workspace/WorkSpaceExecuteCommand.kt +++ b/editor-lsp/src/main/java/io/github/rosemoe/sora/lsp/events/workspace/WorkSpaceExecuteCommand.kt @@ -24,7 +24,6 @@ package io.github.rosemoe.sora.lsp.events.workspace -import android.util.Log import io.github.rosemoe.sora.lsp.editor.LspEditor import io.github.rosemoe.sora.lsp.events.AsyncEventListener import io.github.rosemoe.sora.lsp.events.EventContext @@ -36,7 +35,6 @@ import kotlinx.coroutines.withTimeout import org.eclipse.lsp4j.ExecuteCommandParams import java.util.concurrent.CompletableFuture - class WorkSpaceExecuteCommand : AsyncEventListener() { override val eventName: String = EventType.workSpaceExecuteCommand @@ -44,32 +42,25 @@ class WorkSpaceExecuteCommand : AsyncEventListener() { var future: CompletableFuture? = null - override suspend fun handleAsync(context: EventContext) { + override suspend fun doHandleAsync(context: EventContext) { val command = context.get("command") val args = context.get>("args") val editor = context.get("lsp-editor") - val requestManager = editor.requestManager ?: return + val requestManager = editor.requestManager val executeCommandParams = ExecuteCommandParams(command, args) val future = requestManager.executeCommand(executeCommandParams) this@WorkSpaceExecuteCommand.future = future?.thenAccept { } - try { - val result: Any? - - withTimeout(Timeout[Timeouts.EXECUTE_COMMAND].toLong()) { - result = - future?.await() - } + val result: Any? - context.put("result", result) - - } catch (exception: Exception) { - // throw? - exception.printStackTrace() - Log.e("LSP client", "workspace execute command timeout", exception) + withTimeout(Timeout[Timeouts.EXECUTE_COMMAND].toLong()) { + result = + future?.await() } + + context.put("result", result) } override fun dispose() { diff --git a/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/ColorInlayHintRenderer.kt b/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/ColorInlayHintRenderer.kt index 58e78fc5d..5ec674102 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/ColorInlayHintRenderer.kt +++ b/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/ColorInlayHintRenderer.kt @@ -43,7 +43,7 @@ open class ColorInlayHintRenderer( } override val typeName: String - get() = "color" + get() = ColorInlayHint.TYPE_NAME protected val localPaint = Paint().also { it.isAntiAlias = true diff --git a/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/TextInlayHintRenderer.kt b/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/TextInlayHintRenderer.kt index d6920cbec..6f353460e 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/TextInlayHintRenderer.kt +++ b/editor/src/main/java/io/github/rosemoe/sora/graphics/inlayHint/TextInlayHintRenderer.kt @@ -70,7 +70,7 @@ open class TextInlayHintRenderer : InlayHintRenderer() { protected val localPaint = Paint().also { it.isAntiAlias = true } override val typeName: String - get() = "text" + get() = TextInlayHint.TYPE_NAME override fun onMeasure( inlayHint: InlayHint, diff --git a/editor/src/main/java/io/github/rosemoe/sora/lang/styling/span/SpanExternalRenderer.kt b/editor/src/main/java/io/github/rosemoe/sora/lang/styling/span/SpanExternalRenderer.kt index a880e000f..e448732dd 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/lang/styling/span/SpanExternalRenderer.kt +++ b/editor/src/main/java/io/github/rosemoe/sora/lang/styling/span/SpanExternalRenderer.kt @@ -64,7 +64,7 @@ interface SpanExternalRenderer : SpanExt { * @param canvas The canvas to draw * @param paint Paint for measuring * @param colorScheme Current color scheme - * @param preOrPost True for preDraw, False for postDraw + * @param preOrPost `true` for preDraw, `false` for postDraw */ - fun draw(canvas: Canvas?, paint: Paint?, colorScheme: EditorColorScheme?, preOrPost: Boolean) + fun draw(canvas: Canvas, paint: Paint, colorScheme: EditorColorScheme, preOrPost: Boolean) } \ No newline at end of file diff --git a/editor/src/main/java/io/github/rosemoe/sora/text/TextUtils.java b/editor/src/main/java/io/github/rosemoe/sora/text/TextUtils.java index 1f4b88eee..3e8a2852f 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/text/TextUtils.java +++ b/editor/src/main/java/io/github/rosemoe/sora/text/TextUtils.java @@ -23,7 +23,9 @@ */ package io.github.rosemoe.sora.text; +import androidx.annotation.IntRange; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.Objects; @@ -192,4 +194,14 @@ public static long findLeadingAndTrailingWhitespacePos(ContentLine line, int sta } return IntPair.pack(leading, trailing); } + + public static CharSequence trimToSize(@Nullable CharSequence text, @IntRange(from = 1) int size) { + if (size <= 0) throw new IllegalArgumentException("size must be bigger than 0"); + if (text == null || text.length() <= size) return text; + if (Character.isHighSurrogate(text.charAt(size - 1)) + && Character.isLowSurrogate(text.charAt(size))) { + size = size - 1; + } + return text.subSequence(0, size); + } } diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java b/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java index 65c4a19bf..b4a12de6e 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java @@ -4717,16 +4717,20 @@ protected void onDraw(@NonNull Canvas canvas) { public AccessibilityNodeInfo createAccessibilityNodeInfo() { var info = super.createAccessibilityNodeInfo(); if (isEnabled()) { - info.setEditable(isEditable()); - info.setTextSelection(cursor.getLeft(), cursor.getRight()); - info.setInputType(InputType.TYPE_CLASS_TEXT); - info.setMultiLine(true); - info.setText(getText().toStringBuilder()); + var maxTextLength = props.maxAccessibilityTextLength; + if (maxTextLength > 0) { + info.setEditable(isEditable()); + info.setTextSelection(cursor.getLeft(), cursor.getRight()); + info.setInputType(InputType.TYPE_CLASS_TEXT); + info.setMultiLine(true); + info.setText(TextUtils.trimToSize(getText(), maxTextLength).toString()); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COPY); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CUT); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PASTE); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT); + } + info.setLongClickable(true); - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COPY); - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CUT); - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PASTE); - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT); final int scrollRange = getScrollMaxY(); if (scrollRange > 0) { info.setScrollable(true); diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/DirectAccessProps.java b/editor/src/main/java/io/github/rosemoe/sora/widget/DirectAccessProps.java index f5f5e349f..959bd0d4b 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/DirectAccessProps.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/DirectAccessProps.java @@ -152,6 +152,17 @@ public class DirectAccessProps implements Serializable { @IntRange(from = 0) public int maxIPCTextLength = 32768; + /** + * Max text length for accessibility node info. The text is packed into an + * {@link android.os.Parcel} and transferred to accessibility services. + *

+ * By default, we use the PARCEL_SAFE_TEXT_LENGTH value (100K) in {@link android.text.TextUtils}. + *

+ * If set to {@code 0}, the editor will not send any text related information to accessibility services. + */ + @IntRange(from = 0) + public int maxAccessibilityTextLength = 100000; + /** * Whether over scroll is permitted. * When over scroll is enabled, the user will be able to scroll out of displaying diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/EditorRenderer.java b/editor/src/main/java/io/github/rosemoe/sora/widget/EditorRenderer.java index 3bd08662f..966426e7e 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/EditorRenderer.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/EditorRenderer.java @@ -2134,6 +2134,8 @@ protected void drawLineInfoPanel(Canvas canvas, float topY, float length) { int position = editor.getLnPanelPosition(); String text = editor.getLineNumberTipTextProvider().getCurrentText(editor); float backupSize = paintGeneral.getTextSize(); + paintGeneral.setTextSkewX(0f); + paintGeneral.setFakeBoldText(false); paintGeneral.setTextSize(editor.getLineInfoTextSize()); Paint.FontMetricsInt backupMetrics = metricsText; metricsText = paintGeneral.getFontMetricsInt(); diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/SymbolInputView.java b/editor/src/main/java/io/github/rosemoe/sora/widget/SymbolInputView.java index e12f60752..3ed529c03 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/SymbolInputView.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/SymbolInputView.java @@ -69,7 +69,9 @@ public SymbolInputView(Context context, AttributeSet attrs, int defStyleAttr, in } private void init() { - setBackgroundColor(getContext().getResources().getColor(R.color.defaultSymbolInputBackgroundColor)); + if (getBackground() == null) { + setBackgroundColor(getContext().getResources().getColor(R.color.defaultSymbolInputBackgroundColor)); + } setOrientation(HORIZONTAL); setTextColor(getContext().getResources().getColor(R.color.defaultSymbolInputTextColor)); } diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/component/DefaultCompletionLayout.java b/editor/src/main/java/io/github/rosemoe/sora/widget/component/DefaultCompletionLayout.java index e1d50cda2..c26517da8 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/component/DefaultCompletionLayout.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/component/DefaultCompletionLayout.java @@ -28,6 +28,7 @@ import android.graphics.Outline; import android.graphics.drawable.GradientDrawable; import android.os.SystemClock; +import android.util.Log; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; @@ -44,6 +45,15 @@ public class DefaultCompletionLayout implements CompletionLayout { + private static final String TAG = "DefaultCompletionLayout"; + + /** + * Maximum iterations for scroll operations to prevent infinite loops and ANR. + * This safety limit ensures that even if the scroll state becomes inconsistent, + * the UI thread won't be blocked indefinitely. + */ + private static final int MAX_SCROLL_ITERATIONS = 100; + private ListView listView; private ProgressBar progressBar; private LinearLayout rootView; @@ -194,11 +204,30 @@ public void ensureListPositionVisible(int position, int increment) { listView.setSelectionFromTop(0, 0); return; } - while (listView.getFirstVisiblePosition() + 1 > position && listView.canScrollList(-1)) { + + // a FIX: Add iteration counters to prevent infinite loops that can cause ANR!!! + int upScrollIterations = 0; + while (listView.getFirstVisiblePosition() + 1 > position && + listView.canScrollList(-1) && + upScrollIterations < MAX_SCROLL_ITERATIONS) { performScrollList(increment / 2); + upScrollIterations++; } - while (listView.getLastVisiblePosition() - 1 < position && listView.canScrollList(1)) { + + int downScrollIterations = 0; + while (listView.getLastVisiblePosition() - 1 < position && + listView.canScrollList(1) && + downScrollIterations < MAX_SCROLL_ITERATIONS) { performScrollList(-increment / 2); + downScrollIterations++; + } + + // Log warning if we hit the iteration limit (indicates potential issue) + if (upScrollIterations >= MAX_SCROLL_ITERATIONS || downScrollIterations >= MAX_SCROLL_ITERATIONS) { + Log.w(TAG, "ensureListPositionVisible hit iteration limit: " + + "position=" + position + + ", upScrolls=" + upScrollIterations + + ", downScrolls=" + downScrollIterations); } }); } diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/snippet/SnippetController.kt b/editor/src/main/java/io/github/rosemoe/sora/widget/snippet/SnippetController.kt index 188c106cf..34425a63f 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/snippet/SnippetController.kt +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/snippet/SnippetController.kt @@ -98,7 +98,7 @@ class SnippetController(private val editor: CodeEditor) { private var currentTabStopIndex = -1 private var inSequenceEdits = false - private val variableResolver = CompositeSnippetVariableResolver().also { + val variableResolver = CompositeSnippetVariableResolver().also { it.addResolver(ClipboardBasedSnippetVariableResolver(editor.clipboardManager)) it.addResolver(EditorBasedSnippetVariableResolver(editor)) it.addResolver(RandomBasedSnippetVariableResolver()) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f240b419c..046cb9fee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ lsp4j = { module = "org.eclipse.lsp4j:org.eclipse.lsp4j", version.ref = "lsp4j" leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version = "2.14" } junit = { module = "junit:junit", version = "4.13.2" } gson = { module = "com.google.code.gson:gson", version = "2.13.2" } -jcodings = { module = "org.jruby.jcodings:jcodings", version = "1.0.63" } +jcodings = { module = "org.jruby.jcodings:jcodings", version = "1.0.64" } joni = { module = "org.jruby.joni:joni", version = "2.2.6" } snakeyaml-engine = { module = "org.snakeyaml:snakeyaml-engine", version = "3.0.1" } moshi = { module = "com.squareup.moshi:moshi", version = "1.15.2" } @@ -36,7 +36,7 @@ regex-onig = { module = "io.github.dingyi222666.regex-lib:regex-lib-oniguruma", regex-re2j = { module = "io.github.dingyi222666.regex-lib:regex-lib-re2j", version = "1.0.2" } tests-google-truth = { module = "com.google.truth:truth", version = "1.4.5" } -tests-robolectric = { module = "org.robolectric:robolectric", version = "4.16" } +tests-robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/images/auto_completion.jpg b/images/auto_completion.jpg new file mode 100644 index 000000000..66d5edc9d Binary files /dev/null and b/images/auto_completion.jpg differ diff --git a/images/general.jpg b/images/general.jpg index 81d7e80d8..fd1f20e7b 100644 Binary files a/images/general.jpg and b/images/general.jpg differ diff --git a/images/problem_indicators.jpg b/images/problem_indicators.jpg deleted file mode 100644 index d5286edbd..000000000 Binary files a/images/problem_indicators.jpg and /dev/null differ diff --git a/language-textmate/README.md b/language-textmate/README.md index c76edbe4e..ee4011d25 100644 --- a/language-textmate/README.md +++ b/language-textmate/README.md @@ -1,15 +1,34 @@ -## About +# TextMate Support -**Work In Progress** `language-textmate` module is a module that performs syntax highlighting and other functions dynamically. To use it, you need to introduce several other `textmate-*` modules.Our goal is to achieve the effect of VSCode. However, for many reasons, this may be difficult to achieve in the short term. +## Overview -## Features(already available) +This module provide language support and theme configuration based +on [TextMate](https://macromates.com/) rule files. -1. Highlighting of files based on syntax rules -2. Load color theme from file -3. Code block line based on indent and rule +The core implementation of TextMate functionality is +from [tm4e](https://github.com/eclipse-tm4e/tm4e). -## How to get syntax and theme files -If many people use this module, they may collect the available configuration files into a repository later. -- You can obtain relevant documents from [Textmate](https://github.com/textmate). -- Eclipse also uses Textmate, and you can also get files from its related repository。 -- Textmate is also used in [vscode](https://github.com/microsoft/vscode/tree/main/extensions), but its version is ahead of the version used in this module. You can get the configuration file from its source code, but not all of them can be used normally \ No newline at end of file +## Features + +* MultiLanguage Registry +* Syntax Highlighting based on TextMate Grammars +* TextMate Themes +* Folding Regions +* Indentation Rules +* Symbol Pair Auto-Completion + +## Language Bundles and Themes + +We do not currently maintain a repository of TextMate language bundles and themes. + +- You can obtain relevant documents from [TextMate Projects](https://github.com/textmate). +- Eclipse also uses TextMate, and you can also get files from its related repository. +- TextMate is also used in [VSCode](https://github.com/microsoft/vscode/tree/main/extensions). + You can get the configuration file from its source code + - We don't guarantee that all language bundles can be correctly analyzed, due to regex library + difference + - Include `oniguruma-native` module to use the same regex library as VSCode + +Read +our [documentation](https://project-sora.github.io/sora-editor-docs/guide/using-language#language-textmate) +for more information. \ No newline at end of file