Skip to content

Commit b20e13e

Browse files
Merge branch 'stage' into task/ADFA-5235-claude-md-repos-docdb-provenance
2 parents 8355b3b + 1327bd1 commit b20e13e

42 files changed

Lines changed: 2536 additions & 461 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎ARCHITECTURE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Strategy: **layer-and-subsystem based**, not feature-by-feature. The Gradle buil
7272
| Application | `app` | The IDE itself — activities, fragments, services, DI, agent, web server. Wires everything together. |
7373
| Build engine | `subprojects:tooling-api*`, `gradle-plugin*`, `subprojects:projects`, `subprojects:builder-model-impl` | Runs a real Gradle build of the user's project out-of-process and streams events back. |
7474
| Quick Build (experimental, ADFA-4128) | `quickbuild:core`, `quickbuild:daemon`, `quickbuild:protocol`, `quickbuild:runtime` | Live-reloads the user's app on every save in seconds, by running it as a generated proxy app instead of doing a full Gradle rebuild. |
75-
| Language tooling | `lsp:{api,java,kotlin,xml,indexing,refactor-core,ui,…}`, `lexers`, `editor*`, `editor-treesitter` | Language servers, indexing, the Sora-based editor and highlighting, and the tree-sitter document outline (`editor/.../language/outline`, rendered by `app`'s sidebar `OutlineFragment`). `lsp:refactor-core` holds the language-agnostic half of the refactorings (offset spans, block geometry, rewrite composition, name primitives) so `lsp:java` and `lsp:kotlin` share one copy; `lsp:ui` holds the Compose sheets they share. Neither depends on a language server. |
75+
| Language tooling | `lsp:{api,java,kotlin,xml,external,indexing,refactor-core,ui,…}`, `lexers`, `editor*`, `editor-treesitter` | Language servers, indexing, the Sora-based editor and highlighting, and the tree-sitter document outline (`editor/.../language/outline`, rendered by `app`'s sidebar `OutlineFragment`). `lsp:refactor-core` holds the language-agnostic half of the refactorings (offset spans, block geometry, rewrite composition, name primitives) so `lsp:java` and `lsp:kotlin` share one copy; `lsp:ui` holds the Compose sheets they share. Neither depends on a language server. `lsp:external` adapts a plugin's stdio language server to `ILanguageServer` over LSP4J; `app`'s `PluginLanguageSupport` installs plugin grammars and servers. |
7676
| UI design tooling | `layouteditor`, `uidesigner`, `xml-inflater`, `vectormaster`, `compose-preview` | Visual/XML design surfaces for the *user's* app. |
7777
| Shell | `termux:{termux-app,termux-shared,termux-view,termux-emulator}` | Embedded Termux shell and terminal. |
7878
| Plugin system | `plugin-api`, `plugin-api:plugin-builder`, `plugin-manager` | In-app plugin SDK + manager — `AndroidManifest.xml` `<meta-data>` contract, permissions, extensions. See [plugin-api.md](docs/plugin-api.md) for the API surface & compatibility policy. |

‎app/build.gradle.kts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ dependencies {
426426
implementation(projects.subprojects.toolingApi)
427427
implementation(projects.logsender)
428428
implementation(projects.lsp.api)
429+
implementation(projects.lsp.external)
429430
implementation(projects.lsp.java)
430431
implementation(projects.lsp.kotlin)
431432
implementation(projects.lsp.xml)

‎app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,14 @@ import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent
8181
import com.itsaky.androidide.eventbus.events.file.FileRenameEvent
8282
import com.itsaky.androidide.eventbus.events.plugin.PluginCrashedEvent
8383
import com.itsaky.androidide.eventbus.events.preferences.PreferenceChangeEvent
84+
import com.itsaky.androidide.events.PluginLanguagesChangedEvent
8485
import com.itsaky.androidide.floating.model.DockingManager
8586
import com.itsaky.androidide.floating.window.OverlayDialogs
8687
import com.itsaky.androidide.fragments.sidebar.EditorSidebarFragment
8788
import com.itsaky.androidide.idetooltips.TooltipManager
8889
import com.itsaky.androidide.idetooltips.TooltipTag
8990
import com.itsaky.androidide.interfaces.IEditorHandler
91+
import com.itsaky.androidide.lsp.PluginLanguageSupport
9092
import com.itsaky.androidide.models.DeepLinkOpenRequest
9193
import com.itsaky.androidide.models.DeepLinkRequest
9294
import com.itsaky.androidide.models.EditorIntentExtras
@@ -381,6 +383,7 @@ open class EditorHandlerActivity :
381383
TSLanguageRegistry.instance.registerIfNeeded(LogLanguage.TS_TYPE, LogLanguage.FACTORY)
382384
TSLanguageRegistry.instance.registerIfNeeded(JsonLanguage.TS_TYPE, JsonLanguage.FACTORY)
383385
TSLanguageRegistry.instance.registerIfNeeded(XMLLanguage.TS_TYPE, XMLLanguage.FACTORY)
386+
PluginLanguageSupport.registerGrammars()
384387
IDEColorSchemeProvider.initIfNeeded()
385388
}
386389

@@ -636,6 +639,7 @@ open class EditorHandlerActivity :
636639

637640
override fun onStart() {
638641
super.onStart()
642+
reloadChangedPluginLanguages()
639643

640644
lifecycleScope.launch {
641645
try {
@@ -1961,6 +1965,25 @@ open class EditorHandlerActivity :
19611965
tab.text = if (isModified) "*$baseName" else baseName
19621966
}
19631967

1968+
@Subscribe(threadMode = ThreadMode.MAIN)
1969+
fun onPluginLanguagesChanged(
1970+
@Suppress("UNUSED_PARAMETER") event: PluginLanguagesChangedEvent,
1971+
) {
1972+
reloadChangedPluginLanguages()
1973+
}
1974+
1975+
private fun reloadChangedPluginLanguages() {
1976+
val fileTypes = PluginLanguageSupport.takeChangedFileTypes()
1977+
if (fileTypes.isEmpty()) return
1978+
val editors =
1979+
editorViewModel
1980+
.getOpenedFiles()
1981+
.filter { it.extension.lowercase() in fileTypes }
1982+
.mapNotNull { getEditorForFile(it) }
1983+
editors.forEach { it.releaseLanguage() }
1984+
editors.forEach { it.reloadLanguage() }
1985+
}
1986+
19641987
@Subscribe(threadMode = ThreadMode.MAIN)
19651988
fun onPluginCrashed(event: PluginCrashedEvent) {
19661989
if (event.wasDisabled) {

‎app/src/main/java/com/itsaky/androidide/app/CredentialProtectedApplicationLoader.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider
1616
import com.itsaky.androidide.eventbus.events.plugin.PluginCrashedEvent
1717
import com.itsaky.androidide.eventbus.events.preferences.PreferenceChangeEvent
1818
import com.itsaky.androidide.lookup.Lookup
19+
import com.itsaky.androidide.lsp.PluginLanguageSupport
1920
import com.itsaky.androidide.managers.ToolsManager
2021
import com.itsaky.androidide.plugins.PluginLogger
2122
import com.itsaky.androidide.plugins.base.PluginFragmentHelper
@@ -361,6 +362,8 @@ internal object CredentialProtectedApplicationLoader : ApplicationLoader {
361362
logger = pluginLogger,
362363
)
363364

365+
pluginManager?.setLanguageContributionsListener(PluginLanguageSupport::refresh)
366+
364367
// Set up plugin service providers
365368
setupPluginServices()
366369
setupPluginInflationErrorHandler()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.itsaky.androidide.events
2+
3+
import com.itsaky.androidide.eventbus.events.Event
4+
5+
class PluginLanguagesChangedEvent : Event()

‎app/src/main/java/com/itsaky/androidide/handlers/LspHandler.kt‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package com.itsaky.androidide.handlers
1919

20+
import com.itsaky.androidide.lsp.PluginLanguageSupport
2021
import com.itsaky.androidide.lsp.api.ILanguageClient
2122
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
2223
import com.itsaky.androidide.lsp.debug.IDebugClient
@@ -30,22 +31,21 @@ import com.itsaky.androidide.utils.FeatureFlags
3031
* @author Akash Yadav
3132
*/
3233
object LspHandler {
33-
3434
fun registerLanguageServers() {
3535
ILanguageServerRegistry.default.apply {
3636
getServer(JavaLanguageServer.SERVER_ID) ?: register(JavaLanguageServer())
3737
getServer(KotlinLanguageServer.SERVER_ID) ?: register(KotlinLanguageServer())
3838
getServer(XMLLanguageServer.SERVER_ID) ?: register(XMLLanguageServer())
3939
}
40+
PluginLanguageSupport.registerServers()
4041
}
4142

4243
fun connectClient(client: ILanguageClient) {
4344
ILanguageServerRegistry.default.connectClient(client)
4445
}
4546

4647
@Throws(Throwable::class)
47-
suspend fun connectDebugClient(client: IDebugClient) =
48-
ILanguageServerRegistry.default.connectDebugClient(client)
48+
suspend fun connectDebugClient(client: IDebugClient) = ILanguageServerRegistry.default.connectDebugClient(client)
4949

5050
fun destroyLanguageServers(isConfigurationChange: Boolean) {
5151
if (isConfigurationChange) {
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package com.itsaky.androidide.lsp
2+
3+
import android.os.Handler
4+
import android.os.Looper
5+
import com.itsaky.androidide.app.IDEApplication
6+
import com.itsaky.androidide.editor.language.treesitter.PluginGrammars
7+
import com.itsaky.androidide.editor.language.treesitter.PluginTreeSitterLanguage
8+
import com.itsaky.androidide.editor.language.treesitter.TSLanguageRegistry
9+
import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguage
10+
import com.itsaky.androidide.events.PluginLanguagesChangedEvent
11+
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
12+
import com.itsaky.androidide.lsp.external.ExternalLanguageServer
13+
import com.itsaky.androidide.plugins.extensions.LanguageServerDefinition
14+
import com.itsaky.androidide.plugins.manager.language.PluginLanguageContribution
15+
import com.itsaky.androidide.preferences.internal.EditorPreferences
16+
import com.itsaky.androidide.utils.Environment
17+
import com.itsaky.androidide.utils.TermuxProcessEnvironment
18+
import org.greenrobot.eventbus.EventBus
19+
import org.slf4j.LoggerFactory
20+
import java.io.File
21+
import java.util.concurrent.Executor
22+
import java.util.concurrent.Executors
23+
24+
object PluginLanguageSupport {
25+
private val log = LoggerFactory.getLogger(PluginLanguageSupport::class.java)
26+
private val hostFileTypes = setOf("java", "kt", "kts", "xml", "json", "log", "gradle", "c", "h", "cc", "cpp", "cxx")
27+
private val mainExecutor = Handler(Looper.getMainLooper()).let { handler -> Executor { handler.post(it) } }
28+
private val refreshExecutor = Executors.newSingleThreadExecutor { Thread(it, "plugin-languages") }
29+
private val lock = Any()
30+
private var installed: List<InstalledLanguage> = emptyList()
31+
private val changedFileTypes = mutableSetOf<String>()
32+
33+
fun takeChangedFileTypes(): Set<String> =
34+
synchronized(lock) {
35+
changedFileTypes.toSet().also { changedFileTypes.clear() }
36+
}
37+
38+
fun serverIdFor(file: File): String? {
39+
val type = file.extension.lowercase()
40+
return synchronized(lock) { installed.firstOrNull { type in it.fileTypes }?.serverId }
41+
}
42+
43+
fun refresh() {
44+
refreshExecutor.execute(::reconcile)
45+
}
46+
47+
fun registerServers() {
48+
synchronized(lock) { installed.forEach(::registerServer) }
49+
}
50+
51+
fun registerGrammars() {
52+
synchronized(lock) { installed.forEach(::registerGrammar) }
53+
}
54+
55+
private fun registerGrammar(language: InstalledLanguage) {
56+
language.grammarFactories.forEach { (type, factory) -> TSLanguageRegistry.instance.registerIfNeeded(type, factory) }
57+
}
58+
59+
private fun reconcile() {
60+
val contributions = IDEApplication.getPluginManager()?.getEnabledLanguageContributions() ?: emptyList()
61+
val changed =
62+
synchronized(lock) {
63+
val keep = installed.filter { it.contribution in contributions }
64+
val removed = installed.filterNot { it in keep }
65+
removed.forEach(::uninstall)
66+
67+
val claimed = keep.flatMapTo(mutableSetOf()) { it.fileTypes }
68+
val added =
69+
contributions
70+
.filter { contribution -> keep.none { it.contribution == contribution } }
71+
.mapNotNull { install(it, claimed) }
72+
installed = keep + added
73+
installed.forEach(::registerGrammar)
74+
installed.forEach(::registerServer)
75+
changedFileTypes.addAll((removed + added).flatMap { it.fileTypes })
76+
}
77+
if (changed) {
78+
EventBus.getDefault().post(PluginLanguagesChangedEvent())
79+
}
80+
}
81+
82+
private fun install(
83+
contribution: PluginLanguageContribution,
84+
claimed: MutableSet<String>,
85+
): InstalledLanguage? {
86+
val definition = contribution.definition
87+
val fileTypes =
88+
definition.fileExtensions
89+
.map { it.lowercase() }
90+
.filter { type ->
91+
val free = type !in hostFileTypes && type !in claimed
92+
if (!free) {
93+
log.warn("Plugin {} cannot claim '.{}' for {}: already handled", contribution.pluginId, type, definition.languageId)
94+
}
95+
free
96+
}.toSet()
97+
if (fileTypes.isEmpty()) return null
98+
claimed += fileTypes
99+
100+
val serverId =
101+
definition.server?.let {
102+
"plugin.${contribution.pluginId}.${definition.languageId}.${fileTypes.sorted().joinToString("+")}"
103+
}
104+
val factories = grammarFactories(contribution, fileTypes, serverId)
105+
log.info("Installed {} from plugin {} for {}", definition.languageId, contribution.pluginId, fileTypes)
106+
return InstalledLanguage(contribution, fileTypes, serverId, factories)
107+
}
108+
109+
private fun grammarFactories(
110+
contribution: PluginLanguageContribution,
111+
fileTypes: Set<String>,
112+
serverId: String?,
113+
): Map<String, TreeSitterLanguage.Factory<*>> {
114+
val grammar = contribution.definition.grammar ?: return emptyMap()
115+
val library = File(contribution.nativeLibraryDir, "libtree-sitter-${grammar.name}.so")
116+
if (!library.isFile) {
117+
log.error("Plugin {} grammar library not found: {}", contribution.pluginId, library)
118+
return emptyMap()
119+
}
120+
return fileTypes
121+
.associateWith { type ->
122+
PluginTreeSitterLanguage.Factory(type, library, grammar.name, contribution.assets, grammar.queriesAssetPath, serverId)
123+
}
124+
}
125+
126+
private fun uninstall(language: InstalledLanguage) {
127+
language.grammarFactories.forEach { (type, factory) -> TSLanguageRegistry.instance.unregister(type, factory) }
128+
if (language.grammarFactories.isNotEmpty()) {
129+
language.contribution.definition.grammar
130+
?.let { PluginGrammars.retire(it.name) }
131+
}
132+
val serverId = language.serverId ?: return
133+
val registry = ILanguageServerRegistry.default
134+
if (registry.getServer(serverId) != null) {
135+
registry.unregister(serverId)
136+
}
137+
log.info("Uninstalled {} from plugin {}", language.contribution.definition.languageId, language.contribution.pluginId)
138+
}
139+
140+
private fun registerServer(language: InstalledLanguage) {
141+
val serverId = language.serverId ?: return
142+
val server = language.contribution.definition.server ?: return
143+
val registry = ILanguageServerRegistry.default
144+
if (registry.getServer(serverId) != null) return
145+
registry.register(
146+
ExternalLanguageServer(
147+
serverId = serverId,
148+
languageId = language.contribution.definition.languageId,
149+
fileExtensions = language.fileTypes,
150+
initializationOptions = server.initializationOptions,
151+
processFactory = { workingDirectory -> startProcess(server, workingDirectory) },
152+
indentation = { ExternalLanguageServer.Indentation(EditorPreferences.tabSize, EditorPreferences.useSoftTab) },
153+
uiExecutor = mainExecutor,
154+
),
155+
)
156+
}
157+
158+
private fun startProcess(
159+
server: LanguageServerDefinition,
160+
workingDirectory: File?,
161+
): Process {
162+
val command = server.command.toMutableList()
163+
val executable = command.first()
164+
if ('/' !in executable) {
165+
val termuxExecutable = File(Environment.BIN_DIR, executable)
166+
if (termuxExecutable.canExecute()) {
167+
command[0] = termuxExecutable.absolutePath
168+
}
169+
}
170+
return ProcessBuilder(command)
171+
.apply {
172+
workingDirectory?.let(::directory)
173+
environment().putAll(server.environment)
174+
TermuxProcessEnvironment.applyTo(environment(), Environment.PREFIX.parentFile)
175+
}.start()
176+
}
177+
178+
private data class InstalledLanguage(
179+
val contribution: PluginLanguageContribution,
180+
val fileTypes: Set<String>,
181+
val serverId: String?,
182+
val grammarFactories: Map<String, TreeSitterLanguage.Factory<*>>,
183+
)
184+
}

‎app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import com.itsaky.androidide.eventbus.events.preferences.PreferenceChangeEvent
4545
import com.itsaky.androidide.lsp.BreakpointHandler
4646
import com.itsaky.androidide.lsp.IDEDebugClientImpl
4747
import com.itsaky.androidide.lsp.IDELanguageClientImpl
48+
import com.itsaky.androidide.lsp.PluginLanguageSupport
4849
import com.itsaky.androidide.lsp.api.ILanguageServer
4950
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
5051
import com.itsaky.androidide.lsp.java.JavaLanguageServer
@@ -60,6 +61,7 @@ import com.itsaky.androidide.utils.dpToPx
6061
import io.github.rosemoe.sora.event.ClickEvent
6162
import io.github.rosemoe.sora.event.InterceptTarget
6263
import io.github.rosemoe.sora.event.TextSizeChangeEvent
64+
import io.github.rosemoe.sora.lang.EmptyLanguage
6365
import io.github.rosemoe.sora.text.Content
6466
import io.github.rosemoe.sora.text.LineSeparator
6567
import io.github.rosemoe.sora.util.IntPair
@@ -330,6 +332,15 @@ class CodeEditorView(
330332
postRead(file)
331333
}
332334

335+
fun releaseLanguage() {
336+
_binding?.editor?.setEditorLanguage(EmptyLanguage())
337+
}
338+
339+
fun reloadLanguage() {
340+
if (_binding == null) return
341+
postRead(file ?: return)
342+
}
343+
333344
/**
334345
* Called when the editor has been selected and is visible to the user.
335346
*/
@@ -619,7 +630,7 @@ class CodeEditorView(
619630
"java" -> JavaLanguageServer.SERVER_ID
620631
"kt", "kts" -> KotlinLanguageServer.SERVER_ID
621632
"xml" -> XMLLanguageServer.SERVER_ID
622-
else -> return null
633+
else -> PluginLanguageSupport.serverIdFor(file) ?: return null
623634
}
624635

625636
return ILanguageServerRegistry.default.getServer(serverID)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.itsaky.androidide.utils
2+
3+
import java.io.File
4+
5+
object TermuxProcessEnvironment {
6+
fun applyTo(
7+
env: MutableMap<String, String>,
8+
termuxRoot: File,
9+
) {
10+
val termuxBase = termuxRoot.absolutePath
11+
val termuxBin = "$termuxBase/usr/bin"
12+
val termuxLib = "$termuxBase/usr/lib"
13+
14+
val existingPath = env["PATH"] ?: ""
15+
if (!existingPath.contains(termuxBin)) {
16+
env["PATH"] = "$termuxBin:$existingPath"
17+
}
18+
19+
val existingLdPath = env["LD_LIBRARY_PATH"] ?: ""
20+
if (!existingLdPath.contains(termuxLib)) {
21+
env["LD_LIBRARY_PATH"] = "$termuxLib:$existingLdPath"
22+
}
23+
24+
env.putIfAbsent("HOME", "$termuxBase/home")
25+
env.putIfAbsent("TMPDIR", "$termuxBase/usr/tmp")
26+
env.putIfAbsent("LANG", "en_US.UTF-8")
27+
env.putIfAbsent("PREFIX", "$termuxBase/usr")
28+
}
29+
}

0 commit comments

Comments
 (0)