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
6 changes: 3 additions & 3 deletions Sources/Lithe/Application/UIFeatureModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ final class MavenFeatureModel: ObservableObject {
var issues: [MavenBuildIssue] { service.issues }
var lastExitCode: Int32? { service.lastExitCode }

func loadProject(at workspaceURL: URL) async {
await service.loadProject(at: workspaceURL)
func loadProject(at workspaceURL: URL, files: [URL]) async {
await service.loadProject(at: workspaceURL, files: files)
}

func run(phase: MavenLifecyclePhase, module: MavenModule?, profiles: Set<String>) {
Expand Down Expand Up @@ -197,7 +197,7 @@ final class ProjectDevelopmentFeatureModel {
file.lastPathComponent.lowercased() == "pom.xml"
}
if hasMavenDescriptor {
await mavenFeature.loadProject(at: workspaceURL)
await mavenFeature.loadProject(at: workspaceURL, files: files)
} else {
mavenFeature.reset()
}
Expand Down
17 changes: 13 additions & 4 deletions Sources/Lithe/Core/RustCoreBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ struct RustCoreBridge: Sendable {
}
}

let relativePath: String
let groupID: String?
let artifactID: String
let version: String?
Expand All @@ -237,6 +238,7 @@ struct RustCoreBridge: Sendable {

enum CodingKeys: String, CodingKey {
case groupID = "groupId"
case relativePath
case artifactID = "artifactId"
case version
case packaging
Expand All @@ -245,8 +247,11 @@ struct RustCoreBridge: Sendable {
case hasWrapper
}

func makeProject(rootURL: URL) -> MavenProject {
MavenProject(
func makeProject(workspaceRootURL: URL) -> MavenProject {
let rootURL = relativePath == "."
? workspaceRootURL
: workspaceRootURL.appending(path: relativePath, directoryHint: .isDirectory)
return MavenProject(
rootURL: rootURL,
pomURL: rootURL.appendingPathComponent("pom.xml"),
groupID: groupID,
Expand Down Expand Up @@ -1041,6 +1046,7 @@ struct RustCoreBridge: Sendable {

private struct MavenScanRequest: Encodable {
let root: String
let paths: [String]
}

private struct MarkdownRenderRequest: Encodable {
Expand Down Expand Up @@ -1773,10 +1779,13 @@ struct RustCoreBridge: Sendable {
return response?.relocated == true
}

func scanMaven(at rootURL: URL) -> MavenScanPayload? {
func scanMaven(at rootURL: URL, paths: [String] = []) -> MavenScanPayload? {
execute(
command: "maven.scan",
payload: MavenScanRequest(root: rootURL.standardizedFileURL.path)
payload: MavenScanRequest(
root: rootURL.standardizedFileURL.path,
paths: paths
)
)
}

Expand Down
55 changes: 38 additions & 17 deletions Sources/Lithe/Core/RustJavaMavenOperations.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

protocol JavaMavenOperations: Sendable {
func scanMavenProject(at rootURL: URL) -> MavenProject?
func scanMavenProject(at rootURL: URL, files: [URL]) -> MavenProject?
func mavenDiagnostics(output: String, projectRoot: URL) -> [MavenBuildIssue]
func codeVision(
at rootURL: URL,
Expand Down Expand Up @@ -42,8 +42,18 @@ struct JavaCodeVisionValue: Sendable {
struct RustJavaMavenOperations: JavaMavenOperations, Sendable {
let core: RustCoreBridge

func scanMavenProject(at rootURL: URL) -> MavenProject? {
core.scanMaven(at: rootURL)?.makeProject(rootURL: rootURL.standardizedFileURL)
func scanMavenProject(at rootURL: URL, files: [URL]) -> MavenProject? {
let root = rootURL.standardizedFileURL
let rootComponents = root.pathComponents
let paths = files.compactMap { fileURL -> String? in
let file = fileURL.standardizedFileURL
guard file.lastPathComponent.lowercased() == "pom.xml",
file.pathComponents.starts(with: rootComponents) else { return nil }
return file.pathComponents
.dropFirst(rootComponents.count)
.joined(separator: "/")
}
return core.scanMaven(at: root, paths: paths)?.makeProject(workspaceRootURL: root)
Comment thread
Mucheen marked this conversation as resolved.
}

func mavenDiagnostics(output: String, projectRoot: URL) -> [MavenBuildIssue] {
Expand Down Expand Up @@ -105,12 +115,11 @@ struct RustJavaMavenOperations: JavaMavenOperations, Sendable {
mavenProject: MavenProject?
) -> [JavaRunConfiguration] {
let root = rootURL.standardizedFileURL
let paths = files.compactMap { fileURL -> String? in
let file = fileURL.standardizedFileURL
guard file.path.hasPrefix(root.path + "/") else { return nil }
return String(file.path.dropFirst(root.path.count + 1))
let paths = files.compactMap {
workspaceRelativePath(for: $0, root: root)
}
let modulePaths = mavenProject?.allModules.map(\.relativePath) ?? []
let workspaceModules = workspaceMavenModules(in: mavenProject, relativeTo: root)
let modulePaths = workspaceModules.map(\.0)
guard let payload = core.scanJavaRunConfigurations(
at: root,
paths: paths,
Expand All @@ -119,24 +128,36 @@ struct RustJavaMavenOperations: JavaMavenOperations, Sendable {

return payload.configurations.compactMap { value in
guard let kind = JavaRunConfigurationKind(rawValue: value.kind) else { return nil }
let name: String
if kind == .mavenModule,
let modulePath = value.modulePath,
let module = mavenProject?.allModules.first(where: { $0.relativePath == modulePath }) {
name = module.displayName
} else {
name = value.name
let module = value.modulePath.flatMap { modulePath in
workspaceModules.first(where: { $0.0 == modulePath })?.1
}
return JavaRunConfiguration(
id: value.id,
name: name,
name: kind == .mavenModule ? module?.displayName ?? value.name : value.name,
kind: kind,
modulePath: value.modulePath,
modulePath: module?.relativePath ?? value.modulePath,
mainClass: value.mainClass
)
}
}

func workspaceMavenModules(
in project: MavenProject?,
relativeTo root: URL
) -> [(path: String, module: MavenModule)] {
project?.allModules.compactMap { module in
workspaceRelativePath(for: module.url, root: root).map { ($0, module) }
} ?? []
}

private func workspaceRelativePath(for url: URL, root: URL) -> String? {
let path = url.standardizedFileURL.path
let rootPath = root.standardizedFileURL.path
let prefix = rootPath.hasSuffix("/") ? rootPath : rootPath + "/"
guard path.hasPrefix(prefix) else { return nil }
return String(path.dropFirst(prefix.count))
}

func structure(
source: String,
declarationSources: [String]
Expand Down
4 changes: 2 additions & 2 deletions Sources/Lithe/Services/MavenService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ final class MavenService: ObservableObject {
}
}

func loadProject(at workspaceURL: URL) async {
func loadProject(at workspaceURL: URL, files: [URL]) async {
let loadID = UUID()
projectLoadID = loadID
isLoadingProject = true
let rootURL = workspaceURL.standardizedFileURL
let javaMavenOperations = javaMavenOperations
let scannedProject = await Task.detached(priority: .utility) {
javaMavenOperations.scanMavenProject(at: rootURL)
javaMavenOperations.scanMavenProject(at: rootURL, files: files)
}.value
guard !Task.isCancelled, projectLoadID == loadID else { return }
project = scannedProject
Expand Down
5 changes: 4 additions & 1 deletion Sources/Lithe/Services/RunExecutableResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ final class RunExecutableResolver: RunExecutableResolving {

switch plan.executable {
case .toolchain(let id):
let toolchainProjectURL = id == "project-maven" && plan.workingDirectory != "."
? projectURL.appending(path: plan.workingDirectory, directoryHint: .isDirectory)
: projectURL
let resolved = try toolchainRegistry.resolve(
identifier: id,
projectURL: projectURL,
projectURL: toolchainProjectURL.standardizedFileURL,
options: options,
runtimeService: runtimeService
)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Lithe/Views/MavenView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ struct MavenView: View {

private func refreshProject() {
guard let workspaceURL = model.workspaceURL else { return }
Task { await feature.loadProject(at: workspaceURL) }
Task { await feature.loadProject(at: workspaceURL, files: model.projectFiles) }
}

private func projectPane(_ project: MavenProject) -> some View {
Expand Down
51 changes: 50 additions & 1 deletion Tests/LitheTests/MavenRuntimeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct MavenRuntimeTests {
func mavenScanPayloadDecodesRustCamelCaseIdentifiers() throws {
let json = #"""
{
"relativePath": "services/api",
"groupId": "com.example",
"artifactId": "root",
"version": "1.0",
Expand All @@ -29,16 +30,64 @@ struct MavenRuntimeTests {
from: Data(json.utf8)
)

let project = payload.makeProject(rootURL: URL(fileURLWithPath: "/tmp/maven"))
let workspaceRoot = FileManager.default.temporaryDirectory
.appendingPathComponent("lithe-maven-payload", isDirectory: true)
let project = payload.makeProject(workspaceRootURL: workspaceRoot)
let expectedRoot = URL(
fileURLWithPath: workspaceRoot.path + "/services/api",
isDirectory: true
)
#expect(project.rootURL == expectedRoot)
#expect(!project.rootURL.absoluteString.contains("%2F"))
#expect(project.pomURL == expectedRoot.appendingPathComponent("pom.xml"))
#expect(project.groupID == "com.example")
#expect(project.artifactID == "root")
#expect(project.modules.count == 1)
#expect(project.modules[0].groupID == "com.example")
#expect(project.modules[0].artifactID == "child")
#expect(project.modules[0].url == expectedRoot.appendingPathComponent("module-a"))
#expect(project.profiles == [MavenProfile(id: "dev", isActiveByDefault: true)])
#expect(project.hasWrapper)
}

@Test
func nestedMavenRunConfigurationsUseWorkspaceRelativeModulePaths() {
let workspaceRoot = FileManager.default.temporaryDirectory
.appendingPathComponent("lithe-nested-maven-run-\(UUID().uuidString)", isDirectory: true)
let mavenRoot = URL(
fileURLWithPath: workspaceRoot.path + "/projects/demo",
isDirectory: true
)
let moduleRoot = mavenRoot.appendingPathComponent("service", isDirectory: true)
let module = MavenModule(
relativePath: "service",
url: moduleRoot,
groupID: "com.example",
artifactID: "service-api",
version: "1.0",
packaging: "jar",
modules: []
)
let project = MavenProject(
rootURL: mavenRoot,
pomURL: mavenRoot.appendingPathComponent("pom.xml"),
groupID: "com.example",
artifactID: "demo",
version: "1.0",
packaging: "pom",
modules: [module],
profiles: [],
hasWrapper: false
)

let modules = RustJavaMavenOperations(core: RustCoreBridge()).workspaceMavenModules(
in: project,
relativeTo: workspaceRoot
)
#expect(modules.map { $0.path } == ["projects/demo/service"])
#expect(modules.map { $0.module.relativePath } == ["service"])
}

@Test
@MainActor
func canceledRuntimeDiscoveryClearsDiscoveringState() async throws {
Expand Down
53 changes: 51 additions & 2 deletions Tests/LitheTests/RunConfigurationIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,33 @@ struct RunConfigurationIntegrationTests {
}
}

@Test
func mavenToolchainResolvesTheWrapperFromTheLaunchWorkingDirectory() throws {
let workspaceRoot = FileManager.default.temporaryDirectory
.appendingPathComponent("lithe-nested-maven-wrapper", isDirectory: true)
let mavenRoot = URL(
fileURLWithPath: workspaceRoot.path + "/projects/demo",
isDirectory: true
)
let runtime = ProjectRuntimeService(
runtimeLocator: NestedMavenWrapperRuntimeLocator(wrapperRoot: mavenRoot),
store: RunTestKeyValueStore()
)
let resolver = RunExecutableResolver(runtimeService: runtime)
let resolved = try resolver.resolve(
SharedLaunchPlan(
executable: .toolchain("project-maven"),
arguments: ["verify"],
workingDirectory: "projects/demo"
),
projectURL: workspaceRoot,
options: RunOptions()
)

#expect(resolved.executableURL == mavenRoot.appendingPathComponent("mvnw"))
#expect(!resolved.executableURL.absoluteString.contains("%2F"))
}

@Test
func commandResolverLayersGenericEnvironmentWithoutJavaInjection() throws {
let runtime = ProjectRuntimeService(
Expand Down Expand Up @@ -4113,6 +4140,24 @@ private struct RunTestRuntimeLocator: RuntimeLocator {
func systemJDBExecutable() -> URL? { URL(fileURLWithPath: "/toolchains/jdk/bin/jdb") }
}

private struct NestedMavenWrapperRuntimeLocator: RuntimeLocator {
let wrapperRoot: URL

func environment() -> [String: String] { [:] }
func discover() -> RuntimeDiscoveryResult {
RuntimeDiscoveryResult(javaRuntimes: [], mavenRuntimes: [])
}
func validJavaHome(path: String) -> URL? { nil }
func javaRuntime(at homeURL: URL) -> JavaRuntimeCandidate? { nil }
func isExecutable(at url: URL) -> Bool {
url.standardizedFileURL == wrapperRoot.appendingPathComponent("mvnw").standardizedFileURL
}
func systemMavenExecutable() -> URL? { nil }
func mavenExecutable(forHomePath path: String) -> URL? { nil }
func mavenRuntime(at executableURL: URL) -> MavenRuntimeCandidate? { nil }
func systemJDBExecutable() -> URL? { nil }
}

private struct MissingJavaRuntimeLocator: RuntimeLocator {
func environment() -> [String: String] { ["PATH": "/usr/bin"] }
func discover() -> RuntimeDiscoveryResult {
Expand Down Expand Up @@ -4427,7 +4472,7 @@ private final class RunTestKeyValueStore: KeyValueStore, @unchecked Sendable {
}

private struct RunTestJavaMavenOperations: JavaMavenOperations {
func scanMavenProject(at rootURL: URL) -> MavenProject? { nil }
func scanMavenProject(at rootURL: URL, files: [URL]) -> MavenProject? { nil }
func mavenDiagnostics(output: String, projectRoot: URL) -> [MavenBuildIssue] { [] }
func codeVision(at rootURL: URL, targetPath: String, paths: [String]) -> [JavaCodeVisionValue] { [] }
func className(source: String, simpleName: String) -> String? { nil }
Expand All @@ -4448,6 +4493,7 @@ struct CorePayloadDecodingTests {
func mavenScanDecodesTheCoordinateSpellingTheCoreEmits() throws {
let json = """
{
"relativePath": ".",
"groupId": "com.lithe.demo",
"artifactId": "full-stack-demo",
"version": "1.0.0-SNAPSHOT",
Expand All @@ -4471,7 +4517,10 @@ struct CorePayloadDecodingTests {
RustCoreBridge.MavenScanPayload.self,
from: Data(json.utf8)
)
let project = payload.makeProject(rootURL: URL(fileURLWithPath: "/tmp/demo"))
let project = payload.makeProject(
workspaceRootURL: FileManager.default.temporaryDirectory
.appendingPathComponent("lithe-maven-core-payload", isDirectory: true)
)

#expect(project.artifactID == "full-stack-demo")
#expect(project.modules.map(\.artifactID) == ["backend-api"])
Expand Down
Loading
Loading