дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
* feat(editor): Drag tabs and files between split panes
HTML5 drag and drop never reaches the webview while native file drop is
enabled, so tab, project-file and changelist drags did nothing. They now
run on pointer events:
- Drag an editor tab onto another pane, or onto a pane's edge to split it.
- Drag files from the Project view into an editor pane (center opens them
there, an edge opens them in a new split).
- Drag files between changelists in the Commit view.
- "Drag-and-drop with Alt pressed only" applies to tabs and stripe buttons.
* fix(lsp): Deliver server settings and start servers on a supported JDK
- workspace/configuration answers a server's own section (basedpyright)
from its settings block; basedpyright runs in "standard" mode and keeps
auto search paths on, so `src/` layouts resolve from `tests/`.
- jdtls: formatting follows the editor indent, calls complete without
guessed arguments, and JDK internals stay out of completion.
- `max-java` in a language server config picks an installed JDK no newer
than that (kotlin-language-server fails on JDK 25).
- Server initialization may take up to 5 minutes (first Gradle import).
- `lsp_resend_diagnostics` re-emits stored diagnostics after a reload.
- Spelling only checks declared names (not called methods like
`substring`) and accepts plurals of known software terms.
* feat(editor): Quick fixes, completion and navigation while typing
Quick fixes
- Errors get a red quick-fix bulb; Apply Quick Fix (⌥⇧↵) fixes the most
severe problem at the caret, and the error hover offers the fix directly.
- Code generated by a code action is re-indented to the file's style.
- Python: Implement abstract methods (⌃I or the bulb) from the bases.
- Duplicate code actions are listed once.
Completion
- Overloads sharing a name, and a name importable from several modules,
are no longer merged into one entry.
- Auto-import entries show the module they import from.
- Call snippets without a stop (`new ArrayList<>()`) leave the caret inside
the parentheses with parameter info, or past them when nothing takes
arguments; parameter info also opens after server call snippets.
- The selected item is resolved ahead of acceptance, so its auto-import
is applied (rust-analyzer).
Navigation and diagnostics
- Language server requests wait for pending edits, so Go to Declaration
right after typing answers for the current text; an empty answer right
after an edit is retried while the server catches up.
- Diagnostics come back after a window reload; error underlines stay
visible under typo / unused highlighting.
- Implementation gutter markers no longer refresh in a loop while the
server reports progress.
- A missing alternative server no longer shows as a server error.
* chore(lint): Skip agent worktrees under .claude
* fix(editor): Skip cancelled LSP requests and limit generated-code reformat
- Requests cancelled while waiting for pending edits are not sent.
- Code generated by an action is reformatted per changed hunk, so an added
import doesn't reformat the code between it and the generated lines.
- A long busy spell caused by a settle refresh re-runs it only once.
- Files dropped on a file row of another changelist move to that changelist.
- Python Implement methods: a nested class without bases ends the lookup,
class bodies continue through unindented string lines, and
`@abstractproperty` stubs become `@property`.
- Resend diagnostics goes through the host ops, and custom servers get
`max_java`.
* fix(editor): Rank the exact type first in Java constructor completion
jdtls appends the call or body to filterText (`String(${0})`); matching
on the label instead lets typing `String` rank `String` above
`StringBuilder` and the other types sharing its sort text.
* fix(editor): List identical completion items once
kotlin-language-server repeats a class once per classpath entry declaring
it, so `String()` and `String` were listed three times each.
* fix(editor): Keep the internal cancellation message out of the UI text catalog
554 рядки
24 KiB
Rust
554 рядки
24 KiB
Rust
//! Applying the selected interpreters / toolchains (`languages.*` settings) to
|
||
//! language servers, run configurations and terminals.
|
||
|
||
use crate::discover::{project_venv_python, workspace_tsdk};
|
||
use crate::exec::{path_with, search_path};
|
||
use crate::{bin_dir, exe, venv_of};
|
||
use crate::{godot, vue};
|
||
use ide_core::settings::{LanguageServerSettings, Settings};
|
||
use ide_language::{LanguageConfig, LanguageServerConfig};
|
||
use ide_run::RunConfig;
|
||
use serde_json::{Value, json};
|
||
use std::path::{Path, PathBuf};
|
||
|
||
fn non_empty(s: &str) -> Option<&str> {
|
||
let s = s.trim();
|
||
(!s.is_empty()).then_some(s)
|
||
}
|
||
|
||
/// Resolves `~/` and project-relative paths of a setting.
|
||
pub fn setting_path(root: &Path, s: &str) -> Option<PathBuf> {
|
||
let s = non_empty(s)?;
|
||
let p = match s.strip_prefix("~/") {
|
||
Some(rest) => crate::home_dir()?.join(rest),
|
||
None => PathBuf::from(s),
|
||
};
|
||
Some(if p.is_absolute() { p } else { root.join(p) })
|
||
}
|
||
|
||
/// The project interpreter: the configured one, else a project virtualenv
|
||
/// (`.venv`, `venv`, …) in `dir` or the project root.
|
||
pub fn python_interpreter(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||
setting_path(root, &s.languages.python.interpreter).or_else(|| project_venv_python(root))
|
||
}
|
||
|
||
/// The configured `node` executable's directory.
|
||
pub(crate) fn node_dir(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||
setting_path(root, &s.languages.node.interpreter).and_then(|p| p.parent().map(Path::to_path_buf))
|
||
}
|
||
|
||
/// The configured Rust toolchain's `bin` directory and rustup toolchain name.
|
||
pub(crate) fn rust_bin(root: &Path, s: &Settings) -> Option<(PathBuf, Option<String>)> {
|
||
let dir = setting_path(root, &s.languages.rust.toolchain)?;
|
||
let bin = if dir.join("bin").join(exe("rustc")).is_file() || dir.join("bin").join(exe("cargo")).is_file() {
|
||
dir.join("bin")
|
||
} else {
|
||
dir
|
||
};
|
||
let home = bin.parent();
|
||
let toolchain = home
|
||
.filter(|h| h.parent().is_some_and(|p| p.ends_with("toolchains")))
|
||
.and_then(|h| h.file_name())
|
||
.map(|n| n.to_string_lossy().into_owned());
|
||
Some((bin, toolchain))
|
||
}
|
||
|
||
pub(crate) fn goroot(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||
setting_path(root, &s.languages.go.goroot)
|
||
}
|
||
|
||
/// A JDK setting: a JDK home, its `bin` or its `bin/java`.
|
||
fn jdk_setting(root: &Path, value: &str) -> Option<Result<crate::java::Jdk, PathBuf>> {
|
||
let p = setting_path(root, value)?;
|
||
Some(p.ancestors().take(3).find_map(crate::java::jdk_at).ok_or(p))
|
||
}
|
||
|
||
/// The project JDK (`languages.java.jdk`).
|
||
pub fn project_jdk(root: &Path, s: &Settings) -> Option<crate::java::Jdk> {
|
||
jdk_setting(root, &s.languages.java.jdk)?
|
||
.inspect_err(|p| tracing::warn!("project JDK {} is not a JDK home", p.display()))
|
||
.ok()
|
||
}
|
||
|
||
/// The JDK chosen to run jdtls (`languages.java.jdtlsJdk`): `Ok(None)` when not
|
||
/// set, `Err` (the message) when it is not a JDK home.
|
||
pub fn jdtls_jdk(root: &Path, s: &Settings) -> Result<Option<crate::java::Jdk>, String> {
|
||
match jdk_setting(root, &s.languages.java.jdtls_jdk) {
|
||
None => Ok(None),
|
||
Some(Ok(jdk)) => Ok(Some(jdk)),
|
||
Some(Err(p)) => Err(format!(
|
||
"The language server JDK {} is not a JDK home (Settings › Languages & Frameworks › Java).",
|
||
p.display()
|
||
)),
|
||
}
|
||
}
|
||
|
||
/// `mvn` of the configured Maven home (`languages.java.maven.home`): the
|
||
/// executable itself, `<home>/bin/mvn` or `<bin>/mvn`.
|
||
pub fn maven_command(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||
let p = setting_path(root, &s.languages.java.maven.home)?;
|
||
let name = if cfg!(windows) { "mvn.cmd" } else { "mvn" };
|
||
let found = [p.clone(), p.join("bin").join(name), p.join(name)].into_iter().find(|c| c.is_file());
|
||
if found.is_none() {
|
||
tracing::warn!("Maven home {} has no {name}", p.display());
|
||
}
|
||
found
|
||
}
|
||
|
||
/// Maven options from settings, placed before the goals: `-s <settings.xml>`
|
||
/// and `-Dmaven.repo.local=<dir>`.
|
||
pub fn maven_options(root: &Path, s: &Settings) -> Vec<String> {
|
||
let m = &s.languages.java.maven;
|
||
let mut out = Vec::new();
|
||
if let Some(file) = setting_path(root, &m.user_settings) {
|
||
out.extend(["-s".to_owned(), file.to_string_lossy().into_owned()]);
|
||
}
|
||
if let Some(dir) = setting_path(root, &m.local_repository) {
|
||
out.push(format!("-Dmaven.repo.local={}", dir.to_string_lossy()));
|
||
}
|
||
out
|
||
}
|
||
|
||
/// The Maven local repository from settings: the configured one, else
|
||
/// `<localRepository>` of the configured user `settings.xml`; `None` = Maven's default.
|
||
pub fn maven_local_repository(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||
let m = &s.languages.java.maven;
|
||
setting_path(root, &m.local_repository).or_else(|| {
|
||
let text = std::fs::read_to_string(setting_path(root, &m.user_settings)?).ok()?;
|
||
let repo = crate::java::xml_value(&crate::lombok::strip_xml_comments(&text), "localRepository")?;
|
||
let home = crate::home_dir().map(|h| h.to_string_lossy().into_owned()).unwrap_or_default();
|
||
let repo = repo.replace("${user.home}", &home);
|
||
(!repo.is_empty() && !repo.contains("${")).then(|| PathBuf::from(repo))
|
||
})
|
||
}
|
||
|
||
/// TypeScript `lib` directory for the selected version; `None` = the server's bundled one.
|
||
pub fn tsdk(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||
match s.languages.typescript.version.trim() {
|
||
"" | "bundled" => None,
|
||
"workspace" => workspace_tsdk(root),
|
||
custom => {
|
||
let p = setting_path(root, custom)?;
|
||
Some(if p.extension().is_some_and(|e| e == "js") { p.parent()?.to_path_buf() } else { p })
|
||
}
|
||
}
|
||
}
|
||
|
||
const PYTHON_SERVERS: [&str; 5] = ["basedpyright", "pyright", "pylsp", "jedi-language-server", "ruff"];
|
||
|
||
fn merge(base: &mut Value, layer: Value) {
|
||
match (base, layer) {
|
||
(Value::Object(base), Value::Object(layer)) => {
|
||
for (k, v) in layer {
|
||
match base.get_mut(&k) {
|
||
Some(existing) => merge(existing, v),
|
||
None => {
|
||
base.insert(k, v);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
(base, layer) => *base = layer,
|
||
}
|
||
}
|
||
|
||
pub(crate) fn merge_init_options(cfg: &mut LanguageServerConfig, layer: Value) {
|
||
let mut current =
|
||
cfg.initialization_options.as_ref().and_then(|v| serde_json::to_value(v).ok()).unwrap_or_else(|| json!({}));
|
||
if !current.is_object() {
|
||
current = json!({});
|
||
}
|
||
merge(&mut current, layer);
|
||
match toml::Value::try_from(current) {
|
||
Ok(v) => cfg.initialization_options = Some(v),
|
||
Err(e) => tracing::warn!("{}: cannot apply settings to initialization options: {e}", cfg.name),
|
||
}
|
||
}
|
||
|
||
/// Adjusts a language server's launch configuration from settings: interpreter
|
||
/// paths, toolchain `PATH`, env (`GOROOT`, `RUST_SRC_PATH`, `VIRTUAL_ENV`) and
|
||
/// server settings (`python.pythonPath`, rust-analyzer `check.command`, `typescript.tsdk`).
|
||
/// `root` is the project root, `server_root` the server's workspace root.
|
||
/// The executable to launch for `command`: the one chosen in settings, else for
|
||
/// commands with well-known install locations (Godot) the one found there.
|
||
pub fn tool_command(root: &Path, s: &Settings, command: &str) -> Option<PathBuf> {
|
||
let custom = s.tool_path(command).and_then(|p| setting_path(root, p));
|
||
if command == godot::COMMAND {
|
||
return godot::find(custom);
|
||
}
|
||
custom
|
||
}
|
||
|
||
/// Points a run configuration launching a bare tool name at the executable from
|
||
/// [`tool_command`] (Godot apps live outside the search path).
|
||
pub fn resolve_run_command(config: &mut RunConfig, root: &Path, s: &Settings) {
|
||
if config.command == godot::COMMAND
|
||
&& let Some(path) = tool_command(root, s, &config.command)
|
||
{
|
||
config.command = path.to_string_lossy().into_owned();
|
||
}
|
||
}
|
||
|
||
pub fn configure_server(cfg: &mut LanguageServerConfig, root: &Path, server_root: &Path, s: &Settings) {
|
||
if let Some(custom) = tool_command(root, s, &cfg.command) {
|
||
cfg.command = custom.to_string_lossy().into_owned();
|
||
}
|
||
let mut path_dirs: Vec<PathBuf> = Vec::new();
|
||
let name = cfg.name.clone();
|
||
let name = name.as_str();
|
||
|
||
if PYTHON_SERVERS.contains(&name) {
|
||
let python = project_venv_python(server_root)
|
||
.filter(|_| non_empty(&s.languages.python.interpreter).is_none())
|
||
.or_else(|| python_interpreter(root, s));
|
||
if let Some(python) = python {
|
||
let p = python.to_string_lossy().into_owned();
|
||
match name {
|
||
"basedpyright" | "pyright" => merge_init_options(cfg, json!({ "python": { "pythonPath": p } })),
|
||
"pylsp" => merge_init_options(cfg, json!({ "pylsp": { "plugins": { "jedi": { "environment": p } } } })),
|
||
"jedi-language-server" => merge_init_options(cfg, json!({ "workspace": { "environmentPath": p } })),
|
||
_ => {}
|
||
}
|
||
if let Some(venv) = venv_of(&python) {
|
||
cfg.env.insert("VIRTUAL_ENV".into(), venv.to_string_lossy().into_owned());
|
||
path_dirs.push(venv.join(bin_dir()));
|
||
}
|
||
}
|
||
}
|
||
|
||
if name == "rust-analyzer" {
|
||
let r = &s.languages.rust;
|
||
let extra_args: Vec<&str> = r.rustfmt_args.split_whitespace().collect();
|
||
// Only non-default values: rust-analyzer's own defaults (and the
|
||
// language definition's options) stay untouched otherwise.
|
||
let mut layer = serde_json::Map::new();
|
||
if !r.check_on_save {
|
||
layer.insert("checkOnSave".into(), json!(false));
|
||
}
|
||
if let Some(cmd) = non_empty(&r.check_command).filter(|c| *c != "check") {
|
||
layer.insert("check".into(), json!({ "command": cmd }));
|
||
}
|
||
if !extra_args.is_empty() {
|
||
layer.insert("rustfmt".into(), json!({ "extraArgs": extra_args }));
|
||
}
|
||
if !layer.is_empty() {
|
||
merge_init_options(cfg, Value::Object(layer));
|
||
}
|
||
if let Some((bin, toolchain)) = rust_bin(root, s) {
|
||
path_dirs.push(bin);
|
||
if let Some(t) = toolchain {
|
||
cfg.env.insert("RUSTUP_TOOLCHAIN".into(), t);
|
||
}
|
||
}
|
||
if let Some(src) = setting_path(root, &r.stdlib) {
|
||
cfg.env.insert("RUST_SRC_PATH".into(), src.to_string_lossy().into_owned());
|
||
}
|
||
}
|
||
|
||
if name == "gopls" {
|
||
if let Some(goroot) = goroot(root, s) {
|
||
path_dirs.push(goroot.join("bin"));
|
||
cfg.env.insert("GOROOT".into(), goroot.to_string_lossy().into_owned());
|
||
}
|
||
if let Some(gopath) = setting_path(root, &s.languages.go.gopath) {
|
||
cfg.env.insert("GOPATH".into(), gopath.to_string_lossy().into_owned());
|
||
}
|
||
}
|
||
|
||
if name == "jdtls" {
|
||
// The jdtls launcher forwards `--jvm-arg=` values to the JVM it starts.
|
||
let args = s.languages.java.jdtls_vm_args.split_whitespace().map(|a| format!("--jvm-arg={a}"));
|
||
cfg.args.extend(args);
|
||
// m2e (the Maven import) reads mirrors, servers and `<localRepository>` from it.
|
||
if let Some(file) = setting_path(root, &s.languages.java.maven.user_settings) {
|
||
let file = file.to_string_lossy();
|
||
merge_init_options(
|
||
cfg,
|
||
json!({ "settings": { "java": { "configuration": { "maven": { "userSettings": file } } } } }),
|
||
);
|
||
}
|
||
}
|
||
|
||
if let Some(lib) = tsdk(server_root, s).or_else(|| tsdk(root, s)) {
|
||
let lib_s = lib.to_string_lossy().into_owned();
|
||
match name {
|
||
"vtsls" => merge_init_options(cfg, json!({ "typescript": { "tsdk": lib_s } })),
|
||
"typescript-language-server" => {
|
||
merge_init_options(cfg, json!({ "tsserver": { "path": lib.join("tsserver.js").to_string_lossy() } }))
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
let vue_command = s.tool_path(vue::SERVER).and_then(|p| setting_path(root, p));
|
||
let vue_command = vue_command.as_deref().map_or(vue::SERVER.into(), Path::to_string_lossy);
|
||
if name == "vtsls"
|
||
&& let Some(vue) = vue::server_package(&vue_command, &search_path())
|
||
{
|
||
let plugin = vue::tsserver_plugin(&vue);
|
||
merge_init_options(cfg, json!({ "vtsls": { "tsserver": { "globalPlugins": [plugin] } } }));
|
||
}
|
||
if name == vue::SERVER {
|
||
configure_vue_typescript(cfg, root, server_root, s);
|
||
}
|
||
|
||
// Node-based servers (`#!/usr/bin/env node`) run on the selected Node.js.
|
||
if let Some(dir) = node_dir(root, s) {
|
||
path_dirs.push(dir);
|
||
}
|
||
|
||
if !path_dirs.is_empty() {
|
||
let dirs: Vec<&Path> = path_dirs.iter().map(PathBuf::as_path).collect();
|
||
cfg.env.insert("PATH".into(), path_with(&dirs));
|
||
}
|
||
}
|
||
|
||
/// The servers `language` uses after Settings › Language Servers: the
|
||
/// definition's ones that aren't disabled, then the ones defined in settings for it.
|
||
pub fn language_servers(language: &str, defined: &[LanguageServerConfig], s: &Settings) -> Vec<LanguageServerConfig> {
|
||
let enabled = |name: &str| s.language_servers.get(name).is_none_or(|ls| ls.enabled);
|
||
let mut out: Vec<LanguageServerConfig> = defined.iter().filter(|c| enabled(&c.name)).cloned().collect();
|
||
for (name, ls) in &s.language_servers {
|
||
if ls.enabled && ls.languages.iter().any(|l| l == language) && !out.iter().any(|c| &c.name == name) {
|
||
out.push(custom_server(name, ls));
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Adds the servers defined in settings to the languages they serve (to list
|
||
/// them with the tools); disabled ones stay so they can be turned back on.
|
||
pub fn with_custom_servers(langs: &mut [LanguageConfig], s: &Settings) {
|
||
for (name, ls) in s.language_servers.iter().filter(|(_, ls)| ls.is_custom()) {
|
||
for lang in langs.iter_mut().filter(|l| ls.languages.iter().any(|id| id == l.id.as_str())) {
|
||
if !lang.language_servers.iter().any(|c| &c.name == name) {
|
||
lang.language_servers.push(custom_server(name, ls));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn custom_server(name: &str, ls: &LanguageServerSettings) -> LanguageServerConfig {
|
||
LanguageServerConfig {
|
||
name: name.to_owned(),
|
||
command: ls.command.trim().to_owned(),
|
||
args: Vec::new(),
|
||
env: Default::default(),
|
||
root_markers: ls.root_markers.clone(),
|
||
initialization_options: None,
|
||
settings: None,
|
||
install_hint: Some(match ls.command.trim() {
|
||
"" => format!("Set the command of `{name}` in Settings › Language Servers"),
|
||
command => format!("`{command}` was not found: install it, or set its path in Settings › Language Servers"),
|
||
}),
|
||
connect: None,
|
||
fallback: false,
|
||
max_java: None,
|
||
launch_error: None,
|
||
}
|
||
}
|
||
|
||
/// Command and arguments from Settings › Language Servers; applied before
|
||
/// [`configure_server`] so toolchain adjustments (a custom path, JVM args) still follow.
|
||
pub fn server_launch_overrides(cfg: &mut LanguageServerConfig, s: &Settings) {
|
||
let Some(ls) = s.language_servers.get(&cfg.name) else { return };
|
||
if let Some(command) = non_empty(ls.command.trim()) {
|
||
cfg.command = command.to_owned();
|
||
}
|
||
if let Some(args) = &ls.args {
|
||
cfg.args = args.clone();
|
||
}
|
||
}
|
||
|
||
/// Environment, `initializationOptions` and `workspace/configuration` settings
|
||
/// from Settings › Language Servers; applied last so they win over derived values.
|
||
pub fn server_protocol_overrides(cfg: &mut LanguageServerConfig, s: &Settings) {
|
||
let Some(ls) = s.language_servers.get(&cfg.name) else { return };
|
||
cfg.env.extend(ls.env.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||
if let Some(options) = &ls.initialization_options {
|
||
merge_init_options(cfg, options.clone());
|
||
}
|
||
if let Some(settings) = &ls.settings {
|
||
let mut current = cfg.settings.take().filter(Value::is_object).unwrap_or_else(|| json!({}));
|
||
merge(&mut current, settings.clone());
|
||
cfg.settings = Some(current);
|
||
}
|
||
}
|
||
|
||
/// vue-language-server loads TypeScript as a library (`--tsdk`, else its own
|
||
/// `require('typescript')`): the selected version, else its own, else the
|
||
/// project's — whichever still has the language-service API.
|
||
fn configure_vue_typescript(cfg: &mut LanguageServerConfig, root: &Path, server_root: &Path, s: &Settings) {
|
||
let selected = tsdk(server_root, s).or_else(|| tsdk(root, s)).filter(|l| vue::has_ts_api(l));
|
||
let package = vue::server_package(&cfg.command, &search_path());
|
||
let own = package.as_deref().and_then(vue::own_typescript);
|
||
let workspace = || workspace_tsdk(server_root).or_else(|| workspace_tsdk(root)).filter(|l| vue::has_ts_api(l));
|
||
let lib = match selected {
|
||
Some(lib) => Some(lib),
|
||
None if own.is_some() => None,
|
||
None => workspace(),
|
||
};
|
||
match lib {
|
||
Some(lib) => cfg.args.push(format!("--tsdk={}", lib.to_string_lossy())),
|
||
None if own.is_none() && package.is_some() => cfg.launch_error = Some(vue::missing_typescript_message()),
|
||
None => {}
|
||
}
|
||
}
|
||
|
||
fn is_python_command(cmd: &str) -> bool {
|
||
let name = Path::new(cmd).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||
let name = name.strip_suffix(".exe").unwrap_or(&name);
|
||
name == "python"
|
||
|| name == "python3"
|
||
|| name.strip_prefix("python3.").is_some_and(|v| v.chars().all(|c| c.is_ascii_digit()))
|
||
}
|
||
|
||
const PY_RUNNERS: [&str; 3] = ["uv", "poetry", "pdm"];
|
||
|
||
/// Points run configurations at the selected toolchains: Python configs use the
|
||
/// project interpreter, npm scripts the chosen package manager and Node.js,
|
||
/// cargo / go configs the selected toolchain, Maven / Gradle the project JDK
|
||
/// (and Maven the configured home and options).
|
||
pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings) {
|
||
let explicit_python = setting_path(root, &s.languages.python.interpreter);
|
||
let python = python_interpreter(root, s);
|
||
let pm = non_empty(&s.languages.node.package_manager);
|
||
let node = node_dir(root, s);
|
||
let rust = rust_bin(root, s);
|
||
let go = goroot(root, s);
|
||
let gopath = setting_path(root, &s.languages.go.gopath);
|
||
let jdk = project_jdk(root, s);
|
||
let mvn = maven_command(root, s);
|
||
let mvn_options = maven_options(root, s);
|
||
|
||
for c in configs.iter_mut() {
|
||
resolve_run_command(c, root, s);
|
||
let mut path_dirs: Vec<PathBuf> = Vec::new();
|
||
match c.kind.as_str() {
|
||
"python" | "pytest" => {
|
||
if let Some(py) = &explicit_python {
|
||
let runner = PY_RUNNERS.contains(&c.command.as_str());
|
||
if c.kind == "python" && (runner || is_python_command(&c.command)) {
|
||
if runner && c.args.first().map(String::as_str) == Some("run") {
|
||
// `uv run python …` → `<interpreter> …`
|
||
let skip = if c.args.get(1).is_some_and(|a| is_python_command(a)) { 2 } else { 1 };
|
||
c.args.drain(..skip.min(c.args.len()));
|
||
}
|
||
c.command = py.to_string_lossy().into_owned();
|
||
} else if c.kind == "pytest" && c.detected {
|
||
let rest: Vec<String> = match c.args.as_slice() {
|
||
[run, tool, rest @ ..] if run == "run" && tool == "pytest" => rest.to_vec(),
|
||
[m, tool, rest @ ..] if m == "-m" && tool == "pytest" => rest.to_vec(),
|
||
other => other.to_vec(),
|
||
};
|
||
c.command = py.to_string_lossy().into_owned();
|
||
c.args = ["-m".to_owned(), "pytest".to_owned()].into_iter().chain(rest).collect();
|
||
}
|
||
}
|
||
if let Some(venv) = python.as_deref().and_then(venv_of)
|
||
&& !c.env.contains_key("VIRTUAL_ENV")
|
||
{
|
||
c.env.insert("VIRTUAL_ENV".into(), venv.to_string_lossy().into_owned());
|
||
path_dirs.push(venv.join(bin_dir()));
|
||
}
|
||
}
|
||
"npm" => {
|
||
if let Some(pm) = pm
|
||
&& c.detected
|
||
&& matches!(c.command.as_str(), "npm" | "pnpm" | "yarn" | "bun")
|
||
{
|
||
c.command = pm.to_owned();
|
||
}
|
||
if let Some(dir) = &node {
|
||
path_dirs.push(dir.clone());
|
||
}
|
||
}
|
||
"cargo" => {
|
||
if let Some((bin, toolchain)) = &rust {
|
||
if c.command == "cargo" && bin.join(exe("cargo")).is_file() {
|
||
c.command = bin.join(exe("cargo")).to_string_lossy().into_owned();
|
||
}
|
||
path_dirs.push(bin.clone());
|
||
if let Some(t) = toolchain {
|
||
c.env.entry("RUSTUP_TOOLCHAIN".into()).or_insert_with(|| t.clone());
|
||
}
|
||
}
|
||
}
|
||
"go" => {
|
||
if let Some(goroot) = &go {
|
||
let bin = goroot.join("bin");
|
||
if c.command == "go" && bin.join(exe("go")).is_file() {
|
||
c.command = bin.join(exe("go")).to_string_lossy().into_owned();
|
||
}
|
||
c.env.entry("GOROOT".into()).or_insert_with(|| goroot.to_string_lossy().into_owned());
|
||
path_dirs.push(bin);
|
||
}
|
||
if let Some(gopath) = &gopath {
|
||
c.env.entry("GOPATH".into()).or_insert_with(|| gopath.to_string_lossy().into_owned());
|
||
}
|
||
}
|
||
"maven" | "gradle" => {
|
||
if let Some(jdk) = &jdk {
|
||
c.env.entry("JAVA_HOME".into()).or_insert_with(|| jdk.home.to_string_lossy().into_owned());
|
||
}
|
||
if c.kind == "maven" && c.detected {
|
||
if let Some(mvn) = &mvn {
|
||
c.command = mvn.to_string_lossy().into_owned();
|
||
}
|
||
if !c.args.starts_with(&mvn_options) {
|
||
c.args.splice(0..0, mvn_options.iter().cloned());
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
if !path_dirs.is_empty() && !c.env.contains_key("PATH") {
|
||
let dirs: Vec<&Path> = path_dirs.iter().map(PathBuf::as_path).collect();
|
||
c.env.insert("PATH".into(), path_with(&dirs));
|
||
}
|
||
}
|
||
}
|
||
|
||
fn sh_quote(s: &str) -> String {
|
||
format!("'{}'", s.replace('\'', r"'\''"))
|
||
}
|
||
|
||
/// Command typed into a new terminal to activate the project virtualenv
|
||
/// (PyCharm's Tools › Terminal › "Activate virtualenv"), for the given shell.
|
||
pub fn terminal_activation(root: &Path, s: &Settings, shell: Option<&str>) -> Option<String> {
|
||
if !s.languages.python.activate_virtualenv {
|
||
return None;
|
||
}
|
||
let venv = python_interpreter(root, s).as_deref().and_then(venv_of)?;
|
||
if std::env::var_os("VIRTUAL_ENV").is_some_and(|v| Path::new(&v) == venv) {
|
||
return None;
|
||
}
|
||
let shell = shell
|
||
.map(str::to_owned)
|
||
.or_else(|| std::env::var("SHELL").ok())
|
||
.or_else(|| std::env::var("COMSPEC").ok())
|
||
.unwrap_or_default();
|
||
let name = Path::new(&shell).file_stem().map(|n| n.to_string_lossy().to_lowercase()).unwrap_or_default();
|
||
let script = |file: &str| venv.join(bin_dir()).join(file);
|
||
let line = match name.as_str() {
|
||
"fish" => format!("source {}", sh_quote(&script("activate.fish").to_string_lossy())),
|
||
"csh" | "tcsh" => format!("source {}", sh_quote(&script("activate.csh").to_string_lossy())),
|
||
"nu" => format!("overlay use {}", sh_quote(&script("activate.nu").to_string_lossy())),
|
||
"pwsh" | "powershell" => format!("& '{}'", script("Activate.ps1").to_string_lossy().replace('\'', "''")),
|
||
"cmd" => format!("\"{}\"", script("activate.bat").to_string_lossy()),
|
||
"sh" | "bash" | "zsh" | "dash" | "ksh" | "mksh" | "ash" => {
|
||
format!("source {}", sh_quote(&script("activate").to_string_lossy()))
|
||
}
|
||
// Not a shell (e.g. `docker exec` in a terminal tab): type nothing.
|
||
_ => return None,
|
||
};
|
||
let target = match name.as_str() {
|
||
"fish" => "activate.fish",
|
||
"csh" | "tcsh" => "activate.csh",
|
||
"nu" => "activate.nu",
|
||
"pwsh" | "powershell" => "Activate.ps1",
|
||
"cmd" => "activate.bat",
|
||
_ => "activate",
|
||
};
|
||
script(target).is_file().then_some(line)
|
||
}
|