Skip to content
Open
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
4 changes: 2 additions & 2 deletions build-action/build.gradle.dcl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ javaLibrary {
javaVersion = 8

dependencies {
implementation("org.gradle:gradle-tooling-api:9.0-milestone-1")
implementation("org.gradle:gradle-declarative-dsl-tooling-models:9.0-milestone-1")
implementation("org.gradle:gradle-tooling-api:9.2.0-milestone-2")
implementation("org.gradle:gradle-declarative-dsl-tooling-models:9.2.0-milestone-2")
}
}
1 change: 1 addition & 0 deletions build-logic/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.gradle.kotlin.dsl.dcl=true
2 changes: 1 addition & 1 deletion build-logic/plugins/build.gradle.dcl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ javaGradlePlugin {
description = "Declarative plugins containing custom software types for the gradle-client project."

dependencies {
api("org.gradle.experimental.kmp-ecosystem:org.gradle.experimental.kmp-ecosystem.gradle.plugin:0.1.45")
api("org.gradle.experimental.kmp-ecosystem:org.gradle.experimental.kmp-ecosystem.gradle.plugin:0.1.46")
api("org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin:2.2.0")
api("org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin:2.2.0")
api("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:2.2.0")
Expand Down
8 changes: 4 additions & 4 deletions gradle-client/build.gradle.dcl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ kotlinApplication {
implementation(project(":build-action"))
implementation(project(":mutations-demo"))

implementation("org.gradle:gradle-tooling-api:9.1.0-20250805104018+0000")
implementation("org.gradle:gradle-tooling-api:9.2.0-milestone-2")

implementation("com.arkivanov.decompose:decompose:3.0.0")
implementation("com.arkivanov.decompose:extensions-compose:3.0.0")
Expand All @@ -36,9 +36,9 @@ kotlinApplication {
implementation("org.slf4j:slf4j-api:2.0.14")
implementation("ch.qos.logback:logback-classic:1.5.6")

implementation("org.gradle:gradle-declarative-dsl-core:9.0-milestone-1")
implementation("org.gradle:gradle-declarative-dsl-evaluator:9.0-milestone-1")
implementation("org.gradle:gradle-declarative-dsl-tooling-models:9.0-milestone-1")
implementation("org.gradle:gradle-declarative-dsl-core:9.2.0-milestone-2")
implementation("org.gradle:gradle-declarative-dsl-evaluator:9.2.0-milestone-2")
implementation("org.gradle:gradle-declarative-dsl-tooling-models:9.2.0-milestone-2")

runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.1")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,16 @@ fun DeclarativeDocument.relevantRange(): IntRange {
return IntRange(first.sourceData.indexRange.first, last.sourceData.indexRange.last)
}

fun DocumentWithResolution.errorRanges(): List<IntRange> =
data class ErrorData(
val range: IntRange,
val documentNode: DeclarativeDocument.Node,
val resolution: DocumentResolution.UnsuccessfulResolution
)

fun DocumentWithResolution.errorRanges(): List<ErrorData> =
resolutionContainer.collectToMap(document).entries
.filter { it.value is DocumentResolution.UnsuccessfulResolution }
.map { it.key.sourceData.indexRange }
.map { ErrorData(it.key.sourceData.indexRange, it.key, it.value as DocumentResolution.UnsuccessfulResolution) }

fun DeclarativeDocument.nodeAt(fileIdentifier: String, offset: Int): DeclarativeDocument.DocumentNode? {
var node: DeclarativeDocument.DocumentNode? = null
Expand Down Expand Up @@ -155,5 +161,5 @@ internal fun DocumentResolutionContainer.isUnresolvedBase(node: DeclarativeDocum
is DeclarativeDocument.DocumentNode -> data(node)
is DeclarativeDocument.ValueNode -> data(node)
}
return resolution is DocumentResolution.UnsuccessfulResolution && resolution.reasons != listOf(UnresolvedBase)
return resolution is DocumentResolution.UnsuccessfulResolution && resolution.reasons == listOf(UnresolvedBase)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.gradle.client.core.gradle.dcl

import org.gradle.internal.declarativedsl.dom.AmbiguousName
import org.gradle.internal.declarativedsl.dom.BlockMismatch
import org.gradle.internal.declarativedsl.dom.CrossScopeAccess
import org.gradle.internal.declarativedsl.dom.DeclarativeDocument
import org.gradle.internal.declarativedsl.dom.DocumentResolution
import org.gradle.internal.declarativedsl.dom.IllegalAugmentedAssignment
import org.gradle.internal.declarativedsl.dom.IsError
import org.gradle.internal.declarativedsl.dom.NonEnumValueNamedReference
import org.gradle.internal.declarativedsl.dom.NotAssignable
import org.gradle.internal.declarativedsl.dom.OpaqueValueInIdentityKey
import org.gradle.internal.declarativedsl.dom.SyntaxError
import org.gradle.internal.declarativedsl.dom.UnresolvedBase
import org.gradle.internal.declarativedsl.dom.UnresolvedName
import org.gradle.internal.declarativedsl.dom.UnresolvedSignature
import org.gradle.internal.declarativedsl.dom.UnresolvedValueUsed
import org.gradle.internal.declarativedsl.dom.UnsupportedKotlinFeature
import org.gradle.internal.declarativedsl.dom.UnsupportedSyntax
import org.gradle.internal.declarativedsl.dom.ValueTypeMismatch

@Suppress("CyclomaticComplexMethod")
fun userFriendlyErrorMessages(
node: DeclarativeDocument.Node,
unsuccessfulResolution: DocumentResolution.UnsuccessfulResolution
): List<String> = unsuccessfulResolution.reasons.flatMap { reason ->
when (reason) {
AmbiguousName -> listOf("Ambiguous name")
BlockMismatch -> if (node is DeclarativeDocument.DocumentNode.ElementNode) {
if (node.content.isEmpty())
listOf("Block expected for element ${node.name}")
else listOf("Block not expected for element ${node.name}")
} else listOf("Block mismatch")

CrossScopeAccess -> listOf("Cross-scope access")

IsError -> if (node is DeclarativeDocument.DocumentNode.ErrorNode) {
node.errors.map { error ->
when (error) {
is SyntaxError -> "Syntax error: ${error.parsingError.message}"
is UnsupportedKotlinFeature ->
"Unsupported language feature: ${error.unsupportedConstruct.languageFeature}"
is UnsupportedSyntax -> "Unsupported syntax: ${error.cause}"
}
}
} else listOf("Syntax error")

OpaqueValueInIdentityKey -> listOf("Opaque value passed as identity key")
UnresolvedName -> listOf("Unresolved name")
UnresolvedSignature -> listOf("Unresolved function signature")
IllegalAugmentedAssignment -> listOf("Illegal augmented assignment")
NotAssignable -> listOf("Cannot assign property")
UnresolvedValueUsed -> listOf("Assigned value is not resolved")
ValueTypeMismatch -> listOf("Value type mismatch")
NonEnumValueNamedReference -> listOf("Illegal named reference. Only enum entries can be referenced by name")

UnresolvedBase -> emptyList<String>()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import org.gradle.client.core.gradle.dcl.ErrorData
import org.gradle.client.core.gradle.dcl.userFriendlyErrorMessages
import org.gradle.client.ui.connected.actions.declarativedocuments.HighlightingEntry
import org.gradle.client.ui.theme.spacing
import org.gradle.client.ui.theme.transparency
Expand All @@ -26,7 +28,8 @@ internal data class SourceFileViewInput(
val fileIdentifier: String,
val fileContent: String,
val relevantIndicesRange: IntRange?,
val errorRanges: List<IntRange>,
val allErrorRanges: List<ErrorData>,
val selectedErrorRanges: List<ErrorData>,
val highlightedSourceRange: List<HighlightingEntry>
)

Expand All @@ -50,7 +53,7 @@ internal fun SourcesColumn(

val highlightedString = sourceFileAnnotatedString(
highlightedRangeOrNull,
sourceFileViewInput.errorRanges,
sourceFileViewInput.allErrorRanges.map { it.range },
sourceFileViewInput.fileContent
)

Expand All @@ -61,7 +64,8 @@ internal fun SourcesColumn(
SourceFileData(
identifier,
highlightedString,
relevantHighlightedString
relevantHighlightedString,
sourceFileViewInput.selectedErrorRanges
)
}
}
Expand All @@ -71,6 +75,7 @@ internal fun SourcesColumn(
data.relativePath,
data.annotatedSource,
data.trimmedSource,
data.selectedErrorRanges,
onClick
)
MaterialTheme.spacing.VerticalLevel4()
Expand All @@ -82,7 +87,8 @@ internal fun SourcesColumn(
private data class SourceFileData(
val relativePath: String,
val annotatedSource: AnnotatedString,
val trimmedSource: TrimmedText?
val trimmedSource: TrimmedText?,
val selectedErrorRanges: List<ErrorData>
)

private fun sourceFileAnnotatedString(
Expand All @@ -109,8 +115,13 @@ private fun SourceFileTitleAndText(
fileRelativePath: String,
highlightedSource: AnnotatedString,
trimmedSource: TrimmedText?,
selectedErrors: List<ErrorData>?,
onClick: (String, Int) -> Unit
) {
val errorPopupText = selectedErrors?.takeIf { it.isNotEmpty() }?.let {
it.flatMap { userFriendlyErrorMessages(it.documentNode, it.resolution) }.joinToString("\n")
}

if (trimmedSource != null) {
var isTrimmed by remember { mutableStateOf(true) }

Expand All @@ -130,7 +141,8 @@ private fun SourceFileTitleAndText(
MaterialTheme.spacing.VerticalLevel2()
CodeBlock(
Modifier.fillMaxWidth(),
if (isTrimmed) trimmedSource.annotatedString else highlightedSource
if (isTrimmed) trimmedSource.annotatedString else highlightedSource,
errorPopupText
) { clickOffset ->
val originalOffset = if (isTrimmed)
trimmedSource.mapIndexToIndexInOriginalText(clickOffset)
Expand All @@ -141,7 +153,7 @@ private fun SourceFileTitleAndText(
} else {
TitleMedium(fileRelativePath)
MaterialTheme.spacing.VerticalLevel4()
CodeBlock(Modifier.fillMaxWidth(), highlightedSource) { clickOffset ->
CodeBlock(Modifier.fillMaxWidth(), highlightedSource, errorPopupText) { clickOffset ->
onClick(fileRelativePath, clickOffset)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
@file:Suppress("TooManyFunctions")

package org.gradle.client.ui.composables

import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text2.input.maxLengthInChars
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import org.gradle.client.ui.theme.spacing
import org.gradle.client.ui.theme.transparency

Expand Down Expand Up @@ -41,6 +59,15 @@ fun TitleSmall(text: String, modifier: Modifier = Modifier) {
)
}

@Composable
fun TitleSmall(text: AnnotatedString, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = text,
style = MaterialTheme.typography.titleSmall,
)
}

@Composable
fun BodyMedium(text: String, modifier: Modifier = Modifier) {
Text(
Expand Down Expand Up @@ -81,23 +108,48 @@ fun HeadlineSmall(text: String, modifier: Modifier = Modifier) {
fun CodeBlock(
modifier: Modifier = Modifier,
code: AnnotatedString,
popup: String?,
onClick: (Int) -> Unit = {},
) {
var boxSize by remember { mutableStateOf(IntSize.Zero) }
Surface(
tonalElevation = MaterialTheme.spacing.level1,
color = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
shape = MaterialTheme.shapes.extraSmall,
modifier = modifier
modifier = modifier.onSizeChanged { boxSize = it }
) {
ClickableText(
text = code,
modifier = Modifier.padding(MaterialTheme.spacing.level2),
style = MaterialTheme.typography.labelMedium.copy(fontFamily = FontFamily.Monospace),
onClick = onClick,
)
Box {
ClickableText(
text = code,
modifier = Modifier.padding(MaterialTheme.spacing.level2),
style = MaterialTheme.typography.labelMedium.copy(fontFamily = FontFamily.Monospace),
onClick = onClick,
)
Box(
modifier =
Modifier
.padding(bottom = 8.dp)
.background(MaterialTheme.colorScheme.background)
.width(boxSize.width.toDp() - 8.dp)
.align(Alignment.BottomCenter)
.shadow(2.dp)
.animateContentSize()
) {
if (popup != null) {
Text(
text = popup,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(8.dp)
)
}
}
}
}
}

@Composable
fun Int.toDp() = with(LocalDensity.current) { [email protected]() }

fun Modifier.semiTransparentIfNull(any: Any?) =
if (any == null) alpha(MaterialTheme.transparency.HALF) else this
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ sealed interface Outcome {
data class Failure(val exception: Exception) : Outcome
}

data class ModelActionGroup(
val name: String,
val modelActions: List<GetModelAction<*>>
)

class ConnectedComponent(
context: ComponentContext,
private val appDispatchers: AppDispatchers,
Expand All @@ -55,15 +60,20 @@ class ConnectedComponent(
private val mutableModel = MutableValue<ConnectionModel>(ConnectionModel.Connecting)
val model: Value<ConnectionModel> = mutableModel

val modelActions = listOf(
GetBuildEnvironment(),
GetGradleBuild(),
GetGradleProject(),
GetDeclarativeSchema(),
GetDeclarativeDocuments(),
GetKotlinBaseDslScriptModel(),
GetResilientGradleBuild(),
GetResilientKotlinDslScriptsModel(),
val modelActionGroups = listOf(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Good call adding some structure here

ModelActionGroup(
"Build and project",
listOf(GetBuildEnvironment(), GetGradleBuild(), GetResilientGradleBuild(), GetGradleProject())
),
ModelActionGroup(
"Declarative Gradle",
listOf(GetDeclarativeSchema(), GetDeclarativeDocuments())
),
ModelActionGroup(
"Kotlin DSL",
listOf(GetKotlinBaseDslScriptModel(), GetResilientKotlinDslScriptsModel())
)

)

private val scope = coroutineScope(appDispatchers.main + SupervisorJob())
Expand Down Expand Up @@ -203,7 +213,8 @@ class ConnectedComponent(

@Suppress("UNCHECKED_CAST")
fun <T : Any> actionFor(model: T): GetModelAction<T>? =
modelActions.find { it.modelType.java.isAssignableFrom(model::class.java) } as? GetModelAction<T>
modelActionGroups.flatMap { it.modelActions }
.find { it.modelType.java.isAssignableFrom(model::class.java) } as? GetModelAction<T>

fun onCloseClicked() {
onFinished()
Expand Down
Loading
Loading