дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
* feat(nav): Complete file paths and flag unresolved web file references Path completion in href/src/url()/@import, Markdown links and path-like strings, folders first. HTML and CSS paths that name no file get a 'Cannot resolve file' warning. File references resolve in the backend like IDEA's: relative to the file, /-rooted against the project and resource roots. * feat: Align editor, workbench, run/debug and VCS interactions with IntelliJ IDEA Keymap - Let AltGr characters through instead of firing Ctrl+Alt shortcuts (Windows/Linux) - Double Shift: strict press/release timing, ignored while typing or after Shift+click - Chords wait 2s and swallow an unmatched second stroke - Drop CodeMirror defaults that clash with IDEA keys; ⌘Enter is Split Line, add Scroll to Center and Code Block Start/End - Add missing IDEA defaults (⌃J, ⌘Y, ⇧⌘C, ⌥⌘Y, ⇧⌘M, F9 Resume, ⌘R Rerun, …) Editor - Tab inserts at the caret; smart Backspace to the proper indent; Enter continues a `//` comment; per-language line comment placement - Back/Forward records navigations only; ⌘F searches the selected text - In-place rename; ⌃Space inserts a sole candidate; commit characters only for a focused lookup; usage highlighting survives edits; ⌥Enter offers fixes from the caret line; ⌘-hover shows declaration info only for navigable targets Workbench - Tool windows: primary and split area per side, six Move To anchors, layout migration - ⌘W closes tool window content; Shift+Esc from the editor; closing a tab activates the left neighbour; preview tab and tab limit follow IDEA defaults - Switcher lists open editors and tool windows; Recent Files Delete removes items - Search Everywhere tab/scope behaviour; popups close when the window deactivates - Project view: Copy/Move dialogs on paste, skippable usage search on delete, new-file input cancels on blur; Settings search Esc clears the filter - Status bar breadcrumbs open a children popup; notification timeouts Run / Debug / Build / Terminal - Run asks to stop a running single-instance config; Rerun restarts silently; "Allow multiple instances" option - Run chooser: Enter runs, Shift debugs, F4/Delete/digits - Type into the console for stdin; Stop covers debug sessions - Log (Shift+click) and temporary (Alt+click) breakpoints; independent suspend/log options; Evaluate prefill and history; changed-value highlighting; expandable hints - Build window opens on failure and jumps to the first error - Terminal confirms closing a busy tab and lets IDE shortcuts through VCS - Correct ours/theirs labels during rebase, cherry-pick and revert - Smart/Force Checkout; remote branch checkout updates the local branch - Partial commit by changed block; inline before-commit check results - F7 continues into the next file; Rollback Lines; Commit and Push; Amend message swap - Branch delete with Restore; force push confirmation; editable push target; update summary with the incoming commits * chore: Regenerate bindings
293 рядки
12 KiB
Rust
293 рядки
12 KiB
Rust
use crate::apply::{
|
|
apply_to_run_configs, configure_server, jdtls_jdk, maven_command, maven_options, project_jdk, python_interpreter,
|
|
terminal_activation, tsdk,
|
|
};
|
|
use crate::python::parse_pip_list;
|
|
use crate::{bin_dir, exe, venv_of};
|
|
use ide_core::settings::Settings;
|
|
use ide_language::LanguageServerConfig;
|
|
use ide_run::RunConfig;
|
|
use std::collections::BTreeMap;
|
|
use std::path::Path;
|
|
|
|
fn fake_venv(root: &Path, name: &str) -> std::path::PathBuf {
|
|
let venv = root.join(name);
|
|
std::fs::create_dir_all(venv.join(bin_dir())).unwrap();
|
|
std::fs::write(venv.join("pyvenv.cfg"), "home = /usr/bin\nversion = 3.12.1\n").unwrap();
|
|
let py = venv.join(bin_dir()).join(exe("python"));
|
|
std::fs::write(&py, "").unwrap();
|
|
std::fs::write(venv.join(bin_dir()).join("activate"), "").unwrap();
|
|
py
|
|
}
|
|
|
|
fn server(name: &str) -> LanguageServerConfig {
|
|
LanguageServerConfig {
|
|
name: name.into(),
|
|
command: name.into(),
|
|
args: vec![],
|
|
env: BTreeMap::new(),
|
|
root_markers: vec![],
|
|
initialization_options: None,
|
|
install_hint: None,
|
|
fallback: false,
|
|
launch_error: None,
|
|
}
|
|
}
|
|
|
|
fn run(kind: &str, command: &str, args: &[&str]) -> RunConfig {
|
|
RunConfig {
|
|
id: format!("{kind}:x"),
|
|
name: "x".into(),
|
|
kind: kind.into(),
|
|
command: command.into(),
|
|
args: args.iter().map(|s| (*s).to_owned()).collect(),
|
|
cwd: None,
|
|
env: BTreeMap::new(),
|
|
detected: true,
|
|
is_test: false,
|
|
debug_adapter: None,
|
|
allow_parallel: false,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn project_venv_is_the_default_interpreter() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths and shells
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let s = Settings::default();
|
|
assert_eq!(python_interpreter(tmp.path(), &s), None);
|
|
let py = fake_venv(tmp.path(), ".venv");
|
|
assert_eq!(python_interpreter(tmp.path(), &s), Some(py.clone()));
|
|
assert_eq!(venv_of(&py), Some(tmp.path().join(".venv")));
|
|
let mut s = Settings::default();
|
|
s.languages.python.interpreter = "/opt/py/bin/python3".into();
|
|
assert_eq!(python_interpreter(tmp.path(), &s).unwrap(), Path::new("/opt/py/bin/python3"));
|
|
}
|
|
|
|
#[test]
|
|
fn pyright_gets_python_path_and_venv_env() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let py = fake_venv(tmp.path(), ".venv");
|
|
let mut cfg = server("basedpyright");
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &Settings::default());
|
|
let opts = serde_json::to_value(cfg.initialization_options.unwrap()).unwrap();
|
|
assert_eq!(opts["python"]["pythonPath"], py.to_string_lossy().as_ref());
|
|
assert_eq!(cfg.env["VIRTUAL_ENV"], tmp.path().join(".venv").to_string_lossy());
|
|
assert!(cfg.env["PATH"].starts_with(&*tmp.path().join(".venv").join(bin_dir()).to_string_lossy()));
|
|
}
|
|
|
|
#[test]
|
|
fn jdtls_vm_args() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut s = Settings::default();
|
|
s.languages.java.jdtls_vm_args = " -Xmx2g -javaagent:/opt/lombok.jar ".into();
|
|
let mut cfg = server("jdtls");
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
|
assert_eq!(cfg.args, ["--jvm-arg=-Xmx2g", "--jvm-arg=-javaagent:/opt/lombok.jar"]);
|
|
}
|
|
|
|
#[test]
|
|
fn rust_analyzer_check_command_and_toolchain() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths and shells
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut s = Settings::default();
|
|
s.languages.rust.check_command = "clippy".into();
|
|
s.languages.rust.stdlib = "/src/rust/library".into();
|
|
s.languages.rust.rustfmt_args = "--edition 2024".into();
|
|
let mut cfg = server("rust-analyzer");
|
|
cfg.initialization_options =
|
|
Some(toml::Value::try_from(serde_json::json!({ "cargo": { "features": "all" } })).unwrap());
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
|
let opts = serde_json::to_value(cfg.initialization_options.clone().unwrap()).unwrap();
|
|
assert_eq!(opts["check"]["command"], "clippy");
|
|
assert_eq!(opts["cargo"]["features"], "all", "existing options are kept");
|
|
assert_eq!(opts["rustfmt"]["extraArgs"], serde_json::json!(["--edition", "2024"]));
|
|
assert_eq!(cfg.env["RUST_SRC_PATH"], "/src/rust/library");
|
|
// Unchanged settings produce an identical config (no needless restarts).
|
|
let mut again = server("rust-analyzer");
|
|
again.initialization_options =
|
|
Some(toml::Value::try_from(serde_json::json!({ "cargo": { "features": "all" } })).unwrap());
|
|
configure_server(&mut again, tmp.path(), tmp.path(), &s);
|
|
assert_eq!(again, cfg);
|
|
}
|
|
|
|
#[test]
|
|
fn typescript_workspace_version() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let lib = tmp.path().join("node_modules/typescript/lib");
|
|
std::fs::create_dir_all(&lib).unwrap();
|
|
std::fs::write(lib.join("tsserver.js"), "").unwrap();
|
|
let mut s = Settings::default();
|
|
assert_eq!(tsdk(tmp.path(), &s), None);
|
|
s.languages.typescript.version = "workspace".into();
|
|
assert_eq!(tsdk(tmp.path(), &s), Some(lib.clone()));
|
|
let mut cfg = server("vtsls");
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
|
let opts = serde_json::to_value(cfg.initialization_options.unwrap()).unwrap();
|
|
assert_eq!(opts["typescript"]["tsdk"], lib.to_string_lossy().as_ref());
|
|
}
|
|
|
|
#[test]
|
|
fn run_configs_use_selected_interpreter_and_package_manager() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths and shells
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut s = Settings::default();
|
|
s.languages.python.interpreter = "/opt/py/bin/python3".into();
|
|
s.languages.node.package_manager = "pnpm".into();
|
|
let mut configs = vec![
|
|
run("python", "uv", &["run", "python", "-m", "app"]),
|
|
run("pytest", ".venv/bin/pytest", &[]),
|
|
run("pytest", "uv", &["run", "pytest", "-x"]),
|
|
run("npm", "npm", &["run", "dev"]),
|
|
run("shell", "python3", &["x.py"]),
|
|
];
|
|
apply_to_run_configs(&mut configs, tmp.path(), &s);
|
|
assert_eq!(configs[0].command, "/opt/py/bin/python3");
|
|
assert_eq!(configs[0].args, ["-m", "app"]);
|
|
assert_eq!(configs[1].command, "/opt/py/bin/python3");
|
|
assert_eq!(configs[1].args, ["-m", "pytest"]);
|
|
assert_eq!(configs[2].args, ["-m", "pytest", "-x"]);
|
|
assert_eq!(configs[3].command, "pnpm");
|
|
assert_eq!(configs[4].command, "python3", "shell configs are left alone");
|
|
}
|
|
|
|
fn fake_jdk(dir: &Path) {
|
|
std::fs::create_dir_all(dir.join("bin")).unwrap();
|
|
std::fs::write(dir.join("bin").join(exe("java")), "").unwrap();
|
|
std::fs::write(dir.join("release"), "JAVA_VERSION=\"17.0.9\"\n").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn project_jdk_and_maven_settings() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths and shells
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let jdk = tmp.path().join("jdk17");
|
|
fake_jdk(&jdk);
|
|
let maven = tmp.path().join("maven");
|
|
std::fs::create_dir_all(maven.join("bin")).unwrap();
|
|
std::fs::write(maven.join("bin/mvn"), "").unwrap();
|
|
|
|
let mut s = Settings::default();
|
|
assert_eq!(project_jdk(tmp.path(), &s), None);
|
|
assert_eq!(maven_command(tmp.path(), &s), None);
|
|
assert!(maven_options(tmp.path(), &s).is_empty());
|
|
|
|
// Project-relative paths, a JDK's `bin/java`, a Maven home.
|
|
s.languages.java.jdk = "jdk17/bin/java".into();
|
|
s.languages.java.maven.home = maven.to_string_lossy().into_owned();
|
|
s.languages.java.maven.user_settings = "/m2/settings.xml".into();
|
|
s.languages.java.maven.local_repository = "/m2/repo".into();
|
|
assert_eq!(project_jdk(tmp.path(), &s).map(|j| j.major), Some(17));
|
|
assert_eq!(maven_command(tmp.path(), &s), Some(maven.join("bin/mvn")));
|
|
assert_eq!(maven_options(tmp.path(), &s), ["-s", "/m2/settings.xml", "-Dmaven.repo.local=/m2/repo"]);
|
|
|
|
let mut configs = vec![run("maven", "./mvnw", &["package"]), run("gradle", "./gradlew", &["build"])];
|
|
apply_to_run_configs(&mut configs, tmp.path(), &s);
|
|
let home = project_jdk(tmp.path(), &s).unwrap().home.to_string_lossy().into_owned();
|
|
assert_eq!(configs[0].command, maven.join("bin/mvn").to_string_lossy());
|
|
assert_eq!(configs[0].args, ["-s", "/m2/settings.xml", "-Dmaven.repo.local=/m2/repo", "package"]);
|
|
assert_eq!(configs[0].env["JAVA_HOME"], home);
|
|
assert_eq!(configs[1].env["JAVA_HOME"], home);
|
|
apply_to_run_configs(&mut configs, tmp.path(), &s);
|
|
assert_eq!(configs[0].args.len(), 4, "applied once");
|
|
|
|
s.languages.java.jdk = "missing".into();
|
|
assert_eq!(project_jdk(tmp.path(), &s), None);
|
|
}
|
|
|
|
#[test]
|
|
fn jdtls_jdk_setting() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
fake_jdk(&tmp.path().join("jdk17"));
|
|
let mut s = Settings::default();
|
|
assert_eq!(jdtls_jdk(tmp.path(), &s), Ok(None));
|
|
// Separate from the project JDK, which run configurations keep using.
|
|
s.languages.java.jdk = "jdk17".into();
|
|
assert_eq!(jdtls_jdk(tmp.path(), &s), Ok(None));
|
|
s.languages.java.jdtls_jdk = "jdk17/bin".into();
|
|
assert_eq!(jdtls_jdk(tmp.path(), &s).unwrap().map(|j| j.major), Some(17));
|
|
// A chosen path that is not a JDK is an error, not a silent fallback.
|
|
s.languages.java.jdtls_jdk = "missing".into();
|
|
assert!(jdtls_jdk(tmp.path(), &s).unwrap_err().contains("is not a JDK home"));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_activation_for_project_venv() {
|
|
if cfg!(windows) {
|
|
return; // Unix paths and shells
|
|
}
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut s = Settings::default();
|
|
assert_eq!(terminal_activation(tmp.path(), &s, Some("/bin/zsh")), None);
|
|
fake_venv(tmp.path(), ".venv");
|
|
let line = terminal_activation(tmp.path(), &s, Some("/bin/zsh")).unwrap();
|
|
assert!(line.starts_with("source '") && line.ends_with("/activate'"), "{line}");
|
|
assert_eq!(terminal_activation(tmp.path(), &s, Some("/usr/bin/fish")), None, "no activate.fish");
|
|
assert_eq!(terminal_activation(tmp.path(), &s, Some("docker")), None, "not a shell");
|
|
s.languages.python.activate_virtualenv = false;
|
|
assert_eq!(terminal_activation(tmp.path(), &s, Some("/bin/zsh")), None);
|
|
}
|
|
|
|
#[test]
|
|
fn pip_list_output_is_parsed() {
|
|
let out = "$ python -m pip list\n[{\"name\": \"requests\", \"version\": \"2.32.0\"}]\n";
|
|
let pkgs = parse_pip_list(out).unwrap();
|
|
assert_eq!(pkgs[0].name, "requests");
|
|
assert_eq!(pkgs[0].version, "2.32.0");
|
|
}
|
|
|
|
#[test]
|
|
fn excluded_folders_become_anchored_globs() {
|
|
let mut s = Settings::default();
|
|
s.project_structure.excluded = vec!["build/".into(), " ".into()];
|
|
let globs = s.exclude_globs();
|
|
assert!(globs.contains(&"/build".to_owned()));
|
|
assert_eq!(globs.len(), s.file_excludes().len() + 1);
|
|
}
|
|
|
|
/// Prints what this machine has installed.
|
|
#[test]
|
|
#[ignore]
|
|
fn detect_on_this_machine() {
|
|
for kind in [
|
|
crate::SdkKind::Python,
|
|
crate::SdkKind::Node,
|
|
crate::SdkKind::PackageManager,
|
|
crate::SdkKind::Rust,
|
|
crate::SdkKind::Go,
|
|
] {
|
|
let t = std::time::Instant::now();
|
|
let found = crate::detect(kind, Some(Path::new(env!("CARGO_MANIFEST_DIR"))));
|
|
eprintln!("{kind:?} ({} ms)", t.elapsed().as_millis());
|
|
for sdk in found {
|
|
eprintln!(" {} {:?} [{}] {}", sdk.name, sdk.version, sdk.source, sdk.path.display());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn custom_tool_path_replaces_the_command() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let mut s = Settings::default();
|
|
s.tools.insert("elixir-ls".into(), ide_core::settings::ToolSettings { path: "tools/ls/language_server.sh".into() });
|
|
s.tools.insert("zls".into(), ide_core::settings::ToolSettings { path: " ".into() });
|
|
let mut cfg = server("elixir-ls");
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
|
assert_eq!(Path::new(&cfg.command), tmp.path().join("tools/ls/language_server.sh"));
|
|
// A blank path keeps auto-detection.
|
|
let mut cfg = server("zls");
|
|
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
|
assert_eq!(cfg.command, "zls");
|
|
}
|