diff --git a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt index 3882e8789f..06f2e0d0b8 100644 --- a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt +++ b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt @@ -33,6 +33,8 @@ import com.itsaky.androidide.analytics.gradle.BuildCompletedMetric import com.itsaky.androidide.analytics.gradle.BuildStartedMetric import com.itsaky.androidide.app.BaseApplication import com.itsaky.androidide.app.IDEApplication +import com.itsaky.androidide.eventbus.events.BuildCompletedEvent +import com.itsaky.androidide.eventbus.events.BuildStartedEvent import com.itsaky.androidide.lookup.Lookup import com.itsaky.androidide.lsp.java.debug.JdwpOptions import com.itsaky.androidide.managers.ToolsManager @@ -78,6 +80,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.future.await import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus import org.koin.android.ext.android.inject import org.slf4j.LoggerFactory import java.io.File @@ -405,6 +408,11 @@ class GradleBuildService : ), ) + EventBus.getDefault() + .post( + BuildStartedEvent(buildInfo) + ) + eventListener?.prepareBuild(buildInfo) return@supplyAsync ClientGradleBuildConfig( @@ -446,6 +454,13 @@ class GradleBuildService : .indexingServiceManager .onBuildCompleted() } + + EventBus.getDefault() + .post( + BuildCompletedEvent( + result = result, + ) + ) } override fun onProgressEvent(event: ProgressEvent) { diff --git a/editor/src/main/java/com/itsaky/androidide/editor/adapters/CompletionListAdapter.kt b/editor/src/main/java/com/itsaky/androidide/editor/adapters/CompletionListAdapter.kt index a43471c8cc..67b3bfe11f 100755 --- a/editor/src/main/java/com/itsaky/androidide/editor/adapters/CompletionListAdapter.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/adapters/CompletionListAdapter.kt @@ -25,6 +25,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.core.view.isVisible import com.itsaky.androidide.editor.R import com.itsaky.androidide.editor.databinding.LayoutCompletionItemBinding import com.itsaky.androidide.lookup.Lookup @@ -55,177 +56,178 @@ import com.itsaky.androidide.lsp.models.CompletionItem as LspCompletionItem class CompletionListAdapter : EditorCompletionAdapter() { - override fun getItemHeight(): Int { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - 40f, - Resources.getSystem().displayMetrics - ) - .toInt() - } - - override fun getView( - position: Int, - convertView: View?, - parent: ViewGroup?, - isCurrentCursorPosition: Boolean, - ): View { - val binding = - convertView?.let { LayoutCompletionItemBinding.bind(it) } - ?: LayoutCompletionItemBinding.inflate(LayoutInflater.from(context), parent, false) - val item = getItem(position) as LspCompletionItem - val label = item.ideLabel - val desc = item.detail - var type: String? = item.completionKind.toString() - val header = if (type!!.isEmpty()) "O" else type[0].toString() - if (item.overrideTypeText != null) { - type = item.overrideTypeText - } - binding.completionIconText.text = header - binding.completionLabel.text = label - binding.completionType.text = type - binding.completionDetail.text = desc - binding.completionIconText.setTypeface(customOrJBMono(EditorPreferences.useCustomFont), - Typeface.BOLD) - if (desc.isEmpty()) { - binding.completionDetail.visibility = View.GONE - } - - binding.completionApiInfo.visibility = View.GONE - - applyColorScheme(binding, isCurrentCursorPosition) - showApiInfoIfNeeded(item, binding.completionApiInfo) - return binding.root - } - - private fun applyColorScheme(binding: LayoutCompletionItemBinding, isCurrent: Boolean) { - setItemBackground(binding, isCurrent) - var color = getThemeColor(COMPLETION_WND_TEXT_LABEL) - if (color != 0) { - binding.completionLabel.setTextColor(color) - binding.completionIconText.setTextColor(color) - } - - color = getThemeColor(COMPLETION_WND_TEXT_DETAIL) - if (color != 0) { - binding.completionDetail.setTextColor(color) - } - - color = getThemeColor(COMPLETION_WND_TEXT_API) - if (color != 0) { - binding.completionApiInfo.setTextColor(color) - } - - color = getThemeColor(COMPLETION_WND_TEXT_TYPE) - if (color != 0) { - binding.completionType.setTextColor(color) - } - } - - private fun setItemBackground(binding: LayoutCompletionItemBinding, isCurrent: Boolean) { - val color = - if (isCurrent) getThemeColor(SchemeAndroidIDE.COMPLETION_WND_BG_CURRENT_ITEM) - else 0 - - val cornerRadius = binding.root.context.resources - .getDimensionPixelSize(R.dimen.completion_window_corner_radius).toFloat() - - val gd = GradientDrawable().apply { - setColor(color) - setCornerRadius(cornerRadius) - } - - binding.root.background = gd - } - - private fun showApiInfoIfNeeded(item: LspCompletionItem, textView: TextView) { - executeAsync({ - if (!isValidForApiVersion(item)) { - return@executeAsync null - } - - val data = item.data - val versions = - Lookup.getDefault().lookup(ApiVersions.COMPLETION_LOOKUP_KEY) ?: return@executeAsync null - val className = - when (data) { - is ClassCompletionData -> data.className - is MemberCompletionData -> data.classInfo.className - else -> return@executeAsync null - } - val kind = item.completionKind - - val clazz = versions.getClass(className) ?: return@executeAsync null - var info: Info? = clazz - - if (data is MethodCompletionData) { - if ( - kind == METHOD && data.erasedParameterTypes.isNotEmpty() && data.memberName.isNotBlank() - ) { - val method = clazz.getMethod(data.memberName, *data.erasedParameterTypes.toTypedArray()) - if (method != null) { - info = method - } - } else if (kind == FIELD && data.memberName.isNotBlank()) { - val field = clazz.getField(data.memberName) - if (field != null) { - info = field - } - } - } - val sb = StringBuilder() - if (info!!.since > 1) { - sb.append(textView.context.getString(msg_api_info_since, info.since)) - sb.append("\n") - } - - if (info.removed > 0) { - sb.append(textView.context.getString(msg_api_info_removed, info.removed)) - sb.append("\n") - } - - if (info.deprecated > 0) { - sb.append(textView.context.getString(msg_api_info_deprecated, info.deprecated)) - sb.append("\n") - } - - return@executeAsync sb - }) { - if (it.isNullOrBlank()) { - textView.visibility = View.GONE - return@executeAsync - } - - textView.text = it - textView.visibility = View.VISIBLE - } - } - - private fun isValidForApiVersion(item: LspCompletionItem?): Boolean { - if (item == null) { - return false - } - val type = item.completionKind - val data = item.data - return if ( // These represent a class type - (type === CLASS || - type === INTERFACE || - type === ENUM || - - // These represent a method type - type === METHOD || - type === CONSTRUCTOR || - - // A field type - type === FIELD) && data != null - ) { - val className = - when (data) { - is ClassCompletionData -> data.className - is MemberCompletionData -> data.classInfo.className - else -> null - } - !TextUtils.isEmpty(className) - } else false - } + override fun getItemHeight(): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 40f, + Resources.getSystem().displayMetrics + ) + .toInt() + } + + override fun getView( + position: Int, + convertView: View?, + parent: ViewGroup?, + isCurrentCursorPosition: Boolean, + ): View { + val binding = + convertView?.let { LayoutCompletionItemBinding.bind(it) } + ?: LayoutCompletionItemBinding.inflate(LayoutInflater.from(context), parent, false) + val item = getItem(position) as LspCompletionItem + val label = item.ideLabel + val desc = item.detail + var type: String? = item.completionKind.toString() + val header = if (type!!.isEmpty()) "O" else type[0].toString() + if (item.overrideTypeText != null) { + type = item.overrideTypeText + } + binding.completionIconText.text = header + binding.completionLabel.text = label + binding.completionType.text = type + binding.completionDetail.text = desc + binding.completionIconText.setTypeface( + customOrJBMono(EditorPreferences.useCustomFont), + Typeface.BOLD + ) + binding.completionApiInfo.visibility = View.GONE + binding.completionDetail.isVisible = desc.isNotEmpty() + + applyColorScheme(binding, isCurrentCursorPosition) + showApiInfoIfNeeded(item, binding.completionApiInfo) + return binding.root + } + + private fun applyColorScheme(binding: LayoutCompletionItemBinding, isCurrent: Boolean) { + setItemBackground(binding, isCurrent) + var color = getThemeColor(COMPLETION_WND_TEXT_LABEL) + if (color != 0) { + binding.completionLabel.setTextColor(color) + binding.completionIconText.setTextColor(color) + } + + color = getThemeColor(COMPLETION_WND_TEXT_DETAIL) + if (color != 0) { + binding.completionDetail.setTextColor(color) + } + + color = getThemeColor(COMPLETION_WND_TEXT_API) + if (color != 0) { + binding.completionApiInfo.setTextColor(color) + } + + color = getThemeColor(COMPLETION_WND_TEXT_TYPE) + if (color != 0) { + binding.completionType.setTextColor(color) + } + } + + private fun setItemBackground(binding: LayoutCompletionItemBinding, isCurrent: Boolean) { + val color = + if (isCurrent) getThemeColor(SchemeAndroidIDE.COMPLETION_WND_BG_CURRENT_ITEM) + else 0 + + val cornerRadius = binding.root.context.resources + .getDimensionPixelSize(R.dimen.completion_window_corner_radius).toFloat() + + val gd = GradientDrawable().apply { + setColor(color) + setCornerRadius(cornerRadius) + } + + binding.root.background = gd + } + + private fun showApiInfoIfNeeded(item: LspCompletionItem, textView: TextView) { + executeAsync({ + if (!isValidForApiVersion(item)) { + return@executeAsync null + } + + val data = item.data + val versions = + Lookup.getDefault().lookup(ApiVersions.COMPLETION_LOOKUP_KEY) + ?: return@executeAsync null + val className = + when (data) { + is ClassCompletionData -> data.className + is MemberCompletionData -> data.classInfo.className + else -> return@executeAsync null + } + val kind = item.completionKind + + val clazz = versions.getClass(className) ?: return@executeAsync null + var info: Info? = clazz + + if (data is MethodCompletionData) { + if ( + kind == METHOD && data.erasedParameterTypes.isNotEmpty() && data.memberName.isNotBlank() + ) { + val method = + clazz.getMethod(data.memberName, *data.erasedParameterTypes.toTypedArray()) + if (method != null) { + info = method + } + } else if (kind == FIELD && data.memberName.isNotBlank()) { + val field = clazz.getField(data.memberName) + if (field != null) { + info = field + } + } + } + val sb = StringBuilder() + if (info!!.since > 1) { + sb.append(textView.context.getString(msg_api_info_since, info.since)) + sb.append("\n") + } + + if (info.removed > 0) { + sb.append(textView.context.getString(msg_api_info_removed, info.removed)) + sb.append("\n") + } + + if (info.deprecated > 0) { + sb.append(textView.context.getString(msg_api_info_deprecated, info.deprecated)) + sb.append("\n") + } + + return@executeAsync sb + }) { + if (it.isNullOrBlank()) { + textView.visibility = View.GONE + return@executeAsync + } + + textView.text = it + textView.visibility = View.VISIBLE + } + } + + private fun isValidForApiVersion(item: LspCompletionItem?): Boolean { + if (item == null) { + return false + } + val type = item.completionKind + val data = item.data + return if ( // These represent a class type + (type === CLASS || + type === INTERFACE || + type === ENUM || + + // These represent a method type + type === METHOD || + type === CONSTRUCTOR || + + // A field type + type === FIELD) && data != null + ) { + val className = + when (data) { + is ClassCompletionData -> data.className + is MemberCompletionData -> data.classInfo.className + else -> null + } + !TextUtils.isEmpty(className) + } else false + } } diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/CommonCompletionProvider.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/CommonCompletionProvider.kt index d78c153867..01fc98b186 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/CommonCompletionProvider.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/CommonCompletionProvider.kt @@ -39,59 +39,61 @@ import java.util.concurrent.CancellationException * @author Akash Yadav */ internal class CommonCompletionProvider( - private val server: ILanguageServer, - private val cancelChecker: CompletionCancelChecker + private val server: ILanguageServer, + private val cancelChecker: CompletionCancelChecker ) { - companion object { + companion object { - private val log = LoggerFactory.getLogger(CommonCompletionProvider::class.java) - } + private val log = LoggerFactory.getLogger(CommonCompletionProvider::class.java) + } - /** - * Computes completion items using the provided language server instance. - * - * @param content The reference to the content of the editor. - * @param file The file to compute completions for. - * @param position The position of the cursor in the content. - * @return The computed completion items. May return an empty list if the there was an error - * computing the completion items. - */ - inline fun complete( - content: ContentReference, - file: Path, - position: CharPosition, - prefixMatcher: (Char) -> Boolean - ): List { - val completionResult = - try { - setupLookupForCompletion(file) - val prefix = CompletionHelper.computePrefix(content, position, prefixMatcher) - val params = - CompletionParams(Position(position.line, position.column, position.index), file, - cancelChecker) - params.content = content - params.prefix = prefix - server.complete(params) - } catch (e: Throwable) { + /** + * Computes completion items using the provided language server instance. + * + * @param content The reference to the content of the editor. + * @param file The file to compute completions for. + * @param position The position of the cursor in the content. + * @return The computed completion items. May return an empty list if the there was an error + * computing the completion items. + */ + inline fun complete( + content: ContentReference, + file: Path, + position: CharPosition, + prefixMatcher: (Char) -> Boolean + ): List { + val completionResult = + try { + setupLookupForCompletion(file) + val prefix = CompletionHelper.computePrefix(content, position, prefixMatcher) + val params = + CompletionParams( + Position(position.line, position.column, position.index), file, + cancelChecker + ) + params.content = content + params.prefix = prefix + server.complete(params) + } catch (e: Throwable) { - if (e is CancellationException) { - log.debug("Completion process cancelled") - } + if (e is CancellationException) { + log.debug("Completion process cancelled") + } - // Do not log if completion was interrupted or cancelled - if (!(e is CancellationException || e is CompletionCancelledException)) { - if (!server.handleFailure(LSPFailure(COMPLETION, e))) { - log.error("Unable to compute completions", e) - } - } - CompletionResult.EMPTY - } + // Do not log if completion was interrupted or cancelled + if (!(e is CancellationException || e is CompletionCancelledException)) { + if (!server.handleFailure(LSPFailure(COMPLETION, e))) { + log.error("Unable to compute completions", e) + } + } + CompletionResult.EMPTY + } - if (completionResult == CompletionResult.EMPTY) { - return listOf() - } + if (completionResult == CompletionResult.EMPTY) { + return listOf() + } - return completionResult.items - } + return completionResult.items + } } diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt index 653ffc88ad..7b38f93c05 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt @@ -42,95 +42,97 @@ import java.nio.file.Paths */ abstract class IDELanguage : Language { - private var formatter: Formatter? = null - - protected open val languageServer: ILanguageServer? - get() = null - - open fun getTabSize(): Int { - return EditorPreferences.tabSize - } - - open fun addBreakpoint(line: Int) {} - open fun addBreakpoints(lines: Iterable) = lines.forEach(::addBreakpoint) - open fun removeBreakpoint(line: Int) {} - open fun removeBreakpoints(lines: Iterable) = lines.forEach(::removeBreakpoint) - open fun removeAllBreakpoints() {} - open fun toggleBreakpoint(line: Int) {} - open fun highlightLine(line: Int) {} - open fun unhighlightLines() {} - - @Throws(CompletionCancelledException::class) - override fun requireAutoComplete( - content: ContentReference, - position: CharPosition, - publisher: CompletionPublisher, - extraArguments: Bundle - ) { - try { - val cancelChecker = CompletionCancelChecker(publisher) - Lookup.getDefault().register(ICancelChecker::class.java, cancelChecker) - doComplete(content, position, publisher, cancelChecker, extraArguments) - } finally { - Lookup.getDefault().unregister( - ICancelChecker::class.java) - } - } - - private fun doComplete( - content: ContentReference, - position: CharPosition, - publisher: CompletionPublisher, - cancelChecker: CompletionCancelChecker, - extraArguments: Bundle - ) { - val server = languageServer ?: return - val path = extraArguments.getString(IEditor.KEY_FILE, null) - if (path == null) { - log.warn("Cannot provide completions. No file provided.") - return - } - - val completionProvider = CommonCompletionProvider(server, cancelChecker) - val file = Paths.get(path) - val completionItems = completionProvider.complete(content, file, - position) { checkIsCompletionChar(it) } - publisher.setUpdateThreshold(1) - (publisher as IDECompletionPublisher).addLSPItems(completionItems) - } - - /** - * Check if the given character is a completion character. - * - * @param c The character to check. - * @return `true` if the character is completion char, `false` otherwise. - */ - protected open fun checkIsCompletionChar(c: Char): Boolean { - return false - } - - override fun useTab(): Boolean { - return !EditorPreferences.useSoftTab - } - - override fun getFormatter(): Formatter { - return formatter ?: LSPFormatter(languageServer).also { formatter = it } - } - - override fun getIndentAdvance( - content: ContentReference, - line: Int, - column: Int - ): Int { - return getIndentAdvance(content.getLine(line).substring(0, column)) - } - - open fun getIndentAdvance(line: String): Int { - return 0 - } - - companion object { - - private val log = LoggerFactory.getLogger(IDELanguage::class.java) - } + private var formatter: Formatter? = null + + protected open val languageServer: ILanguageServer? + get() = null + + open fun getTabSize(): Int { + return EditorPreferences.tabSize + } + + open fun addBreakpoint(line: Int) {} + open fun addBreakpoints(lines: Iterable) = lines.forEach(::addBreakpoint) + open fun removeBreakpoint(line: Int) {} + open fun removeBreakpoints(lines: Iterable) = lines.forEach(::removeBreakpoint) + open fun removeAllBreakpoints() {} + open fun toggleBreakpoint(line: Int) {} + open fun highlightLine(line: Int) {} + open fun unhighlightLines() {} + + @Throws(CompletionCancelledException::class) + override fun requireAutoComplete( + content: ContentReference, + position: CharPosition, + publisher: CompletionPublisher, + extraArguments: Bundle + ) { + try { + val cancelChecker = CompletionCancelChecker(publisher) + Lookup.getDefault().register(ICancelChecker::class.java, cancelChecker) + doComplete(content, position, publisher, cancelChecker, extraArguments) + } finally { + Lookup.getDefault().unregister( + ICancelChecker::class.java + ) + } + } + + private fun doComplete( + content: ContentReference, + position: CharPosition, + publisher: CompletionPublisher, + cancelChecker: CompletionCancelChecker, + extraArguments: Bundle + ) { + val server = languageServer ?: return + val path = extraArguments.getString(IEditor.KEY_FILE, null) + if (path == null) { + log.warn("Cannot provide completions. No file provided.") + return + } + + val completionProvider = CommonCompletionProvider(server, cancelChecker) + val file = Paths.get(path) + val completionItems = + completionProvider.complete(content, file, position) { checkIsCompletionChar(it) } + + publisher.setUpdateThreshold(1) + (publisher as IDECompletionPublisher).addLSPItems(completionItems) + } + + /** + * Check if the given character is a completion character. + * + * @param c The character to check. + * @return `true` if the character is completion char, `false` otherwise. + */ + protected open fun checkIsCompletionChar(c: Char): Boolean { + return false + } + + override fun useTab(): Boolean { + return !EditorPreferences.useSoftTab + } + + override fun getFormatter(): Formatter { + return formatter ?: LSPFormatter(languageServer).also { formatter = it } + } + + override fun getIndentAdvance( + content: ContentReference, + line: Int, + column: Int + ): Int { + return getIndentAdvance(content.getLine(line).substring(0, column)) + } + + open fun getIndentAdvance(line: String): Int { + return 0 + } + + companion object { + + private val log = LoggerFactory.getLogger(IDELanguage::class.java) + } } \ No newline at end of file diff --git a/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/BuildEvent.kt b/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/BuildEvent.kt new file mode 100644 index 0000000000..916a0954e6 --- /dev/null +++ b/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/BuildEvent.kt @@ -0,0 +1,32 @@ +package com.itsaky.androidide.eventbus.events + +import com.itsaky.androidide.tooling.api.messages.BuildId +import com.itsaky.androidide.tooling.api.messages.result.BuildInfo +import com.itsaky.androidide.tooling.api.messages.result.BuildResult + +/** + * Events dispatched from the IDE's build service. + * + * @property buildId The build identifier. + */ +abstract class BuildEvent( + val buildId: BuildId, +) : Event() + +/** + * Event dispatched when a Gradle build is started in the IDE. + * + * @property buildInfo Info about the build. + */ +class BuildStartedEvent( + val buildInfo: BuildInfo, +): BuildEvent(buildInfo.buildId) + +/** + * Event dispatched when a Gradle build is completed in the IDE. + * + * @property result The result of the Gradle build. + */ +class BuildCompletedEvent( + val result: BuildResult, +): BuildEvent(result.buildId) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt index bf0e4d9ddd..c7e9223978 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt @@ -124,10 +124,12 @@ class KotlinLanguageServer : ILanguageServer { override fun setupWithProject(workspace: Workspace) { logger.info("setupWithProject called, initialized={}", initialized) - (ProjectManagerImpl.getInstance() + val indexingServiceManager = ProjectManagerImpl.getInstance() .indexingServiceManager - .getService(JvmIndexingService.ID) as? JvmIndexingService?) - ?.refresh() + val jvmIndexingService = + indexingServiceManager.getService(JvmIndexingService.ID) as? JvmIndexingService? + + jvmIndexingService?.refresh() val jdkHome = Environment.JAVA_HOME.toPath() val jdkRelease = IJdkDistributionProvider.DEFAULT_JAVA_RELEASE diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt index 6bb19535d9..c7a322289d 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt @@ -52,7 +52,7 @@ import kotlin.io.path.pathString * @param jdkRelease The JDK release version at [jdkHome]. */ internal class CompilationEnvironment( - val projectModel: KotlinProjectModel, + val project: KotlinProjectModel, val intellijPluginRoot: Path, val jdkHome: Path, val jdkRelease: Int, @@ -82,11 +82,8 @@ internal class CompilationEnvironment( val coreApplicationEnvironment: CoreApplicationEnvironment get() = session.coreApplicationEnvironment - val moduleResolver: ModuleResolver? - get() = projectModel.moduleResolver - val symbolVisibilityChecker: SymbolVisibilityChecker? - get() = projectModel.symbolVisibilityChecker + get() = project.symbolVisibilityChecker private val envMessageCollector = object : MessageCollector { override fun clear() { @@ -115,7 +112,7 @@ internal class CompilationEnvironment( parser = KtPsiFactory(session.project, eventSystemEnabled = enableParserEventSystem) fileManager = KtFileManager(parser, psiManager, psiDocumentManager) - projectModel.addListener(this) + project.addListener(this) } private fun buildSession(): StandaloneAnalysisAPISession { @@ -127,7 +124,7 @@ internal class CompilationEnvironment( compilerConfiguration = configuration, ) { buildKtModuleProvider { - projectModel.configureModules(this) + this@CompilationEnvironment.project.configureModules(this) } } @@ -239,7 +236,7 @@ internal class CompilationEnvironment( override fun close() { fileManager.close() - projectModel.removeListener(this) + project.removeListener(this) disposable.dispose() } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt index 48bde185b6..9d501b0d65 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt @@ -35,7 +35,7 @@ internal class Compiler( init { defaultCompilationEnv = CompilationEnvironment( - projectModel = projectModel, + project = projectModel, intellijPluginRoot = intellijPluginRoot, jdkHome = jdkHome, jdkRelease = jdkRelease, diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt new file mode 100644 index 0000000000..032341591d --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt @@ -0,0 +1,22 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +import org.jetbrains.kotlin.com.intellij.openapi.util.ModificationTracker +import java.util.concurrent.atomic.AtomicLong + +class IncrementalModificationTracker : ModificationTracker { + + private val myCounter = AtomicLong(0) + + /** + * Increment the modification count. + */ + fun incModificationCount() = apply { + myCounter.incrementAndGet() + } + + operator fun inc() = incModificationCount() + + override fun getModificationCount(): Long { + return myCounter.get() + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt index 90fcae59a2..4354826bb1 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt @@ -1,10 +1,13 @@ package com.itsaky.androidide.lsp.kotlin.compiler import com.itsaky.androidide.lsp.kotlin.completion.SymbolVisibilityChecker +import com.itsaky.androidide.projects.ProjectManagerImpl import com.itsaky.androidide.projects.api.AndroidModule import com.itsaky.androidide.projects.api.ModuleProject import com.itsaky.androidide.projects.api.Workspace import com.itsaky.androidide.projects.models.bootClassPaths +import org.appdevforall.codeonthego.indexing.jvm.JVM_LIBRARY_SYMBOL_INDEX +import org.appdevforall.codeonthego.indexing.jvm.JvmLibrarySymbolIndex import org.jetbrains.kotlin.analysis.api.projectStructure.KaLibraryModule import org.jetbrains.kotlin.analysis.api.projectStructure.KaSourceModule import org.jetbrains.kotlin.analysis.project.structure.builder.KtModuleProviderBuilder @@ -44,6 +47,12 @@ internal class KotlinProjectModel { val symbolVisibilityChecker: SymbolVisibilityChecker? get() = _symbolVisibilityChecker + val libraryIndex: JvmLibrarySymbolIndex? + get() = ProjectManagerImpl.getInstance() + .indexingServiceManager + .registry + .get(JVM_LIBRARY_SYMBOL_INDEX) + /** * The kind of change that occurred. */ @@ -77,6 +86,18 @@ internal class KotlinProjectModel { notifyListeners(ChangeKind.STRUCTURE) } + /** + * Called when a build completes and source files may have changed + * (generated sources added/removed), but the module structure is the same. + */ + fun onSourcesChanged() { + if (workspace == null) { + logger.warn("onSourcesChanged called before project model was initialized") + return + } + notifyListeners(ChangeKind.SOURCES) + } + /** * Configures a [KtModuleProviderBuilder] with the current project structure. * diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt index ca003ee1c8..c83fb63749 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt @@ -1,14 +1,26 @@ package com.itsaky.androidide.lsp.kotlin.completion +import com.itsaky.androidide.lsp.api.describeSnippet import com.itsaky.androidide.lsp.kotlin.compiler.CompilationEnvironment +import com.itsaky.androidide.lsp.models.ClassCompletionData import com.itsaky.androidide.lsp.models.Command import com.itsaky.androidide.lsp.models.CompletionItem import com.itsaky.androidide.lsp.models.CompletionItemKind import com.itsaky.androidide.lsp.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.InsertTextFormat +import com.itsaky.androidide.lsp.models.MethodCompletionData +import com.itsaky.androidide.lsp.models.SnippetDescription import com.itsaky.androidide.projects.FileManager +import com.itsaky.androidide.projects.ProjectManagerImpl import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.runBlocking +import org.appdevforall.codeonthego.indexing.jvm.JVM_LIBRARY_SYMBOL_INDEX +import org.appdevforall.codeonthego.indexing.jvm.JvmClassInfo +import org.appdevforall.codeonthego.indexing.jvm.JvmFunctionInfo +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolKind +import org.appdevforall.codeonthego.indexing.jvm.JvmTypeAliasInfo import org.jetbrains.kotlin.analysis.api.KaContextParameterApi import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.KaSession @@ -44,6 +56,7 @@ import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression import org.jetbrains.kotlin.psi.psiUtil.getParentOfType import org.jetbrains.kotlin.types.Variance import org.slf4j.LoggerFactory +import kotlin.math.log private const val KT_COMPLETION_PLACEHOLDER = "KT_COMPLETION_PLACEHOLDER" @@ -118,14 +131,16 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi when (completionContext) { CompletionContext.Scope -> - collectScopeCompletions( - scopeContext = scopeContext, - scope = compositeScope, - symbolVisibilityChecker = symbolVisibilityChecker, - ktElement = ktElement, - partial = partial, - to = items - ) + runBlocking { + collectScopeCompletions( + scopeContext = scopeContext, + scope = compositeScope, + symbolVisibilityChecker = symbolVisibilityChecker, + ktElement = ktElement, + partial = partial, + to = items + ) + } CompletionContext.Member -> collectMemberCompletions( @@ -242,7 +257,7 @@ private fun KaSession.collectExtensionFunctions( to += toCompletionItems(extensionSymbols, partial) } -private fun KaSession.collectScopeCompletions( +private suspend fun KaSession.collectScopeCompletions( scopeContext: KaScopeContext, scope: KaScope, symbolVisibilityChecker: SymbolVisibilityChecker, @@ -274,6 +289,76 @@ private fun KaSession.collectScopeCompletions( to += toCompletionItems(callables, partial) to += toCompletionItems(classifiers, partial) + + val librarySymbolIndex = ProjectManagerImpl + .getInstance() + .indexingServiceManager + .registry + .get(JVM_LIBRARY_SYMBOL_INDEX) + + if (librarySymbolIndex == null) { + logger.warn("Unable to find JVM library symbol index") + return + } + + val useSiteModule = this.useSiteModule + librarySymbolIndex.findByPrefix(partial) + .collect { symbol -> + val isVisible = symbolVisibilityChecker.isVisible( + symbol = symbol, + useSiteModule = useSiteModule, + useSitePackage = ktElement.containingKtFile.packageDirective?.name + ) + + if (!isVisible) return@collect + + if (symbol.kind.isCallable && !symbol.isTopLevel && !symbol.isExtension) { + // member-level, non-imported callable symbols should not be + // completed in scope completions + return@collect + } + + // TODO: filter-out callables with a receiver type whose receiver + // is not an implicit receiver at the current use-site + + val item = ktCompletionItem( + name = symbol.shortName, + kind = kindOf(symbol), + partial = partial, + ) + + item.overrideTypeText = symbol.returnTypeDisplay + when (symbol.kind) { + JvmSymbolKind.FUNCTION, JvmSymbolKind.CONSTRUCTOR -> { + val data = symbol.data as JvmFunctionInfo + item.detail = data.signatureDisplay + item.setInsertTextForFunction(symbol.shortName, data.parameterCount > 0, partial) + + if (symbol.kind == JvmSymbolKind.CONSTRUCTOR) { + item.overrideTypeText = symbol.shortName + } + } + + JvmSymbolKind.TYPE_ALIAS -> { + item.detail = (symbol.data as JvmTypeAliasInfo).expandedTypeFqName + } + + in JvmSymbolKind.CLASSIFIER_KINDS -> { + val classInfo = symbol.data as JvmClassInfo + item.detail = symbol.fqName + item.data = ClassCompletionData( + className = symbol.fqName, + isNested = classInfo.isInner, + topLevelClass = classInfo.containingClassFqName, + ) + } + + else -> {} + } + + logger.debug("Adding completion item: {}", item) + to += item + } } private fun KaSession.collectKeywordCompletions( @@ -335,16 +420,7 @@ private fun KaSession.callableSymbolToCompletionItem( val hasParams = symbol.valueParameters.isNotEmpty() item.detail = "${name}($params)" - item.insertTextFormat = InsertTextFormat.SNIPPET - item.insertText = if (hasParams) { - "${name}($0)" - } else { - "${name}()$0" - } - - if (hasParams) { - item.command = Command("Trigger parameter hints", Command.TRIGGER_PARAMETER_HINTS) - } + item.setInsertTextForFunction(name, hasParams, partial) // TODO(itsaky): provide method completion data in order to show API info // in completion items @@ -360,6 +436,25 @@ private fun KaSession.callableSymbolToCompletionItem( return item } +private fun CompletionItem.setInsertTextForFunction( + name: String, + hasParams: Boolean, + partial: String, +) { + insertTextFormat = InsertTextFormat.SNIPPET + insertText = if (hasParams) { + "${name}($0)" + } else { + "${name}()$0" + } + + snippetDescription = describeSnippet(prefix = partial, allowCommandExecution = true) + + if (hasParams) { + command = Command("Trigger parameter hints", Command.TRIGGER_PARAMETER_HINTS) + } +} + @OptIn(KaExperimentalApi::class) private fun KaSession.classifierSymbolToCompletionItem( symbol: KaClassifierSymbol, @@ -429,6 +524,28 @@ private fun KaSession.kindOf(symbol: KaSymbol): CompletionItemKind { } } +private fun KaSession.kindOf(symbol: JvmSymbol): CompletionItemKind = + when (symbol.kind) { + JvmSymbolKind.CLASS -> CompletionItemKind.CLASS + JvmSymbolKind.INTERFACE -> CompletionItemKind.INTERFACE + JvmSymbolKind.ENUM -> CompletionItemKind.ENUM + JvmSymbolKind.ENUM_ENTRY -> CompletionItemKind.ENUM_MEMBER + JvmSymbolKind.ANNOTATION_CLASS -> CompletionItemKind.ANNOTATION_TYPE + JvmSymbolKind.OBJECT -> CompletionItemKind.CLASS + JvmSymbolKind.COMPANION_OBJECT -> CompletionItemKind.CLASS + JvmSymbolKind.DATA_CLASS -> CompletionItemKind.CLASS + JvmSymbolKind.VALUE_CLASS -> CompletionItemKind.CLASS + JvmSymbolKind.SEALED_CLASS -> CompletionItemKind.CLASS + JvmSymbolKind.SEALED_INTERFACE -> CompletionItemKind.INTERFACE + JvmSymbolKind.FUNCTION -> CompletionItemKind.FUNCTION + JvmSymbolKind.EXTENSION_FUNCTION -> CompletionItemKind.FUNCTION + JvmSymbolKind.CONSTRUCTOR -> CompletionItemKind.CONSTRUCTOR + JvmSymbolKind.PROPERTY -> CompletionItemKind.PROPERTY + JvmSymbolKind.EXTENSION_PROPERTY -> CompletionItemKind.PROPERTY + JvmSymbolKind.FIELD -> CompletionItemKind.FIELD + JvmSymbolKind.TYPE_ALIAS -> CompletionItemKind.CLASS + } + @OptIn(KaExperimentalApi::class, KaContextParameterApi::class) private fun KaSession.renderName( type: KaType, @@ -445,12 +562,6 @@ private fun partialIdentifier(prefix: String): String { } private fun matchesPrefix(name: Name, partial: String): Boolean { - logger.info( - "'{}' matches '{}': {}", - name, - partial, - name.asString().startsWith(partial, ignoreCase = true) - ) if (partial.isEmpty()) return true return name.asString().startsWith(partial, ignoreCase = true) } diff --git a/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt b/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt index 9665847f60..fb49916024 100644 --- a/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt +++ b/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt @@ -35,105 +35,114 @@ import org.slf4j.LoggerFactory */ open class DefaultEditHandler : IEditHandler { - companion object { - - private val log = LoggerFactory.getLogger(DefaultEditHandler::class.java) - } - - override fun performEdits( - item: CompletionItem, - editor: CodeEditor, - text: Content, - line: Int, - column: Int, - index: Int - ) { - if (Looper.myLooper() != Looper.getMainLooper()) { - ThreadUtils.runOnUiThread { performEditsInternal(item, editor, text, line, column, index) } - return - } - - performEditsInternal(item, editor, text, line, column, index) - } - - protected open fun performEditsInternal( - item: CompletionItem, - editor: CodeEditor, - text: Content, - line: Int, - column: Int, - index: Int - ) { - if (item.insertTextFormat == SNIPPET) { - insertSnippet(item, editor, text, line, column, index) - return - } - - val start = getIdentifierStart(text.getLine(line), column) - text.delete(line, start, line, column) - editor.commitText(item.insertText) - - text.beginBatchEdit() - if (item.additionalEditHandler != null) { - item.additionalEditHandler!!.performEdits(item, editor, text, line, column, index) - } else if (item.additionalTextEdits != null && item.additionalTextEdits!!.isNotEmpty()) { - RewriteHelper.performEdits(item.additionalTextEdits!!, editor) - } - text.beginBatchEdit() - - executeCommand(editor, item.command) - } - - protected open fun insertSnippet( - item: CompletionItem, - editor: CodeEditor, - text: Content, - line: Int, - column: Int, - index: Int - ) { - val snippetDescription = item.snippetDescription!! - val snippet = CodeSnippetParser.parse(item.insertText) - val prefixLength = snippetDescription.selectedLength - val selectedText = text.subSequence(index - prefixLength, index).toString() - var actionIndex = index - if (snippetDescription.deleteSelected) { - text.delete(index - prefixLength, index) - actionIndex -= prefixLength - } - editor.snippetController.startSnippet(actionIndex, snippet, selectedText) - - if (snippetDescription.allowCommandExecution) { - executeCommand(editor, item.command) - } - } - - protected open fun executeCommand(editor: CodeEditor, command: Command?) { - if (command == null) { - return - } - - try { - val klass = editor::class.java - val method = klass.getMethod("executeCommand", Command::class.java) - method.isAccessible = true - method.invoke(editor, command) - } catch (th: Throwable) { - log.error("Unable to invoke 'executeCommand(Command) method in IDEEditor.", th) - } - } - - protected open fun getIdentifierStart(text: CharSequence, end: Int): Int { - var start = end - while (start > 0) { - if (isPartialPart(text[start - 1])) { - start-- - continue - } - break - } - return start - } - - protected open fun isPartialPart(c: Char) = Character.isJavaIdentifierPart(c) + companion object { + + private val log = LoggerFactory.getLogger(DefaultEditHandler::class.java) + } + + override fun performEdits( + item: CompletionItem, + editor: CodeEditor, + text: Content, + line: Int, + column: Int, + index: Int + ) { + if (Looper.myLooper() != Looper.getMainLooper()) { + ThreadUtils.runOnUiThread { + performEditsInternal( + item, + editor, + text, + line, + column, + index + ) + } + return + } + + performEditsInternal(item, editor, text, line, column, index) + } + + protected open fun performEditsInternal( + item: CompletionItem, + editor: CodeEditor, + text: Content, + line: Int, + column: Int, + index: Int + ) { + if (item.insertTextFormat == SNIPPET) { + insertSnippet(item, editor, text, line, column, index) + return + } + + val start = getIdentifierStart(text.getLine(line), column) + text.delete(line, start, line, column) + editor.commitText(item.insertText) + + text.beginBatchEdit() + if (item.additionalEditHandler != null) { + item.additionalEditHandler!!.performEdits(item, editor, text, line, column, index) + } else if (item.additionalTextEdits != null && item.additionalTextEdits!!.isNotEmpty()) { + RewriteHelper.performEdits(item.additionalTextEdits!!, editor) + } + text.beginBatchEdit() + + executeCommand(editor, item.command) + } + + protected open fun insertSnippet( + item: CompletionItem, + editor: CodeEditor, + text: Content, + line: Int, + column: Int, + index: Int + ) { + val snippetDescription = item.snippetDescription!! + val snippet = CodeSnippetParser.parse(item.insertText) + val prefixLength = snippetDescription.selectedLength + val selectedText = text.subSequence(index - prefixLength, index).toString() + var actionIndex = index + if (snippetDescription.deleteSelected) { + text.delete(index - prefixLength, index) + actionIndex -= prefixLength + } + editor.snippetController.startSnippet(actionIndex, snippet, selectedText) + + if (snippetDescription.allowCommandExecution) { + executeCommand(editor, item.command) + } + } + + protected open fun executeCommand(editor: CodeEditor, command: Command?) { + if (command == null) { + return + } + + try { + val klass = editor::class.java + val method = klass.getMethod("executeCommand", Command::class.java) + method.isAccessible = true + method.invoke(editor, command) + } catch (th: Throwable) { + log.error("Unable to invoke 'executeCommand(Command) method in IDEEditor.", th) + } + } + + protected open fun getIdentifierStart(text: CharSequence, end: Int): Int { + var start = end + while (start > 0) { + if (isPartialPart(text[start - 1])) { + start-- + continue + } + break + } + return start + } + + protected open fun isPartialPart(c: Char) = Character.isJavaIdentifierPart(c) }