Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 50 additions & 42 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
@file:Suppress("UnstableApiUsage")

import dev.mutwakil.androidide.build.config.BuildConfig
import dev.mutwakil.androidide.build.config.MVN_GROUP_ID
import dev.mutwakil.androidide.build.config.FDroidConfig
import dev.mutwakil.androidide.build.config.MVN_GROUP_ID
import dev.mutwakil.androidide.build.config.publishingVersion
import dev.mutwakil.androidide.plugins.AndroidIDEPlugin
import dev.mutwakil.androidide.plugins.conf.configureAndroidModule
Expand All @@ -29,64 +29,72 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("build-logic.root-project")
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.maven.publish) apply false
alias(libs.plugins.gradle.publish) apply false
alias(libs.plugins.protobuf) apply false
alias(libs.plugins.benchmark) apply false
id("build-logic.root-project")
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.maven.publish) apply false
alias(libs.plugins.gradle.publish) apply false
alias(libs.plugins.protobuf) apply false
alias(libs.plugins.benchmark) apply false
}

buildscript {
dependencies {
classpath(libs.kotlin.gradle.plugin)
classpath(libs.nav.safe.args.gradle.plugin)
}
dependencies {
classpath(libs.kotlin.gradle.plugin)
classpath(libs.nav.safe.args.gradle.plugin)
}
}

val version = "v2.7.24-beta"
// Root project has 'dev.mutwakil.androidide' as the group ID
project.group = MVN_GROUP_ID
project.version = "v"+"2.7.24-beta"
//project.version = version

subprojects {
if (project != rootProject) {
var group = project.parent!!.group
if (project.parent != rootProject) {
group = "${group}.${project.parent!!.name}"
if (project != rootProject) {
var group = project.parent!!.group
if (project.parent != rootProject) {
group = "${group}.${project.parent!!.name}"
}
project.group = group
}
project.group = group
}

// Always load the F-Droid config
FDroidConfig.load(project)
// Always load the F-Droid config
FDroidConfig.load(project)

afterEvaluate {
apply { plugin(AndroidIDEPlugin::class.java) }
}
afterEvaluate {
apply { plugin(AndroidIDEPlugin::class.java) }
}

project.version = rootProject.version
project.version = rootProject.version

plugins.withId("com.android.application") {
configureAndroidModule(libs.androidx.libDesugaring)
}
plugins.withId("com.android.library") {
configureAndroidModule(libs.androidx.libDesugaring)
}
plugins.withId("java-library") { configureJavaModule() }
plugins.withId("com.vanniktech.maven.publish.base") { configureMavenPublish() }
plugins.withId("com.android.application") {
configureAndroidModule(libs.androidx.libDesugaring)
}
plugins.withId("com.android.library") {
configureAndroidModule(libs.androidx.libDesugaring)
}
plugins.withId("java-library") { configureJavaModule() }
plugins.withId("com.vanniktech.maven.publish.base") { configureMavenPublish() }

plugins.withId("com.gradle.plugin-publish") {
configure<GradlePluginDevelopmentExtension> {
version = project.publishingVersion
plugins.withId("com.gradle.plugin-publish") {
configure<GradlePluginDevelopmentExtension> {
version = project.publishingVersion
}
}
}

tasks.withType<KotlinCompile>().configureEach {
compilerOptions.jvmTarget.set(JvmTarget.fromTarget(BuildConfig.JAVA_VERSION.majorVersion))
}
tasks.withType<KotlinCompile>().configureEach {
compilerOptions.jvmTarget.set(JvmTarget.fromTarget(BuildConfig.JAVA_VERSION.majorVersion))
}
}

//tasks.register("MyVersioning") {
// dependsOn(":nyxInfer")
// doLast {
// rootProject.version = version
// }
//}

tasks.register<Delete>("clean") { delete(rootProject.layout.buildDirectory) }
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ import org.gradle.api.provider.Provider
* flavor, the version code will be `100 * 270 + 1` i.e. `27001`
*/
internal val flavorsAbis = mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2)
private val disableCoreLibDesugaringForModules = arrayOf(
":logging:logsender",
":logging:logger"
)

fun Project.configureAndroidModule(
coreLibDesugDep: Provider<MinimalExternalModuleDependency>
Expand Down Expand Up @@ -170,6 +174,7 @@ fun Project.configureAndroidModule(
// see https://issuetracker.google.com/issues/338411137#comment11
isMinifyEnabled = isAppModule
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
consumerProguardFiles("consumer-rules.pro")
}

// development build type
Expand All @@ -192,11 +197,11 @@ private fun Project.configureCoreLibDesugaring(
baseExtension: BaseExtension,
coreLibDesugDep: Provider<MinimalExternalModuleDependency>
) {
val coreLibDesugaringEnabled = !project.plugins.hasPlugin(NoDesugarPlugin::class.java)
val coreLibDesugaringEnabled = !project.plugins.hasPlugin(NoDesugarPlugin::class.java) || project.path !in disableCoreLibDesugaringForModules

baseExtension.compileOptions.isCoreLibraryDesugaringEnabled = coreLibDesugaringEnabled

if (coreLibDesugaringEnabled) {
project.dependencies.add("coreLibraryDesugaring", coreLibDesugDep)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.math.BigInteger
import java.security.MessageDigest

/**
* @author Akash Yadav
Expand All @@ -48,6 +50,20 @@ abstract class SetupAapt2Task : DefaultTask() {

private const val DEFAULT_VERSION = "35.0.2"
private const val AAPT2_DOWNLOAD_URL = "https://github.com/AndroidIDE-Dev/platform-tools/releases/download/v%1\$s/aapt2-%2\$s"

/**
* Computes the SHA-256 checksum of the given file, formatted the same way as
* [DownloadUtils.doDownload] (zero-padded lowercase hex, via BigInteger).
*/
private fun sha256Of(file: File): String {
val digest = MessageDigest.getInstance("SHA-256")
digest.update(file.readBytes())
var checksum = BigInteger(1, digest.digest()).toString(16)
while (checksum.length < 64) {
checksum = "0$checksum"
}
return checksum
}
}

@TaskAction
Expand All @@ -58,8 +74,6 @@ abstract class SetupAapt2Task : DefaultTask() {
val arch = FDroidConfig.fDroidBuildArch!!

val file = outputDirectory.file("${arch}/libaapt2.so").get().asFile
file.parentFile.deleteRecursively()
file.parentFile.mkdirs()

val aapt2File = requireNotNull(FDroidConfig.aapt2Files[arch]) {
"F-Droid build is enabled but path to AAPT2 file for $arch is not set."
Expand All @@ -71,6 +85,15 @@ abstract class SetupAapt2Task : DefaultTask() {
"F-Droid AAPT2 file does not exist or is not a file: $aapt2"
}

// Skip re-copying if the existing output already matches the source file's checksum.
if (file.exists() && file.isFile && sha256Of(file) == sha256Of(aapt2)) {
logger.info("Existing aapt2 at $file already matches source, skipping copy")
assertAapt2Arch(file, ELFUtils.ElfAbi.forName(arch)!!)
return
}

file.parentFile.mkdirs()

logger.info("Copying $aapt2 to $file")
aapt2.copyTo(file, overwrite = true)
assertAapt2Arch(file, ELFUtils.ElfAbi.forName(arch)!!)
Expand All @@ -80,9 +103,12 @@ abstract class SetupAapt2Task : DefaultTask() {
// When not building for F-Droid, download aapt2 files from GitHub
AAPT2_CHECKSUMS.forEach { (arch, checksum) ->
val file = outputDirectory.file("${arch}/libaapt2.so").get().asFile
file.parentFile.deleteRecursively()
file.parentFile.mkdirs()

// NOTE: we deliberately do NOT delete `file` or its parent directory here.
// DownloadUtils.doDownload() already checks whether `file` exists and whether its
// checksum matches `checksum`; if it matches, it returns immediately without touching
// the network. Deleting the file beforehand (as the old code did) defeats that check
// and forces a re-download every time, breaking offline builds.
val remoteUrl = AAPT2_DOWNLOAD_URL.format(DEFAULT_VERSION, arch)
DownloadUtils.doDownload(file, remoteUrl, checksum, logger)
assertAapt2Arch(file, ELFUtils.ElfAbi.forName(arch)!!)
Expand All @@ -95,4 +121,4 @@ abstract class SetupAapt2Task : DefaultTask() {
"Mismatched ABI for aapt2 binary. Required $elfAbi but found $fileAbi"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
import org.jetbrains.kotlin.analysis.api.KaIdeApi
import org.jetbrains.kotlin.analysis.api.KaSession
import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource
import org.jetbrains.kotlin.analysis.api.resolution.singleFunctionCallOrNull
import org.jetbrains.kotlin.analysis.api.resolution.symbol
import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaClassKind
import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol
Expand All @@ -63,12 +65,15 @@ import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtClassBody
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.psi.KtValueArgument
import org.jetbrains.kotlin.psi.KtValueArgumentList
import org.jetbrains.kotlin.psi.KtWhenExpression
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.startOffset
Expand Down Expand Up @@ -339,6 +344,8 @@ private fun KaSession.collectScopeCompletions(
}

abortIfCancelled()

collectNamedArgumentCompletions(to)

val ktElement = ctx.ktElement
val scope = ctx.scope
Expand Down Expand Up @@ -491,6 +498,81 @@ private fun KaSession.buildUnimportedSymbolItem(symbol: JvmSymbol): CompletionIt
return item
}

/**
* Named-argument completion: inside a call's argument list (`foo(█)`, `foo(x = 1, █)`),
* offers the callee's not-yet-supplied parameter names as `name = ` items — mirroring the
* PSI-only implementation's `nameReferenceCandidates` extra, but sourcing the parameter list
* from a resolved [KaFunctionCall] instead of a hand-rolled resolver, so overload resolution,
* default arguments, and vararg/named-only parameters are handled exactly as the compiler sees
* them rather than approximated.
*
* Mirrors the PSI version's editingName handling: when the marker sits on an argument name
* that's ALREADY there (`foo(contai█ = x)`), the item must replace just the name — the ` = ` is
* already typed — so it inserts the bare name instead of `name = `.
*/
context(ctx: AnalysisContext)
private fun KaSession.collectNamedArgumentCompletions(to: MutableList<CompletionItem>) {
abortIfCancelled()

val arg = ctx.psiElement.getParentOfType<KtValueArgument>(strict = false) ?: return
val argList = arg.parent as? KtValueArgumentList ?: return
val call = argList.parent as? KtCallExpression ?: return

// Resolve the call to the actual overload the compiler picked. Adjust the call below to match
val functionSymbol = resolveCalleeSymbol(call) ?: return

val editingName = ctx.psiElement.getParentOfType<KtValueArgument>(strict = false)
?.let { it === arg } == true && arg.isNamed()
// The argument currently carrying the completion marker never counts as "already supplied" —
// its own (garbled, marker-containing) name can't match a real parameter anyway, so this is
// mostly documentation of intent, matching the PSI version's same guarantee.
val supplied = argList.arguments
.filter { it !== arg }
.mapNotNull { it.getArgumentName()?.asName?.asString() }
.toHashSet()

functionSymbol.valueParameters
.filter { it.name.asString() !in supplied }
.filter { param -> matchesFilter(param.name) }
.forEach { param ->
to += namedArgumentItem(param, bareName = editingName)
}
}

/**
* Resolves [call]'s callee to the [KaFunctionSymbol] the compiler picked for it, or null when
* resolution fails (unresolved reference, syntax error, or the call has no matching overload
* yet — e.g. still being typed).
*/
context(ctx: AnalysisContext)
private fun KaSession.resolveCalleeSymbol(call: KtCallExpression): KaFunctionSymbol? {
abortIfCancelled()
return call.resolveToCall()
?.singleFunctionCallOrNull()
?.symbol
}

/** Builds the `name = ` (or bare `name` when [bareName]) completion item for a named argument. */
@OptIn(KaExperimentalApi::class)
context(ctx: AnalysisContext)
private fun KaSession.namedArgumentItem(
param: KaValueParameterSymbol,
bareName: Boolean,
): CompletionItem {
val name = param.name.asString()
val item = ktCompletionItem(
name = if (bareName) name else "$name =",
kind = CompletionItemKind.TYPE_PARAMETER,
)
item.detail = renderName(param.returnType)
item.insertTextFormat = InsertTextFormat.SNIPPET
// sortPriority-equivalent: named-arg items rank ahead of plain scope symbols, matching the
// PSI version's extras-first ordering (extra.distinctBy { ... } + symbolItems).
item.ideSortText = "0$name"
item.insertText = if (bareName) name else "$name = $0"
return item
}

private fun internalNameToClassId(internalName: String): ClassId {
val isLocal = false
val packageName = internalName.substringBeforeLast('/')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ open class ApiVersionsParser {

private fun consumeStartElement(event: StartElement) {
when (event.name.localPart) {
TAG_API -> apiVersion = event.getAttributeByName(QName("version")).value.toInt()
TAG_API -> apiVersion = event.getAttributeByName(QName("version")).value.parseApiLevel()
TAG_CLASS -> consumeClass(event)
TAG_FIELD -> consumeMember(event, TAG_FIELD)
TAG_METHOD -> consumeMember(event, TAG_METHOD)
Expand Down Expand Up @@ -186,9 +186,9 @@ open class ApiVersionsParser {

when (attribute.name.localPart) {
ATTR_NAME -> name = attribute.value
ATTR_SIN -> since = attribute.value.toInt()
ATTR_DEPR -> deprecated = attribute.value.toInt()
ATTR_REM -> removed = attribute.value.toInt()
ATTR_SIN -> since = attribute.value.parseApiLevel()
ATTR_DEPR -> deprecated = attribute.value.parseApiLevel()
ATTR_REM -> removed = attribute.value.parseApiLevel()
}
}

Expand All @@ -211,4 +211,8 @@ open class ApiVersionsParser {
val removed = versions and 0x000000FF
return ApiVersion(since = since, deprecatedIn = deprecated, removedIn = removed)
}
}

private fun String.parseApiLevel(): Int {
return substringBefore('.').toInt()
}
}
Loading