Merge origin/main into feat/agents

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
l0ng-ai committed 2026-10-27 14:44:01 +08:00
коміт 4548cf05ea
286 files changed
+16464 -993

No files matched your search

+1
Переглянути файл
@@ -34,6 +34,7 @@ dirs.workspace = true
ide-core.workspace = true
dunce.workspace = true
ide-clipboard.workspace = true
ide-text.workspace = true
ide-vfs.workspace = true
ide-workspace.workspace = true
+22
Переглянути файл
@@ -42,6 +42,28 @@ pub(crate) fn load(root: &Path, cache: &Path, settings: &Settings) -> BuildModel
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
}
+32
Переглянути файл
@@ -0,0 +1,32 @@
//! The OS clipboard's file list, so the project view and Explorer exchange files.
use crate::commands::doc::blocking;
use ide_core::IdeResult;
use std::path::{Path, PathBuf};
/// Absolute paths on the clipboard, empty when it holds no file list.
#[tauri::command]
#[specta::specta]
pub async fn clipboard_file_list() -> IdeResult<Vec<String>> {
blocking(|| Ok(ide_clipboard::files().iter().map(|p| slash(p)).collect())).await
}
/// Whether the clipboard holds files: cheap, for the Paste item's enabled state.
#[tauri::command]
#[specta::specta]
pub fn clipboard_has_files() -> bool {
ide_clipboard::has_files()
}
/// Replaces the clipboard with `paths`, so Explorer can paste them. Best-effort:
/// false when the clipboard stayed busy; the caller keeps working either way.
#[tauri::command]
#[specta::specta]
pub async fn clipboard_set_file_list(paths: Vec<PathBuf>) -> IdeResult<bool> {
blocking(move || Ok(ide_clipboard::set_files(&paths))).await
}
/// Paths cross the IPC boundary `/`-separated, as every other backend path does.
fn slash(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}
+70 -1
Переглянути файл
@@ -1,7 +1,10 @@
use crate::state::AppState;
use ide_core::{IdeError, IdeResult, IpcPath, RequestId};
use ide_db::split::SqlStatement;
use ide_db::{CompletionModel, DataSource, DbRowEdit, QueryResult, SortKey, TableDetails, TableInfo, TablePage};
use ide_db::{
CompletionModel, DataSource, DbObjectKind, DbRowEdit, HistoryEntry, PlanNode, QueryResult, SchemaObjects, SortKey,
TableDetails, TableInfo, TablePage,
};
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::{State, Window};
@@ -175,6 +178,72 @@ pub async fn db_ddl(
db.ddl(&id, &schema, &table).await
}
/// The routines and sequences of `schema` (the tree's `routines` / `sequences`).
#[tauri::command]
#[specta::specta]
pub async fn db_schema_objects(
window: Window,
state: State<'_, AppState>,
id: String,
schema: String,
) -> IdeResult<SchemaObjects> {
let db = state.project(window.label())?.db.clone();
db.schema_objects(&id, &schema).await
}
/// The definition of a routine, trigger or sequence (Go to DDL, Quick Documentation).
#[tauri::command]
#[specta::specta]
pub async fn db_object_ddl(
window: Window,
state: State<'_, AppState>,
id: String,
schema: String,
kind: DbObjectKind,
name: String,
) -> IdeResult<String> {
let db = state.project(window.label())?.db.clone();
db.object_ddl(&id, &schema, kind, &name).await
}
/// Explain Plan: the execution plan of `statement` on `id`, without running it.
#[tauri::command]
#[specta::specta]
pub async fn db_explain(
window: Window,
state: State<'_, AppState>,
request: RequestId,
id: String,
statement: String,
) -> IdeResult<Vec<PlanNode>> {
let db = state.project(window.label())?.db.clone();
let cancel = state.begin_request(request);
let result = db.explain(&id, &statement, &cancel).await;
state.end_request(request);
result
}
/// The statements data source `id` ran, newest first (Browse Query History).
#[tauri::command]
#[specta::specta]
pub async fn db_history(window: Window, state: State<'_, AppState>, id: String) -> IdeResult<Vec<HistoryEntry>> {
let db = state.project(window.label())?.db.clone();
db.history(&id).await
}
/// Removes statements from the query history of `id` (`None`: all of them).
#[tauri::command]
#[specta::specta]
pub async fn db_remove_history(
window: Window,
state: State<'_, AppState>,
id: String,
texts: Option<Vec<String>>,
) -> IdeResult<()> {
let db = state.project(window.label())?.db.clone();
db.remove_history(&id, texts).await
}
/// Writes exported result data (CSV, JSON, SQL…) to `path`.
#[tauri::command]
#[specta::specta]
+16
Переглянути файл
@@ -264,6 +264,22 @@ pub async fn debug_attachable_processes() -> IdeResult<Vec<attach::AttachablePro
.map_err(|e| ide_core::IdeError::internal(format!("listing processes failed: {e}")))?
}
/// Remote JVM Debug in listen mode: waits (cancellable through `request`) for the remote
/// JVM to connect to `host:port`; returns the loopback port java-debug attaches to.
#[tauri::command]
#[specta::specta]
pub async fn debug_listen_for_jvm(
state: State<'_, AppState>,
request: ide_core::RequestId,
host: String,
port: u16,
) -> IdeResult<u16> {
let token = state.begin_request(request);
let result = jdwp::listen_for_jvm(&host, port, token).await;
state.end_request(request);
result
}
/// HotSwap (Reload Changed Classes) in a java-debug session.
#[tauri::command]
#[specta::specta]
+13 -7
Переглянути файл
@@ -48,17 +48,23 @@ pub async fn java_resolve_launch(
let java = project_jdk.map(|j| ide_sdk::java::java_executable(&j.home)).or(from_jdtls).or_else(|| {
let release = ide_sdk::java::project_java_version(&module);
let jdks = ide_sdk::java::installed_jdks();
info.hints.extend(ide_sdk::java::missing_jdk_message(&jdks, release));
ide_sdk::java::pick_jdk(&jdks, release).map(|j| ide_sdk::java::java_executable(&j.home))
});
info.java_exec = Some(java.map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|| "java".to_owned()));
let exec = PathBuf::from(info.java_exec.as_deref().unwrap_or("java"));
let shorten = ide_run::java_cmdline::shortening_for(&exec, &info.class_paths, &info.module_paths, cfg!(windows));
info.shorten_command_line = Some(shorten.as_str().to_owned());
if info.error_count > 0 {
let vm_args = &settings.languages.java.jdtls_vm_args;
let lombok = ide_sdk::lombok::lombok_agent(
&project.root,
ide_sdk::lombok::agent_in_args(vm_args.split_whitespace()),
ide_tools::managed_bundle(&state.data_dir, "lombok"),
ide_sdk::apply::maven_local_repository(&project.root, &settings),
);
let own = ide_sdk::lombok::agent_in_args(settings.languages.java.jdtls_vm_args.split_whitespace());
let managed = ide_tools::managed_bundle(&state.data_dir, "lombok");
let repo = ide_sdk::apply::maven_local_repository(&project.root, &settings);
let root = project.root.clone();
// Detection may read the project's sources.
let lombok =
tauri::async_runtime::spawn_blocking(move || ide_sdk::lombok::lombok_agent(&root, own, managed, repo))
.await
.map_err(|e| IdeError::internal(e.to_string()))?;
info.hints.extend(lombok.problem());
}
Ok(info)
+15
Переглянути файл
@@ -12,6 +12,7 @@ pub mod app;
pub mod build;
pub mod bytes;
pub mod caches;
pub mod clipboard;
pub mod coverage;
pub mod db;
pub mod debug;
@@ -84,6 +85,9 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
fs::fs_delete,
fs::fs_copy,
fs::fs_read_text,
clipboard::clipboard_file_list,
clipboard::clipboard_has_files,
clipboard::clipboard_set_file_list,
doc::doc_open,
doc::doc_open_virtual,
doc::doc_close,
@@ -168,6 +172,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
vcs::vcs_blame,
vcs::vcs_log,
vcs::vcs_commit_files,
vcs::vcs_commits_changes,
vcs::vcs_branches,
vcs::vcs_stashes,
vcs::vcs_stage,
@@ -263,6 +268,10 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
tools::tools_list,
tools::tools_install,
tools::tools_uninstall,
tools::jdk_download_vendors,
tools::jdk_download_releases,
tools::jdk_download_location,
tools::jdk_download,
sdk::sdk_detect,
sdk::sdk_python_effective,
sdk::sdk_project_libraries,
@@ -301,6 +310,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
debug::debug_set_data_breakpoints,
debug::debug_data_breakpoint_info,
debug::debug_attachable_processes,
debug::debug_listen_for_jvm,
debug::debug_redefine_classes,
coverage::coverage_prepare,
coverage::coverage_collect,
@@ -370,6 +380,11 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
db::db_tables,
db::db_table_details,
db::db_ddl,
db::db_schema_objects,
db::db_object_ddl,
db::db_explain,
db::db_history,
db::db_remove_history,
db::db_completion,
db::db_export_file,
db::db_set_manual_commit,
+5 -2
Переглянути файл
@@ -5,7 +5,7 @@ use crate::state::{AppState, Project};
use ide_core::settings::SpellingSettings;
use ide_core::{DocId, IdeError, IdeResult, RequestId};
use ide_language::SpellingKind;
use ide_spell::{Region, RegionKind, Speller, Typo};
use ide_spell::{Region, RegionKind, SpellScope, Speller, Typo};
use std::path::{Path, PathBuf};
use tauri::{State, Window};
@@ -33,13 +33,15 @@ fn speller(project: &Project) -> Speller {
}
/// Misspelt words in the comments, string literals and declared names of
/// `doc` (all of its text for prose). Cancel with `cancel_request(request)`.
/// `doc` that `scope` includes (all of its text for prose). Cancel with
/// `cancel_request(request)`.
#[tauri::command]
#[specta::specta]
pub async fn spell_check(
window: Window,
state: State<'_, AppState>,
doc: DocId,
scope: SpellScope,
request: RequestId,
) -> IdeResult<Vec<Typo>> {
let project = state.project(window.label())?;
@@ -65,6 +67,7 @@ pub async fn spell_check(
SpellingKind::Name => RegionKind::Name,
},
})
.filter(|r| scope.includes(r.kind))
.collect()
};
if cancel.is_cancelled() {
+79 -1
Переглянути файл
@@ -1,8 +1,11 @@
//! Language server / debug adapter installer (`ide-tools`).
//! Language server / debug adapter installer and JDK downloads (`ide-tools`).
use crate::state::AppState;
use ide_core::{IdeError, IdeResult, RequestId};
use ide_sdk::Sdk;
use ide_tools::jdk::{JdkReleases, JdkVendor};
use ide_tools::{InstallEvent, ToolInfo, Tools};
use std::path::PathBuf;
use tauri::ipc::Channel;
use tauri::{AppHandle, Manager, State, Window};
@@ -81,6 +84,81 @@ pub async fn tools_uninstall(
Ok(info)
}
/// JDK distributions Download JDK offers.
#[tauri::command]
#[specta::specta]
pub fn jdk_download_vendors() -> Vec<JdkVendor> {
ide_tools::jdk::vendors()
}
/// Java versions `vendor` publishes; cancellable with `cancel_request(request)`.
#[tauri::command]
#[specta::specta]
pub async fn jdk_download_releases(
app: AppHandle,
window: Window,
state: State<'_, AppState>,
vendor: String,
request: RequestId,
) -> IdeResult<JdkReleases> {
let tools = tools(&app, &state, &window)?;
let cancel = state.begin_request(request);
let result =
tauri::async_runtime::spawn_blocking(move || tools.jdk_releases(&vendor, &cancel)).await.map_err(join_err);
state.end_request(request);
result?
}
/// Where Download JDK puts JDKs unless told otherwise.
#[tauri::command]
#[specta::specta]
pub fn jdk_download_location(app: AppHandle) -> IdeResult<ide_core::IpcPath> {
let data = app.path().app_data_dir().map_err(|e| IdeError::internal(e.to_string()))?;
Ok(ide_tools::jdk::jdks_dir(&data).into())
}
/// Downloads the latest `vendor` JDK of Java `version` into `location` (default:
/// [`jdk_download_location`]), streaming progress through `on_event`; cancellable
/// with `cancel_request(request)`. The JDK is then detected like an installed one,
/// and language servers waiting for a JDK (jdtls) start on it.
#[tauri::command]
#[specta::specta]
#[allow(clippy::too_many_arguments)]
pub async fn jdk_download(
app: AppHandle,
window: Window,
state: State<'_, AppState>,
vendor: String,
version: u32,
location: Option<PathBuf>,
request: RequestId,
on_event: Channel<InstallEvent>,
) -> IdeResult<Sdk> {
let tools = tools(&app, &state, &window)?;
let location = location.filter(|p| !p.as_os_str().is_empty());
let cancel = state.begin_request(request);
let result = tauri::async_runtime::spawn_blocking(move || {
let jdk = tools.download_jdk(&vendor, version, location.as_deref(), &cancel, &|e| {
let _ = on_event.send(e);
})?;
let home = jdk.home.into_path_buf();
// A chosen location is scanned from now on too (the default one is from startup).
if let Some(location) = &location {
ide_sdk::java::add_jdk_root(location);
}
ide_sdk::java::rescan_jdks();
let jdk = ide_sdk::java::jdk_at(&home)
.ok_or_else(|| IdeError::internal(format!("{} is not a JDK home", home.display())))?;
IdeResult::Ok(ide_sdk::jdk_sdk(&jdk))
})
.await
.map_err(join_err);
state.end_request(request);
let sdk = result??;
reconfigure_servers(&state);
Ok(sdk)
}
fn reconfigure_servers(state: &AppState) {
for project in state.projects() {
let lsp = project.lsp.clone();
+13
Переглянути файл
@@ -121,6 +121,19 @@ pub async fn vcs_commit_files(
blocking(move || project.with_repo_at(repo.as_deref(), |r| r.commit_files(&commit))).await
}
/// Combined changes of several commits, `commits` oldest first.
#[tauri::command]
#[specta::specta]
pub async fn vcs_commits_changes(
window: Window,
state: State<'_, AppState>,
repo: Option<PathBuf>,
commits: Vec<String>,
) -> IdeResult<Vec<RevisionChange>> {
let project = state.project(window.label())?;
blocking(move || project.with_repo_at(repo.as_deref(), |r| r.commits_changes(&commits))).await
}
#[tauri::command]
#[specta::specta]
pub async fn vcs_branches(window: Window, state: State<'_, AppState>, repo: Option<PathBuf>) -> IdeResult<Vec<Branch>> {
+1
Переглянути файл
@@ -41,6 +41,7 @@ pub fn run() {
if let Some(data) = dirs::data_dir() {
ide_tools::extend_process_path(&data.join(&context.config().identifier));
ide_dap::adapter::set_tools_root(ide_tools::tools_dir(&data.join(&context.config().identifier)));
ide_sdk::java::add_jdk_root(&ide_tools::jdk::jdks_dir(&data.join(&context.config().identifier)));
}
if let Some(refresh) = refresh_shell_path {
refresh();
+3
Переглянути файл
@@ -630,6 +630,8 @@ pub fn spec(os: Os) -> Vec<Entry> {
act("db.showToolWindow", "Database"),
act("db.newDataSource", "New Data Source…"),
act("db.openConsole", "Open Query Console"),
SEP,
act("vim.toggle", "Vim"),
],
));
@@ -1432,6 +1434,7 @@ mod tests {
"code.quickDefinition",
"code.quickTypeDefinition",
"localHistory.recentChanges",
"vim.toggle",
"editor.resetFontSize",
"diff.compareWithClipboard",
"view.quickSwitchScheme",
+5 -1
Переглянути файл
@@ -399,7 +399,11 @@ impl AppState {
mark("recent");
tracing::info!("opened {} in window {window} in {} ms {laps:?}", root.display(), started.elapsed().as_millis());
let db = Arc::new(ide_db::DbManager::new(root.clone(), self.data_dir.join("db-passwords.json")));
let db = Arc::new(ide_db::DbManager::new(
root.clone(),
self.data_dir.join("db-passwords.json"),
self.data_dir.join("db-history.json"),
));
let http_history = Arc::new(ide_http::History::new(self.data_dir.join("http-history").join(&project_key)));
let agents = Arc::new(ide_agent::AgentManager::new(
ide_agent::AgentPaths {
+13
Переглянути файл
@@ -240,6 +240,7 @@ fn build_project_window(app: &AppHandle, mut config: WindowConfig) -> IdeResult<
let window = WebviewWindowBuilder::from_config(app, &config)
.and_then(|b| b.build())
.map_err(|e| IdeError::internal(format!("cannot create window: {e}")))?;
apply_zoom(app, &window);
// "Sync with OS" needs the OS mode, known once the window exists.
if let Some(a) = appearance.filter(|a| a.sync_with_os || a.theme == "system")
&& window.theme().is_ok_and(|t| t == Theme::Dark)
@@ -249,6 +250,17 @@ fn build_project_window(app: &AppHandle, mut config: WindowConfig) -> IdeResult<
Ok(window)
}
/// Sets the saved IDE zoom before the page loads; applied later by the frontend,
/// it would visibly scale up the splash.
fn apply_zoom(app: &AppHandle, window: &WebviewWindow) {
let Some(zoom) = app.try_state::<AppState>().map(|s| s.global_settings.effective().appearance.zoom) else {
return;
};
if zoom != 100 {
let _ = window.set_zoom(f64::from(zoom) / 100.0);
}
}
/// The first window (`create: false` in tauri.conf.json so it can take the theme's colour).
pub fn create_main_window(app: &AppHandle) -> IdeResult<WebviewWindow> {
build_project_window(app, window_config(app)?)
@@ -414,6 +426,7 @@ pub fn open_settings_window(
.and_then(|b| b.parent(&parent))
.and_then(|b| b.build())
.map_err(|e| IdeError::internal(format!("cannot create settings window: {e}")))?;
apply_zoom(app, &window);
// window-state restores the remembered size; the position always follows the parent.
if let (Ok(pos), Ok(size), Ok(own), Ok(scale)) =
(parent.outer_position(), parent.outer_size(), window.outer_size(), parent.scale_factor())
+1 -1
Переглянути файл
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenIDEA",
"version": "0.1.14",
"version": "0.1.15",
"identifier": "ai.l0ng.openidea",
"build": {
"beforeDevCommand": "pnpm dev",