mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 18:38:33 -10:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bdb10c7fe | ||
|
|
210f9a78ac | ||
|
|
5c9b131f78 | ||
|
|
49f9c57bcc | ||
|
|
07609ef9c6 | ||
|
|
bf2427bd3a |
No files matched your search
Generated
+36
-35
@@ -4038,7 +4038,7 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "ide-agent"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"encoding_rs",
|
||||
@@ -4061,7 +4061,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-analysis"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ast-grep-core",
|
||||
"ide-core",
|
||||
@@ -4075,7 +4075,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-build"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
@@ -4090,7 +4090,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-clipboard"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
@@ -4099,7 +4099,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-core"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"encoding_rs",
|
||||
@@ -4115,7 +4115,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-coverage"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-run",
|
||||
@@ -4129,7 +4129,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-dap"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-jsonrpc",
|
||||
@@ -4147,7 +4147,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-db"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"bb8",
|
||||
"dirs 6.0.0",
|
||||
@@ -4169,7 +4169,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-docker"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"libc",
|
||||
@@ -4183,7 +4183,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-fonts"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-text",
|
||||
@@ -4195,7 +4195,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-github"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-vcs",
|
||||
@@ -4209,7 +4209,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-host"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
@@ -4254,7 +4254,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-http"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"parking_lot",
|
||||
@@ -4272,7 +4272,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-index"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"criterion",
|
||||
@@ -4294,7 +4294,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-jsonrpc"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"futures",
|
||||
@@ -4310,7 +4310,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-language"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"criterion",
|
||||
@@ -4365,7 +4365,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-lsp"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"futures",
|
||||
@@ -4374,6 +4374,7 @@ dependencies = [
|
||||
"ide-jsonrpc",
|
||||
"ide-language",
|
||||
"ide-text",
|
||||
"ide-vfs",
|
||||
"ignore",
|
||||
"lsp-types",
|
||||
"parking_lot",
|
||||
@@ -4389,7 +4390,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-notebook"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"ide-core",
|
||||
@@ -4412,7 +4413,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-plugin"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-language",
|
||||
@@ -4430,7 +4431,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-profiler"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-run",
|
||||
@@ -4443,7 +4444,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-remote"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"globset",
|
||||
"ide-core",
|
||||
@@ -4460,7 +4461,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-run"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"encoding_rs",
|
||||
@@ -4483,7 +4484,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-sdk"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"ide-core",
|
||||
@@ -4502,7 +4503,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-search"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"globset",
|
||||
@@ -4522,7 +4523,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-spell"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"parking_lot",
|
||||
@@ -4535,7 +4536,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-ssh"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"flate2",
|
||||
@@ -4554,7 +4555,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-syntax"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-language",
|
||||
@@ -4569,7 +4570,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-terminal"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"libc",
|
||||
@@ -4582,7 +4583,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-text"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"ide-core",
|
||||
@@ -4594,7 +4595,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-tools"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"hex",
|
||||
@@ -4615,7 +4616,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-vcs"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"encoding_rs",
|
||||
@@ -4634,7 +4635,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-vfs"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"chardetng",
|
||||
"dunce",
|
||||
@@ -4656,7 +4657,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-web"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"open",
|
||||
@@ -4672,7 +4673,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-workspace"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"dunce",
|
||||
@@ -6143,7 +6144,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openidea"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"block2 0.6.2",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ resolver = "3"
|
||||
members = ["crates/*", "src-tauri"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
edition = "2024"
|
||||
rust-version = "1.96"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
·
|
||||
<a href="#benchmark">Benchmark</a>
|
||||
·
|
||||
<a href="#build">Build</a>
|
||||
·
|
||||
<a href="https://discord.gg/2Zy4yn7gP">Discord</a>
|
||||
</p>
|
||||
|
||||
@@ -47,24 +45,6 @@ IDEA analyses Python more deeply, so "ready" doesn't mean the same work on
|
||||
both sides. Both are measured from the outside, the same way: see
|
||||
[`perf/compare`](perf/compare) to run it on your machine and projects.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
Rust 1.96, Node 22, pnpm and the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/).
|
||||
|
||||
Native notebook execution tests are opt-in and need two distinct Python environments
|
||||
with `ipykernel` installed:
|
||||
|
||||
```sh
|
||||
OPENIDEA_TEST_PYTHON=/first/env/bin/python \
|
||||
OPENIDEA_TEST_PYTHON_SECOND=/second/env/bin/python \
|
||||
cargo test -p ide-notebook --test kernel_runtime -- --ignored --test-threads=1
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0](LICENSE). Bundled third-party assets are listed in [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md).
|
||||
|
||||
@@ -371,6 +371,12 @@ pub struct TextAttributesOverride {
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct LanguageCodeStyle {
|
||||
/// Java formatter: `default` | `intellij` | `custom` (other languages ignore it).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub formatter: Option<String>,
|
||||
/// Eclipse formatter XML; relative paths start at the project root.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub formatter_path: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub use_tab: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -58,6 +58,43 @@ pub struct Settings {
|
||||
pub deployment: DeploymentSettings,
|
||||
/// Build, Execution, Deployment › Run Targets.
|
||||
pub run_targets: Vec<RunTarget>,
|
||||
pub cmake: CMakeSettings,
|
||||
}
|
||||
|
||||
/// Absent profiles retain automatic discovery for existing projects.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct CMakeSettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(optional)]
|
||||
pub profiles: Option<Vec<CMakeProfile>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct CMakeProfile {
|
||||
pub name: String,
|
||||
pub build_type: String,
|
||||
pub generator: String,
|
||||
pub cmake_args: Vec<String>,
|
||||
/// Empty uses cmake-build-<lowercase build type>.
|
||||
pub build_dir: String,
|
||||
pub build_args: Vec<String>,
|
||||
pub env: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for CMakeProfile {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "Debug".into(),
|
||||
build_type: "Debug".into(),
|
||||
generator: String::new(),
|
||||
cmake_args: Vec::new(),
|
||||
build_dir: String::new(),
|
||||
build_args: Vec::new(),
|
||||
env: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||
@@ -154,6 +191,7 @@ impl Default for Settings {
|
||||
ssh: SshSettings::default(),
|
||||
deployment: DeploymentSettings::default(),
|
||||
run_targets: Vec::new(),
|
||||
cmake: CMakeSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -765,6 +803,24 @@ impl Settings {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cmake_profile_argument_arrays_round_trip() {
|
||||
let mut settings = Settings::default();
|
||||
assert!(settings.cmake.profiles.is_none());
|
||||
assert!(CMakeProfile::default().cmake_args.is_empty());
|
||||
assert!(CMakeProfile::default().build_args.is_empty());
|
||||
let profile = CMakeProfile {
|
||||
cmake_args: ["-DMESSAGE=two words", "-DQUOTE=\"hello\"", ""].map(String::from).into(),
|
||||
build_args: ["-j", "8", "--", "LABEL=it's a build"].map(String::from).into(),
|
||||
..Default::default()
|
||||
};
|
||||
settings.cmake.profiles = Some(vec![profile.clone()]);
|
||||
let value = serde_json::to_value(&settings).unwrap();
|
||||
assert_eq!(value["cmake"]["profiles"][0]["cmakeArgs"], serde_json::json!(profile.cmake_args));
|
||||
assert_eq!(value["cmake"]["profiles"][0]["buildArgs"], serde_json::json!(profile.build_args));
|
||||
assert_eq!(serde_json::from_value::<Settings>(value).unwrap(), settings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_font_fallback_defaults_empty_and_round_trips() {
|
||||
let old: Settings = serde_json::from_str(r#"{"appearance":{"useCustomFont":true}}"#).unwrap();
|
||||
|
||||
@@ -172,12 +172,14 @@ pub fn set_settings(
|
||||
host.apply_proxy();
|
||||
}
|
||||
// A new interpreter / toolchain / server executable restarts the language servers it affects.
|
||||
if ["languages", "tools", "languageServers", "projectStructure"]
|
||||
let code_style = key == "editor" || key == "editor.codeStyle" || key.starts_with("editor.codeStyle.");
|
||||
let toolchain = ["languages", "tools", "languageServers", "projectStructure"]
|
||||
.iter()
|
||||
.any(|k| key == *k || key.starts_with(&format!("{k}.")))
|
||||
{
|
||||
.any(|k| key == *k || key.starts_with(&format!("{k}.")));
|
||||
// Code style only reaches servers as settings (e.g. jdtls' formatter profile); builds don't depend on it.
|
||||
if code_style || toolchain {
|
||||
for project in affected() {
|
||||
if let Some(bridge) = &project.kotlin_bridge {
|
||||
if toolchain && let Some(bridge) = &project.kotlin_bridge {
|
||||
bridge.schedule();
|
||||
}
|
||||
let lsp = project.lsp.clone();
|
||||
|
||||
@@ -50,10 +50,11 @@ fn detected_and_saved(
|
||||
suppressed: &[String],
|
||||
) -> Vec<RunConfig> {
|
||||
let excluded_poms = crate::project::unlinked_maven_poms(root, settings);
|
||||
let mut all: Vec<RunConfig> = ide_run::detect_excluding(root, &excluded_poms)
|
||||
.into_iter()
|
||||
.filter(|d| !suppressed.contains(&d.id) && !saved.iter().any(|u| u.id == d.id))
|
||||
.collect();
|
||||
let mut all: Vec<RunConfig> =
|
||||
ide_run::detect_with_profiles(root, &excluded_poms, settings.cmake.profiles.as_deref())
|
||||
.into_iter()
|
||||
.filter(|d| !suppressed.contains(&d.id) && !saved.iter().any(|u| u.id == d.id))
|
||||
.collect();
|
||||
all.extend(saved.iter().cloned());
|
||||
for config in all.iter_mut().filter(|c| c.kind != "gradle") {
|
||||
ide_sdk::apply::apply_to_run_configs(std::slice::from_mut(config), root, settings);
|
||||
|
||||
@@ -129,6 +129,8 @@ impl Host {
|
||||
let root = root.clone();
|
||||
let data_dir = self.data_dir.clone();
|
||||
let config_dir = self.config_dir.clone();
|
||||
let sink = sink.clone();
|
||||
let formatter_problem = std::sync::Mutex::new(None);
|
||||
lsp.set_config_hook(Box::new(move |cfg, server_root| {
|
||||
let member = ide_workspace::workspace::Workspace::load(&root).ok().flatten().and_then(|w| {
|
||||
w.projects.into_iter().filter(|p| server_root.starts_with(p)).max_by_key(|p| p.components().count())
|
||||
@@ -142,6 +144,27 @@ impl Host {
|
||||
ide_sdk::apply::configure_server(cfg, root, server_root, &effective);
|
||||
ide_sdk::java::cap_java_home(cfg, &ide_sdk::java::installed_jdks());
|
||||
if cfg.name == "jdtls" {
|
||||
let problem = ide_lsp::jdtls::configure_formatter(
|
||||
cfg,
|
||||
effective.editor.code_style.languages.get("java"),
|
||||
root,
|
||||
&data_dir,
|
||||
effective.language_servers.get("jdtls"),
|
||||
)
|
||||
.err()
|
||||
.map(|e| e.to_string());
|
||||
let mut previous = formatter_problem.lock().unwrap();
|
||||
if problem != *previous {
|
||||
if let Some(body) = &problem {
|
||||
sink.emit(IdeEvent::Notify {
|
||||
level: ide_core::event::NotifyLevel::Warning,
|
||||
title: "Java formatter configuration unavailable".into(),
|
||||
body: Some(body.clone()),
|
||||
});
|
||||
}
|
||||
*previous = problem;
|
||||
}
|
||||
drop(previous);
|
||||
ide_lsp::jdtls::configure_source_paths(
|
||||
cfg,
|
||||
ide_workspace::roots::unmanaged_java_source_paths(
|
||||
|
||||
@@ -7,6 +7,7 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ide-core.workspace = true
|
||||
ide-vfs.workspace = true
|
||||
dunce.workspace = true
|
||||
ide-text.workspace = true
|
||||
ide-language.workspace = true
|
||||
|
||||
@@ -17,6 +17,9 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const SERVER: &str = "jdtls";
|
||||
|
||||
mod formatter;
|
||||
pub use formatter::configure_formatter;
|
||||
/// First start imports the Maven / Gradle model, which can take minutes.
|
||||
const READY_TIMEOUT: Duration = Duration::from_secs(240);
|
||||
/// A workspace build of a large multi-module project can take far longer than
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
use ide_core::IdeResult;
|
||||
use ide_core::editor_settings::LanguageCodeStyle;
|
||||
use ide_core::settings::LanguageServerSettings;
|
||||
use ide_language::LanguageServerConfig;
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
|
||||
const PROFILE: &str = "OpenIDEA IntelliJ IDEA";
|
||||
const XML: &str = include_str!("intellij.xml");
|
||||
|
||||
/// Protocol settings only: switching profiles reaches a running jdtls through
|
||||
/// didChangeConfiguration. Explicit language-server settings are applied later.
|
||||
pub fn configure_formatter(
|
||||
cfg: &mut LanguageServerConfig,
|
||||
style: Option<&LanguageCodeStyle>,
|
||||
project_root: &Path,
|
||||
data_dir: &Path,
|
||||
overrides: Option<&LanguageServerSettings>,
|
||||
) -> IdeResult<()> {
|
||||
if cfg.name != "jdtls" {
|
||||
return Ok(());
|
||||
}
|
||||
// jdtls retains omitted preferences. Send empty values when returning to
|
||||
// Default, while preserving any profile from the base server configuration.
|
||||
let inherited = crate::handler::all_settings(cfg);
|
||||
let defaults = json!({
|
||||
"url": inherited.pointer("/java/format/settings/url").cloned().unwrap_or(json!("")),
|
||||
"profile": inherited.pointer("/java/format/settings/profile").cloned().unwrap_or(json!("")),
|
||||
});
|
||||
let mut settings = cfg.settings.take().unwrap_or_else(|| json!({}));
|
||||
settings["java"]["format"]["settings"] = formatter_overrides(defaults, overrides);
|
||||
cfg.settings = Some(settings);
|
||||
let Some(style) = style else { return Ok(()) };
|
||||
let (path, profile) = match style.formatter.as_deref() {
|
||||
Some("intellij") => {
|
||||
let path = data_dir.join("lsp/jdtls/intellij.xml");
|
||||
std::fs::create_dir_all(path.parent().unwrap())?;
|
||||
if std::fs::read_to_string(&path).ok().as_deref() != Some(XML) {
|
||||
ide_vfs::write_atomic(&path, XML.as_bytes())?;
|
||||
}
|
||||
(path, PROFILE)
|
||||
}
|
||||
Some("custom") => {
|
||||
let value = style.formatter_path.as_deref().unwrap_or_default().trim();
|
||||
if value.is_empty() {
|
||||
return Err(ide_core::IdeError::invalid("Choose an Eclipse formatter XML file."));
|
||||
}
|
||||
let path = project_root.join(value);
|
||||
// Fail without poisoning the server launch; code intelligence remains available.
|
||||
std::fs::File::open(&path).map_err(|error| {
|
||||
let path = path.display();
|
||||
ide_core::IdeError::invalid(format!("Cannot read Java formatter XML {path}: {error}"))
|
||||
})?;
|
||||
if !path.is_file() {
|
||||
return Err(ide_core::IdeError::invalid(format!("Not a formatter XML file: {}", path.display())));
|
||||
}
|
||||
(path, "")
|
||||
}
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let mut settings = cfg.settings.take().unwrap_or_else(|| json!({}));
|
||||
settings["java"]["format"]["settings"] = formatter_overrides(
|
||||
json!({
|
||||
"url": crate::uri::path_to_uri_string(&path),
|
||||
"profile": profile,
|
||||
}),
|
||||
overrides,
|
||||
);
|
||||
cfg.settings = Some(settings);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn formatter_overrides(mut format: serde_json::Value, overrides: Option<&LanguageServerSettings>) -> serde_json::Value {
|
||||
if let Some(overrides) = overrides {
|
||||
// all_settings merges protocol settings over initializationOptions.
|
||||
// Mirror explicit formatter keys so generated protocol values cannot
|
||||
// accidentally mask an advanced initializationOptions override.
|
||||
for layer in [
|
||||
overrides.initialization_options.as_ref().and_then(|v| v.pointer("/settings/java/format/settings")),
|
||||
overrides.settings.as_ref().and_then(|v| v.pointer("/settings/java/format/settings")),
|
||||
overrides.settings.as_ref().and_then(|v| v.pointer("/java/format/settings")),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
for key in ["url", "profile"] {
|
||||
if let Some(value) = layer.get(key) {
|
||||
format[key] = value.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
format
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn config(name: &str) -> LanguageServerConfig {
|
||||
serde_json::from_value(json!({"name": name, "command": name})).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profiles_resolve_paths_preserve_settings_and_leave_other_servers_alone() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let root = dir.path().join("project with 空格");
|
||||
let data = dir.path().join("data");
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let custom = root.join("style.xml");
|
||||
std::fs::write(&custom, XML).unwrap();
|
||||
for name in ["jdtls", "rust-analyzer"] {
|
||||
for mode in ["default", "intellij", "custom", "unknown"] {
|
||||
let mut cfg = LanguageServerConfig {
|
||||
name: name.into(),
|
||||
settings: Some(json!({"java": {"autobuild": {"enabled": false}}})),
|
||||
..config(name)
|
||||
};
|
||||
let initial = cfg.clone();
|
||||
let style = LanguageCodeStyle {
|
||||
formatter: Some(mode.into()),
|
||||
formatter_path: Some("style.xml".into()),
|
||||
..Default::default()
|
||||
};
|
||||
configure_formatter(&mut cfg, Some(&style), &root, &data, None).unwrap();
|
||||
if name != "jdtls" {
|
||||
assert_eq!(cfg, initial);
|
||||
continue;
|
||||
}
|
||||
let settings = crate::handler::all_settings(&cfg);
|
||||
if !["intellij", "custom"].contains(&mode) {
|
||||
assert_eq!(settings["java"]["format"]["settings"], json!({"url": "", "profile": ""}));
|
||||
continue;
|
||||
}
|
||||
let path = if mode == "custom" { custom.clone() } else { data.join("lsp/jdtls/intellij.xml") };
|
||||
assert_eq!(settings["java"]["format"]["settings"]["url"], crate::uri::path_to_uri_string(&path));
|
||||
assert_eq!(
|
||||
settings["java"]["format"]["settings"]["profile"],
|
||||
if mode == "custom" { "" } else { PROFILE }
|
||||
);
|
||||
assert_eq!(settings["java"]["autobuild"]["enabled"], false);
|
||||
assert_eq!(std::fs::read_to_string(path).unwrap(), XML);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_custom_paths_are_nonfatal_and_do_not_set_launch_error() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
for path in ["", "missing.xml", "."] {
|
||||
let mut cfg = config("jdtls");
|
||||
let style = LanguageCodeStyle {
|
||||
formatter: Some("custom".into()),
|
||||
formatter_path: Some(path.into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(configure_formatter(&mut cfg, Some(&style), dir.path(), dir.path(), None).is_err());
|
||||
assert!(cfg.launch_error.is_none());
|
||||
assert_eq!(crate::handler::all_settings(&cfg)["java"]["format"]["settings"]["url"], "");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_keeps_explicit_server_configuration_and_custom_accepts_absolute_paths() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("custom.xml");
|
||||
std::fs::write(&path, XML).unwrap();
|
||||
let mut cfg = config("jdtls");
|
||||
cfg.initialization_options = Some(
|
||||
toml::Value::try_from(json!({
|
||||
"settings": {"java": {"format": {"settings": {"url": "existing.xml", "profile": "Existing"}}}}
|
||||
}))
|
||||
.unwrap(),
|
||||
);
|
||||
configure_formatter(&mut cfg, None, dir.path(), dir.path(), None).unwrap();
|
||||
assert_eq!(
|
||||
crate::handler::all_settings(&cfg)["java"]["format"]["settings"],
|
||||
json!({"url": "existing.xml", "profile": "Existing"})
|
||||
);
|
||||
let style = LanguageCodeStyle {
|
||||
formatter: Some("custom".into()),
|
||||
formatter_path: Some(path.to_string_lossy().into_owned()),
|
||||
..Default::default()
|
||||
};
|
||||
configure_formatter(&mut cfg, Some(&style), &dir.path().join("different-root"), dir.path(), None).unwrap();
|
||||
assert_eq!(
|
||||
crate::handler::all_settings(&cfg)["java"]["format"]["settings"]["url"],
|
||||
crate::uri::path_to_uri_string(&path)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn advanced_formatter_options_win_for_each_profile_and_protocol_layer() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("custom.xml"), XML).unwrap();
|
||||
for mode in ["default", "intellij", "custom"] {
|
||||
for layer in ["initialization", "wrapped", "direct"] {
|
||||
let mut overrides = LanguageServerSettings {
|
||||
initialization_options: Some(json!({"settings": {"java": {"format": {"settings": {
|
||||
"url": "advanced.xml", "profile": "Advanced"
|
||||
}}}}})),
|
||||
..Default::default()
|
||||
};
|
||||
if layer != "initialization" {
|
||||
let value = json!({"java": {"format": {"settings": {"profile": "Protocol"}}}});
|
||||
overrides.settings = Some(if layer == "wrapped" { json!({"settings": value}) } else { value });
|
||||
}
|
||||
let mut cfg = config("jdtls");
|
||||
let style = LanguageCodeStyle {
|
||||
formatter: Some(mode.into()),
|
||||
formatter_path: Some("custom.xml".into()),
|
||||
..Default::default()
|
||||
};
|
||||
configure_formatter(&mut cfg, Some(&style), dir.path(), dir.path(), Some(&overrides)).unwrap();
|
||||
assert_eq!(
|
||||
crate::handler::all_settings(&cfg)["java"]["format"]["settings"],
|
||||
json!({
|
||||
"url": "advanced.xml",
|
||||
"profile": if layer == "initialization" { "Advanced" } else { "Protocol" },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
IntelliJ IDEA Java defaults, approximated using Eclipse JDT formatter options.
|
||||
Source revision: JetBrains/intellij-community 8fe3709d185cd13053d17b76d9690aef674d4268
|
||||
platform/code-style-api/src/com/intellij/psi/codeStyle/CommonCodeStyleSettings.java
|
||||
platform/code-style-api/src/com/intellij/psi/codeStyle/CodeStyleSettings.java
|
||||
platform/code-style-api/src/com/intellij/psi/codeStyle/CodeStyleDefaults.java
|
||||
java/java-frontback-impl/src/com/intellij/psi/codeStyle/JavaCodeStyleSettings.java
|
||||
JavaLanguageCodeStyleSettingsProvider.customizeDefaults does not override these values.
|
||||
https://www.jetbrains.com/help/idea/code-style-java.html describes the corresponding controls.
|
||||
Each option below names its IDEA setting; JDT has no exact equivalent for every rule.
|
||||
A formatter profile does not sort/group imports (Organize Imports is a separate action).
|
||||
Preserve IDEA KEEP_LINE_BREAKS=true using Wrap where necessary (WRAP_COMPACT) and
|
||||
join_wrapped_lines=false (Never join already wrapped lines). JDT NO_SPLIT joins some
|
||||
manual parameter/call/chain breaks even when joining is disabled. Preserving manual
|
||||
breaks takes priority: wrappable code exceeding 120 columns will also wrap, a known
|
||||
difference from IDEA defaults (DO_NOT_WRAP / WRAP_LONG_LINES=false).
|
||||
Alignment values: 16 = WRAP_COMPACT + INDENT_DEFAULT; 18 = WRAP_COMPACT + INDENT_ON_COLUMN.
|
||||
The comment width 999999 approximates WRAP_COMMENTS=false without JDT integer overflow.
|
||||
JDT has a shared blank-line preservation limit and different empty-body/Javadoc handling.
|
||||
-->
|
||||
<profiles version="23">
|
||||
<profile kind="CodeFormatterProfile" name="OpenIDEA IntelliJ IDEA" version="23">
|
||||
<!-- IndentOptions.USE_TAB_CHARACTER = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
|
||||
<!-- IndentOptions.TAB_SIZE = 4 -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
|
||||
<!-- IndentOptions.INDENT_SIZE = 4 -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
|
||||
<!-- IndentOptions.CONTINUATION_INDENT_SIZE = 8 (two 4-space units) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
|
||||
<!-- IndentOptions.CONTINUATION_INDENT_SIZE = 8 -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
|
||||
<!-- CodeStyleSettings.RIGHT_MARGIN = 120; wrap where necessary to preserve manual line breaks (see known difference above) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="120"/>
|
||||
<!-- KEEP_LINE_BREAKS = true; Never join already wrapped lines -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="false"/>
|
||||
<!-- KEEP_BLANK_LINES_IN_DECLARATIONS / IN_CODE / BEFORE_RBRACE = 2 (JDT has one shared limit) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="2"/>
|
||||
<!-- METHOD_PARAMETERS_WRAP = DO_NOT_WRAP; ALIGN_MULTILINE_PARAMETERS = true (indent on column); JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="18"/>
|
||||
<!-- METHOD_PARAMETERS_WRAP = DO_NOT_WRAP; ALIGN_MULTILINE_PARAMETERS = true (indent on column); JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="18"/>
|
||||
<!-- CALL_PARAMETERS_WRAP / ANNOTATION_PARAMETER_WRAP = DO_NOT_WRAP; ALIGN_MULTILINE_PARAMETERS_IN_CALLS = false; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
|
||||
<!-- CALL_PARAMETERS_WRAP / ANNOTATION_PARAMETER_WRAP = DO_NOT_WRAP; ALIGN_MULTILINE_PARAMETERS_IN_CALLS = false; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
|
||||
<!-- CALL_PARAMETERS_WRAP / ANNOTATION_PARAMETER_WRAP = DO_NOT_WRAP; ALIGN_MULTILINE_PARAMETERS_IN_CALLS = false; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
|
||||
<!-- CALL_PARAMETERS_WRAP / ANNOTATION_PARAMETER_WRAP = DO_NOT_WRAP; ALIGN_MULTILINE_PARAMETERS_IN_CALLS = false; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
|
||||
<!-- CALL_PARAMETERS_WRAP / ANNOTATION_PARAMETER_WRAP = DO_NOT_WRAP; ALIGN_MULTILINE_PARAMETERS_IN_CALLS = false; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="16"/>
|
||||
<!-- METHOD_CALL_CHAIN_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
|
||||
<!-- ARRAY_INITIALIZER_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
|
||||
<!-- ASSIGNMENT_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="16"/>
|
||||
<!-- TERNARY_OPERATION_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="16"/>
|
||||
<!-- EXTENDS_LIST_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
|
||||
<!-- EXTENDS_LIST_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
|
||||
<!-- THROWS_LIST_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
|
||||
<!-- THROWS_LIST_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
|
||||
<!-- RESOURCE_LIST_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="16"/>
|
||||
<!-- BINARY_OPERATION_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator" value="16"/>
|
||||
<!-- BINARY_OPERATION_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_additive_operator" value="16"/>
|
||||
<!-- BINARY_OPERATION_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_string_concatenation" value="16"/>
|
||||
<!-- BINARY_OPERATION_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_shift_operator" value="16"/>
|
||||
<!-- BINARY_OPERATION_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_relational_operator" value="16"/>
|
||||
<!-- BINARY_OPERATION_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator" value="16"/>
|
||||
<!-- BINARY_OPERATION_WRAP = DO_NOT_WRAP; JDT wrap where necessary preserves manual breaks -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_logical_operator" value="16"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_lambda_body" value="end_of_line"/>
|
||||
<!-- CLASS_BRACE_STYLE / METHOD_BRACE_STYLE / LAMBDA_BRACE_STYLE / BRACE_STYLE = END_OF_LINE; ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
|
||||
<!-- ELSE_ON_NEW_LINE / CATCH_ON_NEW_LINE / FINALLY_ON_NEW_LINE / WHILE_ON_NEW_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
|
||||
<!-- ELSE_ON_NEW_LINE / CATCH_ON_NEW_LINE / FINALLY_ON_NEW_LINE / WHILE_ON_NEW_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
|
||||
<!-- ELSE_ON_NEW_LINE / CATCH_ON_NEW_LINE / FINALLY_ON_NEW_LINE / WHILE_ON_NEW_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
|
||||
<!-- ELSE_ON_NEW_LINE / CATCH_ON_NEW_LINE / FINALLY_ON_NEW_LINE / WHILE_ON_NEW_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
|
||||
<!-- CLASS / METHOD / FIELD_ANNOTATION_WRAP = WRAP_ALWAYS; DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
|
||||
<!-- CLASS / METHOD / FIELD_ANNOTATION_WRAP = WRAP_ALWAYS; DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
|
||||
<!-- CLASS / METHOD / FIELD_ANNOTATION_WRAP = WRAP_ALWAYS; DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/>
|
||||
<!-- PARAMETER / VARIABLE / ENUM_FIELD_ANNOTATION_WRAP = DO_NOT_WRAP -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
|
||||
<!-- PARAMETER / VARIABLE / ENUM_FIELD_ANNOTATION_WRAP = DO_NOT_WRAP -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="do not insert"/>
|
||||
<!-- PARAMETER / VARIABLE / ENUM_FIELD_ANNOTATION_WRAP = DO_NOT_WRAP -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant" value="do not insert"/>
|
||||
<!-- BLANK_LINES_BEFORE_PACKAGE = 0 (minimum) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
|
||||
<!-- BLANK_LINES_AFTER_PACKAGE = 1 (minimum) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
|
||||
<!-- BLANK_LINES_BEFORE_IMPORTS = 1 (minimum) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
|
||||
<!-- BLANK_LINES_AFTER_IMPORTS = 1 (minimum) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
|
||||
<!-- BLANK_LINES_AROUND_FIELD / BLANK_LINES_AROUND_FIELD_IN_INTERFACE = 0 (minimum) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
|
||||
<!-- BLANK_LINES_AROUND_METHOD = 1 (minimum) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
|
||||
<!-- BLANK_LINES_AROUND_METHOD_IN_INTERFACE = 1 (minimum) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method" value="1"/>
|
||||
<!-- BLANK_LINES_AROUND_CLASS = 1 (minimum) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
|
||||
<!-- BLANK_LINES_AFTER_CLASS_HEADER = 0 (minimum) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
|
||||
<!-- BLANK_LINES_BEFORE_CLASS_END = 0 (minimum) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration" value="0"/>
|
||||
<!-- BLANK_LINES_AROUND_METHOD / AROUND_CLASS = 1; JDT chunks separate member kinds, not consecutive fields -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
|
||||
<!-- DO_NOT_INDENT_TOP_LEVEL_CLASS_MEMBERS = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
|
||||
<!-- DO_NOT_INDENT_TOP_LEVEL_CLASS_MEMBERS = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
|
||||
<!-- DO_NOT_INDENT_TOP_LEVEL_CLASS_MEMBERS = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
|
||||
<!-- DO_NOT_INDENT_TOP_LEVEL_CLASS_MEMBERS = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
|
||||
<!-- INDENT_CASE_FROM_SWITCH = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
|
||||
<!-- INDENT_BREAK_FROM_CASE = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / LAMBDAS / CLASSES_IN_ONE_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_code_block_on_one_line" value="one_line_never"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / LAMBDAS / CLASSES_IN_ONE_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_method_body_on_one_line" value="one_line_never"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / LAMBDAS / CLASSES_IN_ONE_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line" value="one_line_never"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / LAMBDAS / CLASSES_IN_ONE_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line" value="one_line_never"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / LAMBDAS / CLASSES_IN_ONE_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line" value="one_line_never"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / LAMBDAS / CLASSES_IN_ONE_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line" value="one_line_never"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / LAMBDAS / CLASSES_IN_ONE_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line" value="one_line_never"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / LAMBDAS / CLASSES_IN_ONE_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line" value="one_line_never"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / LAMBDAS / CLASSES_IN_ONE_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line" value="one_line_never"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / CLASSES_IN_ONE_LINE = false; empty-block handling may differ in JDT -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / CLASSES_IN_ONE_LINE = false; empty-block handling may differ in JDT -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / CLASSES_IN_ONE_LINE = false; empty-block handling may differ in JDT -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
|
||||
<!-- KEEP_SIMPLE_BLOCKS / METHODS / CLASSES_IN_ONE_LINE = false; empty-block handling may differ in JDT -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
|
||||
<!-- ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE / RBRACE_ON_NEXT_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="true"/>
|
||||
<!-- ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE = false; KEEP_LINE_BREAKS = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
|
||||
<!-- ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE = false; KEEP_LINE_BREAKS = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
|
||||
<!-- SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE / SPACE_WITHIN_ARRAY_INITIALIZER_BRACES / SPACE_WITHIN_EMPTY_ARRAY_INITIALIZER_BRACES = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="do not insert"/>
|
||||
<!-- SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE / SPACE_WITHIN_ARRAY_INITIALIZER_BRACES / SPACE_WITHIN_EMPTY_ARRAY_INITIALIZER_BRACES = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="do not insert"/>
|
||||
<!-- SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE / SPACE_WITHIN_ARRAY_INITIALIZER_BRACES / SPACE_WITHIN_EMPTY_ARRAY_INITIALIZER_BRACES = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="do not insert"/>
|
||||
<!-- SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE / SPACE_WITHIN_ARRAY_INITIALIZER_BRACES / SPACE_WITHIN_EMPTY_ARRAY_INITIALIZER_BRACES = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
|
||||
<!-- SPACE_AROUND_LAMBDA_ARROW = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/>
|
||||
<!-- SPACE_AROUND_LAMBDA_ARROW = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow" value="insert"/>
|
||||
<!-- WRAP_COMMENTS = false; KEEP_FIRST_COLUMN_COMMENT = true (JDT comment formatting disabled to preserve text) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="false"/>
|
||||
<!-- WRAP_COMMENTS = false (JDT comment formatting disabled to preserve text) -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="false"/>
|
||||
<!-- JavaCodeStyleSettings.ENABLE_JAVADOC_FORMATTING = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
|
||||
<!-- WRAP_COMMENTS = false; JDT has a separate comment width, so disable its width-based wrapping -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="999999"/>
|
||||
<!-- JavaCodeStyleSettings.JD_INDENT_ON_CONTINUATION = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="false"/>
|
||||
<!-- JavaCodeStyleSettings.JD_PARAM_DESCRIPTION_ON_NEW_LINE = false -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="false"/>
|
||||
<!-- CodeStyleSettings.FORMATTER_TAGS_ENABLED = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/>
|
||||
<!-- KEEP_CONTROL_STATEMENT_IN_ONE_LINE = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="true"/>
|
||||
<!-- KEEP_CONTROL_STATEMENT_IN_ONE_LINE = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="true"/>
|
||||
<!-- KEEP_CONTROL_STATEMENT_IN_ONE_LINE = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="true"/>
|
||||
<!-- KEEP_CONTROL_STATEMENT_IN_ONE_LINE = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line" value="true"/>
|
||||
<!-- KEEP_CONTROL_STATEMENT_IN_ONE_LINE = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line" value="true"/>
|
||||
<!-- KEEP_CONTROL_STATEMENT_IN_ONE_LINE = true -->
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line" value="true"/>
|
||||
</profile>
|
||||
</profiles>
|
||||
@@ -0,0 +1,25 @@
|
||||
package demo;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
@Deprecated @SuppressWarnings("unused") public class FormatterSample {
|
||||
@Deprecated @SuppressWarnings("unused") private String first="first";
|
||||
private String second="second";
|
||||
/** A description which remains on a very long single line to check whether formatting introduces wrapping when the right margin is exceeded with ordinary words in a JavaDoc comment. */
|
||||
@Deprecated @SuppressWarnings("unused") public String combine(@Deprecated @SuppressWarnings("unused") String firstParameterWithAVeryLongName, String secondParameterWithAVeryLongName, String thirdParameterWithAVeryLongName){if(firstParameterWithAVeryLongName==null){return "none";}else{try{return List.of(firstParameterWithAVeryLongName,secondParameterWithAVeryLongName,thirdParameterWithAVeryLongName).stream().map(value->value.trim()).filter(value->!value.isEmpty()).findFirst().orElse("none");}catch(RuntimeException problem){throw problem;}finally{System.out.println("done");}}}
|
||||
public void multiline(String firstParameter,
|
||||
String secondParameter,
|
||||
String thirdParameter){
|
||||
combine(firstParameter,
|
||||
secondParameter,
|
||||
thirdParameter);
|
||||
List.of(firstParameter, secondParameter)
|
||||
.stream()
|
||||
.map(value->value.trim())
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
public void retained(String firstParameter, // retain declaration break
|
||||
String secondParameter){combine(firstParameter, // retain call break
|
||||
secondParameter, "third");}
|
||||
public void arrays(){int[] values=new int[]{1,2,3};int[] empty=new int[]{};Function<String,String> clean=value->{return value.trim();};Runnable run=()->{System.out.println(values.length);};}
|
||||
// A comment which intentionally exceeds the right margin and should remain unwrapped with the built-in profile even when it contains many ordinary words and spaces to split on.
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package demo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Deprecated
|
||||
@SuppressWarnings("unused")
|
||||
public class FormatterSample {
|
||||
@Deprecated
|
||||
@SuppressWarnings("unused")
|
||||
private String first = "first";
|
||||
private String second = "second";
|
||||
|
||||
/** A description which remains on a very long single line to check whether formatting introduces wrapping when the right margin is exceeded with ordinary words in a JavaDoc comment. */
|
||||
@Deprecated
|
||||
@SuppressWarnings("unused")
|
||||
public String combine(@Deprecated @SuppressWarnings("unused") String firstParameterWithAVeryLongName,
|
||||
String secondParameterWithAVeryLongName, String thirdParameterWithAVeryLongName) {
|
||||
if (firstParameterWithAVeryLongName == null) {
|
||||
return "none";
|
||||
} else {
|
||||
try {
|
||||
return List
|
||||
.of(firstParameterWithAVeryLongName, secondParameterWithAVeryLongName,
|
||||
thirdParameterWithAVeryLongName)
|
||||
.stream().map(value -> value.trim()).filter(value -> !value.isEmpty()).findFirst()
|
||||
.orElse("none");
|
||||
} catch (RuntimeException problem) {
|
||||
throw problem;
|
||||
} finally {
|
||||
System.out.println("done");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void multiline(String firstParameter,
|
||||
String secondParameter,
|
||||
String thirdParameter) {
|
||||
combine(firstParameter,
|
||||
secondParameter,
|
||||
thirdParameter);
|
||||
List.of(firstParameter, secondParameter)
|
||||
.stream()
|
||||
.map(value -> value.trim())
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
|
||||
public void retained(String firstParameter, // retain declaration break
|
||||
String secondParameter) {
|
||||
combine(firstParameter, // retain call break
|
||||
secondParameter, "third");
|
||||
}
|
||||
|
||||
public void arrays() {
|
||||
int[] values = new int[]{1, 2, 3};
|
||||
int[] empty = new int[]{};
|
||||
Function<String, String> clean = value -> {
|
||||
return value.trim();
|
||||
};
|
||||
Runnable run = () -> {
|
||||
System.out.println(values.length);
|
||||
};
|
||||
}
|
||||
// A comment which intentionally exceeds the right margin and should remain unwrapped with the built-in profile even when it contains many ordinary words and spaces to split on.
|
||||
}
|
||||
@@ -1017,6 +1017,70 @@ async fn settings_changes_reach_servers_without_restarts() {
|
||||
assert_eq!(s.launches.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn java_formatter_profiles_reach_initialization_and_live_configuration_without_restart() {
|
||||
let h = harness_configured("java", "java", &[("jdtls", incremental_caps())], |cfg| {
|
||||
cfg.initialization_options = Some(
|
||||
toml::Value::try_from(json!({
|
||||
"settings": {"java": {"autobuild": {"enabled": false}}}
|
||||
}))
|
||||
.unwrap(),
|
||||
);
|
||||
});
|
||||
let data = tempfile::tempdir().unwrap();
|
||||
std::fs::write(h.root.join("custom.xml"), "<profiles/>").unwrap();
|
||||
let style = Arc::new(Mutex::new(ide_core::editor_settings::LanguageCodeStyle {
|
||||
formatter: Some("intellij".into()),
|
||||
formatter_path: Some("custom.xml".into()),
|
||||
..Default::default()
|
||||
}));
|
||||
h.mgr.set_config_hook(Box::new({
|
||||
let style = style.clone();
|
||||
let root = h.root.clone();
|
||||
let data = data.path().to_path_buf();
|
||||
move |cfg, _| ide_lsp::jdtls::configure_formatter(cfg, Some(&style.lock()), &root, &data, None).unwrap()
|
||||
}));
|
||||
h.open("Main.java", "public class Main {}");
|
||||
h.wait_status("jdtls", ServerStatus::Ready).await;
|
||||
let state = h.launcher.state("jdtls");
|
||||
let init = state.requested("initialize")[0].clone();
|
||||
assert_eq!(
|
||||
init["initializationOptions"]["settings"]["java"]["format"]["settings"]["profile"],
|
||||
"OpenIDEA IntelliJ IDEA"
|
||||
);
|
||||
for mode in ["custom", "default", "intellij", "default"] {
|
||||
style.lock().formatter = Some(mode.into());
|
||||
h.mgr.reconfigure().await;
|
||||
let expected = match mode {
|
||||
"custom" => uri_of(&h.root.join("custom.xml")),
|
||||
"intellij" => uri_of(&data.path().join("lsp/jdtls/intellij.xml")),
|
||||
_ => String::new(),
|
||||
};
|
||||
eventually(|| {
|
||||
state
|
||||
.notified("workspace/didChangeConfiguration")
|
||||
.last()
|
||||
.is_some_and(|p| p["settings"]["java"]["format"]["settings"]["url"] == expected)
|
||||
})
|
||||
.await;
|
||||
let result: Value = state
|
||||
.peer()
|
||||
.request(
|
||||
"workspace/configuration",
|
||||
json!({
|
||||
"items": [{"section": "java.format.settings"}, {"section": "java.autobuild.enabled"}]
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result[0]["url"], expected);
|
||||
assert_eq!(result[0]["profile"], if mode == "intellij" { "OpenIDEA IntelliJ IDEA" } else { "" });
|
||||
assert_eq!(result[1], false);
|
||||
assert_eq!(state.launches.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn java_source_paths_reach_initialization_live_configuration_and_builds() {
|
||||
let h = harness_configured("java", "java", &[("jdtls", incremental_caps())], |cfg| {
|
||||
|
||||
@@ -62,6 +62,53 @@ fn lang(id: &str, ext: &str, server: LanguageServerConfig) -> LanguageConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore = "requires jdtls on PATH and a compatible JDK"]
|
||||
async fn intellij_java_formatter_preserves_manual_breaks_and_wraps_long_lines() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let root = dir.path().canonicalize().unwrap();
|
||||
let source = include_str!("fixtures/FormatterSample.input.java");
|
||||
let expected = include_str!("fixtures/FormatterSample.intellij.java");
|
||||
let path = root.join("src/demo/FormatterSample.java");
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&path, source).unwrap();
|
||||
let mut java = LanguageRegistry::load(None).unwrap().get(&LanguageId::new("java")).unwrap();
|
||||
let server = &mut java.language_servers[0];
|
||||
if let Ok(bin) = std::env::var("OPENIDEA_TEST_JDTLS") {
|
||||
server.command = bin;
|
||||
}
|
||||
server.args.extend(["-data".into(), root.join("jdtls-workspace").to_string_lossy().into_owned()]);
|
||||
server.settings = Some(serde_json::json!({"java": {"autobuild": {"enabled": false}}}));
|
||||
let (mgr, events) = manager(&root, LanguageRegistry::from_configs(vec![java]));
|
||||
mgr.set_config_hook(Box::new({
|
||||
let root = root.clone();
|
||||
move |cfg, _| {
|
||||
ide_lsp::jdtls::configure_source_paths(cfg, Some(vec!["src".into()]));
|
||||
let style = ide_core::editor_settings::LanguageCodeStyle {
|
||||
formatter: Some("intellij".into()),
|
||||
..Default::default()
|
||||
};
|
||||
ide_lsp::jdtls::configure_formatter(cfg, Some(&style), &root, &root.join("data"), None).unwrap();
|
||||
}
|
||||
}));
|
||||
let doc = DocId::next();
|
||||
mgr.did_open(doc, &path, &LanguageId::new("java"), 1, source);
|
||||
retry("jdtls ready", Duration::from_secs(120), || {
|
||||
let status = last_status(&events, "jdtls");
|
||||
if let Some((ServerStatus::Crashed | ServerStatus::Missing | ServerStatus::Misconfigured, msg)) = &status {
|
||||
panic!("jdtls failed: {msg:?}");
|
||||
}
|
||||
async move { matches!(status, Some((ServerStatus::Ready | ServerStatus::Busy, _))).then_some(()) }
|
||||
})
|
||||
.await;
|
||||
let edits =
|
||||
mgr.format(doc, None, ide_lsp::types::FormatOptions { tab_size: 4, insert_spaces: true, on_save: false }).await;
|
||||
tokio::time::timeout(Duration::from_secs(10), mgr.shutdown_all()).await.unwrap();
|
||||
let mut document = ide_text::Document::new(source);
|
||||
document.apply_edits(&edits.unwrap()).unwrap();
|
||||
assert_eq!(document.text(), expected);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn missing_binary_is_reported_with_install_hint() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -16,6 +16,14 @@ pub(crate) fn detect(root: &Path) -> Vec<RunConfig> {
|
||||
}
|
||||
|
||||
pub(crate) fn detect_excluding(root: &Path, excluded_poms: &[PathBuf]) -> Vec<RunConfig> {
|
||||
detect_with_profiles(root, excluded_poms, None)
|
||||
}
|
||||
|
||||
pub(crate) fn detect_with_profiles(
|
||||
root: &Path,
|
||||
excluded_poms: &[PathBuf],
|
||||
profiles: Option<&[ide_core::settings::CMakeProfile]>,
|
||||
) -> Vec<RunConfig> {
|
||||
let mut out = Vec::new();
|
||||
detect_node(root, &mut out);
|
||||
detect_deno(root, &mut out);
|
||||
@@ -24,7 +32,7 @@ pub(crate) fn detect_excluding(root: &Path, excluded_poms: &[PathBuf]) -> Vec<Ru
|
||||
detect_maven(root, excluded_poms, &mut out);
|
||||
detect_go(root, &mut out);
|
||||
detect_python(root, &mut out);
|
||||
cmake::detect(root, &mut out);
|
||||
cmake::detect(root, profiles, &mut out);
|
||||
detect_make(root, &mut out);
|
||||
detect_just(root, &mut out);
|
||||
detect_godot(root, &mut out);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use super::{ConfigExt, config};
|
||||
use crate::{BeforeLaunchTask, RunConfig};
|
||||
use ide_core::settings::CMakeProfile;
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -19,10 +20,16 @@ fn before(config: &mut RunConfig, id: &str) {
|
||||
config.extras.before_launch.push(BeforeLaunchTask { kind: "run".into(), target: id.into() });
|
||||
}
|
||||
|
||||
pub(super) fn detect(root: &Path, out: &mut Vec<RunConfig>) {
|
||||
pub(super) fn detect(root: &Path, profiles: Option<&[CMakeProfile]>, out: &mut Vec<RunConfig>) {
|
||||
if !root.join("CMakeLists.txt").is_file() {
|
||||
return;
|
||||
}
|
||||
if let Some(profiles) = profiles {
|
||||
for (index, profile) in profiles.iter().enumerate() {
|
||||
detect_profile(root, profile, index, profiles.len() > 1, out);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut builds = Vec::new();
|
||||
if let Ok(entries) = fs::read_dir(root) {
|
||||
builds.extend(entries.flatten().filter_map(|e| {
|
||||
@@ -79,11 +86,69 @@ pub(super) fn detect(root: &Path, out: &mut Vec<RunConfig>) {
|
||||
);
|
||||
before(&mut test, &build_id);
|
||||
out.push(test);
|
||||
targets(root, &build, &dir, &prefix, out);
|
||||
targets(root, &build, &dir, &prefix, None, out);
|
||||
}
|
||||
}
|
||||
|
||||
fn targets(root: &Path, build: &Path, dir: &str, prefix: &str, out: &mut Vec<RunConfig>) {
|
||||
fn detect_profile(root: &Path, profile: &CMakeProfile, index: usize, multiple: bool, out: &mut Vec<RunConfig>) {
|
||||
let dir = if profile.build_dir.trim().is_empty() {
|
||||
format!("cmake-build-{}", profile.build_type.to_lowercase())
|
||||
} else {
|
||||
profile.build_dir.clone()
|
||||
};
|
||||
let build = root.join(&dir);
|
||||
let prefix = format!("cmake:profile:{index}");
|
||||
let configure_id = format!("{prefix}:configure");
|
||||
let build_id = format!("{prefix}:build");
|
||||
let start = out.len();
|
||||
let mut configure =
|
||||
config(&configure_id, format!("CMake Configure ({dir})"), "cmake", "cmake", &["-S", ".", "-B", &dir]);
|
||||
if !profile.generator.trim().is_empty() {
|
||||
configure.args.extend(["-G".into(), profile.generator.clone()]);
|
||||
}
|
||||
configure.args.push(format!("-DCMAKE_BUILD_TYPE={}", profile.build_type));
|
||||
configure.args.push("-DCMAKE_EXPORT_COMPILE_COMMANDS=ON".into());
|
||||
configure.args.extend_from_slice(&profile.cmake_args);
|
||||
configure.extras.params.insert("buildDir".into(), dir.clone());
|
||||
out.push(configure);
|
||||
let mut all = config(
|
||||
&build_id,
|
||||
format!("CMake Build ({dir})"),
|
||||
"cmake",
|
||||
"cmake",
|
||||
&["--build", &dir, "--config", &profile.build_type],
|
||||
);
|
||||
all.args.extend_from_slice(&profile.build_args);
|
||||
if !build.join("CMakeCache.txt").is_file() {
|
||||
before(&mut all, &configure_id);
|
||||
}
|
||||
out.push(all);
|
||||
let mut test = config(
|
||||
format!("{prefix}:test"),
|
||||
format!("CTest ({dir})"),
|
||||
"cmake",
|
||||
"ctest",
|
||||
&["--test-dir", &dir, "-C", &profile.build_type, "--output-on-failure"],
|
||||
);
|
||||
before(&mut test, &build_id);
|
||||
out.push(test);
|
||||
targets(root, &build, &dir, &prefix, Some(profile), out);
|
||||
for config in &mut out[start..] {
|
||||
config.env = profile.env.clone();
|
||||
if multiple {
|
||||
config.name = format!("{} [{}]", config.name, profile.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn targets(
|
||||
root: &Path,
|
||||
build: &Path,
|
||||
dir: &str,
|
||||
prefix: &str,
|
||||
profile: Option<&CMakeProfile>,
|
||||
out: &mut Vec<RunConfig>,
|
||||
) {
|
||||
let reply = build.join(".cmake/api/v1/reply");
|
||||
let Some(index_path) = fs::read_dir(&reply).ok().and_then(|entries| {
|
||||
entries
|
||||
@@ -113,6 +178,9 @@ fn targets(root: &Path, build: &Path, dir: &str, prefix: &str, out: &mut Vec<Run
|
||||
let source_root = dunce::canonicalize(&source_root).unwrap_or(source_root);
|
||||
for configuration in configurations {
|
||||
let name = configuration["name"].as_str().unwrap_or("");
|
||||
if profile.is_some_and(|p| !name.is_empty() && name != p.build_type) {
|
||||
continue;
|
||||
}
|
||||
let Some(targets) = configuration["targets"].as_array() else { continue };
|
||||
for target in targets {
|
||||
let Some(target) = target["jsonFile"].as_str().and_then(|n| reply_file(&reply, n)) else { continue };
|
||||
@@ -130,8 +198,17 @@ fn targets(root: &Path, build: &Path, dir: &str, prefix: &str, out: &mut Vec<Run
|
||||
if !name.is_empty() {
|
||||
args.extend(["--config", name]);
|
||||
}
|
||||
let build_config =
|
||||
let mut build_config =
|
||||
config(&target_build_id, format!("Build {target_name} ({dir} {name})"), "cmake", "cmake", &args);
|
||||
if let Some(profile) = profile {
|
||||
if name.is_empty() {
|
||||
build_config.args.extend(["--config".into(), profile.build_type.clone()]);
|
||||
}
|
||||
build_config.args.extend_from_slice(&profile.build_args);
|
||||
if !build.join("CMakeCache.txt").is_file() {
|
||||
before(&mut build_config, &format!("{prefix}:configure"));
|
||||
}
|
||||
}
|
||||
out.push(build_config);
|
||||
let binary = build.join(artifact);
|
||||
let mut run =
|
||||
@@ -165,9 +242,99 @@ pub(crate) fn prepare(config: &RunConfig, cwd: &Path) -> crate::IdeResult<()> {
|
||||
&& config.args.first().is_some_and(|a| a == "-S")
|
||||
&& let Some(dir) = config.extras.params.get("buildDir")
|
||||
{
|
||||
let query = cwd.join(dir).join(".cmake/api/v1/query/client-openidea");
|
||||
let build = cwd.join(dir);
|
||||
let generator = config.args.windows(2).find(|a| a[0] == "-G").map(|a| a[1].as_str());
|
||||
if let Some(generator) = generator
|
||||
&& let Ok(cache) = fs::read_to_string(build.join("CMakeCache.txt"))
|
||||
&& let Some(cached) = cache.lines().find_map(|l| l.strip_prefix("CMAKE_GENERATOR:INTERNAL="))
|
||||
&& cached != generator
|
||||
{
|
||||
fs::remove_file(build.join("CMakeCache.txt"))?;
|
||||
let files = build.join("CMakeFiles");
|
||||
if files.is_dir() {
|
||||
fs::remove_dir_all(files)?;
|
||||
}
|
||||
}
|
||||
let query = build.join(".cmake/api/v1/query/client-openidea");
|
||||
fs::create_dir_all(&query)?;
|
||||
fs::write(query.join("codemodel-v2"), "")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn profiles_select_file_api_configuration_and_apply_target_options() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
fs::write(root.path().join("CMakeLists.txt"), "project(app)").unwrap();
|
||||
let reply = root.path().join("output/.cmake/api/v1/reply");
|
||||
fs::create_dir_all(&reply).unwrap();
|
||||
let write = |name: &str, value: Value| fs::write(reply.join(name), value.to_string()).unwrap();
|
||||
write(
|
||||
"index-1.json",
|
||||
serde_json::json!({"objects":[{"kind":"codemodel","version":{"major":2},"jsonFile":"model.json"}]}),
|
||||
);
|
||||
write(
|
||||
"model.json",
|
||||
serde_json::json!({"paths":{"source":root.path()},"configurations":[
|
||||
{"name":"Debug","targets":[{"jsonFile":"debug.json"}]},
|
||||
{"name":"Release","targets":[{"jsonFile":"release.json"}]}
|
||||
]}),
|
||||
);
|
||||
for name in ["debug", "release"] {
|
||||
write(
|
||||
&format!("{name}.json"),
|
||||
serde_json::json!({"name":"app","type":"EXECUTABLE","artifacts":[{"path":format!("{name}/app")}]}),
|
||||
);
|
||||
}
|
||||
let profile = CMakeProfile {
|
||||
name: "Production".into(),
|
||||
build_type: "Release".into(),
|
||||
build_dir: "output".into(),
|
||||
build_args: ["-j", "8", "--", "verbose=1"].map(String::from).into(),
|
||||
env: [("FOO".into(), "bar".into())].into(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut configs = Vec::new();
|
||||
detect(root.path(), Some(std::slice::from_ref(&profile)), &mut configs);
|
||||
let run = configs.iter().find(|c| c.kind == "native").unwrap();
|
||||
assert!(run.command.ends_with("release/app"));
|
||||
assert_eq!(run.env, profile.env);
|
||||
assert_eq!(configs.iter().filter(|c| c.kind == "native").count(), 1);
|
||||
let build = configs.iter().find(|c| c.id == run.extras.before_launch[0].target).unwrap();
|
||||
assert_eq!(
|
||||
build.args,
|
||||
["--build", "output", "--target", "app", "--config", "Release", "-j", "8", "--", "verbose=1"]
|
||||
);
|
||||
assert_eq!(build.env, profile.env);
|
||||
assert_eq!(build.extras.before_launch[0].target, "cmake:profile:0:configure");
|
||||
fs::write(root.path().join("output/CMakeCache.txt"), "CMAKE_GENERATOR:INTERNAL=Ninja\n").unwrap();
|
||||
let mut configured = Vec::new();
|
||||
detect(root.path(), Some(std::slice::from_ref(&profile)), &mut configured);
|
||||
for id in ["cmake:profile:0:build", build.id.as_str()] {
|
||||
let build = configured.iter().find(|c| c.id == id).unwrap();
|
||||
assert!(build.extras.before_launch.is_empty(), "{id} should use CMake's automatic regeneration");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generator_change_clears_only_cmake_state_at_launch() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let build = root.path().join("cmake-build-debug");
|
||||
fs::create_dir_all(build.join("CMakeFiles")).unwrap();
|
||||
fs::write(build.join("CMakeCache.txt"), "CMAKE_GENERATOR:INTERNAL=Unix Makefiles\n").unwrap();
|
||||
fs::write(build.join("keep.txt"), "artifact").unwrap();
|
||||
fs::write(root.path().join("CMakeLists.txt"), "project(app)").unwrap();
|
||||
let mut configs = Vec::new();
|
||||
detect(root.path(), Some(&[CMakeProfile { generator: "Ninja".into(), ..Default::default() }]), &mut configs);
|
||||
assert!(build.join("CMakeCache.txt").exists());
|
||||
prepare(&configs[0], root.path()).unwrap();
|
||||
assert!(!build.join("CMakeCache.txt").exists());
|
||||
assert!(!build.join("CMakeFiles").exists());
|
||||
assert!(build.join("keep.txt").exists());
|
||||
assert!(build.join(".cmake/api/v1/query/client-openidea/codemodel-v2").exists());
|
||||
}
|
||||
}
|
||||
@@ -201,6 +201,15 @@ 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)
|
||||
|
||||
@@ -652,3 +652,72 @@ fn go_default_build_output_is_ignored_by_version_control() {
|
||||
let ignore = fs::read_to_string(dir.path().join(".openidea/.gitignore")).unwrap();
|
||||
assert!(ignore.lines().any(|l| l == "/bin/"), "{ignore}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmake_profiles_control_commands_names_and_environment() {
|
||||
use ide_core::settings::CMakeProfile;
|
||||
let dir = project(&[("CMakeLists.txt", "project(app)")]);
|
||||
for generator in ["", "Ninja", "Unix Makefiles", "Ninja Multi-Config", "Visual Studio 17 2022"] {
|
||||
let profile = CMakeProfile {
|
||||
name: "Production".into(),
|
||||
build_type: "Release".into(),
|
||||
generator: generator.into(),
|
||||
cmake_args: ["-DFEATURE=ON", "-DMESSAGE=two words", "-DQUOTE=\"hello world\"", ""].map(String::from).into(),
|
||||
build_dir: "output dir".into(),
|
||||
build_args: ["-j", "8", "--", "VERBOSE=1"].map(String::from).into(),
|
||||
env: [("MY_ENV".into(), "yes".into())].into(),
|
||||
};
|
||||
let configs = ide_run::detect_with_profiles(dir.path(), &[], Some(std::slice::from_ref(&profile)));
|
||||
let configure = find(&configs, "cmake:profile:0:configure");
|
||||
let mut expected = vec!["-S", ".", "-B", "output dir"];
|
||||
if !generator.is_empty() {
|
||||
expected.extend(["-G", generator]);
|
||||
}
|
||||
expected.extend([
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
||||
"-DFEATURE=ON",
|
||||
"-DMESSAGE=two words",
|
||||
"-DQUOTE=\"hello world\"",
|
||||
"",
|
||||
]);
|
||||
assert_eq!(configure.args, expected);
|
||||
assert_eq!(configure.name, "CMake Configure (output dir)");
|
||||
let build = find(&configs, "cmake:profile:0:build");
|
||||
assert_eq!(build.args, ["--build", "output dir", "--config", "Release", "-j", "8", "--", "VERBOSE=1"]);
|
||||
assert_eq!(build.extras.before_launch[0].target, configure.id);
|
||||
assert!(configs.iter().all(|c| c.env == profile.env));
|
||||
let configs = ide_run::detect_with_profiles(dir.path(), &[], Some(&[CMakeProfile::default(), profile]));
|
||||
assert!(find(&configs, "cmake:profile:0:build").name.ends_with("[Debug]"));
|
||||
assert!(find(&configs, "cmake:profile:1:build").name.ends_with("[Production]"));
|
||||
assert_eq!(find(&configs, "cmake:profile:0:build").args[1], "cmake-build-debug");
|
||||
}
|
||||
assert_eq!(ide_run::detect_with_profiles(dir.path(), &[], None), detect(dir.path()));
|
||||
assert!(ide_run::detect_with_profiles(dir.path(), &[], Some(&[])).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmake_profile_build_configures_only_without_a_cache() {
|
||||
use ide_core::settings::CMakeProfile;
|
||||
let root = project(&[("CMakeLists.txt", "project(app)")]);
|
||||
for dir in ["", "custom output"] {
|
||||
let profile = CMakeProfile { build_dir: dir.into(), generator: "Ninja".into(), ..Default::default() };
|
||||
let build_dir = root.path().join(if dir.is_empty() { "cmake-build-debug" } else { dir });
|
||||
for cached in [false, true] {
|
||||
if cached {
|
||||
fs::create_dir_all(&build_dir).unwrap();
|
||||
// Generator changes are handled by an explicit Configure, not every Build.
|
||||
fs::write(build_dir.join("CMakeCache.txt"), "CMAKE_GENERATOR:INTERNAL=Unix Makefiles\n").unwrap();
|
||||
}
|
||||
let configs = ide_run::detect_with_profiles(root.path(), &[], Some(std::slice::from_ref(&profile)));
|
||||
let build = find(&configs, "cmake:profile:0:build");
|
||||
if cached {
|
||||
assert!(build.extras.before_launch.is_empty());
|
||||
} else {
|
||||
assert_eq!(build.extras.before_launch.len(), 1);
|
||||
assert_eq!(build.extras.before_launch[0].target, "cmake:profile:0:configure");
|
||||
}
|
||||
assert_eq!(find(&configs, "cmake:profile:0:test").extras.before_launch[0].target, build.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "openidea",
|
||||
"private": true,
|
||||
"description": "An open-source IDE inspired by IntelliJ IDEA",
|
||||
"version": "0.1.32",
|
||||
"version": "0.1.33",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpenIDEA",
|
||||
"version": "0.1.32",
|
||||
"version": "0.1.33",
|
||||
"identifier": "ai.l0ng.openidea",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -1639,6 +1639,30 @@ export type ByteRange = {
|
||||
total: number,
|
||||
};
|
||||
|
||||
export type CMakeProfile = {
|
||||
name?: string,
|
||||
buildType?: string,
|
||||
generator?: string,
|
||||
cmakeArgs?: string[],
|
||||
/** Empty uses cmake-build-<lowercase build type>. */
|
||||
buildDir?: string,
|
||||
buildArgs?: string[],
|
||||
env?: { [key in string]: string },
|
||||
};
|
||||
|
||||
/** Absent profiles retain automatic discovery for existing projects. */
|
||||
export type CMakeSettings = CMakeSettings_Serialize | CMakeSettings_Deserialize;
|
||||
|
||||
/** Absent profiles retain automatic discovery for existing projects. */
|
||||
export type CMakeSettings_Deserialize = {
|
||||
profiles?: CMakeProfile[] | null,
|
||||
};
|
||||
|
||||
/** Absent profiles retain automatic discovery for existing projects. */
|
||||
export type CMakeSettings_Serialize = {
|
||||
profiles?: CMakeProfile[] | null,
|
||||
};
|
||||
|
||||
export type CallHierarchy = {
|
||||
items: HierarchyItem[],
|
||||
incomplete: ReferencesIncomplete | null,
|
||||
@@ -3903,6 +3927,10 @@ export type LanguageCodeStyle = LanguageCodeStyle_Serialize | LanguageCodeStyle_
|
||||
|
||||
/** Editor › Code Style › <Language> › Tabs and Indents (unset fields inherit). */
|
||||
export type LanguageCodeStyle_Deserialize = {
|
||||
/** Java formatter: `default` | `intellij` | `custom` (other languages ignore it). */
|
||||
formatter?: string | null,
|
||||
/** Eclipse formatter XML; relative paths start at the project root. */
|
||||
formatterPath?: string | null,
|
||||
useTab?: boolean | null,
|
||||
tabSize?: number | null,
|
||||
indent?: number | null,
|
||||
@@ -3915,6 +3943,10 @@ export type LanguageCodeStyle_Deserialize = {
|
||||
|
||||
/** Editor › Code Style › <Language> › Tabs and Indents (unset fields inherit). */
|
||||
export type LanguageCodeStyle_Serialize = {
|
||||
/** Java formatter: `default` | `intellij` | `custom` (other languages ignore it). */
|
||||
formatter?: string | null,
|
||||
/** Eclipse formatter XML; relative paths start at the project root. */
|
||||
formatterPath?: string | null,
|
||||
useTab?: boolean | null,
|
||||
tabSize?: number | null,
|
||||
indent?: number | null,
|
||||
@@ -5627,6 +5659,7 @@ export type Settings_Deserialize = {
|
||||
deployment?: DeploymentSettings,
|
||||
/** Build, Execution, Deployment › Run Targets. */
|
||||
runTargets?: RunTarget[],
|
||||
cmake?: CMakeSettings_Deserialize,
|
||||
};
|
||||
|
||||
export type Settings_Serialize = {
|
||||
@@ -5682,6 +5715,7 @@ export type Settings_Serialize = {
|
||||
deployment: DeploymentSettings,
|
||||
/** Build, Execution, Deployment › Run Targets. */
|
||||
runTargets: RunTarget[],
|
||||
cmake: CMakeSettings_Serialize,
|
||||
};
|
||||
|
||||
export type Severity = "error" | "warning" | "info" | "hint";
|
||||
|
||||
@@ -6,6 +6,7 @@ const ipc = vi.hoisted(() => {
|
||||
return {
|
||||
ok,
|
||||
debugStep: vi.fn(() => ok(null)),
|
||||
debugResume: vi.fn(() => ok(null)),
|
||||
debugForceStepOver: vi.fn(() => ok(null)),
|
||||
debugRunTo: vi.fn(() => ok(null)),
|
||||
debugStepInTargets: vi.fn(() => ok([] as Array<{ id: number; label: string }>)),
|
||||
@@ -54,7 +55,7 @@ import type { DebugCapabilities } from "@/platform/ipc";
|
||||
import { registerDebugActions } from "../actions";
|
||||
import { breakpointsOf, loadBreakpoints } from "../breakpoints/store";
|
||||
import { loadSpecialBreakpoints, removeAllSpecial, useSpecialBreakpoints } from "../breakpoints/special";
|
||||
import { publishContext, useDebug, type DebugSession } from "../state";
|
||||
import { publishContext, updateExecutionPoint, useExecutionPoint, useDebug, type DebugSession } from "../state";
|
||||
import { focusDebugView, blurDebugView } from "../ui/focusedView";
|
||||
import { useOverlays } from "../ui/popoverStore";
|
||||
|
||||
@@ -97,6 +98,7 @@ function paused(p: Partial<DebugSession> = {}) {
|
||||
};
|
||||
useDebug.setState({ sessions: { 3: s }, active: 3 });
|
||||
publishContext();
|
||||
updateExecutionPoint();
|
||||
}
|
||||
|
||||
function caret(path: string, language: string, doc: string, line: number) {
|
||||
@@ -154,15 +156,41 @@ describe("debugger actions (IDEA parity)", () => {
|
||||
expect(getAction("debug.jumpToSource")!.keyWhen).toBe("debugFramesFocus || debugVariablesFocus");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["debug.resume", "debugResume"],
|
||||
["debug.stepOver", "debugStep"],
|
||||
["debug.stepInto", "debugStep"],
|
||||
["debug.stepOut", "debugStep"],
|
||||
["debug.forceStepOver", "debugForceStepOver"],
|
||||
["debug.forceStepInto", "debugStep"],
|
||||
["debug.smartStepInto", "debugStep"],
|
||||
["debug.runToCursor", "debugRunTo"],
|
||||
["debug.forceRunToCursor", "debugRunTo"],
|
||||
["debug.resetFrame", "debugRestartFrame"],
|
||||
] as const)("%s clears the old execution point before its request", async (id, command) => {
|
||||
paused({ caps: caps({ restartFrame: true }) });
|
||||
caret("/p/app.py", "python", "a\nb\nc", 2);
|
||||
const request = ipc[command].mockImplementationOnce(() => {
|
||||
expect(useExecutionPoint.getState().path).toBeNull();
|
||||
expect(useDebug.getState().sessions[3]?.status).toBe("running");
|
||||
return ipc.ok(null);
|
||||
});
|
||||
await executeAction(id);
|
||||
expect(request).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("Force Step Over / Force Step Into / Force Run to Cursor", async () => {
|
||||
paused();
|
||||
caret("/p/app.py", "python", "a\nb\nc\nd", 2);
|
||||
await executeAction("debug.forceStepOver");
|
||||
expect(ipc.debugForceStepOver).toHaveBeenCalledWith(3, 1);
|
||||
paused();
|
||||
await executeAction("debug.forceStepInto");
|
||||
expect(ipc.debugStep).toHaveBeenCalledWith(3, 1, "into");
|
||||
paused();
|
||||
await executeAction("debug.forceRunToCursor");
|
||||
expect(ipc.debugRunTo).toHaveBeenCalledWith(3, 1, "/p/app.py", 2, true);
|
||||
paused();
|
||||
await executeAction("debug.runToCursor");
|
||||
expect(ipc.debugRunTo).toHaveBeenLastCalledWith(3, 1, "/p/app.py", 2, false);
|
||||
});
|
||||
@@ -180,6 +208,7 @@ describe("debugger actions (IDEA parity)", () => {
|
||||
expect(ipc.debugStepInTargets).toHaveBeenCalledWith(3, 71);
|
||||
expect(useOverlays.getState().smartStep?.targets.map((t) => t.label)).toEqual(["str(i)", "parse(str(i))"]);
|
||||
|
||||
paused({ caps: caps({ stepInTargets: true }) });
|
||||
ipc.debugStepInTargets.mockReturnValueOnce(ipc.ok([{ id: 5, label: "parse()" }]));
|
||||
await executeAction("debug.smartStepInto");
|
||||
await settle();
|
||||
|
||||
@@ -166,6 +166,60 @@ describe("breakpoint gutter clicks", () => {
|
||||
});
|
||||
|
||||
describe("execution point", () => {
|
||||
it("keeps the execution line synchronized if inline rendering throws", async () => {
|
||||
useExecutionPoint.setState({ path: null, values: {} });
|
||||
const errors: unknown[] = [];
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: "x\nx\nx",
|
||||
extensions: [executionPoint(ctx), EditorView.exceptionSink.of((e) => errors.push(e))],
|
||||
}),
|
||||
parent: document.body,
|
||||
});
|
||||
views.push(view);
|
||||
const values: Record<string, string> = Object.defineProperty({}, "x", {
|
||||
enumerable: true,
|
||||
get() {
|
||||
throw new Error("inline rendering failed");
|
||||
},
|
||||
});
|
||||
useExecutionPoint.setState({ path: ctx.path, line: 0, top: true, values });
|
||||
await flush();
|
||||
expect(errors).toHaveLength(1);
|
||||
useExecutionPoint.setState({ line: 2, values: {} });
|
||||
await flush();
|
||||
expect(view.dom.querySelectorAll(".cm-line")[2]!.className).toContain(styles.execLine);
|
||||
useExecutionPoint.setState({ path: null });
|
||||
await flush();
|
||||
expect(view.dom.querySelector(`.${styles.execLine}`)).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps refreshing the execution line around Object.prototype names", async () => {
|
||||
useExecutionPoint.setState({ path: null, values: {} });
|
||||
const view = makeView("String text = Integer.toString(40);\nint count = 40;\ncount++;\ncount++;");
|
||||
useExecutionPoint.setState({ path: ctx.path, line: 1, top: true, values: { text: "40" } });
|
||||
await flush();
|
||||
useExecutionPoint.setState({ line: 3, values: { text: "40", count: "42" } });
|
||||
await flush();
|
||||
expect(view.dom.querySelector(`.${styles.execLine}`)?.textContent).toContain("count++");
|
||||
useExecutionPoint.setState({ path: null, values: {} });
|
||||
await flush();
|
||||
expect(view.dom.querySelector(`.${styles.execLine}`)).toBeNull();
|
||||
});
|
||||
|
||||
it.each([2, null])("refreshes a cached editor state when remounted at %s", async (line) => {
|
||||
useExecutionPoint.setState({ path: ctx.path, line: 0, top: true, values: {} });
|
||||
const view = makeView("first\nsecond\nthird");
|
||||
const state = view.state;
|
||||
view.destroy();
|
||||
views.splice(views.indexOf(view), 1);
|
||||
useExecutionPoint.setState({ path: line == null ? null : ctx.path, line: line ?? 0, values: {} });
|
||||
const restored = new EditorView({ state, parent: document.body });
|
||||
views.push(restored);
|
||||
await flush();
|
||||
expect(restored.dom.querySelector(`.${styles.execLine}`)?.textContent ?? null).toBe(line == null ? null : "third");
|
||||
});
|
||||
|
||||
it("highlights the suspended line and shows inline values", async () => {
|
||||
const view = makeView("x = 1\ny = x + 1\nprint(y)");
|
||||
useExecutionPoint.setState({ path: ctx.path, line: 1, top: true, values: { x: "1", y: "2" } });
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { StackFrame, Variable } from "@/platform/ipc";
|
||||
|
||||
const ipc = vi.hoisted(() => {
|
||||
const ok = <T>(data: T) => ({ status: "ok" as const, data });
|
||||
return {
|
||||
ok,
|
||||
debugCapabilities: vi.fn(async () => ok(null)),
|
||||
debugThreads: vi.fn(async () =>
|
||||
ok([
|
||||
{ id: 5, name: "http-nio-8179-exec-5" },
|
||||
{ id: 1, name: "main" },
|
||||
]),
|
||||
),
|
||||
debugStackTrace: vi.fn(async () => ok([] as StackFrame[])),
|
||||
debugScopes: vi.fn(async () => ok([{ name: "Locals", variablesReference: 10, expensive: false }])),
|
||||
debugVariables: vi.fn(async () => ok([] as Variable[])),
|
||||
};
|
||||
});
|
||||
vi.mock("@/platform/ipc", async (orig) => {
|
||||
const real = await orig<typeof import("@/platform/ipc")>();
|
||||
const { ok: _ok, ...commands } = ipc;
|
||||
return { ...real, commands: { ...real.commands, ...commands } };
|
||||
});
|
||||
vi.mock("@/workbench/contributions", async (orig) => ({
|
||||
...(await orig<typeof import("@/workbench/contributions")>()),
|
||||
maybeEditorService: () => ({ openFile: vi.fn(async () => {}) }),
|
||||
}));
|
||||
import { adoptSession, handleDebugEvent, selectThread, resumeExecution, useDebug, useExecutionPoint } from "../state";
|
||||
|
||||
const path = "/proj/MsMemberUtils.java";
|
||||
const frame = (line: number, id = 100): StackFrame => ({
|
||||
id,
|
||||
name: "queryMsTicket",
|
||||
path,
|
||||
line,
|
||||
column: 0,
|
||||
presentationHint: null,
|
||||
});
|
||||
const stopped = (threadId = 5, allThreadsStopped = false) =>
|
||||
handleDebugEvent(1, { kind: "stopped", threadId, allThreadsStopped, reason: "step", description: null });
|
||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
function deferred<T>() {
|
||||
let resolve!: (v: T) => void;
|
||||
const promise = new Promise<T>((r) => {
|
||||
resolve = r;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useDebug.setState({ sessions: {}, active: null, stopSeq: 0 });
|
||||
useExecutionPoint.setState({ path: null, line: 0, top: true, values: {} });
|
||||
adoptSession(1, { adapter: "java-debug" });
|
||||
ipc.debugStackTrace.mockResolvedValue(ipc.ok([frame(6840)]));
|
||||
ipc.debugVariables.mockResolvedValue(ipc.ok([]));
|
||||
});
|
||||
|
||||
describe("execution point state", () => {
|
||||
it.each([false, true])("selects each stopped thread and its fresh top frame (all=%s)", async (all) => {
|
||||
stopped(5, all);
|
||||
await settle();
|
||||
expect(useExecutionPoint.getState()).toMatchObject({ path, line: 6840 });
|
||||
ipc.debugStackTrace.mockResolvedValueOnce(ipc.ok([frame(6842)]));
|
||||
stopped(5, all);
|
||||
await settle();
|
||||
expect(useDebug.getState().sessions[1]).toMatchObject({ selectedThread: 5, selectedFrame: 100 });
|
||||
expect(useExecutionPoint.getState()).toMatchObject({ path, line: 6842 });
|
||||
ipc.debugStackTrace.mockResolvedValueOnce(ipc.ok([frame(9)]));
|
||||
stopped(1, all);
|
||||
await settle();
|
||||
expect(useDebug.getState().sessions[1]?.selectedThread).toBe(1);
|
||||
expect(useExecutionPoint.getState()).toMatchObject({ path, line: 9 });
|
||||
});
|
||||
|
||||
it("clears the old point while a new stack or selected thread is loading", async () => {
|
||||
stopped();
|
||||
await settle();
|
||||
const pending = deferred<ReturnType<typeof ipc.ok<StackFrame[]>>>();
|
||||
ipc.debugStackTrace.mockReturnValueOnce(pending.promise);
|
||||
stopped();
|
||||
expect(useExecutionPoint.getState().path).toBeNull();
|
||||
pending.resolve(ipc.ok([frame(6842)]));
|
||||
await settle();
|
||||
const other = deferred<ReturnType<typeof ipc.ok<StackFrame[]>>>();
|
||||
ipc.debugStackTrace.mockReturnValueOnce(other.promise);
|
||||
const selecting = selectThread(1, 1);
|
||||
expect(useExecutionPoint.getState().path).toBeNull();
|
||||
other.resolve(ipc.ok([frame(9)]));
|
||||
await selecting;
|
||||
expect(useExecutionPoint.getState().line).toBe(9);
|
||||
});
|
||||
|
||||
it("ignores a stack response from an earlier stop on the same thread", async () => {
|
||||
const old = deferred<ReturnType<typeof ipc.ok<StackFrame[]>>>();
|
||||
ipc.debugStackTrace.mockReturnValueOnce(old.promise);
|
||||
stopped();
|
||||
ipc.debugStackTrace.mockResolvedValueOnce(ipc.ok([frame(6842)]));
|
||||
stopped();
|
||||
await settle();
|
||||
old.resolve(ipc.ok([frame(6840)]));
|
||||
await settle();
|
||||
expect(useExecutionPoint.getState()).toMatchObject({ path, line: 6842 });
|
||||
expect(useDebug.getState().sessions[1]?.frames[0]?.line).toBe(6842);
|
||||
});
|
||||
|
||||
it("keeps the selected paused thread when only another thread continues", async () => {
|
||||
stopped();
|
||||
await settle();
|
||||
handleDebugEvent(1, { kind: "continued", threadId: 1, allThreadsContinued: false });
|
||||
expect(useExecutionPoint.getState().path).toBe(path);
|
||||
expect(useDebug.getState().sessions[1]?.status).toBe("paused");
|
||||
handleDebugEvent(1, { kind: "continued", threadId: 5, allThreadsContinued: false });
|
||||
expect(useExecutionPoint.getState().path).toBeNull();
|
||||
});
|
||||
|
||||
it.each(["continued", "terminated"] as const)("clears the execution point on %s", async (kind) => {
|
||||
stopped();
|
||||
await settle();
|
||||
handleDebugEvent(1, kind === "continued" ? { kind, threadId: 5, allThreadsContinued: true } : { kind });
|
||||
expect(useExecutionPoint.getState()).toMatchObject({ path: null, values: {} });
|
||||
});
|
||||
|
||||
it("retains locals whose names are Object.prototype properties", async () => {
|
||||
const names = Object.getOwnPropertyNames(Object.prototype);
|
||||
ipc.debugVariables.mockResolvedValueOnce(
|
||||
ipc.ok(
|
||||
names.map((name) => ({ name, value: "known", typeName: null, variablesReference: 0, evaluateName: name })),
|
||||
),
|
||||
);
|
||||
stopped();
|
||||
await settle();
|
||||
for (const name of names) expect(Object.hasOwn(useExecutionPoint.getState().values, name), name).toBe(true);
|
||||
});
|
||||
it.each(["step", "continue"])("clears suspended data before a %s request resolves", async () => {
|
||||
stopped();
|
||||
await settle();
|
||||
const pending = deferred<void>();
|
||||
const request = vi.fn(() => {
|
||||
expect(useExecutionPoint.getState().path).toBeNull();
|
||||
expect(useDebug.getState().sessions[1]).toMatchObject({ status: "running", frames: [], selectedFrame: null });
|
||||
return pending.promise;
|
||||
});
|
||||
const result = resumeExecution(1, request);
|
||||
expect(request).toHaveBeenCalledOnce();
|
||||
ipc.debugStackTrace.mockResolvedValueOnce(ipc.ok([frame(6842)]));
|
||||
stopped();
|
||||
await settle();
|
||||
pending.resolve();
|
||||
await result;
|
||||
expect(useExecutionPoint.getState()).toMatchObject({ path, line: 6842 });
|
||||
});
|
||||
|
||||
it("restores suspended data if the command fails without a newer event", async () => {
|
||||
stopped();
|
||||
await settle();
|
||||
await expect(
|
||||
resumeExecution(1, async () => {
|
||||
throw new Error("rejected");
|
||||
}),
|
||||
).rejects.toThrow("rejected");
|
||||
expect(useDebug.getState().sessions[1]?.status).toBe("paused");
|
||||
expect(useExecutionPoint.getState()).toMatchObject({ path, line: 6840 });
|
||||
});
|
||||
|
||||
it("does not restore an older stop when a failed command has already stopped again", async () => {
|
||||
stopped();
|
||||
await settle();
|
||||
let reject!: (e: Error) => void;
|
||||
const result = resumeExecution(
|
||||
1,
|
||||
() =>
|
||||
new Promise((_, r) => {
|
||||
reject = r;
|
||||
}),
|
||||
);
|
||||
const rejected = expect(result).rejects.toThrow("rejected");
|
||||
ipc.debugStackTrace.mockResolvedValueOnce(ipc.ok([frame(6842)]));
|
||||
stopped();
|
||||
await settle();
|
||||
reject(new Error("rejected"));
|
||||
await rejected;
|
||||
expect(useExecutionPoint.getState()).toMatchObject({ path, line: 6842 });
|
||||
});
|
||||
|
||||
it("discards locals from an earlier stop even when the frame ID is reused", async () => {
|
||||
const old = deferred<ReturnType<typeof ipc.ok<Variable[]>>>();
|
||||
ipc.debugVariables.mockReturnValueOnce(old.promise);
|
||||
stopped();
|
||||
await settle();
|
||||
ipc.debugStackTrace.mockResolvedValueOnce(ipc.ok([frame(6842)]));
|
||||
ipc.debugVariables.mockResolvedValueOnce(
|
||||
ipc.ok([{ name: "count", value: "42", typeName: null, variablesReference: 0, evaluateName: "count" }]),
|
||||
);
|
||||
stopped();
|
||||
await settle();
|
||||
old.resolve(ipc.ok([{ name: "count", value: "40", typeName: null, variablesReference: 0, evaluateName: "count" }]));
|
||||
await settle();
|
||||
expect(useExecutionPoint.getState().values.count).toBe("42");
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { _addEditor, _removeEditor, type TextEditorHandle } from "@/editor/api";
|
||||
import { useExecutionPoint } from "../state";
|
||||
import { adoptSession, useDebug, useExecutionPoint } from "../state";
|
||||
import { useOverlays } from "../ui/popoverStore";
|
||||
import {
|
||||
calleeName,
|
||||
@@ -92,6 +92,8 @@ describe("picking in the editor", () => {
|
||||
});
|
||||
|
||||
function setup() {
|
||||
useDebug.setState({ sessions: {}, active: null });
|
||||
adoptSession(3, { status: "paused", selectedThread: 1, stoppedThread: 1 });
|
||||
const ctx = { doc: 1, path: "/p/app.py", language: "python", uiMode: null, readOnly: false };
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({ doc: "def f():\n x = parse(str(i))\n", extensions: [smartStepTargets(ctx)] }),
|
||||
|
||||
@@ -52,6 +52,12 @@ describe("value classification", () => {
|
||||
);
|
||||
expect(inlineValuesFor("return 1", values)).toBeNull();
|
||||
});
|
||||
|
||||
it.each(Object.getOwnPropertyNames(Object.prototype))("only displays an own local named %s", (name) => {
|
||||
expect(inlineValuesFor(name, { count: "40" })).toBeNull();
|
||||
expect(inlineValuesFor(name, Object.fromEntries([[name, "known"]]))).toBe(`${name}: known`);
|
||||
expect(inlineValuesFor(name, Object.create({ [name]: "inherited" }))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const frame = (id: number, path: string | null, hint: string | null = null, line = 9): StackFrame => ({
|
||||
|
||||
@@ -24,6 +24,7 @@ import { debugUnsupportedReason, launchConfigFor } from "./launch";
|
||||
import { isRemoteConfig, REMOTE_JVM, remoteAttachConfig, remoteJvmTarget } from "./remote";
|
||||
import {
|
||||
activeSession,
|
||||
resumeExecution,
|
||||
evaluate,
|
||||
isLive,
|
||||
rerunSession,
|
||||
@@ -89,7 +90,10 @@ function breakpointPathAllowed(path: string): boolean {
|
||||
export function runToLine(path: string, line: number) {
|
||||
const t = pausedTarget();
|
||||
if (t && breakpointPathAllowed(path))
|
||||
void report("Run to Cursor failed", call(commands.debugRunTo(t.s.id, t.thread, path, line, false)));
|
||||
void report(
|
||||
"Run to Cursor failed",
|
||||
resumeExecution(t.s.id, () => call(commands.debugRunTo(t.s.id, t.thread, path, line, false))),
|
||||
);
|
||||
}
|
||||
|
||||
function notSupported(what: string, s: DebugSession) {
|
||||
@@ -115,15 +119,17 @@ async function smartStepInto() {
|
||||
if (!t) return;
|
||||
const frame = selectedFrame(t.s);
|
||||
if (!frame || !sessionSupports("stepInTargets", t.s)) {
|
||||
await call(commands.debugStep(t.s.id, t.thread, "into"));
|
||||
await resumeExecution(t.s.id, () => call(commands.debugStep(t.s.id, t.thread, "into")));
|
||||
return;
|
||||
}
|
||||
const targets = pausedLineTargets(await call(commands.debugStepInTargets(t.s.id, frame.id)));
|
||||
if (targets.length <= 1) {
|
||||
await call(
|
||||
targets[0]
|
||||
? commands.debugStepInTarget(t.s.id, t.thread, targets[0].id)
|
||||
: commands.debugStep(t.s.id, t.thread, "into"),
|
||||
await resumeExecution(t.s.id, () =>
|
||||
call(
|
||||
targets[0]
|
||||
? commands.debugStepInTarget(t.s.id, t.thread, targets[0].id)
|
||||
: commands.debugStep(t.s.id, t.thread, "into"),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -317,7 +323,11 @@ export function registerDebugActions(): () => void {
|
||||
when: paused,
|
||||
run: () => {
|
||||
const t = pausedTarget();
|
||||
if (t) void report(title, call(commands.debugStep(t.s.id, t.thread, kind)));
|
||||
if (t)
|
||||
void report(
|
||||
title,
|
||||
resumeExecution(t.s.id, () => call(commands.debugStep(t.s.id, t.thread, kind))),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -400,7 +410,11 @@ export function registerDebugActions(): () => void {
|
||||
when: paused,
|
||||
run: () => {
|
||||
const t = pausedTarget();
|
||||
if (t) void report("Resume failed", call(commands.debugResume(t.s.id, t.thread)));
|
||||
if (t)
|
||||
void report(
|
||||
"Resume failed",
|
||||
resumeExecution(t.s.id, () => call(commands.debugResume(t.s.id, t.thread))),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -432,7 +446,11 @@ export function registerDebugActions(): () => void {
|
||||
when: paused,
|
||||
run: () => {
|
||||
const t = pausedTarget();
|
||||
if (t) void report("Force Step Over failed", call(commands.debugForceStepOver(t.s.id, t.thread)));
|
||||
if (t)
|
||||
void report(
|
||||
"Force Step Over failed",
|
||||
resumeExecution(t.s.id, () => call(commands.debugForceStepOver(t.s.id, t.thread))),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -445,7 +463,11 @@ export function registerDebugActions(): () => void {
|
||||
when: paused,
|
||||
run: () => {
|
||||
const t = pausedTarget();
|
||||
if (t) void report("Force Step Into failed", call(commands.debugStep(t.s.id, t.thread, "into")));
|
||||
if (t)
|
||||
void report(
|
||||
"Force Step Into failed",
|
||||
resumeExecution(t.s.id, () => call(commands.debugStep(t.s.id, t.thread, "into"))),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -484,7 +506,10 @@ export function registerDebugActions(): () => void {
|
||||
notSupported("Reset Frame", t.s);
|
||||
return;
|
||||
}
|
||||
void report("Reset Frame failed", call(commands.debugRestartFrame(t.s.id, frame.id)));
|
||||
void report(
|
||||
"Reset Frame failed",
|
||||
resumeExecution(t.s.id, () => call(commands.debugRestartFrame(t.s.id, frame.id))),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -531,7 +556,10 @@ export function registerDebugActions(): () => void {
|
||||
const t = pausedTarget();
|
||||
const c = cursor();
|
||||
if (t && c && breakpointPathAllowed(c.path))
|
||||
void report("Run to Cursor failed", call(commands.debugRunTo(t.s.id, t.thread, c.path, c.line, false)));
|
||||
void report(
|
||||
"Run to Cursor failed",
|
||||
resumeExecution(t.s.id, () => call(commands.debugRunTo(t.s.id, t.thread, c.path, c.line, false))),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -545,7 +573,10 @@ export function registerDebugActions(): () => void {
|
||||
const t = pausedTarget();
|
||||
const c = cursor();
|
||||
if (t && c && breakpointPathAllowed(c.path))
|
||||
void report("Force Run to Cursor failed", call(commands.debugRunTo(t.s.id, t.thread, c.path, c.line, true)));
|
||||
void report(
|
||||
"Force Run to Cursor failed",
|
||||
resumeExecution(t.s.id, () => call(commands.debugRunTo(t.s.id, t.thread, c.path, c.line, true))),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -49,6 +49,7 @@ function currentExec(path: string): Exec | null {
|
||||
export function inlineValuesFor(text: string, values: Record<string, string>): string | null {
|
||||
const parts: string[] = [];
|
||||
for (const id of identifiersIn(text)) {
|
||||
if (!Object.hasOwn(values, id)) continue;
|
||||
const v = values[id];
|
||||
if (v === undefined) continue;
|
||||
parts.push(`${id}: ${inlineValue(v, 40)}`);
|
||||
@@ -80,33 +81,48 @@ export function executionPoint(ctx: EditorDocContext): Extension {
|
||||
provide: (f) => EditorView.decorations.compute([f], (state) => lineDecorations(state, state.field(f))),
|
||||
});
|
||||
|
||||
// Keep the execution line alive even if an inline-value renderer fails.
|
||||
const sync = ViewPlugin.fromClass(
|
||||
class {
|
||||
private unsub: () => void;
|
||||
private pending = false;
|
||||
private destroyed = false;
|
||||
constructor(readonly view: EditorView) {
|
||||
this.unsub = useExecutionPoint.subscribe(() => this.refresh());
|
||||
// Hidden tabs retain EditorState while their views are unmounted.
|
||||
this.refresh();
|
||||
}
|
||||
refresh() {
|
||||
if (this.pending) return;
|
||||
this.pending = true;
|
||||
queueMicrotask(() => {
|
||||
this.pending = false;
|
||||
if (this.destroyed) return;
|
||||
const next = currentExec(path);
|
||||
const cur = this.view.state.field(execField);
|
||||
const moved = cur?.line !== next?.line || cur?.top !== next?.top || !!cur !== !!next;
|
||||
// Locals can change without moving the execution point.
|
||||
this.view.dispatch(moved ? { effects: setExec.of(next) } : {});
|
||||
});
|
||||
}
|
||||
destroy() {
|
||||
this.destroyed = true;
|
||||
this.unsub();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const inline = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet = Decoration.none;
|
||||
private unsub: () => void;
|
||||
private pending = false;
|
||||
private destroyed = false;
|
||||
private unsubSettings: () => void;
|
||||
constructor(readonly view: EditorView) {
|
||||
this.decorations = this.build();
|
||||
// Settings › Debugger › Data Views › Show values inline.
|
||||
this.unsubSettings = onWorkflowSettingsChange(
|
||||
(s) => s.debugger.inlineValues,
|
||||
() => queueMicrotask(() => !this.destroyed && this.view.dispatch({})),
|
||||
);
|
||||
this.unsub = useExecutionPoint.subscribe(() => {
|
||||
if (this.pending) return;
|
||||
this.pending = true;
|
||||
queueMicrotask(() => {
|
||||
this.pending = false;
|
||||
if (this.destroyed) return;
|
||||
const next = currentExec(path);
|
||||
const cur = this.view.state.field(execField);
|
||||
const moved = cur?.line !== next?.line || cur?.top !== next?.top || !!cur !== !!next;
|
||||
// An empty transaction still rebuilds the inline values (locals arrive after the frame).
|
||||
this.view.dispatch(moved ? { effects: setExec.of(next) } : {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
update(u: ViewUpdate) {
|
||||
@@ -136,12 +152,11 @@ export function executionPoint(ctx: EditorDocContext): Extension {
|
||||
|
||||
destroy() {
|
||||
this.destroyed = true;
|
||||
this.unsub();
|
||||
this.unsubSettings();
|
||||
}
|
||||
},
|
||||
{ decorations: (v) => v.decorations },
|
||||
);
|
||||
|
||||
return [execField, inline];
|
||||
return [execField, sync, inline];
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { editorsFor, activeEditor, type EditorDocContext, type TextEditorHandle
|
||||
import { call, commands, type DebugSessionId, type StepInTarget } from "@/platform/ipc";
|
||||
import { suspendKeyboardDispatch } from "@/platform/keymap";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { useExecutionPoint } from "../state";
|
||||
import { useExecutionPoint, resumeExecution } from "../state";
|
||||
import { useOverlays, type InlineStepTarget } from "../ui/popoverStore";
|
||||
import styles from "./editor.module.css";
|
||||
|
||||
@@ -139,7 +139,7 @@ export function chooseStepTarget(id?: number) {
|
||||
const target = id ?? s.inline?.ranges[s.inline.index]?.id;
|
||||
cancelSmartStep();
|
||||
if (target == null) return;
|
||||
void call(commands.debugStepInTarget(s.session, s.thread, target)).catch((e) =>
|
||||
void resumeExecution(s.session, () => call(commands.debugStepInTarget(s.session, s.thread, target))).catch((e) =>
|
||||
notifications().notify({
|
||||
group: "run",
|
||||
level: "error",
|
||||
|
||||
@@ -644,6 +644,7 @@ export function selectSession(id: DebugSessionId) {
|
||||
|
||||
export async function selectThread(id: DebugSessionId, threadId: number) {
|
||||
patch(id, { selectedThread: threadId, frames: [], selectedFrame: null });
|
||||
updateExecutionPoint();
|
||||
await loadFrames(id, threadId, true);
|
||||
}
|
||||
|
||||
@@ -652,7 +653,7 @@ export function selectFrame(id: DebugSessionId, frameId: number, reveal = true)
|
||||
updateExecutionPoint();
|
||||
const s = useDebug.getState().sessions[id];
|
||||
const frame = s?.frames.find((f) => f.id === frameId);
|
||||
if (reveal && frame?.path) {
|
||||
if (reveal && useDebug.getState().active === id && frame?.path) {
|
||||
void maybeEditorService()?.openFile(frame.path, {
|
||||
selection: { line: frame.line, col: frame.column },
|
||||
focus: false,
|
||||
@@ -662,6 +663,7 @@ export function selectFrame(id: DebugSessionId, frameId: number, reveal = true)
|
||||
}
|
||||
|
||||
async function loadFrames(id: DebugSessionId, threadId: number, reveal: boolean) {
|
||||
const before = useDebug.getState().sessions[id];
|
||||
let frames: StackFrame[] = [];
|
||||
try {
|
||||
frames = await call(commands.debugStackTrace(id, threadId));
|
||||
@@ -669,7 +671,7 @@ async function loadFrames(id: DebugSessionId, threadId: number, reveal: boolean)
|
||||
console.warn("stackTrace failed", e);
|
||||
}
|
||||
const s = useDebug.getState().sessions[id];
|
||||
if (!s || s.status !== "paused" || s.selectedThread !== threadId) return;
|
||||
if (!s || s.status !== "paused" || s.selectedThread !== threadId || s.frames !== before?.frames) return;
|
||||
patch(id, { frames });
|
||||
const top = frames.find((f) => f.path && f.presentationHint !== "label") ?? frames[0];
|
||||
if (top?.path && (s.stopReason === "breakpoint" || s.stopReason === "function breakpoint")) {
|
||||
@@ -707,19 +709,21 @@ async function loadThreads(id: DebugSessionId) {
|
||||
|
||||
/** Locals of `frameId` (first non-expensive scope) for inline values. */
|
||||
async function loadLocals(id: DebugSessionId, frameId: number) {
|
||||
const values: Record<string, string> = {};
|
||||
const frames = useDebug.getState().sessions[id]?.frames;
|
||||
const values: Record<string, string> = Object.create(null);
|
||||
try {
|
||||
const scopes = await call(commands.debugScopes(id, frameId));
|
||||
const scope = scopes.find((sc) => !sc.expensive);
|
||||
if (scope) {
|
||||
const vars = await call(commands.debugVariables(id, scope.variablesReference));
|
||||
for (const v of vars) if (v.name && !(v.name in values)) values[v.name] = v.value;
|
||||
for (const v of vars) if (v.name && !Object.hasOwn(values, v.name)) values[v.name] = v.value;
|
||||
}
|
||||
} catch {
|
||||
// No inline values.
|
||||
}
|
||||
const s = useDebug.getState().sessions[id];
|
||||
if (useDebug.getState().active !== id || s?.selectedFrame !== frameId) return;
|
||||
if (useDebug.getState().active !== id || s?.status !== "paused" || s.selectedFrame !== frameId || s.frames !== frames)
|
||||
return;
|
||||
useExecutionPoint.setState({ values });
|
||||
}
|
||||
|
||||
@@ -729,6 +733,7 @@ export async function showExecutionPoint(id: DebugSessionId) {
|
||||
if (!s || s.status !== "paused" || s.stoppedThread == null) return;
|
||||
if (s.selectedThread !== s.stoppedThread) {
|
||||
patch(id, { selectedThread: s.stoppedThread, frames: [], selectedFrame: null });
|
||||
updateExecutionPoint();
|
||||
await loadFrames(id, s.stoppedThread, false);
|
||||
}
|
||||
const cur = useDebug.getState().sessions[id];
|
||||
@@ -757,6 +762,32 @@ export function updateExecutionPoint() {
|
||||
useExecutionPoint.setState({ path: frame.path, line: frame.line, top, values: {} });
|
||||
}
|
||||
|
||||
/** Invalidate suspended data before sending a command that resumes execution. */
|
||||
export async function resumeExecution(id: DebugSessionId, request: () => Promise<unknown>) {
|
||||
const before = useDebug.getState().sessions[id];
|
||||
if (!before || before.status !== "paused") return;
|
||||
const frames: StackFrame[] = [];
|
||||
patch(id, { status: "running", frames, selectedFrame: null, stoppedThread: null });
|
||||
updateExecutionPoint();
|
||||
try {
|
||||
await request();
|
||||
} catch (e) {
|
||||
const current = useDebug.getState().sessions[id];
|
||||
// A stop/continue/termination received during the request owns the newer state.
|
||||
if (current?.status === "running" && current.frames === frames) {
|
||||
patch(id, {
|
||||
status: before.status,
|
||||
frames: before.frames,
|
||||
selectedFrame: before.selectedFrame,
|
||||
stoppedThread: before.stoppedThread,
|
||||
});
|
||||
updateExecutionPoint();
|
||||
if (before.selectedFrame != null) void loadLocals(id, before.selectedFrame);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// --- watches -------------------------------------------------------------------
|
||||
|
||||
export function addWatch(expression: string) {
|
||||
@@ -862,6 +893,7 @@ export function handleDebugEvent(id: DebugSessionId, event: DebugEvent) {
|
||||
});
|
||||
useDebug.setState((st) => ({ active: id, stopSeq: st.stopSeq + 1 }));
|
||||
publishContext();
|
||||
updateExecutionPoint();
|
||||
void loadThreads(id);
|
||||
void loadFrames(id, event.threadId, true);
|
||||
const dbg = workflowSettings().debugger;
|
||||
@@ -876,6 +908,7 @@ export function handleDebugEvent(id: DebugSessionId, event: DebugEvent) {
|
||||
case "continued": {
|
||||
const cur = useDebug.getState().sessions[id]!;
|
||||
if (cur.status === "terminated") break;
|
||||
if (!event.allThreadsContinued && event.threadId !== (cur.selectedThread ?? cur.stoppedThread)) break;
|
||||
patch(id, {
|
||||
status: "running",
|
||||
frames: [],
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
updateBreakpoint,
|
||||
useBreakpoints,
|
||||
} from "../breakpoints/store";
|
||||
import { activeSession, addWatch, evaluate, isLive, startSession, useDebug } from "../state";
|
||||
import { activeSession, addWatch, evaluate, isLive, startSession, useDebug, resumeExecution } from "../state";
|
||||
import {
|
||||
addMethodBreakpoint,
|
||||
removeAllSpecial,
|
||||
@@ -978,7 +978,9 @@ function SmartStepChooser() {
|
||||
const pick = (targetId: number) => {
|
||||
if (!state) return;
|
||||
close();
|
||||
void call(commands.debugStepInTarget(state.session, state.thread, targetId)).catch((e) =>
|
||||
void resumeExecution(state.session, () =>
|
||||
call(commands.debugStepInTarget(state.session, state.thread, targetId)),
|
||||
).catch((e) =>
|
||||
notifications().notify({ group: "run", level: "error", title: "Smart Step Into failed", body: errorText(e) }),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -538,7 +538,14 @@ export function activate(): () => void {
|
||||
}),
|
||||
// Detected configs carry the selected toolchains (interpreter, JDK, Maven home and options).
|
||||
onIdeEvent("settingsChanged", (e) => {
|
||||
if (e.key === "languages" || e.key.startsWith("languages.")) scheduleReload();
|
||||
if (
|
||||
!e.key ||
|
||||
e.key === "languages" ||
|
||||
e.key.startsWith("languages.") ||
|
||||
e.key === "cmake" ||
|
||||
e.key.startsWith("cmake.")
|
||||
)
|
||||
scheduleReload();
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { validateSettings } from "../model/schema";
|
||||
import { DEFAULT_CMAKE_PROFILE } from "@/platform/cmakeSettings";
|
||||
|
||||
describe("CMake settings", () => {
|
||||
it("preserves absent and explicitly empty profiles", () => {
|
||||
expect(validateSettings({}).cmake).toEqual({});
|
||||
expect(validateSettings({ cmake: { profiles: [] } }).cmake.profiles).toEqual([]);
|
||||
});
|
||||
it("preserves argument arrays and drops malformed argument values", () => {
|
||||
const cmakeArgs = ["-DMESSAGE=two words", '-DQUOTE="hello"', ""];
|
||||
const buildArgs = ["-j", "8", "--", "LABEL=it's a build"];
|
||||
const profiles = validateSettings({ cmake: { profiles: [{ cmakeArgs, buildArgs }] } }).cmake.profiles;
|
||||
expect(profiles?.[0]).toMatchObject({ cmakeArgs, buildArgs });
|
||||
expect(
|
||||
validateSettings({ cmake: { profiles: [{ cmakeArgs: ["-DFOO=bar", 42, null] }] } }).cmake.profiles?.[0]
|
||||
?.cmakeArgs,
|
||||
).toEqual(["-DFOO=bar"]);
|
||||
});
|
||||
it("retains custom types and generators while normalizing malformed fields", () => {
|
||||
expect(
|
||||
validateSettings({
|
||||
cmake: {
|
||||
profiles: [
|
||||
{
|
||||
name: "Custom",
|
||||
buildType: "Coverage",
|
||||
generator: "Custom generator",
|
||||
buildArgs: 5,
|
||||
env: { KEEP: "yes", DROP: 5 },
|
||||
},
|
||||
null,
|
||||
],
|
||||
},
|
||||
}).cmake.profiles,
|
||||
).toEqual([
|
||||
{
|
||||
...DEFAULT_CMAKE_PROFILE,
|
||||
name: "Custom",
|
||||
buildType: "Coverage",
|
||||
generator: "Custom generator",
|
||||
env: { KEEP: "yes" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { DEFAULT_CMAKE_PROFILE } from "@/platform/cmakeSettings";
|
||||
import { parseArgs } from "@/platform/commandLine";
|
||||
import { PageContext } from "../components/context";
|
||||
import { validateSettings } from "../model/schema";
|
||||
import { useSettingsDialog } from "../model/staging";
|
||||
import { CMakeProfiles } from "../pages/cmake";
|
||||
|
||||
const savedDialog = useSettingsDialog.getState();
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
useSettingsDialog.setState(savedDialog, true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["CMake arguments", "cmakeArgs"],
|
||||
["Build arguments", "buildArgs"],
|
||||
] as const)("round-trips spaces, quotes and empty values in %s", async (label, key) => {
|
||||
const args = ["two words", 'a "quote"', "it's", "", String.raw`C:\some path\file`];
|
||||
useSettingsDialog.setState({
|
||||
base: validateSettings({ cmake: { profiles: [{ ...DEFAULT_CMAKE_PROFILE, [key]: args }] } }),
|
||||
staged: {},
|
||||
});
|
||||
const mount = () =>
|
||||
render(
|
||||
<PageContext.Provider value={{ pageId: "build.cmake", scope: "project" }}>
|
||||
<CMakeProfiles />
|
||||
</PageContext.Provider>,
|
||||
);
|
||||
const view = mount();
|
||||
const input = screen.getByLabelText<HTMLInputElement>(label);
|
||||
expect(parseArgs(input.value)).toEqual(args);
|
||||
await userEvent.clear(input);
|
||||
const text = String.raw`-DNAME="two words" "-DQUOTE=it's" ""`;
|
||||
await userEvent.type(input, text);
|
||||
expect(input).toHaveValue(text);
|
||||
fireEvent.blur(input);
|
||||
const expected = ["-DNAME=two words", "-DQUOTE=it's", ""];
|
||||
expect(parseArgs(input.value)).toEqual(expected);
|
||||
expect(useSettingsDialog.getState().staged["project|cmake.profiles"]?.value).toEqual([
|
||||
{ ...DEFAULT_CMAKE_PROFILE, [key]: expected },
|
||||
]);
|
||||
view.unmount();
|
||||
mount();
|
||||
expect(parseArgs(screen.getByLabelText<HTMLInputElement>(label).value)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("displays each profile's arguments when switching profiles", () => {
|
||||
useSettingsDialog.setState({
|
||||
base: validateSettings({
|
||||
cmake: {
|
||||
profiles: [
|
||||
{ ...DEFAULT_CMAKE_PROFILE, name: "Debug" },
|
||||
{ ...DEFAULT_CMAKE_PROFILE, name: "Release", cmakeArgs: ["-DMESSAGE=release build"] },
|
||||
],
|
||||
},
|
||||
}),
|
||||
staged: {},
|
||||
});
|
||||
render(
|
||||
<PageContext.Provider value={{ pageId: "build.cmake", scope: "project" }}>
|
||||
<CMakeProfiles />
|
||||
</PageContext.Provider>,
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText("CMake arguments"), { target: { value: '"unfinished ' } });
|
||||
fireEvent.click(screen.getByRole("option", { name: "Release" }));
|
||||
expect(parseArgs(screen.getByLabelText<HTMLInputElement>("CMake arguments").value)).toEqual([
|
||||
"-DMESSAGE=release build",
|
||||
]);
|
||||
});
|
||||
@@ -84,6 +84,29 @@ describe("Settings dialog", () => {
|
||||
expect(settingsSet).toHaveBeenLastCalledWith("global", "editor.encodings.nativeToAscii", "false");
|
||||
});
|
||||
|
||||
it("edits CMake profiles in project settings and persists them on Apply", async () => {
|
||||
useProject.setState({ project: { root: "/project" } as never });
|
||||
const dialog = await openDialog("build.cmake");
|
||||
expect(within(dialog).getByRole("option", { name: "Debug" })).toBeInTheDocument();
|
||||
fireEvent.click(
|
||||
within(within(dialog).getByRole("toolbar", { name: "CMake profile actions" })).getByRole("button", {
|
||||
name: "Add",
|
||||
}),
|
||||
);
|
||||
fireEvent.change(within(dialog).getByLabelText("Name"), { target: { value: "Release" } });
|
||||
fireEvent.change(within(dialog).getByLabelText("Build type"), { target: { value: "Release" } });
|
||||
fireEvent.change(within(dialog).getByLabelText("Generator"), { target: { value: "Ninja" } });
|
||||
fireEvent.change(within(dialog).getByLabelText("Build arguments"), { target: { value: "-j 8" } });
|
||||
expect(settingsSet).not.toHaveBeenCalled();
|
||||
await act(async () => fireEvent.click(within(dialog).getByRole("button", { name: "Apply" })));
|
||||
const write = settingsSet.mock.calls.find((c) => c[1] === "cmake.profiles")!;
|
||||
expect(write[0]).toBe("project");
|
||||
expect(JSON.parse(write[2])).toEqual([
|
||||
expect.objectContaining({ name: "Debug", buildType: "Debug" }),
|
||||
expect.objectContaining({ name: "Release", buildType: "Release", generator: "Ninja", buildArgs: ["-j", "8"] }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("finds Gradle JVM and applies project Gradle settings", async () => {
|
||||
useProject.setState({ project: { root: "/project" } as never });
|
||||
const dialog = await openDialog("build.gradle");
|
||||
|
||||
@@ -87,6 +87,9 @@ describe("code style scheme", () => {
|
||||
it("copies every Code Style value to the other scheme", () => {
|
||||
stage("editor.codeStyle", "global", "editor.codeStyle.hardWrapAt", 100);
|
||||
stage("editor.codeStyle", "global", "editor.codeStyle.formatterTagsEnabled", true);
|
||||
stage("editor.codeStyle", "global", "editor.codeStyle.languages", {
|
||||
java: { formatter: "custom", formatterPath: "config/formatter.xml" },
|
||||
});
|
||||
copyScheme("editor.codeStyle", "global", "project");
|
||||
const s = useSettingsDialog.getState();
|
||||
expect(stagedValue(s, "project", "editor.codeStyle.hardWrapAt")).toBe(100);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { PageContext } from "../components/context";
|
||||
import { DEFAULT_SETTINGS } from "../model/schema";
|
||||
import { openSettings, stagedValue, useSettingsDialog } from "../model/staging";
|
||||
import { codeStyleLanguagePages } from "../pages/editor/codeStyle";
|
||||
|
||||
const mocks = vi.hoisted(() => ({ open: vi.fn() }));
|
||||
vi.mock("@tauri-apps/plugin-dialog", () => ({ open: mocks.open }));
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const actual = await original<typeof import("@/platform/ipc")>();
|
||||
return {
|
||||
...actual,
|
||||
isTauri: () => true,
|
||||
commands: {
|
||||
...actual.commands,
|
||||
languages: async () => [
|
||||
{ id: "java", name: "Java", indent: { tabSize: 4, insertSpaces: true } },
|
||||
{ id: "rust", name: "Rust" },
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
useProject.setState({ settings: structuredClone(DEFAULT_SETTINGS), project: null });
|
||||
openSettings("editor.codeStyle.java");
|
||||
});
|
||||
afterEach(cleanup);
|
||||
|
||||
async function page(language: string) {
|
||||
const pages = await codeStyleLanguagePages();
|
||||
const spec = pages.find((p) => p.id === `editor.codeStyle.${language}`)!;
|
||||
const Page = spec.component!;
|
||||
render(
|
||||
<PageContext.Provider value={{ pageId: spec.id, scope: "global" }}>
|
||||
<Page />
|
||||
</PageContext.Provider>,
|
||||
);
|
||||
}
|
||||
const value = () => stagedValue(useSettingsDialog.getState(), "global", "editor.codeStyle.languages");
|
||||
|
||||
describe("Java formatting profile", () => {
|
||||
it("stages profile and relative XML paths, browses for XML, and resets language defaults", async () => {
|
||||
await page("java");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Formatting profile" }));
|
||||
fireEvent.click(screen.getByRole("option", { name: "Custom Eclipse formatter XML" }));
|
||||
fireEvent.change(screen.getByLabelText("Eclipse formatter XML"), { target: { value: "config/style.xml" } });
|
||||
expect(value()).toEqual({ java: { formatter: "custom", formatterPath: "config/style.xml" } });
|
||||
mocks.open.mockResolvedValue("/project/custom.xml");
|
||||
await act(async () => fireEvent.click(screen.getByRole("button", { name: "Browse…" })));
|
||||
expect(value()).toEqual({ java: { formatter: "custom", formatterPath: "/project/custom.xml" } });
|
||||
expect(mocks.open).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ filters: [{ name: "XML", extensions: ["xml"] }] }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Formatting profile" }));
|
||||
fireEvent.click(screen.getByRole("option", { name: "IntelliJ IDEA" }));
|
||||
expect(screen.queryByLabelText("Eclipse formatter XML")).toBeNull();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Reset to Language Defaults" }));
|
||||
expect(value()).toEqual({});
|
||||
});
|
||||
|
||||
it("keeps the profile controls specific to Java", async () => {
|
||||
await page("rust");
|
||||
expect(screen.queryByRole("button", { name: "Formatting profile" })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
* so `validateSettings` never throws — anything unknown or malformed falls
|
||||
* back to the default (mirrors `ide-core::settings` defaults).
|
||||
*/
|
||||
import { validateCMake } from "@/platform/cmakeSettings";
|
||||
import { DEFAULT_IGNORED_NAMES } from "@/platform/ignoredNames";
|
||||
import { validateFileTypeOverrides, validateFileTypes, type FileTypeAssociations } from "@/platform/fileTypes";
|
||||
import type { LanguageServerSettings, Settings } from "@/platform/ipc";
|
||||
@@ -82,6 +83,7 @@ export interface ValidSettings extends WorkflowSettings {
|
||||
/** System Settings › HTTP Proxy (global). */
|
||||
proxy: ProxySettingsValid;
|
||||
languages: LanguagesValid;
|
||||
cmake: ReturnType<typeof validateCMake>;
|
||||
projectStructure: ProjectStructureValid;
|
||||
inspections: InspectionSettingsValid;
|
||||
/** Editor › Natural Languages › Spelling. */
|
||||
@@ -150,6 +152,7 @@ export const DEFAULT_SETTINGS: ValidSettings = {
|
||||
notifications: DEFAULT_NOTIFICATIONS,
|
||||
proxy: DEFAULT_PROXY,
|
||||
languages: DEFAULT_LANGUAGES,
|
||||
cmake: {},
|
||||
projectStructure: DEFAULT_PROJECT_STRUCTURE,
|
||||
inspections: DEFAULT_INSPECTIONS,
|
||||
spelling: { ideWords: [], projectWords: [], customDictionaries: [] },
|
||||
@@ -254,6 +257,7 @@ export function validateSettings(raw: Settings | unknown): ValidSettings {
|
||||
notifications: validateNotifications(root.notifications),
|
||||
proxy: validateProxy(root.proxy),
|
||||
languages: validateLanguages(root.languages),
|
||||
cmake: validateCMake(root.cmake),
|
||||
projectStructure: validateProjectStructure(root.projectStructure),
|
||||
inspections: validateInspections(root.inspections),
|
||||
spelling: validateSpelling(root.spelling),
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.profile {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useId, useState } from "react";
|
||||
import { DEFAULT_CMAKE_PROFILE, type CMakeProfileValid } from "@/platform/cmakeSettings";
|
||||
import { formatArgs, parseArgs } from "@/platform/commandLine";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { IconButton, Input, T } from "@/ui";
|
||||
import { Row, Section } from "../components/Form";
|
||||
import { useSetting } from "../components/context";
|
||||
import { TableEditor } from "./workflowControls";
|
||||
import styles from "./Workflow.module.css";
|
||||
import own from "./CMake.module.css";
|
||||
|
||||
function ArgumentsInput({ id, value, onChange }: { id: string; value: string[]; onChange: (args: string[]) => void }) {
|
||||
const [text, setText] = useState<string>();
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
className={styles.wide}
|
||||
value={text ?? formatArgs(value)}
|
||||
onChange={(e) => {
|
||||
setText(e.target.value);
|
||||
onChange(parseArgs(e.target.value));
|
||||
}}
|
||||
onBlur={() => setText(undefined)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CMakeProfiles() {
|
||||
const [saved, setProfiles] = useSetting<CMakeProfileValid[] | undefined>("cmake.profiles");
|
||||
const profiles = saved ?? [DEFAULT_CMAKE_PROFILE];
|
||||
const [selected, setSelected] = useState(0);
|
||||
const index = Math.min(selected, Math.max(0, profiles.length - 1));
|
||||
const current = profiles[index];
|
||||
const id = useId();
|
||||
const update = (patch: Partial<CMakeProfileValid>) =>
|
||||
setProfiles(profiles.map((p, i) => (i === index ? { ...p, ...patch } : p)));
|
||||
const fields = [
|
||||
["name", "Name"],
|
||||
["buildType", "Build type"],
|
||||
["generator", "Generator"],
|
||||
["cmakeArgs", "CMake arguments"],
|
||||
["buildDir", "Build directory"],
|
||||
["buildArgs", "Build arguments"],
|
||||
] as const;
|
||||
return (
|
||||
<div className={styles.toolsLayout}>
|
||||
<Section title="CMake profiles">
|
||||
<div className={styles.tableBox}>
|
||||
<div className={styles.toolbar} role="toolbar" aria-label={tx("CMake profile actions")}>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
title="Add"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setProfiles([
|
||||
...profiles,
|
||||
{ ...DEFAULT_CMAKE_PROFILE, name: tx`CMake profile ${profiles.length + 1}`, env: {} },
|
||||
]);
|
||||
setSelected(profiles.length);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
title="Remove"
|
||||
size="small"
|
||||
disabled={!current}
|
||||
onClick={() => {
|
||||
setProfiles(profiles.filter((_, i) => i !== index));
|
||||
setSelected(Math.max(0, index - 1));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.toolList} role="listbox" aria-label={tx("CMake profiles")}>
|
||||
{profiles.map((p, i) => (
|
||||
<button
|
||||
type="button"
|
||||
key={i}
|
||||
role="option"
|
||||
aria-selected={index === i}
|
||||
className={`${own.profile} ${styles.toolRow} ${index === i ? styles.toolRowSelected : ""}`}
|
||||
onClick={() => setSelected(i)}
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
{profiles.length === 0 && <T>No CMake profiles. Click + to add one.</T>}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
{current && (
|
||||
<Section title="Profile settings">
|
||||
{fields.map(([key, label]) => (
|
||||
<Row key={key} label={label} htmlFor={`${id}-${key}`} className={styles.alignedRow}>
|
||||
{key === "cmakeArgs" || key === "buildArgs" ? (
|
||||
<ArgumentsInput
|
||||
key={index}
|
||||
id={`${id}-${key}`}
|
||||
value={current[key]}
|
||||
onChange={(args) => update({ [key]: args })}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
id={`${id}-${key}`}
|
||||
className={styles.wide}
|
||||
value={current[key]}
|
||||
list={key === "buildType" || key === "generator" ? `${id}-${key}-options` : undefined}
|
||||
placeholder={
|
||||
key === "buildDir"
|
||||
? "cmake-build-" + current.buildType.toLowerCase()
|
||||
: key === "generator"
|
||||
? tx("CMake default")
|
||||
: undefined
|
||||
}
|
||||
onChange={(e) => update({ [key]: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
))}
|
||||
<datalist id={`${id}-buildType-options`}>
|
||||
{["Debug", "Release", "RelWithDebInfo", "MinSizeRel"].map((v) => (
|
||||
<option key={v} value={v} />
|
||||
))}
|
||||
</datalist>
|
||||
<datalist id={`${id}-generator-options`}>
|
||||
{["Ninja", "Unix Makefiles", "Ninja Multi-Config", "Visual Studio 17 2022", "Xcode"].map((v) => (
|
||||
<option key={v} value={v} />
|
||||
))}
|
||||
</datalist>
|
||||
<Row label="Environment variables" stacked>
|
||||
<TableEditor
|
||||
rows={Object.entries(current.env).map(([name, value]) => ({ name, value }))}
|
||||
columns={[
|
||||
{ key: "name", title: "Name", placeholder: "NAME" },
|
||||
{ key: "value", title: "Value", placeholder: "value" },
|
||||
]}
|
||||
onChange={(rows) => update({ env: Object.fromEntries(rows.map((r) => [r.name ?? "", r.value ?? ""])) })}
|
||||
empty="No variables"
|
||||
aria-label="Environment variables"
|
||||
/>
|
||||
</Row>
|
||||
<div className={styles.note}>
|
||||
<T>
|
||||
Quote arguments containing spaces. Build arguments go to cmake --build; use -- before native build tool
|
||||
options.
|
||||
</T>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { Button, Checkbox, Input, type SelectOption, T } from "@/ui";
|
||||
import { commands, isTauri, type LanguageInfo_Serialize as LanguageInfo } from "@/platform/ipc";
|
||||
import { ENCODING_CHOICES, lineCommentDefaults, type LanguageCodeStyle } from "@/platform/editorSettings";
|
||||
import { NATIVE_TO_ASCII_ACTION } from "../../nativeToAscii";
|
||||
import { NumberInput, Row } from "../../components/Form";
|
||||
import { NumberInput, Row, Select } from "../../components/Form";
|
||||
import { useSetting } from "../../components/context";
|
||||
import type { PageSpec } from "../spec";
|
||||
import { CODE_STYLE_SCOPE, CodeStyleMain, SchemeRow, formatterLabels } from "./codeStyleScheme";
|
||||
@@ -159,6 +159,7 @@ function LanguageStyle({ lang }: { lang: LanguageInfo }) {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<SchemeRow />
|
||||
{lang.id === "java" && <JavaFormatter style={own} onChange={set} />}
|
||||
<section aria-label={tx("Tabs and Indents")}>
|
||||
<div className={styles.previewTitle}>
|
||||
<T>Tabs and Indents</T>
|
||||
@@ -238,6 +239,62 @@ function LanguageStyle({ lang }: { lang: LanguageInfo }) {
|
||||
);
|
||||
}
|
||||
|
||||
const JAVA_FORMATTERS: SelectOption<NonNullable<LanguageCodeStyle["formatter"]>>[] = [
|
||||
{ value: "default", label: "Default" },
|
||||
{ value: "intellij", label: "IntelliJ IDEA" },
|
||||
{ value: "custom", label: "Custom Eclipse formatter XML" },
|
||||
];
|
||||
|
||||
function JavaFormatter({
|
||||
style,
|
||||
onChange,
|
||||
}: {
|
||||
style: LanguageCodeStyle;
|
||||
onChange: (patch: Partial<LanguageCodeStyle>) => void;
|
||||
}) {
|
||||
const id = useId();
|
||||
const browse = async () => {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const path = await open({
|
||||
multiple: false,
|
||||
title: tx("Eclipse formatter XML"),
|
||||
filters: [{ name: "XML", extensions: ["xml"] }],
|
||||
});
|
||||
if (typeof path === "string") onChange({ formatterPath: path });
|
||||
};
|
||||
return (
|
||||
<section aria-label={tx("Formatting profile")}>
|
||||
<Row
|
||||
label="Formatting profile"
|
||||
hint="Changes take effect after Apply without restarting the Java language server."
|
||||
>
|
||||
<Select
|
||||
value={style.formatter ?? "default"}
|
||||
options={JAVA_FORMATTERS}
|
||||
onChange={(formatter) => onChange({ formatter })}
|
||||
aria-label="Formatting profile"
|
||||
/>
|
||||
</Row>
|
||||
{style.formatter === "custom" && (
|
||||
<Row
|
||||
label="Eclipse formatter XML"
|
||||
htmlFor={id}
|
||||
hint="Absolute or project-relative path. To reload an edited XML file, switch to Default, apply, then select Custom again."
|
||||
>
|
||||
<Input
|
||||
id={id}
|
||||
value={style.formatterPath ?? ""}
|
||||
onChange={(e) => onChange({ formatterPath: e.target.value })}
|
||||
/>
|
||||
<Button size="small" onClick={browse}>
|
||||
<T>Browse…</T>
|
||||
</Button>
|
||||
</Row>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const SKIP = new Set(["plaintext", "log", "diff", "ignore", "gitattributes"]);
|
||||
|
||||
/** One Code Style page per language (registered once the language list is known). */
|
||||
@@ -261,6 +318,9 @@ export async function codeStyleLanguagePages(): Promise<PageSpec[]> {
|
||||
"Hard wrap at",
|
||||
"Comment Code",
|
||||
"Line comment at first column",
|
||||
...(lang.id === "java"
|
||||
? ["Formatting profile", "IntelliJ IDEA", "Custom Eclipse formatter XML", "Eclipse formatter XML"]
|
||||
: []),
|
||||
],
|
||||
component: () => <LanguageStyle lang={lang} />,
|
||||
}));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Stepping), Python Debugger and Run/Debug. Consumed by the run and debugger
|
||||
* features (and `ide-run` for encoding / termination timeout).
|
||||
*/
|
||||
import { CMakeProfiles } from "./cmake";
|
||||
import { WORKFLOW_LIMITS } from "@/platform/workflowSettings";
|
||||
import type { SelectOption } from "@/ui";
|
||||
import { GlobListEditor, NumberInput, Row } from "../components/Form";
|
||||
@@ -90,6 +91,25 @@ function TooltipDelayRow() {
|
||||
}
|
||||
|
||||
export const EXECUTION_PAGES: PageSpec[] = [
|
||||
{
|
||||
id: "build.cmake",
|
||||
title: "CMake",
|
||||
group: BUILD_GROUP,
|
||||
order: 357,
|
||||
projectScope: true,
|
||||
defaultScope: "project",
|
||||
keywords: [
|
||||
"profile",
|
||||
"generator",
|
||||
"Ninja",
|
||||
"Build type",
|
||||
"CMake arguments",
|
||||
"Build directory",
|
||||
"Build arguments",
|
||||
"Environment variables",
|
||||
],
|
||||
component: CMakeProfiles,
|
||||
},
|
||||
{
|
||||
id: "build.gradle",
|
||||
projectScope: true,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { CMakeProfile } from "@/bindings";
|
||||
|
||||
export type CMakeProfileValid = Required<CMakeProfile>;
|
||||
|
||||
export const DEFAULT_CMAKE_PROFILE: CMakeProfileValid = {
|
||||
name: "Debug",
|
||||
buildType: "Debug",
|
||||
generator: "",
|
||||
cmakeArgs: [],
|
||||
buildDir: "",
|
||||
buildArgs: [],
|
||||
env: {},
|
||||
};
|
||||
|
||||
export function validateCMake(raw: unknown): { profiles?: CMakeProfileValid[] } {
|
||||
const profiles = (raw as { profiles?: unknown } | undefined)?.profiles;
|
||||
if (!Array.isArray(profiles)) return {};
|
||||
const string = (value: unknown, fallback = "") => (typeof value === "string" ? value : fallback);
|
||||
const args = (value: unknown): string[] =>
|
||||
Array.isArray(value) ? value.filter((arg): arg is string => typeof arg === "string") : [];
|
||||
return {
|
||||
profiles: profiles
|
||||
.filter((p) => p && typeof p === "object")
|
||||
.map((p) => ({
|
||||
name: string(p.name, "Debug"),
|
||||
buildType: string(p.buildType, "Debug"),
|
||||
generator: string(p.generator),
|
||||
cmakeArgs: args(p.cmakeArgs),
|
||||
buildDir: string(p.buildDir),
|
||||
buildArgs: args(p.buildArgs),
|
||||
env: Object.fromEntries(
|
||||
Object.entries(p.env && typeof p.env === "object" ? p.env : {}).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||
),
|
||||
),
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,9 @@ export interface CustomColorScheme {
|
||||
export type CustomColorSchemes = Record<string, CustomColorScheme>;
|
||||
|
||||
export interface LanguageCodeStyle {
|
||||
/** Java formatter; relative XML paths start at the project root. */
|
||||
formatter?: "default" | "intellij" | "custom";
|
||||
formatterPath?: string;
|
||||
useTab?: boolean;
|
||||
tabSize?: number;
|
||||
indent?: number;
|
||||
|
||||
@@ -1364,12 +1364,10 @@
|
||||
"Open console for tool output": "为工具输出打开控制台",
|
||||
"Open documents": "打开的文档",
|
||||
"Open in Browser": "在浏览器中打开",
|
||||
"Open in Editor": "在编辑器中打开",
|
||||
"Open in Find Tool Window": "在“查找”工具窗口中打开",
|
||||
"Open in Find Window": "在查找窗口中打开",
|
||||
"Open in Opposite Group": "在相对组中打开",
|
||||
"Open in New Window": "在新窗口中打开",
|
||||
"Open in Tool Window": "在工具窗口中打开",
|
||||
"Open in Terminal": "在终端中打开",
|
||||
"Open in editor": "在编辑器中打开",
|
||||
"Open moved files in editor": "在编辑器中打开已移动的文件",
|
||||
@@ -5447,5 +5445,26 @@
|
||||
"Chat in Tool Window": "在工具窗口中聊天",
|
||||
"Chat in Editor": "在编辑器中聊天",
|
||||
"Agent settings": "智能体设置",
|
||||
"Describe a task…": "描述一个任务…"
|
||||
"Describe a task…": "描述一个任务…",
|
||||
"Formatting profile": "格式化方案",
|
||||
"Custom Eclipse formatter XML": "自定义 Eclipse 格式化 XML",
|
||||
"Eclipse formatter XML": "Eclipse 格式化 XML",
|
||||
"Changes take effect after Apply without restarting the Java language server.": "应用后即生效,无需重启 Java 语言服务器。",
|
||||
"Absolute or project-relative path. To reload an edited XML file, switch to Default, apply, then select Custom again.": "使用绝对路径或项目相对路径。修改 XML 文件后,请切换到默认方案并应用,再重新选择自定义方案。",
|
||||
"Cannot read Java formatter XML {path}: {error}": "无法读取 Java 格式化 XML {path}:{error}",
|
||||
"Java formatter configuration unavailable": "Java 格式化配置不可用",
|
||||
"Choose an Eclipse formatter XML file.": "请选择 Eclipse 格式化 XML 文件。",
|
||||
"Not a formatter XML file: {path}": "不是格式化 XML 文件:{path}",
|
||||
"CMake profiles": "CMake 配置",
|
||||
"CMake profile actions": "CMake 配置操作",
|
||||
"No CMake profiles. Click + to add one.": "没有 CMake 配置。点击 + 添加。",
|
||||
"Profile settings": "配置设置",
|
||||
"Build type": "构建类型",
|
||||
"Generator": "生成器",
|
||||
"CMake arguments": "CMake 参数",
|
||||
"Build directory": "构建目录",
|
||||
"Build arguments": "构建参数",
|
||||
"CMake default": "CMake 默认",
|
||||
"Quote arguments containing spaces. Build arguments go to cmake --build; use -- before native build tool options.": "包含空格的参数请加引号。构建参数传给 cmake --build;原生构建工具参数前请加 --。",
|
||||
"CMake profile {0}": "CMake 配置 {0}"
|
||||
}
|
||||
@@ -80,7 +80,9 @@ const BUILT_AT_RUNTIME = new Set<string>([]);
|
||||
/** Text that stays as written in every locale: names, code, formats, shortcuts. */
|
||||
const UNTRANSLATED = new Set<string>([
|
||||
// Product names
|
||||
"CMake",
|
||||
"Prettier",
|
||||
"IntelliJ IDEA",
|
||||
// Go environment variables and placeholder values
|
||||
"CGO",
|
||||
"GOPROXY",
|
||||
|
||||
Reference in new issue
Block a user