дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
Run and Debug now share one Java preparation path: save documents, build with the configured policy, resolve the module classpath and JDK, and stop on a failed build. Incremental Make is the default; Rebuild, NoBuild and Build-no-error-check are persisted on the run configuration, and the form offers only what a build tool supports. Delegated Gradle goes through the same path, Debug reports save failures, Maven snapshots honour Rebuild, and scoped diagnostics are retained so incremental Maven cannot erase compilation errors. Co-authored-by: misswell <[email protected]>
340 рядки
12 KiB
Rust
340 рядки
12 KiB
Rust
//! Run configurations and process management.
|
||
|
||
mod config;
|
||
mod detect;
|
||
pub mod dotnet;
|
||
pub mod envfile;
|
||
mod external;
|
||
pub mod go;
|
||
pub mod gradle_jvm;
|
||
pub mod import;
|
||
pub mod java_cmdline;
|
||
pub mod junit;
|
||
mod process;
|
||
pub mod python_path;
|
||
pub mod ruby_preload;
|
||
pub mod ruby_reporter;
|
||
pub mod scope;
|
||
pub mod tomcat;
|
||
mod utf8;
|
||
pub mod which;
|
||
pub mod wrap;
|
||
|
||
pub use detect::cmake::{build_configs as cmake_build_configs, prepare as prepare_cmake};
|
||
pub use process::{ProcessManager, StartOptions};
|
||
|
||
use ide_core::{IdeResult, RunId};
|
||
use serde::{Deserialize, Serialize};
|
||
use specta::Type;
|
||
use std::collections::BTreeMap;
|
||
use std::path::{Path, PathBuf};
|
||
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RunConfig {
|
||
/// Stable id, e.g. `npm:dev`, `cargo:run:app`, or a uuid for user-defined.
|
||
pub id: String,
|
||
pub name: String,
|
||
/// `npm` | `cargo` | `gradle` | `maven` | `go` | `python` | `pytest` | `make` | `shell` …
|
||
pub kind: String,
|
||
pub command: String,
|
||
pub args: Vec<String>,
|
||
pub cwd: Option<ide_core::IpcPath>,
|
||
pub env: BTreeMap<String, String>,
|
||
/// Detected configs are regenerated; user configs persist in `.openidea/run.toml`.
|
||
pub detected: bool,
|
||
/// Test runner configs render results in the test tree.
|
||
pub is_test: bool,
|
||
/// DAP adapter that can debug this config, if any.
|
||
pub debug_adapter: Option<String>,
|
||
/// "Allow multiple instances": Run on a running config starts another one instead of restarting it.
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
#[specta(optional)]
|
||
pub allow_parallel: bool,
|
||
/// Options of user-defined configurations (all optional; old `run.toml` files load unchanged).
|
||
#[serde(flatten)]
|
||
pub extras: RunExtras,
|
||
}
|
||
|
||
/// How a Java launch builds the project before starting (IDEA's Before launch › Build).
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub enum BuildPolicy {
|
||
/// Build the way this project's backend does it: incremental when possible.
|
||
#[default]
|
||
Automatic,
|
||
/// Incremental build (IDEA's "Build"/Make).
|
||
Make,
|
||
/// Clean the output, then build (IDEA's Rebuild).
|
||
Rebuild,
|
||
/// Do not build; still resolve the module, JDK and classpath.
|
||
NoBuild,
|
||
/// Incremental build; a project with compile errors may still start with its last
|
||
/// successfully compiled classes (IDEA's "Build, no error check").
|
||
MakeAndContinueOnErrors,
|
||
}
|
||
|
||
impl BuildPolicy {
|
||
/// The spelling used in `run.toml` and the run configuration dialog.
|
||
pub fn key(self) -> &'static str {
|
||
match self {
|
||
Self::Automatic => "automatic",
|
||
Self::Make => "make",
|
||
Self::Rebuild => "rebuild",
|
||
Self::NoBuild => "noBuild",
|
||
Self::MakeAndContinueOnErrors => "makeAndContinueOnErrors",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A step run before a configuration launches; a failing step aborts the launch.
|
||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct BeforeLaunchTask {
|
||
/// `run` (another configuration id) | `build` (project, or `target` = CMake build id) |
|
||
/// `tool` (external tool id) |
|
||
/// `command` (a shell command line) | `toolWindow` (tool window id).
|
||
pub kind: String,
|
||
#[serde(default)]
|
||
pub target: String,
|
||
}
|
||
|
||
fn yes() -> bool {
|
||
true
|
||
}
|
||
|
||
fn is_true(b: &bool) -> bool {
|
||
*b
|
||
}
|
||
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RunExtras {
|
||
/// Owning module, independent of the working directory; also supplies a JVM application's classpath.
|
||
/// Empty means the project root.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
#[specta(optional)]
|
||
pub module: Option<String>,
|
||
/// Values of the configuration type's own form fields (the command line derives from them).
|
||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||
#[specta(optional)]
|
||
pub params: BTreeMap<String, String>,
|
||
/// Folder in the Run/Debug Configurations tree.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
#[specta(optional)]
|
||
pub folder: Option<String>,
|
||
/// Stored in the workspace (`.openidea/run.local.toml`, not shared) instead of `run.toml`.
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
#[specta(optional)]
|
||
pub local: bool,
|
||
/// Off: the process gets only the configured variables.
|
||
#[serde(default = "yes", skip_serializing_if = "is_true")]
|
||
#[specta(optional)]
|
||
pub include_system_env: bool,
|
||
/// `.env` files (relative to the project root) loaded before `env`, in order.
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
#[specta(optional)]
|
||
pub env_files: Vec<String>,
|
||
/// "Redirect input from": the process reads this file as stdin.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
#[specta(optional)]
|
||
pub stdin_file: Option<String>,
|
||
/// "Save console output to file" (overwritten on each run).
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
#[specta(optional)]
|
||
pub output_file: Option<String>,
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
#[specta(optional)]
|
||
pub before_launch: Vec<BeforeLaunchTask>,
|
||
/// Before launch build for JVM launches; unset keeps the project's default (incremental).
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
#[specta(optional)]
|
||
pub build_policy: Option<BuildPolicy>,
|
||
/// Compound configurations: ids of the configurations started together.
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
#[specta(optional)]
|
||
pub compound: Vec<String>,
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
#[specta(optional)]
|
||
pub show_on_stdout: bool,
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
#[specta(optional)]
|
||
pub show_on_stderr: bool,
|
||
/// Overrides Settings › Run › "Show the Run tool window when a process starts".
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
#[specta(optional)]
|
||
pub open_tool_window: Option<bool>,
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
#[specta(optional)]
|
||
pub focus_tool_window: bool,
|
||
/// "Emulate terminal in output console": the process runs on a pseudo-terminal
|
||
/// (colours, line editing, full-screen programs); the console renders it as a terminal.
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
#[specta(optional)]
|
||
pub emulate_terminal: bool,
|
||
/// Shell command line run on Stop before the process is signalled (e.g. `docker stop <name>`),
|
||
/// with the run's working directory and environment.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
#[specta(optional)]
|
||
pub stop_command: Option<String>,
|
||
/// Settings › Run Targets id the process runs on (over SSH); `None` = this machine.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
#[specta(optional)]
|
||
pub target: Option<String>,
|
||
}
|
||
|
||
impl Default for RunExtras {
|
||
fn default() -> Self {
|
||
Self {
|
||
module: None,
|
||
params: BTreeMap::new(),
|
||
folder: None,
|
||
local: false,
|
||
include_system_env: true,
|
||
env_files: Vec::new(),
|
||
stdin_file: None,
|
||
output_file: None,
|
||
before_launch: Vec::new(),
|
||
build_policy: None,
|
||
compound: Vec::new(),
|
||
show_on_stdout: false,
|
||
show_on_stderr: false,
|
||
open_tool_window: None,
|
||
focus_tool_window: false,
|
||
emulate_terminal: false,
|
||
stop_command: None,
|
||
target: None,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||
pub enum OutputChunk {
|
||
Stdout {
|
||
text: String,
|
||
},
|
||
Stderr {
|
||
text: String,
|
||
},
|
||
/// System message from the IDE (command line echo, exit code).
|
||
System {
|
||
text: String,
|
||
},
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RunInfo {
|
||
pub id: RunId,
|
||
pub config_id: String,
|
||
pub name: String,
|
||
pub pid: Option<u32>,
|
||
pub running: bool,
|
||
/// Runs on a pseudo-terminal: output is raw terminal data, input goes in keystroke by keystroke.
|
||
pub pty: bool,
|
||
}
|
||
|
||
pub use external::{EXTERNAL_PREFIX, ExternalDetection, set_external_detection};
|
||
|
||
/// Detects configs from package.json scripts, Cargo.toml targets, build.gradle(.kts),
|
||
/// pom.xml, go.mod, pyproject/pytest, Makefile.
|
||
pub fn detect(root: &Path) -> Vec<RunConfig> {
|
||
detect::detect(root)
|
||
}
|
||
|
||
/// Detects configurations excluding the supplied project-relative Maven POM paths.
|
||
pub fn detect_excluding(root: &Path, excluded_poms: &[PathBuf]) -> Vec<RunConfig> {
|
||
detect::detect_excluding(root, excluded_poms)
|
||
}
|
||
|
||
/// Detects configurations with explicit CMake profiles, or legacy discovery when absent.
|
||
pub fn detect_with_profiles(
|
||
root: &Path,
|
||
excluded_poms: &[PathBuf],
|
||
profiles: Option<&[ide_core::settings::CMakeProfile]>,
|
||
) -> Vec<RunConfig> {
|
||
detect::detect_with_profiles(root, excluded_poms, profiles)
|
||
}
|
||
|
||
/// Reads `<root>/.openidea/run.toml`; a missing file yields no configs.
|
||
pub fn load_user_configs(root: &Path) -> IdeResult<Vec<RunConfig>> {
|
||
config::load(root)
|
||
}
|
||
|
||
/// Writes `<root>/.openidea/run.toml` (and the workspace-local `run.local.toml` for
|
||
/// configurations not stored as project files), preserving unrelated content.
|
||
pub fn save_user_configs(root: &Path, configs: &[RunConfig]) -> IdeResult<()> {
|
||
config::save(root, configs, &[])
|
||
}
|
||
|
||
/// Detected ids explicitly removed in this workspace.
|
||
pub fn load_suppressed_config_ids(root: &Path) -> IdeResult<Vec<String>> {
|
||
config::load_suppressed(root)
|
||
}
|
||
|
||
/// Saves user configurations and suppresses explicitly removed detections.
|
||
/// Saving a configuration with a suppressed id restores that id.
|
||
pub fn save_user_configs_with_removals(
|
||
root: &Path,
|
||
configs: &[RunConfig],
|
||
removed_detected_ids: &[String],
|
||
) -> IdeResult<()> {
|
||
config::save(root, configs, removed_detected_ids)
|
||
}
|
||
|
||
/// Configuration templates (per-type defaults for new configurations) from `run.toml`.
|
||
pub fn load_templates(root: &Path) -> IdeResult<Vec<RunConfig>> {
|
||
config::load_templates(root)
|
||
}
|
||
|
||
pub fn save_templates(root: &Path, templates: &[RunConfig]) -> IdeResult<()> {
|
||
config::save_templates(root, templates)
|
||
}
|
||
|
||
/// The environment runs inherit (unless "Include system environment variables" is off).
|
||
pub fn system_env() -> BTreeMap<String, String> {
|
||
ide_core::process::host_vars().filter_map(|(k, v)| Some((k.into_string().ok()?, v.into_string().ok()?))).collect()
|
||
}
|
||
|
||
/// Complete environment for a preparatory tool belonging to this run configuration.
|
||
pub fn resolved_env(root: &Path, config: &RunConfig) -> IdeResult<BTreeMap<String, String>> {
|
||
process::resolved_env(root, config)
|
||
}
|
||
|
||
/// Run configurations from another IDE's project file (`.vscode/launch.json`), not yet saved:
|
||
/// names already in `existing` are skipped, so importing again adds nothing twice.
|
||
pub fn import_configs(
|
||
root: &Path,
|
||
source: import::RunImportSource,
|
||
existing: &[RunConfig],
|
||
) -> IdeResult<import::RunImport> {
|
||
// `${env:NAME}` reads what a run inherits, the login shell's variables included.
|
||
import::import(root, source, existing, &process::inherited_env())
|
||
}
|
||
|
||
/// Path of the user run configuration file for `root`.
|
||
pub fn user_configs_path(root: &Path) -> PathBuf {
|
||
config::config_path(root)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod linkage_tests {
|
||
#[test]
|
||
fn detection_skips_unlinked_maven_roots() {
|
||
let tmp = tempfile::tempdir().unwrap();
|
||
for name in ["a", "b"] {
|
||
std::fs::create_dir(tmp.path().join(name)).unwrap();
|
||
std::fs::write(tmp.path().join(name).join("pom.xml"), "<project/>").unwrap();
|
||
}
|
||
let configs = super::detect_excluding(tmp.path(), &["a/pom.xml".into()]);
|
||
assert!(configs.iter().any(|c| c.kind == "maven"));
|
||
assert!(
|
||
configs
|
||
.iter()
|
||
.filter(|c| c.kind == "maven")
|
||
.all(|c| c.cwd.as_ref().is_some_and(|d| d.as_ref() == std::path::Path::new("b")))
|
||
);
|
||
}
|
||
}
|