Files
openidea/crates/ide-run/src/lib.rs
T
l0ng-ai ac360b6143 feat(go): add core Go development workflows (#524)
Go settings, execution, debugging and test reporting were disconnected, leaving Go projects without editable build/test fields and workspace maintenance actions.

- Add project build constraints that update gopls in place, and notices on excluded files.
- Add editable Go Build and Go Test configurations with directory, package and file targets, separate tool and program arguments, modules, environment, output selection and build-only execution, and pass the same settings to Delve.
- Decode Go JSON test output into the results view, with subtests, failures, skips and rerun failed. Add gutter targets for benchmarks and generate directives.
- Add module environment and vendoring settings, read go.work members, and offer tidy, download, sync and use from file menus.
- Add file and package generate actions, and format and organize imports through gopls on Save and Reformat.
- Show what a Go program prints in the debug console: Delve's debuggee shares its stdio, which no DAP event carries.
- Keep the default build output out of version control, and keep configurations produced by an earlier run out of the gutter choices.

Fixes #507
Fixes #237
2026-10-31 13:56:06 +08:00

273 lines
9.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Run configurations and process management.
mod config;
mod detect;
pub mod envfile;
pub mod go;
pub mod gradle_jvm;
pub mod java_cmdline;
pub mod junit;
mod process;
pub mod python_path;
pub mod tomcat;
mod utf8;
pub mod which;
pub mod wrap;
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,
}
/// 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, `target` = its id) | `build` | `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 {
/// Module supplying 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>,
/// 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(),
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,
}
/// 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)
}
/// 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)
}
/// 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")))
);
}
}