дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
Maven sync ran a plain `dependency:tree`, which drops the nodes Maven omits (conflict losers, duplicates), so there was no data to show where a conflict comes from. Sync now runs `maven-dependency-plugin:3.8.1:tree -Dverbose` (the project's own plugin version when offline) and records omitted / conflictWith / managedFrom per node; omitted nodes stay out of the Maven tool window and External Libraries, and older caches are marked stale. The new Dependency Analyzer (Maven toolbar, module / Dependencies node menus, pom.xml menus, Find Action) lists a module's resolved artifacts or tree with conflict markers, search and scope filters, and every path to the selected artifact with its status. Exclude writes an `<exclusion>` into the declaring pom (module or in-project parent), keeping indentation and line endings, then reloads; Jump to Source and Copy Coordinates are on each path. Gradle is not covered yet. Fixes #87 Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
239 рядки
9.7 KiB
Rust
239 рядки
9.7 KiB
Rust
//! Maven / Gradle tool windows (the build model and its background sync) and
|
||
//! Build › Build Project (compiling with the project's build system).
|
||
|
||
use crate::state::{AppState, apply_workspace_edit};
|
||
use ide_build::compile::{self, BuildEvent, BuildKind, BuildResult};
|
||
use ide_build::pom_edit::{ArtifactKey, Exclusion};
|
||
use ide_build::{BuildModel, BuildTool, SyncOptions, SyncProgress};
|
||
use ide_core::settings::Settings;
|
||
use ide_core::{FileEdit, IdeError, IdeEvent, IdeResult, Location, Range, RequestId, WorkspaceEdit};
|
||
use std::collections::BTreeMap;
|
||
use std::path::Path;
|
||
use tauri::ipc::Channel;
|
||
use tauri::{State, Window};
|
||
|
||
fn join_err(e: tauri::Error) -> IdeError {
|
||
IdeError::internal(e.to_string())
|
||
}
|
||
|
||
/// `JAVA_HOME`: the project JDK, else the installed JDK matching `release` (none
|
||
/// when the project names no version).
|
||
fn java_env(project_jdk: Option<&ide_sdk::java::Jdk>, release: Option<u32>) -> BTreeMap<String, String> {
|
||
let jdks = ide_sdk::java::installed_jdks();
|
||
let jdk = project_jdk.or_else(|| release.and_then(|r| ide_sdk::java::pick_jdk(&jdks, Some(r))));
|
||
jdk.map(|j| BTreeMap::from([("JAVA_HOME".to_owned(), j.home.to_string_lossy().into_owned())])).unwrap_or_default()
|
||
}
|
||
|
||
/// The build model with the launch settings applied: the JDK (`JAVA_HOME`), and
|
||
/// the Maven home and options from Settings › Maven.
|
||
pub(crate) fn load(root: &Path, cache: &Path, settings: &Settings) -> BuildModel {
|
||
let mut model = ide_build::load(root, cache, ide_sdk::apply::maven_local_repository(root, settings));
|
||
let jdk = ide_sdk::apply::project_jdk(root, settings);
|
||
if let Some(m) = &mut model.maven {
|
||
let mvn = ide_sdk::apply::maven_command(root, settings);
|
||
let options = ide_sdk::apply::maven_options(root, settings);
|
||
for p in &mut m.projects {
|
||
p.env = java_env(jdk.as_ref(), p.java_version);
|
||
if let Some(mvn) = &mvn {
|
||
p.command = mvn.to_string_lossy().into_owned();
|
||
}
|
||
p.options = options.clone();
|
||
}
|
||
}
|
||
if let Some(g) = &mut model.gradle {
|
||
g.env = java_env(jdk.as_ref(), g.java_version);
|
||
}
|
||
// A failed sync of a project no installed JDK can build says so first (with a Download JDK action).
|
||
let missing = |release: Option<u32>| match jdk {
|
||
Some(_) => None,
|
||
None => ide_sdk::java::missing_jdk_message(&ide_sdk::java::installed_jdks(), release),
|
||
};
|
||
let explain = |error: &mut Option<String>, hint: Option<String>| {
|
||
if let (Some(e), Some(hint)) = (error.as_mut(), hint) {
|
||
*e = format!("{hint}\n{e}");
|
||
}
|
||
};
|
||
if let Some(m) = &mut model.maven
|
||
&& m.sync_error.is_some()
|
||
{
|
||
let hint = m.projects.iter().find_map(|p| missing(p.java_version));
|
||
explain(&mut m.sync_error, hint);
|
||
}
|
||
if let Some(g) = &mut model.gradle
|
||
&& g.sync_error.is_some()
|
||
{
|
||
let hint = missing(g.java_version);
|
||
explain(&mut g.sync_error, hint);
|
||
}
|
||
model
|
||
}
|
||
|
||
/// Maven and Gradle projects of the window's project, from the build files plus the last sync.
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn build_model(window: Window, state: State<'_, AppState>) -> IdeResult<BuildModel> {
|
||
let project = state.project(window.label())?;
|
||
let (root, cache, settings) = (project.root.clone(), project.build_cache_dir(), project.settings.effective());
|
||
tauri::async_runtime::spawn_blocking(move || load(&root, &cache, &settings)).await.map_err(join_err)
|
||
}
|
||
|
||
/// Runs Maven (`help:effective-pom dependency:tree`) or Gradle (`tasks --all`) in the
|
||
/// background, reporting progress on `on_progress`; cancellable with `cancel_request(request)`.
|
||
/// Returns the refreshed model; a failed build is reported in its `syncError`.
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn build_sync(
|
||
window: Window,
|
||
state: State<'_, AppState>,
|
||
tool: BuildTool,
|
||
request: RequestId,
|
||
offline: bool,
|
||
profiles: Vec<String>,
|
||
on_progress: Channel<SyncProgress>,
|
||
) -> IdeResult<BuildModel> {
|
||
let project = state.project(window.label())?;
|
||
let (root, cache, settings) = (project.root.clone(), project.build_cache_dir(), project.settings.effective());
|
||
let token = state.begin_request(request);
|
||
let result = tauri::async_runtime::spawn_blocking(move || {
|
||
let current = load(&root, &cache, &settings);
|
||
let env = match tool {
|
||
BuildTool::Maven => current
|
||
.maven
|
||
.as_ref()
|
||
.and_then(|m| m.projects.iter().find(|p| !p.env.is_empty()))
|
||
.map(|p| p.env.clone())
|
||
.unwrap_or_default(),
|
||
BuildTool::Gradle => current.gradle.as_ref().map(|g| g.env.clone()).unwrap_or_default(),
|
||
};
|
||
let options = SyncOptions {
|
||
offline,
|
||
profiles,
|
||
env,
|
||
maven_command: ide_sdk::apply::maven_command(&root, &settings).map(|p| p.to_string_lossy().into_owned()),
|
||
maven_options: ide_sdk::apply::maven_options(&root, &settings),
|
||
};
|
||
let progress = |p: SyncProgress| {
|
||
let _ = on_progress.send(p);
|
||
};
|
||
match tool {
|
||
BuildTool::Maven => ide_build::sync_maven(&root, &cache, &options, &token, &progress)?,
|
||
BuildTool::Gradle => ide_build::sync_gradle(&root, &cache, &options, &token, &progress)?,
|
||
}
|
||
Ok(load(&root, &cache, &settings))
|
||
})
|
||
.await
|
||
.map_err(join_err);
|
||
state.end_request(request);
|
||
result?
|
||
}
|
||
|
||
/// The POM declaring `dependency` for the Maven module `pom` (project-relative): the
|
||
/// module's own, else its nearest parent in the project; its path, text and declaration.
|
||
fn declaring_pom(
|
||
project: &crate::state::Project,
|
||
pom: &str,
|
||
dependency: &ArtifactKey,
|
||
) -> IdeResult<(std::path::PathBuf, String, Range)> {
|
||
let model = ide_build::maven::load(&project.root);
|
||
for rel in ide_build::pom_edit::declaring_poms(&model, pom) {
|
||
let path = project.root.join(&rel);
|
||
let text = project.text_of(&path)?;
|
||
if let Some(range) = ide_build::pom_edit::declaration(&text, dependency) {
|
||
return Ok((path, text, range));
|
||
}
|
||
}
|
||
Err(IdeError::not_found(format!(
|
||
"{dependency} is not declared in a pom.xml of this project (it may come from an imported BOM or an external parent POM)"
|
||
)))
|
||
}
|
||
|
||
/// Where the direct dependency `dependency` of the Maven module `pom` is declared.
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn maven_dependency_declaration(
|
||
window: Window,
|
||
state: State<'_, AppState>,
|
||
pom: String,
|
||
dependency: ArtifactKey,
|
||
) -> IdeResult<Location> {
|
||
let project = state.project(window.label())?;
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
let (path, _, range) = declaring_pom(&project, &pom, &dependency)?;
|
||
Ok(Location::file(path, range))
|
||
})
|
||
.await
|
||
.map_err(join_err)?
|
||
}
|
||
|
||
/// Adds an `<exclusion>` of `exclusion` to the declaration of the direct dependency
|
||
/// `dependency` of the Maven module `pom`, through the open document when there is one
|
||
/// (saved). Returns the declaration; nothing changes when it already excludes it.
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn maven_exclude_dependency(
|
||
window: Window,
|
||
state: State<'_, AppState>,
|
||
pom: String,
|
||
dependency: ArtifactKey,
|
||
exclusion: ArtifactKey,
|
||
) -> IdeResult<Location> {
|
||
let project = state.project(window.label())?;
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
let (path, text, range) = declaring_pom(&project, &pom, &dependency)?;
|
||
match ide_build::pom_edit::add_exclusion(&text, &dependency, &exclusion) {
|
||
Some(Exclusion::Edit(edit)) => {
|
||
let edit =
|
||
WorkspaceEdit { files: vec![FileEdit::Edit { path: path.clone().into(), edits: vec![edit] }] };
|
||
apply_workspace_edit(&project.documents, &edit)?;
|
||
}
|
||
Some(Exclusion::Present) => {}
|
||
None => return Err(IdeError::not_found(format!("{dependency} is not declared in {}", path.display()))),
|
||
}
|
||
Ok(Location::file(path, range))
|
||
})
|
||
.await
|
||
.map_err(join_err)?
|
||
}
|
||
|
||
/// Build › Build Project / Rebuild Project / Build Module (`file`: a file or folder of the
|
||
/// module) / Build Artifacts, streaming the console and messages on `on_event`; cancellable
|
||
/// with `cancel_request(request)`. Diagnostics of `clear` (the files of the previous build)
|
||
/// are cleared first; the new messages are published as `build` diagnostics.
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn build_run(
|
||
window: Window,
|
||
state: State<'_, AppState>,
|
||
kind: BuildKind,
|
||
file: Option<std::path::PathBuf>,
|
||
request: RequestId,
|
||
clear: Vec<std::path::PathBuf>,
|
||
on_event: Channel<BuildEvent>,
|
||
) -> IdeResult<BuildResult> {
|
||
let project = state.project(window.label())?;
|
||
let (root, cache, sink) = (project.root.clone(), project.build_cache_dir(), project.sink.clone());
|
||
let settings = project.settings.effective();
|
||
let token = state.begin_request(request);
|
||
let result = tauri::async_runtime::spawn_blocking(move || {
|
||
for path in clear {
|
||
sink.emit(IdeEvent::Diagnostics {
|
||
path: path.into(),
|
||
source: compile::DIAGNOSTICS_SOURCE.into(),
|
||
items: Vec::new(),
|
||
});
|
||
}
|
||
let plan = compile::plan(&root, &load(&root, &cache, &settings), kind, file.as_deref())?;
|
||
let result = compile::run(&root, &plan, &token, &|e| {
|
||
let _ = on_event.send(e);
|
||
})?;
|
||
for (path, items) in compile::diagnostics(&result.messages) {
|
||
sink.emit(IdeEvent::Diagnostics { path: path.into(), source: compile::DIAGNOSTICS_SOURCE.into(), items });
|
||
}
|
||
Ok(result)
|
||
})
|
||
.await
|
||
.map_err(join_err);
|
||
state.end_request(request);
|
||
result?
|
||
}
|