Compare commits

..
30 changed files with 131 additions and 1155 deletions

No files matched your search

+59 -10
View File
@@ -215,13 +215,21 @@ fn fix_kind(
}
/// `vmArgs` of a launch by java-debug, which decodes the JVM's output as `encoding`
/// (its launch attribute, UTF-8 when unset) and sets `file.encoding` itself.
pub fn java_debug_vm_args(vm_args: &str, encoding: Option<&str>, windows: bool) -> Option<String> {
/// (its launch attribute, UTF-8 when unset). java-debug sets `file.encoding` only
/// when that attribute is nonblank; use the same defaults and user overrides as [`fix`].
pub fn java_debug_vm_args(
vm_args: &str,
vars: &BTreeMap<String, String>,
inherited: impl Fn(&str) -> Option<String>,
encoding: Option<&str>,
windows: bool,
) -> Option<String> {
let encoding = encoding.map(str::trim).filter(|e| !e.is_empty()).unwrap_or("UTF-8");
if (!windows && is_utf8(encoding)) || sets(vm_args, STD_PROPERTIES) {
let added = fix_kind(Kind::Java, Some(&[vm_args.to_owned()]), vars, inherited, encoding, windows).java_args;
if added.is_empty() {
return None;
}
let added = options(STD_PROPERTIES, encoding).join(" ");
let added = added.join(" ");
Some(match vm_args.trim() {
"" => added,
own => format!("{own} {added}"),
@@ -446,12 +454,53 @@ mod tests {
#[test]
fn java_debug_launches_write_what_java_debug_decodes() {
assert_eq!(java_debug_vm_args("", None, true).as_deref(), Some(TOOL_UTF8));
assert_eq!(java_debug_vm_args("-Xmx1g", Some(""), true), Some(format!("-Xmx1g {TOOL_UTF8}")));
assert_eq!(java_debug_vm_args("", Some("GBK"), true).unwrap().split(' ').next(), Some("-Dstdout.encoding=GBK"));
assert_eq!(java_debug_vm_args("-Dstdout.encoding=GBK", None, true), None);
assert_eq!(java_debug_vm_args("-Xmx1g", None, false), None);
assert!(java_debug_vm_args("", Some("GBK"), false).is_some());
let debug =
|args: &str, encoding, windows| java_debug_vm_args(args, &BTreeMap::new(), |_| None, encoding, windows);
for encoding in [None, Some(""), Some(" "), Some("UTF-8"), Some("utf8")] {
let streams = encoding.filter(|e| !e.trim().is_empty()).unwrap_or("UTF-8");
let utf8 = format!("{} -Dfile.encoding=UTF-8", options(STD_PROPERTIES, streams).join(" "));
assert_eq!(debug("", encoding, true), Some(utf8.clone()));
assert_eq!(debug("-Xmx1g", encoding, true), Some(format!("-Xmx1g {utf8}")));
assert_eq!(debug("-Xmx1g", encoding, false), None);
}
for windows in [true, false] {
assert_eq!(debug("", Some("GBK"), windows), Some(options(STD_PROPERTIES, "GBK").join(" ")));
}
}
#[test]
fn java_debug_preserves_user_options_independently() {
let debug = |args: &str| java_debug_vm_args(args, &BTreeMap::new(), |_| None, None, true);
assert_eq!(debug("-Dfile.encoding=GBK"), Some(format!("-Dfile.encoding=GBK {TOOL_UTF8}")));
for property in STD_PROPERTIES {
let args = format!("-D{property}=GBK");
assert_eq!(debug(&args), Some(format!("{args} -Dfile.encoding=UTF-8")));
assert_eq!(debug(&format!("{args} -Dfile.encoding=GBK")), None);
}
for key in ["JDK_JAVA_OPTIONS", TOOL_OPTIONS] {
for (own, expected) in [
("-Dfile.encoding=GBK", Some(TOOL_UTF8.to_owned())),
("-Dstdout.encoding=GBK", Some("-Dfile.encoding=UTF-8".to_owned())),
("-Dstdout.encoding=GBK -Dfile.encoding=GBK", None),
] {
assert_eq!(java_debug_vm_args("", &vars(&[(key, own)]), |_| None, None, true), expected);
assert_eq!(
java_debug_vm_args("", &BTreeMap::new(), |k| (k == key).then(|| own.to_owned()), None, true),
expected
);
}
// A launch environment value overrides the inherited one, as in Run.
assert_eq!(
java_debug_vm_args(
"",
&vars(&[(key, "")]),
|k| (k == key).then(|| "-Dfile.encoding=GBK".into()),
None,
true
),
Some(format!("{TOOL_UTF8} -Dfile.encoding=UTF-8"))
);
}
}
fn filtered(chunks: &[&str]) -> String {
-56
View File
@@ -58,43 +58,6 @@ 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)]
@@ -191,7 +154,6 @@ impl Default for Settings {
ssh: SshSettings::default(),
deployment: DeploymentSettings::default(),
run_targets: Vec::new(),
cmake: CMakeSettings::default(),
}
}
}
@@ -803,24 +765,6 @@ 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();
+18 -3
View File
@@ -96,7 +96,13 @@ fn match_java_debug_encoding(mut config: LaunchConfig, windows: bool) -> LaunchC
};
let vm_args = extra.get("vmArgs").and_then(|v| v.as_str()).unwrap_or_default();
let encoding = extra.get("encoding").and_then(|v| v.as_str());
if let Some(vm_args) = ide_core::output_encoding::java_debug_vm_args(vm_args, encoding, windows) {
if let Some(vm_args) = ide_core::output_encoding::java_debug_vm_args(
vm_args,
&config.env,
|key| std::env::var(key).ok(),
encoding,
windows,
) {
extra.insert("vmArgs".into(), vm_args.into());
config.extra_json = Some(serde_json::Value::Object(extra).to_string());
}
@@ -452,7 +458,7 @@ mod tests {
program: None,
args: vec![],
cwd: None,
env: Default::default(),
env: [("JDK_JAVA_OPTIONS".into(), "".into()), ("JAVA_TOOL_OPTIONS".into(), "".into())].into(),
extra_json: Some(extra.to_string()),
}
}
@@ -468,7 +474,7 @@ mod tests {
let fixed = match_java_debug_encoding(launch("java-debug", "launch", extra.clone()), true);
assert_eq!(
vm_args(&fixed),
"-Xmx1g -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8"
"-Xmx1g -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -Dfile.encoding=UTF-8"
);
assert_eq!(
serde_json::from_str::<serde_json::Value>(fixed.extra_json.as_deref().unwrap()).unwrap()["mainClass"],
@@ -481,4 +487,13 @@ mod tests {
assert_eq!(vm_args(&match_java_debug_encoding(launch(adapter, request, extra.clone()), windows)), "-Xmx1g");
}
}
#[test]
fn java_debug_launches_respect_environment_encoding() {
for key in ["JDK_JAVA_OPTIONS", "JAVA_TOOL_OPTIONS"] {
let mut config = launch("java-debug", "launch", serde_json::json!({"vmArgs": "-Dstdout.encoding=GBK"}));
config.env.insert(key.into(), "-Dfile.encoding=GBK".into());
assert_eq!(vm_args(&match_java_debug_encoding(config, true)), "-Dstdout.encoding=GBK");
}
}
}
+4 -5
View File
@@ -50,11 +50,10 @@ 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_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();
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();
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);
+1 -9
View File
@@ -16,14 +16,6 @@ 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);
@@ -32,7 +24,7 @@ pub(crate) fn detect_with_profiles(
detect_maven(root, excluded_poms, &mut out);
detect_go(root, &mut out);
detect_python(root, &mut out);
cmake::detect(root, profiles, &mut out);
cmake::detect(root, &mut out);
detect_make(root, &mut out);
detect_just(root, &mut out);
detect_godot(root, &mut out);
+5 -172
View File
@@ -2,7 +2,6 @@
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};
@@ -20,16 +19,10 @@ 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, profiles: Option<&[CMakeProfile]>, out: &mut Vec<RunConfig>) {
pub(super) fn detect(root: &Path, 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| {
@@ -86,69 +79,11 @@ pub(super) fn detect(root: &Path, profiles: Option<&[CMakeProfile]>, out: &mut V
);
before(&mut test, &build_id);
out.push(test);
targets(root, &build, &dir, &prefix, None, out);
targets(root, &build, &dir, &prefix, out);
}
}
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>,
) {
fn targets(root: &Path, build: &Path, dir: &str, prefix: &str, 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
@@ -178,9 +113,6 @@ fn targets(
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 };
@@ -198,17 +130,8 @@ fn targets(
if !name.is_empty() {
args.extend(["--config", name]);
}
let mut build_config =
let 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 =
@@ -242,99 +165,9 @@ 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 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");
let query = cwd.join(dir).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());
}
}
-9
View File
@@ -201,15 +201,6 @@ 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)
-69
View File
@@ -652,72 +652,3 @@ 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);
}
}
}
-26
View File
@@ -1639,30 +1639,6 @@ 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,
@@ -5659,7 +5635,6 @@ export type Settings_Deserialize = {
deployment?: DeploymentSettings,
/** Build, Execution, Deployment › Run Targets. */
runTargets?: RunTarget[],
cmake?: CMakeSettings_Deserialize,
};
export type Settings_Serialize = {
@@ -5715,7 +5690,6 @@ export type Settings_Serialize = {
deployment: DeploymentSettings,
/** Build, Execution, Deployment › Run Targets. */
runTargets: RunTarget[],
cmake: CMakeSettings_Serialize,
};
export type Severity = "error" | "warning" | "info" | "hint";
@@ -6,7 +6,6 @@ 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 }>)),
@@ -55,7 +54,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, updateExecutionPoint, useExecutionPoint, useDebug, type DebugSession } from "../state";
import { publishContext, useDebug, type DebugSession } from "../state";
import { focusDebugView, blurDebugView } from "../ui/focusedView";
import { useOverlays } from "../ui/popoverStore";
@@ -98,7 +97,6 @@ function paused(p: Partial<DebugSession> = {}) {
};
useDebug.setState({ sessions: { 3: s }, active: 3 });
publishContext();
updateExecutionPoint();
}
function caret(path: string, language: string, doc: string, line: number) {
@@ -156,41 +154,15 @@ 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);
});
@@ -208,7 +180,6 @@ 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,60 +166,6 @@ 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" } });
@@ -1,202 +0,0 @@
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 { adoptSession, useDebug, useExecutionPoint } from "../state";
import { useExecutionPoint } from "../state";
import { useOverlays } from "../ui/popoverStore";
import {
calleeName,
@@ -92,8 +92,6 @@ 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,12 +52,6 @@ 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 => ({
+13 -44
View File
@@ -24,7 +24,6 @@ import { debugUnsupportedReason, launchConfigFor } from "./launch";
import { isRemoteConfig, REMOTE_JVM, remoteAttachConfig, remoteJvmTarget } from "./remote";
import {
activeSession,
resumeExecution,
evaluate,
isLive,
rerunSession,
@@ -90,10 +89,7 @@ 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",
resumeExecution(t.s.id, () => call(commands.debugRunTo(t.s.id, t.thread, path, line, false))),
);
void report("Run to Cursor failed", call(commands.debugRunTo(t.s.id, t.thread, path, line, false)));
}
function notSupported(what: string, s: DebugSession) {
@@ -119,17 +115,15 @@ async function smartStepInto() {
if (!t) return;
const frame = selectedFrame(t.s);
if (!frame || !sessionSupports("stepInTargets", t.s)) {
await resumeExecution(t.s.id, () => call(commands.debugStep(t.s.id, t.thread, "into")));
await 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 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"),
),
await call(
targets[0]
? commands.debugStepInTarget(t.s.id, t.thread, targets[0].id)
: commands.debugStep(t.s.id, t.thread, "into"),
);
return;
}
@@ -323,11 +317,7 @@ export function registerDebugActions(): () => void {
when: paused,
run: () => {
const t = pausedTarget();
if (t)
void report(
title,
resumeExecution(t.s.id, () => call(commands.debugStep(t.s.id, t.thread, kind))),
);
if (t) void report(title, call(commands.debugStep(t.s.id, t.thread, kind)));
},
});
@@ -410,11 +400,7 @@ export function registerDebugActions(): () => void {
when: paused,
run: () => {
const t = pausedTarget();
if (t)
void report(
"Resume failed",
resumeExecution(t.s.id, () => call(commands.debugResume(t.s.id, t.thread))),
);
if (t) void report("Resume failed", call(commands.debugResume(t.s.id, t.thread)));
},
},
{
@@ -446,11 +432,7 @@ export function registerDebugActions(): () => void {
when: paused,
run: () => {
const t = pausedTarget();
if (t)
void report(
"Force Step Over failed",
resumeExecution(t.s.id, () => call(commands.debugForceStepOver(t.s.id, t.thread))),
);
if (t) void report("Force Step Over failed", call(commands.debugForceStepOver(t.s.id, t.thread)));
},
},
{
@@ -463,11 +445,7 @@ export function registerDebugActions(): () => void {
when: paused,
run: () => {
const t = pausedTarget();
if (t)
void report(
"Force Step Into failed",
resumeExecution(t.s.id, () => call(commands.debugStep(t.s.id, t.thread, "into"))),
);
if (t) void report("Force Step Into failed", call(commands.debugStep(t.s.id, t.thread, "into")));
},
},
{
@@ -506,10 +484,7 @@ export function registerDebugActions(): () => void {
notSupported("Reset Frame", t.s);
return;
}
void report(
"Reset Frame failed",
resumeExecution(t.s.id, () => call(commands.debugRestartFrame(t.s.id, frame.id))),
);
void report("Reset Frame failed", call(commands.debugRestartFrame(t.s.id, frame.id)));
},
},
{
@@ -556,10 +531,7 @@ export function registerDebugActions(): () => void {
const t = pausedTarget();
const c = cursor();
if (t && c && breakpointPathAllowed(c.path))
void report(
"Run to Cursor failed",
resumeExecution(t.s.id, () => call(commands.debugRunTo(t.s.id, t.thread, c.path, c.line, false))),
);
void report("Run to Cursor failed", call(commands.debugRunTo(t.s.id, t.thread, c.path, c.line, false)));
},
},
{
@@ -573,10 +545,7 @@ export function registerDebugActions(): () => void {
const t = pausedTarget();
const c = cursor();
if (t && c && breakpointPathAllowed(c.path))
void report(
"Force Run to Cursor failed",
resumeExecution(t.s.id, () => call(commands.debugRunTo(t.s.id, t.thread, c.path, c.line, true))),
);
void report("Force Run to Cursor failed", call(commands.debugRunTo(t.s.id, t.thread, c.path, c.line, true)));
},
},
{
+18 -33
View File
@@ -49,7 +49,6 @@ 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)}`);
@@ -81,48 +80,33 @@ 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) {
@@ -152,11 +136,12 @@ export function executionPoint(ctx: EditorDocContext): Extension {
destroy() {
this.destroyed = true;
this.unsub();
this.unsubSettings();
}
},
{ decorations: (v) => v.decorations },
);
return [execField, sync, inline];
return [execField, inline];
}
+2 -2
View File
@@ -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, resumeExecution } from "../state";
import { useExecutionPoint } 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 resumeExecution(s.session, () => call(commands.debugStepInTarget(s.session, s.thread, target))).catch((e) =>
void call(commands.debugStepInTarget(s.session, s.thread, target)).catch((e) =>
notifications().notify({
group: "run",
level: "error",
+5 -38
View File
@@ -644,7 +644,6 @@ 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);
}
@@ -653,7 +652,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 && useDebug.getState().active === id && frame?.path) {
if (reveal && frame?.path) {
void maybeEditorService()?.openFile(frame.path, {
selection: { line: frame.line, col: frame.column },
focus: false,
@@ -663,7 +662,6 @@ 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));
@@ -671,7 +669,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 || s.frames !== before?.frames) return;
if (!s || s.status !== "paused" || s.selectedThread !== threadId) 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")) {
@@ -709,21 +707,19 @@ async function loadThreads(id: DebugSessionId) {
/** Locals of `frameId` (first non-expensive scope) for inline values. */
async function loadLocals(id: DebugSessionId, frameId: number) {
const frames = useDebug.getState().sessions[id]?.frames;
const values: Record<string, string> = Object.create(null);
const values: Record<string, string> = {};
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 && !Object.hasOwn(values, v.name)) values[v.name] = v.value;
for (const v of vars) if (v.name && !(v.name in values)) values[v.name] = v.value;
}
} catch {
// No inline values.
}
const s = useDebug.getState().sessions[id];
if (useDebug.getState().active !== id || s?.status !== "paused" || s.selectedFrame !== frameId || s.frames !== frames)
return;
if (useDebug.getState().active !== id || s?.selectedFrame !== frameId) return;
useExecutionPoint.setState({ values });
}
@@ -733,7 +729,6 @@ 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];
@@ -762,32 +757,6 @@ 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) {
@@ -893,7 +862,6 @@ 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;
@@ -908,7 +876,6 @@ 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: [],
+2 -4
View File
@@ -13,7 +13,7 @@ import {
updateBreakpoint,
useBreakpoints,
} from "../breakpoints/store";
import { activeSession, addWatch, evaluate, isLive, startSession, useDebug, resumeExecution } from "../state";
import { activeSession, addWatch, evaluate, isLive, startSession, useDebug } from "../state";
import {
addMethodBreakpoint,
removeAllSpecial,
@@ -978,9 +978,7 @@ function SmartStepChooser() {
const pick = (targetId: number) => {
if (!state) return;
close();
void resumeExecution(state.session, () =>
call(commands.debugStepInTarget(state.session, state.thread, targetId)),
).catch((e) =>
void call(commands.debugStepInTarget(state.session, state.thread, targetId)).catch((e) =>
notifications().notify({ group: "run", level: "error", title: "Smart Step Into failed", body: errorText(e) }),
);
};
+1 -8
View File
@@ -538,14 +538,7 @@ export function activate(): () => void {
}),
// Detected configs carry the selected toolchains (interpreter, JDK, Maven home and options).
onIdeEvent("settingsChanged", (e) => {
if (
!e.key ||
e.key === "languages" ||
e.key.startsWith("languages.") ||
e.key === "cmake" ||
e.key.startsWith("cmake.")
)
scheduleReload();
if (e.key === "languages" || e.key.startsWith("languages.")) scheduleReload();
}),
);
@@ -1,46 +0,0 @@
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" },
},
]);
});
});
@@ -1,72 +0,0 @@
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,29 +84,6 @@ 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");
-4
View File
@@ -4,7 +4,6 @@
* 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";
@@ -83,7 +82,6 @@ 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. */
@@ -152,7 +150,6 @@ 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: [] },
@@ -257,7 +254,6 @@ 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),
@@ -1,4 +0,0 @@
.profile {
width: 100%;
text-align: left;
}
-151
View File
@@ -1,151 +0,0 @@
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>
);
}
-20
View File
@@ -3,7 +3,6 @@
* 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";
@@ -91,25 +90,6 @@ 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,
-38
View File
@@ -1,38 +0,0 @@
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",
),
),
})),
};
}
+1 -13
View File
@@ -5454,17 +5454,5 @@
"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}"
"Not a formatter XML file: {path}": "不是格式化 XML 文件:{path}"
}
-1
View File
@@ -80,7 +80,6 @@ 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