mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-13 15:41:36 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a05a40e0a3 | ||
|
|
3b978721e3 | ||
|
|
9793857024 | ||
|
|
8fb3b7b084 | ||
|
|
22fe1702be | ||
|
|
ad434af78c | ||
|
|
6d103df5cf | ||
|
|
e2d089a0c4 | ||
|
|
47fa0e6f79 | ||
|
|
921f1f2b2d | ||
|
|
88f73630a6 |
No files matched your search
@@ -6,7 +6,7 @@ use crate::rpc::Dispatcher;
|
||||
use ide_core::{IdeError, IdeResult, RequestId};
|
||||
use ide_sdk::Sdk;
|
||||
use ide_tools::jdk::{JdkReleases, JdkVendor};
|
||||
use ide_tools::{InstallEvent, ToolInfo, Tools};
|
||||
use ide_tools::{InstallEvent, ToolInfo, ToolUpdate, Tools};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// The installer, reporting the executables chosen in the window's settings.
|
||||
@@ -89,6 +89,16 @@ pub async fn tools_install(
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// Newer versions of the tools the IDE installed (release downloads and npm packages).
|
||||
/// Cancel with `cancel_request(request)`.
|
||||
pub async fn tools_updates(host: &Host, window: &str, request: RequestId) -> IdeResult<Vec<ToolUpdate>> {
|
||||
let tools = tools(host, window)?;
|
||||
let cancel = host.begin_request(request);
|
||||
let result = tokio::task::spawn_blocking(move || tools.updates(&cancel)).await.map_err(join_err);
|
||||
host.end_request(request);
|
||||
result?
|
||||
}
|
||||
|
||||
/// Removes a tool the IDE installed.
|
||||
pub async fn tools_uninstall(host: &Host, window: &str, name: String) -> IdeResult<ToolInfo> {
|
||||
let tools = tools(host, window)?;
|
||||
@@ -185,6 +195,7 @@ pub(crate) fn register(d: &mut Dispatcher) {
|
||||
tools_install(&c.host, &c.window, c.arg("name")?, c.arg("request")?, c.sender("onEvent")?).await
|
||||
});
|
||||
d.add("tools_uninstall", |c| async move { tools_uninstall(&c.host, &c.window, c.arg("name")?).await });
|
||||
d.add("tools_updates", |c| async move { tools_updates(&c.host, &c.window, c.arg("request")?).await });
|
||||
d.add("jdk_download_vendors", |c| async move { Ok(jdk_download_vendors(&c.host, &c.window)) });
|
||||
d.add("jdk_download_releases", |c| async move {
|
||||
jdk_download_releases(&c.host, &c.window, c.arg("vendor")?, c.arg("request")?).await
|
||||
|
||||
@@ -1642,6 +1642,9 @@ impl HostApi for PluginApi {
|
||||
) -> Result<(), HostError> {
|
||||
self.set_decorations(path, set, Vec::new(), item, true)
|
||||
}
|
||||
fn file_icon_decorations(&self, set: &str, items: Vec<(PathBuf, String)>) -> Result<(), HostError> {
|
||||
self.set_file_icon_decorations(set, items)
|
||||
}
|
||||
fn vcs_log(
|
||||
&self,
|
||||
root: &Path,
|
||||
@@ -4124,6 +4127,9 @@ impl PluginApi {
|
||||
{
|
||||
return Err(HostError::Invalid("a file note is one non-empty line of at most 200 characters".into()));
|
||||
}
|
||||
if let Some(icon) = file.as_ref().and_then(|f| f.icon.as_ref()) {
|
||||
ide_extension::validate_item_id(icon, "file icon id").map_err(HostError::Invalid)?;
|
||||
}
|
||||
items.retain(|d| decoration_style(&d.style));
|
||||
if items.iter().any(|d| d.range.start > d.range.end) {
|
||||
return Err(HostError::Invalid("reversed decoration range".into()));
|
||||
@@ -4132,24 +4138,14 @@ impl PluginApi {
|
||||
// The decoration follows the file, not the window's project: editors render
|
||||
// whatever they have open, including files outside every project root. The
|
||||
// set's root stays the path's own (deepest open) project for tree filtering.
|
||||
let root = self
|
||||
.shared
|
||||
.workspace()
|
||||
.map(|w| {
|
||||
w.projects()
|
||||
.into_iter()
|
||||
.filter(|root| path.starts_with(root))
|
||||
.max_by_key(|root| root.components().count())
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_else(|| path.parent().map(Path::to_path_buf).unwrap_or_else(|| path.to_path_buf()));
|
||||
let root = self.decoration_root(path);
|
||||
Some(super::DecorationSet {
|
||||
root: root.into(),
|
||||
path: ide_core::IpcPath(path.to_path_buf()),
|
||||
set_id: set.into(),
|
||||
tree,
|
||||
items,
|
||||
file: file.filter(|f| f.note.is_some() || decoration_style(&f.style)),
|
||||
file: file.filter(|f| f.note.is_some() || f.icon.is_some() || decoration_style(&f.style)),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -4177,6 +4173,60 @@ impl PluginApi {
|
||||
self.shared.sink.emit(None, PluginEvent::DecorationsChanged { plugin_id: self.id().into(), sets });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decoration_root(&self, path: &Path) -> PathBuf {
|
||||
self.shared
|
||||
.workspace()
|
||||
.map(|w| {
|
||||
w.projects()
|
||||
.into_iter()
|
||||
.filter(|root| path.starts_with(root))
|
||||
.max_by_key(|root| root.components().count())
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_else(|| path.parent().map(Path::to_path_buf).unwrap_or_else(|| path.to_path_buf()))
|
||||
}
|
||||
|
||||
fn set_file_icon_decorations(&self, set: &str, items: Vec<(PathBuf, String)>) -> Result<(), HostError> {
|
||||
if set.is_empty() || set.len() > 128 {
|
||||
return Err(HostError::Invalid("set id must be 1-128 bytes".into()));
|
||||
}
|
||||
if items.len() > 10_000 {
|
||||
return Err(HostError::Invalid("at most 10000 file icons per set".into()));
|
||||
}
|
||||
let mut by_path = BTreeMap::new();
|
||||
for (path, icon) in items {
|
||||
ide_extension::validate_item_id(&icon, "file icon id").map_err(HostError::Invalid)?;
|
||||
by_path.insert(path, icon);
|
||||
}
|
||||
let mut all = self.shared.decorations.lock();
|
||||
let mut sets = all.get(self.id()).cloned().unwrap_or_default();
|
||||
sets.retain(|s| !(s.tree && s.set_id == set));
|
||||
for (path, icon) in by_path {
|
||||
let root = self.decoration_root(&path);
|
||||
sets.push(super::DecorationSet {
|
||||
root: root.into(),
|
||||
path: ide_core::IpcPath(path),
|
||||
set_id: set.into(),
|
||||
tree: true,
|
||||
items: Vec::new(),
|
||||
file: Some(ide_plugin::FileDecoration {
|
||||
badge: None,
|
||||
style: String::new(),
|
||||
tooltip: None,
|
||||
note: None,
|
||||
icon: Some(icon),
|
||||
}),
|
||||
});
|
||||
}
|
||||
let names: BTreeSet<_> = sets.iter().filter(|s| s.tree).map(|s| &s.set_id).collect();
|
||||
if names.len() > 50 || serde_json::to_vec(&sets).map_err(failed)?.len() > 4 << 20 {
|
||||
return Err(HostError::Invalid("decoration sets exceed plugin limits".into()));
|
||||
}
|
||||
all.insert(self.id().into(), sets.clone());
|
||||
self.shared.sink.emit(None, PluginEvent::DecorationsChanged { plugin_id: self.id().into(), sets });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn decoration_style(style: &str) -> bool {
|
||||
matches!(
|
||||
@@ -4330,7 +4380,8 @@ mod workbench_tests {
|
||||
badge: Some("abc".into()),
|
||||
style: "info".into(),
|
||||
tooltip: None,
|
||||
note: None
|
||||
note: None,
|
||||
icon: None,
|
||||
})
|
||||
)
|
||||
.is_err()
|
||||
@@ -4340,6 +4391,7 @@ mod workbench_tests {
|
||||
style: String::new(),
|
||||
tooltip: Some("why".into()),
|
||||
note: Some(text.into()),
|
||||
icon: None,
|
||||
};
|
||||
api.file_decorations(Some(&path), "notes", Some(note("entry point"))).unwrap();
|
||||
let notes = |api: &PluginApi| {
|
||||
@@ -4353,6 +4405,18 @@ mod workbench_tests {
|
||||
for bad in ["", "two\nlines", &"x".repeat(201)] {
|
||||
assert!(api.file_decorations(Some(&path), "notes", Some(note(bad))).is_err(), "{bad:?}");
|
||||
}
|
||||
api.file_icon_decorations(
|
||||
"icons",
|
||||
vec![(path.clone(), "helm-chart".into()), (root.join("chart"), "helm-dir".into())],
|
||||
)
|
||||
.unwrap();
|
||||
let icons = api.shared.decoration_sets()["test"]
|
||||
.iter()
|
||||
.filter(|s| s.tree && s.set_id == "icons")
|
||||
.filter_map(|s| s.file.as_ref().and_then(|f| f.icon.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(icons.len(), 2);
|
||||
assert!(api.file_icon_decorations("icons", vec![(path.clone(), "BAD ID".into())]).is_err());
|
||||
api.shared.clear_plugin("test", true);
|
||||
assert!(api.shared.decoration_sets().is_empty());
|
||||
}
|
||||
|
||||
@@ -743,6 +743,7 @@ pub(super) fn custom_editor(pkg: &Package, decl: toml::Value) -> Result<UiDecl,
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct FileIconDecl {
|
||||
id: String,
|
||||
#[serde(default)]
|
||||
patterns: Vec<String>,
|
||||
icon: PathBuf,
|
||||
#[serde(default)]
|
||||
@@ -754,13 +755,19 @@ pub fn file_icon_asset(id: &str, dark: bool) -> String {
|
||||
format!("fileIcon/{id}/{}", if dark { "dark" } else { "light" })
|
||||
}
|
||||
|
||||
/// `[[contributes.fileIcons]]`: an icon for the files matching `patterns`,
|
||||
/// whatever icon theme is in use (the user's own file type icons still win).
|
||||
/// `[[contributes.fileIcons]]`: an icon for the files matching `patterns` (or
|
||||
/// assigned dynamically through `decorations.set-file-icon` when `patterns` is
|
||||
/// omitted in API 1.8), whatever icon theme is in use (the user's own file type
|
||||
/// icons still win).
|
||||
pub(super) fn file_icon(pkg: &Package, decl: toml::Value) -> Result<UiDecl, String> {
|
||||
let d: FileIconDecl = decode(FILE_ICONS, decl)?;
|
||||
validate_item_id(&d.id, "file icon id")?;
|
||||
let what = format!("file icon {:?}", d.id);
|
||||
file_patterns(&d.patterns, &what)?;
|
||||
if d.patterns.is_empty() {
|
||||
pkg.manifest.needs_api("contributes.fileIcons without patterns", 8)?;
|
||||
} else {
|
||||
file_patterns(&d.patterns, &what)?;
|
||||
}
|
||||
let mut files = Vec::new();
|
||||
let mut assets = Vec::new();
|
||||
for (path, dark) in [(Some(&d.icon), false), (d.icon_dark.as_ref(), true)] {
|
||||
@@ -769,8 +776,16 @@ pub(super) fn file_icon(pkg: &Package, decl: toml::Value) -> Result<UiDecl, Stri
|
||||
assets.push((file_icon_asset(&d.id, dark), svg));
|
||||
files.push(path.clone());
|
||||
}
|
||||
let json = serde_json::json!({ "id": d.id, "patterns": d.patterns, "dark": d.icon_dark.is_some() });
|
||||
let mut out = UiDecl::new(d.id.clone(), d.patterns.join(", "), json);
|
||||
let bytes = serde_json::to_vec(&assets).map_err(|e| e.to_string())?;
|
||||
let revision = blake3::hash(&bytes).to_hex().to_string();
|
||||
let json = serde_json::json!({
|
||||
"id": d.id,
|
||||
"patterns": d.patterns,
|
||||
"dark": d.icon_dark.is_some(),
|
||||
"revision": revision,
|
||||
});
|
||||
let label = if d.patterns.is_empty() { d.id.clone() } else { d.patterns.join(", ") };
|
||||
let mut out = UiDecl::new(d.id.clone(), label, json);
|
||||
out.files = files;
|
||||
out.assets = assets;
|
||||
Ok(out)
|
||||
|
||||
@@ -3760,6 +3760,43 @@ path = "templates/new.draw"
|
||||
assert!(f.host.web_page("web-demo", "draw").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_icons_without_patterns_require_api_1_8_and_project_assets() {
|
||||
let f = fixture(Limits::default());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let pkg = tmp.path().join("dynamic-icons");
|
||||
std::fs::create_dir_all(pkg.join("icons")).unwrap();
|
||||
std::fs::write(pkg.join("icons/helm-dir.svg"), r#"<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6"/></svg>"#)
|
||||
.unwrap();
|
||||
let write_manifest = |api: &str| {
|
||||
std::fs::write(
|
||||
pkg.join("plugin.toml"),
|
||||
format!(
|
||||
r#"id = "dynamic-icons"
|
||||
name = "Dynamic Icons"
|
||||
version = "0.1.0"
|
||||
engines.openidea = "{api}"
|
||||
|
||||
[[contributes.fileIcons]]
|
||||
id = "helm-dir"
|
||||
icon = "icons/helm-dir.svg"
|
||||
"#
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
write_manifest("^1.7");
|
||||
let err = f.host.install_from_dir(&pkg, &Consent::all()).unwrap_err();
|
||||
assert!(err.message.contains("1.8"), "{err:?}");
|
||||
|
||||
write_manifest("^1.8");
|
||||
let info = f.install(&pkg);
|
||||
assert!(info.error.is_none(), "{:?}", info.error);
|
||||
let assets = f.host.assets("dynamic-icons", "fileIcon/helm-dir/").unwrap();
|
||||
assert_eq!(assets.len(), 1);
|
||||
assert!(assets[0].1.contains("<circle"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_templates_fixture_installs_without_write_capabilities() {
|
||||
let f = fixture(Limits::default());
|
||||
|
||||
@@ -367,6 +367,7 @@ async fn remote_projects_get_plugin_views_and_custom_editors() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn api14_remote_calls_keep_browser_listeners_and_secrets_on_the_desktop() {
|
||||
use ide_core::{Diagnostic, Pos, Range, Severity};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let desktop = Arc::new(
|
||||
@@ -426,42 +427,70 @@ async fn api14_remote_calls_keep_browser_listeners_and_secrets_on_the_desktop()
|
||||
"severity": "warning", "message": message, "source": "remote-check", "code": null
|
||||
}]})
|
||||
};
|
||||
let publish = |path: &Path, source: &str, message: Option<&str>| {
|
||||
let items = message
|
||||
.map(|message| Diagnostic {
|
||||
range: Range::new(Pos::new(0, 1), Pos::new(0, 3)),
|
||||
severity: Severity::Warning,
|
||||
message: message.into(),
|
||||
source: Some(source.into()),
|
||||
code: None,
|
||||
unnecessary: false,
|
||||
deprecated: false,
|
||||
server: None,
|
||||
})
|
||||
.into_iter()
|
||||
.collect();
|
||||
host.project("main").unwrap().sink.emit(IdeEvent::Diagnostics {
|
||||
path: path.to_path_buf().into(),
|
||||
source: source.into(),
|
||||
items,
|
||||
});
|
||||
};
|
||||
let files = json!([
|
||||
problem(&remote_file, "remote warning"),
|
||||
problem(&other_file, "other warning"),
|
||||
problem(&outside_file, "outside warning")
|
||||
]);
|
||||
invoke(&peer, "mcp_report_problems", json!({"reset": true, "files": files})).await.unwrap();
|
||||
// UI reports are not the source of the plugin's host-owned snapshot.
|
||||
assert_eq!(invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap(), json!("[]"));
|
||||
publish(&remote_file, "remote-check", Some("remote warning"));
|
||||
publish(&other_file, "remote-check", Some("other warning"));
|
||||
publish(&outside_file, "remote-check", Some("outside warning"));
|
||||
|
||||
let problems = invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap();
|
||||
let problems = problems.as_str().unwrap();
|
||||
assert!(problems.contains("remote warning"), "{problems}");
|
||||
assert!(problems.contains("other warning"), "{problems}");
|
||||
assert!(!problems.contains("outside warning"), "{problems}");
|
||||
assert!(problems.contains("outside warning"), "{problems}");
|
||||
for path in ["remote.txt", remote_file.to_str().unwrap()] {
|
||||
let problems = invoke(&peer, "plugin_run_command", run("problems", Some(path))).await.unwrap();
|
||||
let problems = problems.as_str().unwrap();
|
||||
assert!(problems.contains("remote warning"), "{problems}");
|
||||
assert!(!problems.contains("other warning"), "{problems}");
|
||||
assert!(!problems.contains("outside warning"), "{problems}");
|
||||
assert!(problems.contains(remote_file.to_str().unwrap()), "{problems}");
|
||||
}
|
||||
assert_eq!(invoke(&peer, "plugin_run_command", run("problems", Some("missing.txt"))).await.unwrap(), json!("[]"));
|
||||
invoke(&peer, "mcp_report_problems", json!({"reset": false, "files": [{"path": remote_file, "items": []}]}))
|
||||
.await
|
||||
.unwrap();
|
||||
publish(&remote_file, "second-check", Some("second warning"));
|
||||
invoke(&peer, "mcp_report_problems", json!({"reset": true, "files": []})).await.unwrap();
|
||||
let problems = invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap();
|
||||
assert!(problems.as_str().unwrap().contains("remote warning"));
|
||||
assert!(problems.as_str().unwrap().contains("second warning"));
|
||||
publish(&remote_file, "remote-check", None);
|
||||
let problems = invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap();
|
||||
assert!(!problems.as_str().unwrap().contains("remote warning"));
|
||||
assert!(problems.as_str().unwrap().contains("second warning"));
|
||||
assert!(problems.as_str().unwrap().contains("other warning"));
|
||||
invoke(
|
||||
&peer,
|
||||
"mcp_report_problems",
|
||||
json!({"reset": true, "files": [problem(&remote_file, "replacement warning")]}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
publish(&remote_file, "second-check", Some("replacement warning"));
|
||||
let problems = invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap();
|
||||
assert!(problems.as_str().unwrap().contains("replacement warning"));
|
||||
assert!(!problems.as_str().unwrap().contains("other warning"));
|
||||
invoke(&peer, "mcp_report_problems", json!({"reset": true, "files": []})).await.unwrap();
|
||||
assert!(!problems.as_str().unwrap().contains("second warning"));
|
||||
assert!(problems.as_str().unwrap().contains("other warning"));
|
||||
publish(&remote_file, "second-check", None);
|
||||
publish(&other_file, "remote-check", None);
|
||||
publish(&outside_file, "remote-check", None);
|
||||
assert_eq!(invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap(), json!("[]"));
|
||||
invoke(&peer, "plugin_run_command", run("open-editor", Some("remote.txt"))).await.unwrap();
|
||||
invoke(&peer, "plugin_run_command", run("decorations", remote_file.to_str())).await.unwrap();
|
||||
@@ -513,11 +542,11 @@ async fn api14_remote_calls_keep_browser_listeners_and_secrets_on_the_desktop()
|
||||
stream.read_to_string(&mut response).await.unwrap();
|
||||
assert!(response.contains("You can close this tab"));
|
||||
assert_eq!(oauth.await.unwrap().unwrap(), json!("code=desktop%2Bcode"));
|
||||
invoke(&peer, "mcp_report_problems", json!({"reset": true, "files": files})).await.unwrap();
|
||||
publish(&remote_file, "remote-check", Some("remote warning"));
|
||||
invoke(&peer, "close_project", json!({})).await.unwrap();
|
||||
invoke(&peer, "open_project", json!({"path": project})).await.unwrap();
|
||||
assert_eq!(invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap(), json!("[]"));
|
||||
invoke(&peer, "mcp_report_problems", json!({"reset": true, "files": files})).await.unwrap();
|
||||
publish(&remote_file, "remote-check", Some("remote warning"));
|
||||
peer.kill();
|
||||
tokio::time::timeout(Duration::from_secs(5), server).await.unwrap().unwrap();
|
||||
let plugin_host = plugins.host().clone();
|
||||
@@ -537,7 +566,11 @@ async fn api14_remote_calls_keep_browser_listeners_and_secrets_on_the_desktop()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.as_deref(), Some("[]"));
|
||||
// Disconnect clears desktop reports, not the still-live host project snapshot.
|
||||
let problems = result.as_deref().unwrap();
|
||||
assert!(problems.contains("remote warning"), "{problems}");
|
||||
assert!(problems.contains("remote-check"), "{problems}");
|
||||
assert!(problems.contains(remote_file.to_str().unwrap()), "{problems}");
|
||||
plugins.host().shutdown();
|
||||
desktop.shutdown();
|
||||
}
|
||||
|
||||
@@ -234,6 +234,11 @@ pub trait HostApi: Send + Sync {
|
||||
let _ = (path, set, item);
|
||||
Err(HostError::Failed("decorations unavailable".into()))
|
||||
}
|
||||
/// Replace all `(path, icon)` entries of a tree decoration set at once (API 1.8).
|
||||
fn file_icon_decorations(&self, set: &str, items: Vec<(PathBuf, String)>) -> Result<(), HostError> {
|
||||
let _ = (set, items);
|
||||
Err(HostError::Failed("decorations unavailable".into()))
|
||||
}
|
||||
/// Trusted repository history.
|
||||
fn vcs_log(
|
||||
&self,
|
||||
|
||||
@@ -932,6 +932,9 @@ pub struct FileDecoration {
|
||||
/// Text shown after the name (API 1.7 `set-file-note`).
|
||||
#[serde(default)]
|
||||
pub note: Option<String>,
|
||||
/// `[[contributes.fileIcons]]` id for this file or folder (API 1.8 `set-file-icon`).
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
/// Commit metadata, date in Unix seconds.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
||||
|
||||
@@ -2505,7 +2505,13 @@ impl bindings::openidea::plugin::decorations::Host for StoreData {
|
||||
.file_decorations(
|
||||
Some(&real),
|
||||
&set_id,
|
||||
Some(crate::FileDecoration { badge: item.badge, style: item.style, tooltip: item.tooltip, note: None }),
|
||||
Some(crate::FileDecoration {
|
||||
badge: item.badge,
|
||||
style: item.style,
|
||||
tooltip: item.tooltip,
|
||||
note: None,
|
||||
icon: None,
|
||||
}),
|
||||
)
|
||||
.map_err(to_wit))
|
||||
}
|
||||
@@ -2523,9 +2529,36 @@ impl bindings::openidea::plugin::decorations::Host for StoreData {
|
||||
Ok(real) => real,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
let item = crate::FileDecoration { badge: None, style: String::new(), tooltip, note: Some(note) };
|
||||
let item = crate::FileDecoration { badge: None, style: String::new(), tooltip, note: Some(note), icon: None };
|
||||
Ok(self.host.file_decorations(Some(&real), &set_id, Some(item)).map_err(to_wit))
|
||||
}
|
||||
fn set_file_icon(&mut self, path: String, set_id: String, icon: String, tooltip: Option<String>) -> Fallible<()> {
|
||||
if let Err(e) = self.require(Capability::ProjectRead) {
|
||||
return Ok(Err(e));
|
||||
}
|
||||
let real = match self.decoration_path(&path) {
|
||||
Ok(real) => real,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
let item = crate::FileDecoration { badge: None, style: String::new(), tooltip, note: None, icon: Some(icon) };
|
||||
Ok(self.host.file_decorations(Some(&real), &set_id, Some(item)).map_err(to_wit))
|
||||
}
|
||||
fn set_file_icons(&mut self, set_id: String, items: Vec<(String, String)>) -> Fallible<()> {
|
||||
if let Err(e) = self.require(Capability::ProjectRead) {
|
||||
return Ok(Err(e));
|
||||
}
|
||||
if items.len() > 10_000 {
|
||||
return Ok(Err(wit::Error::Invalid("at most 10000 file icons per set".into())));
|
||||
}
|
||||
let mut resolved = Vec::with_capacity(items.len());
|
||||
for (path, icon) in items {
|
||||
match self.decoration_path(&path) {
|
||||
Ok(real) => resolved.push((real, icon)),
|
||||
Err(e) => return Ok(Err(e)),
|
||||
}
|
||||
}
|
||||
Ok(self.host.file_icon_decorations(&set_id, resolved).map_err(to_wit))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod language_features_bindings {
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
6c50332c868a890b91a7f92ef7f92e76a4818b7435291e99c77c0b9f0931b81c
|
||||
e616b87b24371581ad648f9c7072259de7732d122b5887b488f7bd480ebd69c2
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
fdcb208365db90bcd06d0d0df3d436b69d1229c40f7c63dd6f1ab768e0d236b8
|
||||
75eeecc467bff141bda14f1f080633348af348f7259e793be81e6548d2008669
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
3108abe8d381b224ec4b44ecf4c43c045671bd5e221ee21582e25497f8c14dff
|
||||
4aaf7754e895f022d34dad05bd2b8018e3a61ba18771b8c95d9f070cb212530c
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
876a84b7da5a4b27fce47cf71a4dc28eb1ea6c9d54283c1b73a2ae2411653bf5
|
||||
e573144879a7caa0975e17b233476eb317ff54bef23fd8fa64280d3649bad700
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
234dc693e01e484b91fa7f2d0ac0e7757fb69c9fba81f06bf8ef1cd3624228d8
|
||||
36dd40ef751a617f1ad59491312889c12481b05666e580f21f7c2552df76edbc
|
||||
+2
-6
@@ -39,12 +39,8 @@ impl Plugin for Fixture {
|
||||
hover: None,
|
||||
after: Some(after.into()),
|
||||
};
|
||||
host::set_decorations(
|
||||
path,
|
||||
"fixture",
|
||||
&[item("typo", "typo message"), item("weak", "weak message")],
|
||||
)
|
||||
.map(|()| None)
|
||||
host::set_decorations(path, "fixture", &[item("typo", "typo message"), item("weak", "weak message")])
|
||||
.map(|()| None)
|
||||
}
|
||||
_ => Err(Error::NotFound(id.into())),
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
927c329fd21d12fd5f423b937e1be756be45a4c6dceee08679e762d5d9f511f2
|
||||
724ed3bbd828e4fc1925c1b555bdaec9268633cb587806a661f8838e5a540545
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
e7c77fde915e0765f91929dba2aa8eff606f2302e078b7305d272a4de5b9d80b
|
||||
1a3685234f1b3b0957184c44c603c0eefe7e3084b88d0df32da268b29292ee92
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
3feb9df8b408dedbc3586864db00b92c8cb244c8ad5b8f10c95edb8eb3d30df1
|
||||
4283de821d69a18438f58062e4bc28b63ba5025cc5f9fa51f21ff475b677a6a5
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
3bfce29f90b9d2f20082b9bdd0957c6c58c265a346305ec948f514537a4579c8
|
||||
0f25487b8e488e5ec01fabd2951bdb03d80bc26e4e9aa508394a7f9cd09d1759
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
ca72c5cfa2f76a93bc07495d50e58181a9c6c2fc75854cf548bb0278814ddb87
|
||||
80f9dc28e4f168dd22ccee1c53e1f6b08c446622eb10f07de29be5fc45e9fb1c
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
8a4a7df8fd6cf397750ac24cedf06174ed50e4214ef44029e29f7d0557d426ba
|
||||
5355cb4ef373eba3c54806eb00294fcd5fccb3cf05dae3e1c2a4ad45ab958f03
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
8d12313675e0869e087ac4c79bdfb1a9738867e28af2437eff27cb07254f572b
|
||||
fc75ede42e6db7619fa28fc8999f091d39e3c1ed401356db3a3f07de54287c99
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
1b2ad52d4cbb4abb662be5772f050844836f8e3f0c525edd0de19ff45e95a148
|
||||
8793601496f1727cf912e4cdf86ec48fc340a17585098e98dee52a125cb8a16d
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
13babf73d8d3d029aba26641b01a4add3abb3dab84394d8bd676edb064dc0c19
|
||||
fb2db14a645cad42941d20a25a3ff87ca6fdee635fdb40953383ff30ce16af8a
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
a7790bf28783d57ed47f635a58981b80ddaecd7074cf88c539465b57132a9f8f
|
||||
fc66b3b56a1ab1c9d547e3efe6bb4693dca908bb18be8b8ebd26f2fb41e4436c
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
af7070f6ca3a14cf480260aa21b7a6e95038996d224d40e53a487c96a8f3acf2
|
||||
c76830df9dee3caba932ffaddaabce88ac7ef23b3957b3e8e570b4939dd58d9d
|
||||
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
16930ecfe74d096e6dcaa5fbf35664128b4d3844f47993a947aabe9d2a9650de
|
||||
3a41a06d54939a9e85fcf6b0bf15b336f928a3caf66576bd2da62c5b8b177c1d
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
25eaf7ac2837ade4b3204698d3af41854324ece6ec196daba84bea6bb41f6cde
|
||||
f30a8c0a7893519872628c001314efd567f87f4dfaf718a737ea3631b55881b0
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
f7cd477be706ee5da2656ffb5435cb2b52443aec1270b3b1d804b2bdc6ed8bc8
|
||||
cb65533984ffd8684c9e9f5ddeef92a8f387735c12310b25282ee7bbf2d73cd7
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
c97989134a8e8a0a12013909f4726288b6d9cdaa44d02a58ff505a6cbb1fb9a1
|
||||
d11f9cb1130f3f3aaa89514bf47ec8ece9b2ca0edf1d82c11f80b262325f16be
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
67660f5ebbfdd3967fabcc0b5c41f215d4a38d9092b91eb0166f12bccfdffffa
|
||||
48ea4414469ebd70a89da8a5df7b7bddc4d32f5c50f1759ee3cea14558073c92
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
09c810ce7ebd820ea21525dc9faea99b13e7665b308cb131510e02b86e763c9f
|
||||
0c02303182be04ed3776fb532fd8b72e6057aa0fdb92b7d235000146fef4120e
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
9b3a210440560e05326947c7d549ce691315839daea2931c6a78720f7b0d9e7c
|
||||
8162a70e848172663463119e6357984a73e54bc71a99b2845621d0733cf9333e
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
57d8b00b5853ca789fe704f21e4394d1524dd73dfd27ea4a0b34f4b0e4f1a5f8
|
||||
4524cab0ceac631153a945f6b76f7d40efd3c854740ae22c64580381e9532397
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
53e355922819b8034c93070323c7eb2b1505d3bf561dd5de6abb930fe514748c
|
||||
a92f4def072ca4cc805414516ab4f046fc54bcf11394a5413a27ea151c156174
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
7af929a1adf5d89929d66e78068253ccde9ad5cedee329c2d4db6b7a5e29790e
|
||||
994fca61bc5a10f0229301406af76c188a9cb2f593db85a403c1ece746e9f7b5
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
511fab979382c5d64054ba4f5fdfc98d50e252dfd5896265ee589336bf8e7d70
|
||||
48eaf97f57cc08c3854a1effa37ebf29defca4782f76c7de1da6ef9a02c23441
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
444497128ec5e1e01229a93d2340b49ccc5bc27e67defb2c4fe19683042442a3
|
||||
b896043cf261ff4bb19977f4648a736907fa897b8d4f0804077678a5895d1845
|
||||
@@ -26,8 +26,10 @@ pub struct TestCaseReport {
|
||||
pub duration_ms: Option<f64>,
|
||||
/// Failure / error / skip message.
|
||||
pub message: Option<String>,
|
||||
/// Failure stack trace and captured output.
|
||||
/// Failure / error stack trace.
|
||||
pub output: Option<String>,
|
||||
/// What the test printed (`<system-out>`, `<system-err>`).
|
||||
pub stdout: Option<String>,
|
||||
}
|
||||
|
||||
fn decode(s: &str) -> String {
|
||||
@@ -124,11 +126,13 @@ pub fn parse_report(xml: &str) -> Vec<TestCaseReport> {
|
||||
if let Some(name) = tag.strip_prefix('/') {
|
||||
match name.trim() {
|
||||
"testcase" => out.extend(current.take()),
|
||||
"failure" | "error" | "rerunFailure" | "rerunError" | "skipped" | "system-out" | "system-err" => {
|
||||
name @ ("failure" | "error" | "rerunFailure" | "rerunError" | "skipped" | "system-out"
|
||||
| "system-err") => {
|
||||
if let (Some(t), Some(c)) = (text.take(), current.as_mut()) {
|
||||
let t = t.trim_matches('\n');
|
||||
if !t.trim().is_empty() {
|
||||
let o = c.output.get_or_insert_with(String::new);
|
||||
let field = if name.starts_with("system-") { &mut c.stdout } else { &mut c.output };
|
||||
let o = field.get_or_insert_with(String::new);
|
||||
if !o.is_empty() {
|
||||
o.push('\n');
|
||||
}
|
||||
@@ -160,6 +164,7 @@ pub fn parse_report(xml: &str) -> Vec<TestCaseReport> {
|
||||
duration_ms,
|
||||
message: None,
|
||||
output: None,
|
||||
stdout: None,
|
||||
};
|
||||
if self_closing {
|
||||
out.push(case);
|
||||
@@ -238,6 +243,8 @@ mod tests {
|
||||
<failure message="expected: <1> but was: <2>" type="org.opentest4j.AssertionFailedError"><![CDATA[org.opentest4j.AssertionFailedError: expected: <1> but was: <2>
|
||||
at com.example.demo.todo.TodoServiceTests.fails(TodoServiceTests.java:30)
|
||||
]]></failure>
|
||||
<system-out><![CDATA[printed before failing
|
||||
]]></system-out>
|
||||
</testcase>
|
||||
<testcase name="later" classname="com.example.demo.todo.TodoServiceTests" time="0">
|
||||
<skipped message="not yet"/>
|
||||
@@ -254,10 +261,13 @@ mod tests {
|
||||
assert_eq!(cases[0].status, TestCaseStatus::Passed);
|
||||
assert_eq!(cases[0].duration_ms, Some(31.0));
|
||||
assert_eq!(cases[1].name, "deleteReturnsFalseForUnknownId");
|
||||
assert_eq!(cases[1].output.as_deref(), Some("hello <world>"));
|
||||
assert_eq!(cases[1].stdout.as_deref(), Some("hello <world>"));
|
||||
assert_eq!(cases[1].output, None);
|
||||
assert_eq!(cases[2].status, TestCaseStatus::Failed);
|
||||
assert_eq!(cases[2].message.as_deref(), Some("expected: <1> but was: <2>"));
|
||||
assert!(cases[2].output.as_deref().unwrap().contains("TodoServiceTests.java:30"));
|
||||
assert!(!cases[2].output.as_deref().unwrap().contains("printed"));
|
||||
assert_eq!(cases[2].stdout.as_deref(), Some("printed before failing"));
|
||||
assert_eq!(cases[2].duration_ms, Some(1_234_500.0));
|
||||
assert_eq!(cases[3].status, TestCaseStatus::Skipped);
|
||||
assert_eq!(cases[3].message.as_deref(), Some("not yet"));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::discover::{project_venv_python, workspace_tsdk};
|
||||
use crate::exec::{path_with, search_path, which};
|
||||
use crate::npm::{has_ts_api, own_typescript};
|
||||
use crate::npm::{has_ts_api, native_typescript, own_typescript};
|
||||
use crate::{angular, astro, godot, pinned, vue};
|
||||
use crate::{bin_dir, exe, venv_of};
|
||||
use ide_core::settings::{InspectionLevel, InspectionSettings, LanguageServerSettings, Settings};
|
||||
@@ -722,6 +722,16 @@ pub fn tsdk(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
/// TypeScript 7's native executable when the selected TypeScript is one (see
|
||||
/// [`native_typescript`]); `None` for the bundled one and TypeScript 6 and older.
|
||||
pub fn native_tsdk(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||||
match s.languages.typescript.version.trim() {
|
||||
"" | "bundled" => None,
|
||||
"workspace" => native_typescript(&root.join("node_modules/typescript")),
|
||||
custom => native_typescript(&setting_path(root, custom)?),
|
||||
}
|
||||
}
|
||||
|
||||
const PYTHON_SERVERS: [&str; 7] = ["basedpyright", "pyright", "ty", "pyrefly", "pylsp", "jedi-language-server", "ruff"];
|
||||
|
||||
pub(crate) fn merge(base: &mut Value, layer: Value) {
|
||||
@@ -957,10 +967,15 @@ pub fn resolve_run_command(config: &mut RunConfig, root: &Path, s: &Settings) {
|
||||
}
|
||||
|
||||
pub fn configure_server(cfg: &mut LanguageServerConfig, root: &Path, server_root: &Path, s: &Settings) {
|
||||
let mut path_dirs: Vec<PathBuf> = Vec::new();
|
||||
if let Some(custom) = tool_command(root, s, &cfg.command) {
|
||||
// An executable chosen from a Node.js installation (`~/.nvm/versions/node/v26/bin/tsc`,
|
||||
// its `npx`) runs on that Node: its `#!/usr/bin/env node` finds the `node` beside it.
|
||||
if let Some(bin) = custom.parent().filter(|bin| bin.join(exe("node")).is_file()) {
|
||||
path_dirs.push(bin.to_path_buf());
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -1135,6 +1150,16 @@ pub fn configure_server(cfg: &mut LanguageServerConfig, root: &Path, server_root
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// TypeScript 7 serves LSP itself: it replaces the tsserver-based server.
|
||||
if matches!(name, "vtsls" | "typescript-language-server")
|
||||
&& let Some(native) = native_tsdk(server_root, s).or_else(|| native_tsdk(root, s))
|
||||
{
|
||||
cfg.command = native.to_string_lossy().into_owned();
|
||||
cfg.args = vec!["--lsp".into(), "--stdio".into()];
|
||||
cfg.initialization_options = None;
|
||||
cfg.tsserver_project = false;
|
||||
return;
|
||||
}
|
||||
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"
|
||||
|
||||
@@ -749,14 +749,26 @@ pub(crate) fn workspace_tsdk(root: &Path) -> Option<PathBuf> {
|
||||
|
||||
fn typescript(root: Option<&Path>) -> Vec<Sdk> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(lib) = root.and_then(workspace_tsdk) {
|
||||
// TypeScript 7 has no `tsserver.js`; its package directory stands for it (`native_tsdk`).
|
||||
let native = || {
|
||||
let package = root?.join("node_modules/typescript");
|
||||
crate::npm::native_typescript(&package).map(|_| package.join("lib"))
|
||||
};
|
||||
if let Some(lib) = root.and_then(workspace_tsdk).or_else(native) {
|
||||
let version = std::fs::read_to_string(lib.parent().unwrap_or(&lib).join("package.json"))
|
||||
.ok()
|
||||
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok())
|
||||
.and_then(|v| v.get("version").and_then(|v| v.as_str()).map(str::to_owned));
|
||||
out.push(Sdk {
|
||||
path: lib.clone().into(),
|
||||
name: format!("TypeScript {}", version.clone().unwrap_or_default()).trim().to_owned(),
|
||||
name: format!(
|
||||
"TypeScript {}{}",
|
||||
version.clone().unwrap_or_default(),
|
||||
if crate::npm::has_ts_api(&lib) { "" } else { " (native)" }
|
||||
)
|
||||
.replace(" ", " ")
|
||||
.trim()
|
||||
.to_owned(),
|
||||
version,
|
||||
source: "workspace".into(),
|
||||
home: lib.parent().map(Into::into),
|
||||
|
||||
@@ -10,6 +10,41 @@ pub(crate) fn has_ts_api(lib: &Path) -> bool {
|
||||
lib.join("typescript.js").is_file()
|
||||
}
|
||||
|
||||
/// The native executable of TypeScript 7 (`typescript` 7+, whose `tsc` is native, or
|
||||
/// `@typescript/native-preview`'s `tsgo`), from its package directory or its `lib`.
|
||||
/// These packages have no JavaScript API (`lib/typescript.js`); their platform package
|
||||
/// (`@typescript/typescript-darwin-arm64`) holds the executable, which serves LSP itself
|
||||
/// (`--lsp --stdio`).
|
||||
pub(crate) fn native_typescript(dir: &Path) -> Option<PathBuf> {
|
||||
let package = if dir.join("package.json").is_file() { dir } else { dir.parent()? };
|
||||
if has_ts_api(&package.join("lib")) {
|
||||
return None;
|
||||
}
|
||||
let manifest: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(package.join("package.json")).ok()?).ok()?;
|
||||
let (base, bin) = match manifest.get("name")?.as_str()? {
|
||||
"typescript" => ("typescript", "tsc"),
|
||||
"@typescript/native-preview" => ("native-preview", "tsgo"),
|
||||
_ => return None,
|
||||
};
|
||||
let os = match std::env::consts::OS {
|
||||
"macos" => "darwin",
|
||||
"windows" => "win32",
|
||||
os => os,
|
||||
};
|
||||
let arch = match std::env::consts::ARCH {
|
||||
"aarch64" => "arm64",
|
||||
"x86_64" => "x64",
|
||||
arch => arch,
|
||||
};
|
||||
let platform = format!("{base}-{os}-{arch}");
|
||||
// Next to the package (`node_modules/@typescript/…`) or nested in it.
|
||||
package
|
||||
.ancestors()
|
||||
.map(|a| a.join("node_modules/@typescript").join(&platform).join("lib").join(crate::exe(bin)))
|
||||
.find(|exe| exe.is_file())
|
||||
}
|
||||
|
||||
/// The usable TypeScript library Node resolves from an installed server package.
|
||||
pub(crate) fn own_typescript(package: &Path) -> Option<PathBuf> {
|
||||
package.ancestors().map(|a| a.join("node_modules/typescript/lib")).find(|l| has_ts_api(l))
|
||||
|
||||
@@ -1325,6 +1325,85 @@ fn custom_tool_path_replaces_the_command() {
|
||||
assert_eq!(cfg.command, "zls");
|
||||
}
|
||||
|
||||
/// A server picked from a Node.js installation (nvm's `bin/tsc`, `bin/npx`) is a
|
||||
/// `#!/usr/bin/env node` script: it runs on that installation's Node, ahead of the
|
||||
/// project's Node.js interpreter.
|
||||
#[test]
|
||||
fn a_server_chosen_from_a_node_installation_runs_on_that_node() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let v26 = tmp.path().join("nvm/versions/node/v26.4.0/bin");
|
||||
let v20 = tmp.path().join("node20/bin");
|
||||
for dir in [&v26, &v20] {
|
||||
std::fs::create_dir_all(dir).unwrap();
|
||||
std::fs::write(dir.join(exe("node")), "").unwrap();
|
||||
}
|
||||
std::fs::write(v26.join("tsc"), "#!/usr/bin/env node\n").unwrap();
|
||||
let mut s = Settings::default();
|
||||
s.languages.node.interpreter = v20.join(exe("node")).to_string_lossy().into_owned();
|
||||
s.tools.insert(
|
||||
"typescript-language-server".into(),
|
||||
ide_core::settings::ToolSettings { path: v26.join("tsc").to_string_lossy().into_owned() },
|
||||
);
|
||||
let mut cfg = server("typescript-language-server");
|
||||
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
||||
let path: Vec<std::path::PathBuf> = std::env::split_paths(&cfg.env["PATH"]).collect();
|
||||
assert_eq!(path[..2], [v26.clone(), v20.clone()]);
|
||||
|
||||
// Elsewhere, the project's Node.js interpreter comes first.
|
||||
s.tools.insert(
|
||||
"typescript-language-server".into(),
|
||||
ide_core::settings::ToolSettings { path: tmp.path().join("tools/tsc").to_string_lossy().into_owned() },
|
||||
);
|
||||
let mut cfg = server("typescript-language-server");
|
||||
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
||||
assert_eq!(std::env::split_paths(&cfg.env["PATH"]).next(), Some(v20));
|
||||
}
|
||||
|
||||
/// TypeScript 7 (`typescript` 7+, `@typescript/native-preview`) has no tsserver: choosing it
|
||||
/// runs its native executable's own language server instead of vtsls / typescript-language-server.
|
||||
#[test]
|
||||
fn typescript_7_runs_its_own_language_server() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let modules = tmp.path().join("node_modules");
|
||||
let os = match std::env::consts::OS {
|
||||
"macos" => "darwin",
|
||||
"windows" => "win32",
|
||||
os => os,
|
||||
};
|
||||
let arch = match std::env::consts::ARCH {
|
||||
"aarch64" => "arm64",
|
||||
"x86_64" => "x64",
|
||||
arch => arch,
|
||||
};
|
||||
std::fs::create_dir_all(modules.join("typescript/lib")).unwrap();
|
||||
std::fs::write(modules.join("typescript/package.json"), r#"{"name":"typescript","version":"7.0.2"}"#).unwrap();
|
||||
let native = modules.join(format!("@typescript/typescript-{os}-{arch}/lib")).join(exe("tsc"));
|
||||
std::fs::create_dir_all(native.parent().unwrap()).unwrap();
|
||||
std::fs::write(&native, "").unwrap();
|
||||
let mut s = Settings::default();
|
||||
s.languages.typescript.version = "workspace".into();
|
||||
for name in ["vtsls", "typescript-language-server"] {
|
||||
let mut cfg = server(name);
|
||||
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
||||
assert_eq!(Path::new(&cfg.command), native, "{name}");
|
||||
assert_eq!(cfg.args, ["--lsp", "--stdio"]);
|
||||
assert!(cfg.initialization_options.is_none());
|
||||
}
|
||||
// Picked as a directory (its `lib`, as for older versions), too.
|
||||
s.languages.typescript.version = modules.join("typescript/lib").to_string_lossy().into_owned();
|
||||
let mut cfg = server("vtsls");
|
||||
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
||||
assert_eq!(Path::new(&cfg.command), native);
|
||||
|
||||
// TypeScript 6 keeps the tsserver-based server, pointed at it.
|
||||
std::fs::write(modules.join("typescript/lib/typescript.js"), "").unwrap();
|
||||
std::fs::write(modules.join("typescript/lib/tsserver.js"), "").unwrap();
|
||||
s.languages.typescript.version = "workspace".into();
|
||||
let mut cfg = server("vtsls");
|
||||
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
|
||||
assert_eq!(cfg.command, "vtsls");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_godot_setting_applies_to_server_debugger_and_run_configurations() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -104,6 +104,17 @@ pub enum InstallEvent {
|
||||
Progress { fraction: Option<f64>, message: String },
|
||||
}
|
||||
|
||||
/// A newer version of a tool the IDE installed, as its install source offers it now.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolUpdate {
|
||||
pub name: String,
|
||||
/// The installed version.
|
||||
pub current: String,
|
||||
/// The version installing the tool again would get.
|
||||
pub latest: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Manifest {
|
||||
version: Option<String>,
|
||||
@@ -1437,6 +1448,77 @@ impl Tools {
|
||||
self.write_manifest(spec.name, &Manifest { version: Some(dl.version.clone()), source: Some(dl.url.clone()) })
|
||||
}
|
||||
|
||||
/// Newer versions of the tools the IDE installed into its tools folder, for the install
|
||||
/// sources that can be asked: release downloads (GitHub, Open VSX, Maven Central, …) and
|
||||
/// npm packages. A tool whose lookup fails (offline, rate limited) is left out.
|
||||
pub fn updates(&self, cancel: &CancellationToken) -> IdeResult<Vec<ToolUpdate>> {
|
||||
let installed: Vec<(&ToolSpec, Method, String)> = TOOLS
|
||||
.iter()
|
||||
.filter_map(|spec| {
|
||||
let method = self.install_method(spec);
|
||||
Some((spec, method, self.installed_version(spec, method)?))
|
||||
})
|
||||
.collect();
|
||||
let updates = std::thread::scope(|scope| {
|
||||
let lookups: Vec<_> = installed
|
||||
.iter()
|
||||
.map(|(spec, method, current)| {
|
||||
scope.spawn(move || match self.for_method(*method).latest_version(*method, cancel) {
|
||||
Ok(Some(latest)) if latest != *current => {
|
||||
Some(ToolUpdate { name: spec.name.into(), current: current.clone(), latest })
|
||||
}
|
||||
Ok(_) => None,
|
||||
Err(e) => {
|
||||
tracing::info!("update check for {}: {}", spec.name, e.message);
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
lookups.into_iter().filter_map(|lookup| lookup.join().ok().flatten()).collect()
|
||||
});
|
||||
if cancel.is_cancelled() {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
Ok(updates)
|
||||
}
|
||||
|
||||
/// The version of `spec` in the IDE's tools folder, when `method` (how it was installed)
|
||||
/// can tell the latest one.
|
||||
fn installed_version(&self, spec: &ToolSpec, method: Method) -> Option<String> {
|
||||
match method {
|
||||
Method::Release { .. } => {
|
||||
managed_bin_dir(&self.root, spec, self.platform).and_then(|dir| self.exe_in(&dir, spec.commands[0]))?;
|
||||
self.manifest(spec.name)?.version
|
||||
}
|
||||
// A pinned package (`typescript@6`) stays on its major version.
|
||||
Method::Npm { packages } if npm_name(packages[0]) == packages[0] => self.npm_package_version(packages[0]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The version installing with `method` would get now.
|
||||
fn latest_version(&self, method: Method, cancel: &CancellationToken) -> IdeResult<Option<String>> {
|
||||
match method {
|
||||
Method::Release { source, assets, .. } => {
|
||||
let http = Http::new(&self.system_path)?;
|
||||
Ok(Some(self.resolve_release(&http, source, assets, cancel)?.version))
|
||||
}
|
||||
Method::Npm { packages } => {
|
||||
let (manager, _) = self.npm_manager()?;
|
||||
let mut c = self.command(manager);
|
||||
// The registry the user's npm configuration names (a mirror, a proxy). A cold npm
|
||||
// on a slow registry takes several seconds per package, more when run side by side.
|
||||
c.args(["view", packages[0], "version"]).current_dir(&self.root);
|
||||
match run_quiet(c, Duration::from_secs(60)) {
|
||||
Some((true, out)) => Ok(out.lines().map(str::trim).rfind(|l| !l.is_empty()).map(String::from)),
|
||||
_ => Err(IdeError::unavailable(format!("cannot look up the latest {}", packages[0]))),
|
||||
}
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a tool the IDE installed.
|
||||
pub fn uninstall(&self, name: &str, langs: &[LanguageConfig]) -> IdeResult<ToolInfo> {
|
||||
match registry::find(name) {
|
||||
|
||||
@@ -935,3 +935,26 @@ fn a_cancelled_install_leaves_the_previous_one_in_place() {
|
||||
assert!(dest.join("new").exists() && !dest.join("old").exists());
|
||||
assert!(aside.join("old").exists(), "the previous install is moved aside");
|
||||
}
|
||||
|
||||
/// An npm tool the IDE installed is offered the version the user's npm registry has now.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn npm_tools_report_a_newer_registry_version() {
|
||||
let env = Env::new();
|
||||
let sdk = env._tmp.path().join("node-sdk/bin");
|
||||
let modules = env.root.join("node_modules");
|
||||
env.script(&modules.join(".bin"), "bash-language-server", "echo 5.4.0");
|
||||
fs::create_dir_all(modules.join("bash-language-server")).unwrap();
|
||||
fs::write(modules.join("bash-language-server/package.json"), r#"{"version":"5.4.0"}"#).unwrap();
|
||||
env.script(&sdk, "node", "exit 0");
|
||||
let tools = |latest: &str| {
|
||||
env.script(&sdk, "npm", &format!(r#"[ "$1 $2 $3" = "view bash-language-server version" ] && echo {latest}"#));
|
||||
env.tools().with_runtime(BTreeMap::from([("node".into(), sdk.join("node"))]), BTreeMap::new())
|
||||
};
|
||||
let cancel = CancellationToken::new();
|
||||
assert_eq!(
|
||||
tools("5.6.1").updates(&cancel).unwrap(),
|
||||
vec![ToolUpdate { name: "bash-language-server".into(), current: "5.4.0".into(), latest: "5.6.1".into() }]
|
||||
);
|
||||
assert_eq!(tools("5.4.0").updates(&cancel).unwrap(), vec![]);
|
||||
}
|
||||
@@ -51,7 +51,7 @@ RSpec has its own configuration type (**RSpec**) and a detected `rspec` configur
|
||||
|
||||
A test run opens a tab in the **Run** tool window with the test tree next to the console. Failed suites expand automatically.
|
||||
|
||||
- Click a test to see its own output in the console.
|
||||
- Click a test to see its own output in the console, or a class or suite to see its setup and teardown output followed by its tests'. **Show all output** above the console goes back to the whole run.
|
||||
- Double-click, `Enter` or `F4` jumps to the test's source (**Jump to Source**).
|
||||
- **Show Passed** and **Show Ignored** filter the tree. Passed tests are hidden by default; the filters are remembered.
|
||||
- Right-click a test or a selection for **Run '…'**, **Debug '…'** and **Jump to Source**.
|
||||
|
||||
@@ -47,7 +47,7 @@ RSpec 有自己的配置类型(**RSpec 测试**)和检测到的 `rspec` 配
|
||||
|
||||
测试运行会在 **运行** 工具窗口中打开一个标签页,控制台旁边是测试树。失败的测试套件会自动展开。
|
||||
|
||||
- 单击某个测试,控制台只显示它自己的输出。
|
||||
- 单击某个测试,控制台只显示它自己的输出;单击类或套件,先显示它自己的准备和清理输出,再显示其下各个测试的输出。控制台上方的 **显示所有输出** 可回到整个运行的输出。
|
||||
- 双击、按 `Enter` 或 `F4` 跳到测试源码(**跳转到源代码**)。
|
||||
- **显示已通过** 和 **显示已忽略** 用于筛选测试树。默认隐藏已通过的测试,筛选状态会被记住。
|
||||
- 右键单个测试或多选后,可以 **运行 '…'**、**调试 '…'** 或 **跳转到源代码**。
|
||||
|
||||
+84
-75
@@ -430,6 +430,11 @@ matching SDK. Released 1.7 plugins keep working. New in 1.8:
|
||||
annotations follow the file instead of the window that published them.
|
||||
`set_decorations` accepts files outside the open project for the same reason,
|
||||
and the `typo` / `weak` styles are no longer dropped.
|
||||
- `decorations.set-file-icon` / `set-file-icons` (`host::set_file_icon`,
|
||||
`set_file_icons`): give a file or plain folder one of the plugin's
|
||||
`[[contributes.fileIcons]]` icons by path, for icons that depend on where a
|
||||
file is or what it holds (a Helm chart folder, a Kustomize overlay). Such
|
||||
icons may omit `patterns`. Requires `project.read`.
|
||||
|
||||
### API 1.7
|
||||
|
||||
@@ -595,80 +600,81 @@ then run `openidea-plugin check`.
|
||||
|
||||
### The API
|
||||
|
||||
| Rust SDK | WIT | Notes |
|
||||
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Plugin::run_command(id, ctx, arg)` | export `commands.run` | `ctx` is the window: `ctx.project()`, `ctx.editor()`. An error shows _"<title> failed: …"_; the plugin keeps running. `"msg".into()` is `Error::Failed`; `Cancelled` and `Denied` don't count as the plugin failing. |
|
||||
| `Plugin::handle_events(events, ctx)` | export `event-handler.handle` | Batches of subscribed events, oldest first, per window. |
|
||||
| `Plugin::activate` / `deactivate` | export `lifecycle` | Per instance: after a crash or idle reclaim a new one is activated. |
|
||||
| `TextEditor::document()` → `{version, text}` | `editor.text-editor.document` | `selections()` are those when the command ran (0-based lines, UTF-16 columns). |
|
||||
| `Project::document`, `apply_document_edits` | `editor.{document,apply-edits}-at` | API 1.6: versioned access to an explicitly named open project document from background events. Requires `project.read` and `editor.read`/`editor.write`; stale edits change nothing. |
|
||||
| `WebSocket::{connect,send,drain,close}` | `net.websocket-*` | API 1.6: project-bound, bounded host-owned WSS transport with secret-filled headers, close state and event-driven wakeups. |
|
||||
| `Project::{upsert_data_source,remove_data_source,data_sources,set_data_source_credential,connect_data_source}` | `database` | API 1.4: owned project sources; requires `database.sources` and trust. See _Database sources_. |
|
||||
| `host::mcp_stdio_command()` | `mcp.stdio-command` | API 1.4: invoking project bridge argv, `None` while off. No URL/token. |
|
||||
| `McpProvider::call(tool, arguments, ctx)` | export `mcp-provider.call` | API 1.4: declared tools under plugin grants and command budget. See _Plugin MCP tools_. |
|
||||
| `host::open_editors()` | `editor.open-editors` | API 1.4: invoking window's path/language/dirty/active snapshots; requires `editor.read`. Commands/events of a window only. |
|
||||
| `host::open_editor(path, selection)` | `editor.open` | API 1.4: open/select a project file (`project.read`) or plugin-dir file; same path rules as `views.open-location`. Commands/events of a window only. |
|
||||
| `TextEditor::set_selections(ranges)`, `reveal(range)` | `editor.text-editor` | API 1.4: selections or scrolling in this window's already open editor; no capability. Commands/events only, UTF-16 coordinates. |
|
||||
| `host::set_decorations`, `clear_decorations` | `decorations.{set,clear}` | API 1.4: project-scoped editor annotations cleared on plugin stop. Requires `project.read`. |
|
||||
| `host::set_file_decoration`, `clear_file_decorations` | `decorations.{set-file,clear-file}` | API 1.4: project-tree badges/styles. Requires `project.read`. |
|
||||
| `host::set_file_note` | `decorations.set-file-note` | API 1.7: a note after the name in the project tree (one line, at most 200 characters). Requires `project.read`. |
|
||||
| `Project::search_text(query, limit)` | `workspace.project.search-text` | API 1.4: cancellable Find in Files with unsaved documents and project excludes, at most 10,000 matches. Requires `project.read`. |
|
||||
| `host::problems(path)` | `diagnostics.problems` | API 1.4: project Problems reports grouped by file/source, optional file filter. Requires `project.read` and an invoking window. Empty for remote projects. |
|
||||
| `Project::vcs_log(path, limit)` | `vcs.log` | API 1.4: newest first, at most 1000 commits, optional file history. Requires `vcs.read` and trust. |
|
||||
| `Project::vcs_blame(path)` | `vcs.blame` | API 1.4: attribution including unsaved editor text. Requires `vcs.read` and trust. |
|
||||
| `Project::vcs_diff(path, against)` | `vcs.diff` | API 1.4: disk working tree against HEAD/revision, optionally one file. None outside Git; fails above 4 MiB. Requires `vcs.read` and trust. |
|
||||
| `TextEditor::apply_edits(version, edits)` | `editor.text-editor.apply-edits` | One undoable change, only if the document is still at `version` — else `Error::Stale` and nothing changes. `transform_selections` retries once. |
|
||||
| `Project::root()`, `read_file(path)` | `workspace.project` | Absolute or project-relative; `..`/symlinks escaping the root are rejected; UTF-8 only, ≤ 16 MiB; unsaved editor contents win; a missing path is `Error::NotFound`. |
|
||||
| `Project::read_bytes(path)` | `workspace.project.read-bytes` | API 1.5: the exact bytes of a file inside the project (binary content is the point), at most 16 MiB, the file on disk. Same paths and `Error::NotFound` as `read_file`. |
|
||||
| `Project::exists(path)`, `stat(path)` | `workspace.project` | `exists` covers files and directories, hidden and ignored included, without reading content; `stat` gives kind, size and modified time (`Error::NotFound` for a missing path). |
|
||||
| `host::projects()` | `workspace.projects` | Every open project (for scheduled work without a window). |
|
||||
| `Project::vcs_status()`, `vcs_remotes()`, `vcs_fetch()` | `vcs` | `vcs_remotes` returns `(name, url)` pairs; fetch runs git, so only in trusted projects (call it inside a task). |
|
||||
| `host::pending_commit()`, `host::set_commit_message(revision, text)` | `commit` | The commit being prepared in the calling window and its message; see _Commits_. |
|
||||
| `host::subscribe(kind)` | `events.subscribe` | For the session, across idle reclaim (an event activates the plugin). Or declare `on-event:<kind>`. |
|
||||
| `host::begin_task(title, cancellable)` → `Task` | `tasks.begin` | Shown with the background tasks; `report`, `is_cancelled`; the call may run 60 s instead of 5 s while it is open; dropping it ends it. |
|
||||
| `host::storage::{get, set, delete, keys}` | `storage` | Survives restarts; the instance's memory does not. Deleted on uninstall. |
|
||||
| `host::set_status_item(id, item)`, `remove_status_item` | `ui` | `text`, `tooltip`, `command` (one of yours, run on click); at most 8; kept while the plugin is idle. |
|
||||
| `host::set_window_status_item(id, item)`, `remove_window_status_item(id)` | `ui.{set,remove}-window-status-item` | API 1.4: window-only override; removed on window close or plugin stop; removing falls back to the global item. Click runs its command in that window. |
|
||||
| `host::execute_command(id, arg)` → `Option<String>` | `command-runner.execute` | API 1.4: requires high-risk `ide.commands` for IDE actions and other plugins; this plugin's own commands are exempt. Stable IDE action or `plugin.<plugin-id>.<command-id>` in the invoking window. IDE actions return none; plugin commands return their result. Unknown ids are not-found; at most 8 nested calls, no re-entry into a waiting instance. |
|
||||
| `host::ipc_connect(name)` → `Connection` | `ipc.connect` | API 1.4: `write(bytes)`, `read(max_bytes, timeout_ms)`, `close()`; requires `ipc:<path>`, at most 8 connections, closed on drop or plugin stop. Runs on the project server in remote projects. |
|
||||
| `host::secret_set(name, value)`, `secret_delete(name)` | `secrets.{set,delete}` | API 1.4: requires `secrets` and a declared name. Never returns the value. |
|
||||
| `host::begin_loopback(path)` → `Loopback` | `auth.begin-loopback` | API 1.4: `redirect_uri()`, `wait(timeout_ms)` → raw query or none on timeout. Desktop-only, even for remote projects; no capability. At most 2 listeners; one-shot, closed on timeout, drop or stop. Waiting excludes the call budget. |
|
||||
| `host::output_append(channel, text)`, `output_clear`, `output_reveal` | `output.{append,clear,reveal}` | API 1.4: read-only Output tabs; 32 named channels per plugin, each a 1 MiB UTF-8 ring buffer. Reveal requires the invoking window. |
|
||||
| `host::set_context(key, value)`, `remove_context` | `ui.{set,remove}-context` | Sets `plugin.<id>.<key>` for `when` clauses in the window the call came from (`true`, `3.5`, `"text"`; `ContextValue`). Key 1–64 of `A-Za-z0-9_-`, 100 per plugin; gone when the plugin stops. |
|
||||
| `host::notify(level, title, body)` | `ui.show-notification` | Balloon in the window the command or events came from (every window otherwise). |
|
||||
| `Project::notify(level, title, body, actions)` | `ui.notify-project` | API 1.5: balloon only in a window of that project (the most recently focused one), with or without an invoking window; see _Notifications with commands_. |
|
||||
| `host::copy_to_clipboard(text)` | `ui.copy-to-clipboard` | Copies `text` to the system clipboard. Needs `clipboard`. |
|
||||
| `host::open_url(url)` | `ui.open-url` | Opens an `http://` or `https://` URL in the user's default browser. |
|
||||
| `host::pick(title, items)`, `pick_many` | `ui.pick`, `ui.pick-many` | Picks key(s) from `PickItem`s, or `None` when dismissed. From a command/event or code-action resolution in its editor window (else `invalid`); waiting is excluded from the budget. |
|
||||
| `host::input(prompt, value)` | `ui.input` | A one-line text box pre-filled with `value`; `None` when dismissed. Same rules as `pick`. |
|
||||
| `host::input_multiline(prompt, value)` | `ui.input-multiline` | A multi-line text box pre-filled with `value`; `None` when dismissed; Cmd/Ctrl+Enter confirms. Only from a command or event of a window. |
|
||||
| `host::confirm(title, body)` | `ui.confirm` | OK / Cancel; `true` on OK. Same rules as `pick`. |
|
||||
| `host::locale()` | `ui.locale` | The IDE's UI language as a BCP 47 tag (`en`, `fr`, `zh-CN`); no capability. See _Translations_. |
|
||||
| `host::log(level, msg)` | `log` | IDE log, tagged with the plugin id. `eprintln!` output is logged too and shown when the plugin crashes. |
|
||||
| `Project::find_files(glob, limit)` | `workspace.project.find-files` | Project-relative glob (`**/*Mapper.xml`); honours `.gitignore`; at most `limit` paths. |
|
||||
| `Project::symbols(query, limit)` | `workspace.symbols` | New in 1.3; `project.read`. Fuzzy in-memory search (no LSP): name, camelCase kind, container and UTF-16 location. Queries ≤ 256 UTF-8 bytes; limits capped at 1000. Snapshot notes below. |
|
||||
| `host::publish_diagnostics(path, items)` | `diagnostics.publish` | Replaces this plugin's problems for the file (editor squiggles and the Problems view); an empty list clears them. |
|
||||
| `host::fetch(request)` → `{status, headers, body}` | `net.fetch` | See _Network_. |
|
||||
| `Project::run_process(program, args, stdin)` → `{code, stdout, stderr}` | `process.run` | Only the pinned program of a `process:` grant; runs in the project root of a trusted project; up to 16 MiB of stdout (fails if exceeded); killed when the call is cancelled or times out. |
|
||||
| `host::run_command(cmd, project, stdin)` → `{code, stdout, stderr}` | `process.run-command` | API 1.3: same limits as `run_process`, with append-only environment variables; needs the pinned `process:<program>` grant. |
|
||||
| `host::start(cmd, project)` → `Child` | `process.start` | API 1.3: `poll(max_bytes)`, `write(text)`, `close_stdin()`, `wait(timeout_ms)`, `kill()`; at most 8 handles; dropping one kills its process. |
|
||||
| `host::refresh_view(view)` | `views.refresh` | The view (or custom editors) render again; call it when the content changed. |
|
||||
| `host::reveal_view(view)` | `views.reveal` | Brings one of your declared `[[contributes.views]]` to the front in the calling window. |
|
||||
| `host::post_view_message(view, text)` | `views.post-message` | To the script of the view's `html` body. |
|
||||
| `host::open_terminal(title, program, args)` | `views.open-terminal` | A terminal tab in the invoking window running the approved `process:<program>` in the project root: output that keeps coming, interactive sessions. |
|
||||
| `host::register_run_target(project, target)` | `run-targets.register` | API 1.4: project-scoped, read-only argv wrapper; requires its process grant, executable pin and project trust; remote projects run on the server. |
|
||||
| `host::unregister_run_target(project, id)` | `run-targets.unregister` | Withdraw this plugin's target; remembered prompt answers remain. |
|
||||
| `host::start_run(config, args, debug)` | `run.start` | API 1.4: start an owned configuration in this window's Run tool window; returns an id, followed by `run-exited`. Approved `process:` program; commands/events only. |
|
||||
| `host::open_terminal_session(key, title, cmd)` | `views.open-terminal-session` | API 1.4: keyed, pinned terminal in this window, append-only env; returns its operation id and later `terminal-exited`. |
|
||||
| `host::write_terminal(key, text)` | `views.write-terminal` | API 1.4: write only to this plugin's still-running terminal in this window; trusted project, commands/events only. |
|
||||
| `host::open_content_tab(tab)` | `views.open-content-tab` | API 1.4: the shared content editor protocol, without a web view; actions arrive as `content-tab-action`. |
|
||||
| `host::reply_content_tab(request_id, reply)` | `views.reply-content-tab` | API 1.4: reply to an owned action in the originating window. No capability; commands/events only. |
|
||||
| `host::open_diff(diff, file_name)` | `views.open-diff` | API 1.7: two texts in the invoking window's diff viewer; `file_name` (at most 256 characters) picks the highlighting. Commands/events only; each side at most 1 MiB. |
|
||||
| `RunConfigurations::configurations(root)` | export `run-configurations.configurations` | API 1.4: optional `plugin-run-configurations` world; SDK registration `run_configurations`. Full project list, refreshed on configuration listing. |
|
||||
| `TestResults::parse(config, chunk)` | export `test-results.parse` | API 1.4: optional `plugin-test-results` world; SDK registration `test_results`. Bounded output batches for `framework = "plugin"`; failures use built-in parsers. |
|
||||
| `TestResults::select_tests(config, tests)` | export `test-results.select-tests` | API 1.4: full replacement argv for test-tree name paths, or `None` when unsupported (the SDK default). Enables rerunning failed tests and running tree selections. |
|
||||
| `host::open_location(path, line, column)` | `views.open-location` | Opens a file inside the project in an editor of the invoking window, the caret at `line`/`column` (0-based, UTF-16): a view's links to code. Needs `project.read`. |
|
||||
| `host::setting(key)`, `setting_bool`, `setting_string`, `setting_number` | `settings.get` | The user's value (project over global) or the schema's default. |
|
||||
| Rust SDK | WIT | Notes |
|
||||
| -------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Plugin::run_command(id, ctx, arg)` | export `commands.run` | `ctx` is the window: `ctx.project()`, `ctx.editor()`. An error shows _"<title> failed: …"_; the plugin keeps running. `"msg".into()` is `Error::Failed`; `Cancelled` and `Denied` don't count as the plugin failing. |
|
||||
| `Plugin::handle_events(events, ctx)` | export `event-handler.handle` | Batches of subscribed events, oldest first, per window. |
|
||||
| `Plugin::activate` / `deactivate` | export `lifecycle` | Per instance: after a crash or idle reclaim a new one is activated. |
|
||||
| `TextEditor::document()` → `{version, text}` | `editor.text-editor.document` | `selections()` are those when the command ran (0-based lines, UTF-16 columns). |
|
||||
| `Project::document`, `apply_document_edits` | `editor.{document,apply-edits}-at` | API 1.6: versioned access to an explicitly named open project document from background events. Requires `project.read` and `editor.read`/`editor.write`; stale edits change nothing. |
|
||||
| `WebSocket::{connect,send,drain,close}` | `net.websocket-*` | API 1.6: project-bound, bounded host-owned WSS transport with secret-filled headers, close state and event-driven wakeups. |
|
||||
| `Project::{upsert_data_source,remove_data_source,data_sources,set_data_source_credential,connect_data_source}` | `database` | API 1.4: owned project sources; requires `database.sources` and trust. See _Database sources_. |
|
||||
| `host::mcp_stdio_command()` | `mcp.stdio-command` | API 1.4: invoking project bridge argv, `None` while off. No URL/token. |
|
||||
| `McpProvider::call(tool, arguments, ctx)` | export `mcp-provider.call` | API 1.4: declared tools under plugin grants and command budget. See _Plugin MCP tools_. |
|
||||
| `host::open_editors()` | `editor.open-editors` | API 1.4: invoking window's path/language/dirty/active snapshots; requires `editor.read`. Commands/events of a window only. |
|
||||
| `host::open_editor(path, selection)` | `editor.open` | API 1.4: open/select a project file (`project.read`) or plugin-dir file; same path rules as `views.open-location`. Commands/events of a window only. |
|
||||
| `TextEditor::set_selections(ranges)`, `reveal(range)` | `editor.text-editor` | API 1.4: selections or scrolling in this window's already open editor; no capability. Commands/events only, UTF-16 coordinates. |
|
||||
| `host::set_decorations`, `clear_decorations` | `decorations.{set,clear}` | API 1.4: project-scoped editor annotations cleared on plugin stop. Requires `project.read`. |
|
||||
| `host::set_file_decoration`, `clear_file_decorations` | `decorations.{set-file,clear-file}` | API 1.4: project-tree badges/styles. Requires `project.read`. |
|
||||
| `host::set_file_note` | `decorations.set-file-note` | API 1.7: a note after the name in the project tree (one line, at most 200 characters). Requires `project.read`. |
|
||||
| `host::set_file_icon`, `set_file_icons` | `decorations.{set-file-icon,set-file-icons}` | API 1.8: assign one of the plugin's declared `[[contributes.fileIcons]]` icons to a file or plain folder by path (individually or up to 10,000 entries per set). Requires `project.read`. |
|
||||
| `Project::search_text(query, limit)` | `workspace.project.search-text` | API 1.4: cancellable Find in Files with unsaved documents and project excludes, at most 10,000 matches. Requires `project.read`. |
|
||||
| `host::problems(path)` | `diagnostics.problems` | API 1.4: project Problems reports grouped by file/source, optional file filter. Requires `project.read` and an invoking window. Empty for remote projects. |
|
||||
| `Project::vcs_log(path, limit)` | `vcs.log` | API 1.4: newest first, at most 1000 commits, optional file history. Requires `vcs.read` and trust. |
|
||||
| `Project::vcs_blame(path)` | `vcs.blame` | API 1.4: attribution including unsaved editor text. Requires `vcs.read` and trust. |
|
||||
| `Project::vcs_diff(path, against)` | `vcs.diff` | API 1.4: disk working tree against HEAD/revision, optionally one file. None outside Git; fails above 4 MiB. Requires `vcs.read` and trust. |
|
||||
| `TextEditor::apply_edits(version, edits)` | `editor.text-editor.apply-edits` | One undoable change, only if the document is still at `version` — else `Error::Stale` and nothing changes. `transform_selections` retries once. |
|
||||
| `Project::root()`, `read_file(path)` | `workspace.project` | Absolute or project-relative; `..`/symlinks escaping the root are rejected; UTF-8 only, ≤ 16 MiB; unsaved editor contents win; a missing path is `Error::NotFound`. |
|
||||
| `Project::read_bytes(path)` | `workspace.project.read-bytes` | API 1.5: the exact bytes of a file inside the project (binary content is the point), at most 16 MiB, the file on disk. Same paths and `Error::NotFound` as `read_file`. |
|
||||
| `Project::exists(path)`, `stat(path)` | `workspace.project` | `exists` covers files and directories, hidden and ignored included, without reading content; `stat` gives kind, size and modified time (`Error::NotFound` for a missing path). |
|
||||
| `host::projects()` | `workspace.projects` | Every open project (for scheduled work without a window). |
|
||||
| `Project::vcs_status()`, `vcs_remotes()`, `vcs_fetch()` | `vcs` | `vcs_remotes` returns `(name, url)` pairs; fetch runs git, so only in trusted projects (call it inside a task). |
|
||||
| `host::pending_commit()`, `host::set_commit_message(revision, text)` | `commit` | The commit being prepared in the calling window and its message; see _Commits_. |
|
||||
| `host::subscribe(kind)` | `events.subscribe` | For the session, across idle reclaim (an event activates the plugin). Or declare `on-event:<kind>`. |
|
||||
| `host::begin_task(title, cancellable)` → `Task` | `tasks.begin` | Shown with the background tasks; `report`, `is_cancelled`; the call may run 60 s instead of 5 s while it is open; dropping it ends it. |
|
||||
| `host::storage::{get, set, delete, keys}` | `storage` | Survives restarts; the instance's memory does not. Deleted on uninstall. |
|
||||
| `host::set_status_item(id, item)`, `remove_status_item` | `ui` | `text`, `tooltip`, `command` (one of yours, run on click); at most 8; kept while the plugin is idle. |
|
||||
| `host::set_window_status_item(id, item)`, `remove_window_status_item(id)` | `ui.{set,remove}-window-status-item` | API 1.4: window-only override; removed on window close or plugin stop; removing falls back to the global item. Click runs its command in that window. |
|
||||
| `host::execute_command(id, arg)` → `Option<String>` | `command-runner.execute` | API 1.4: requires high-risk `ide.commands` for IDE actions and other plugins; this plugin's own commands are exempt. Stable IDE action or `plugin.<plugin-id>.<command-id>` in the invoking window. IDE actions return none; plugin commands return their result. Unknown ids are not-found; at most 8 nested calls, no re-entry into a waiting instance. |
|
||||
| `host::ipc_connect(name)` → `Connection` | `ipc.connect` | API 1.4: `write(bytes)`, `read(max_bytes, timeout_ms)`, `close()`; requires `ipc:<path>`, at most 8 connections, closed on drop or plugin stop. Runs on the project server in remote projects. |
|
||||
| `host::secret_set(name, value)`, `secret_delete(name)` | `secrets.{set,delete}` | API 1.4: requires `secrets` and a declared name. Never returns the value. |
|
||||
| `host::begin_loopback(path)` → `Loopback` | `auth.begin-loopback` | API 1.4: `redirect_uri()`, `wait(timeout_ms)` → raw query or none on timeout. Desktop-only, even for remote projects; no capability. At most 2 listeners; one-shot, closed on timeout, drop or stop. Waiting excludes the call budget. |
|
||||
| `host::output_append(channel, text)`, `output_clear`, `output_reveal` | `output.{append,clear,reveal}` | API 1.4: read-only Output tabs; 32 named channels per plugin, each a 1 MiB UTF-8 ring buffer. Reveal requires the invoking window. |
|
||||
| `host::set_context(key, value)`, `remove_context` | `ui.{set,remove}-context` | Sets `plugin.<id>.<key>` for `when` clauses in the window the call came from (`true`, `3.5`, `"text"`; `ContextValue`). Key 1–64 of `A-Za-z0-9_-`, 100 per plugin; gone when the plugin stops. |
|
||||
| `host::notify(level, title, body)` | `ui.show-notification` | Balloon in the window the command or events came from (every window otherwise). |
|
||||
| `Project::notify(level, title, body, actions)` | `ui.notify-project` | API 1.5: balloon only in a window of that project (the most recently focused one), with or without an invoking window; see _Notifications with commands_. |
|
||||
| `host::copy_to_clipboard(text)` | `ui.copy-to-clipboard` | Copies `text` to the system clipboard. Needs `clipboard`. |
|
||||
| `host::open_url(url)` | `ui.open-url` | Opens an `http://` or `https://` URL in the user's default browser. |
|
||||
| `host::pick(title, items)`, `pick_many` | `ui.pick`, `ui.pick-many` | Picks key(s) from `PickItem`s, or `None` when dismissed. From a command/event or code-action resolution in its editor window (else `invalid`); waiting is excluded from the budget. |
|
||||
| `host::input(prompt, value)` | `ui.input` | A one-line text box pre-filled with `value`; `None` when dismissed. Same rules as `pick`. |
|
||||
| `host::input_multiline(prompt, value)` | `ui.input-multiline` | A multi-line text box pre-filled with `value`; `None` when dismissed; Cmd/Ctrl+Enter confirms. Only from a command or event of a window. |
|
||||
| `host::confirm(title, body)` | `ui.confirm` | OK / Cancel; `true` on OK. Same rules as `pick`. |
|
||||
| `host::locale()` | `ui.locale` | The IDE's UI language as a BCP 47 tag (`en`, `fr`, `zh-CN`); no capability. See _Translations_. |
|
||||
| `host::log(level, msg)` | `log` | IDE log, tagged with the plugin id. `eprintln!` output is logged too and shown when the plugin crashes. |
|
||||
| `Project::find_files(glob, limit)` | `workspace.project.find-files` | Project-relative glob (`**/*Mapper.xml`); honours `.gitignore`; at most `limit` paths. |
|
||||
| `Project::symbols(query, limit)` | `workspace.symbols` | New in 1.3; `project.read`. Fuzzy in-memory search (no LSP): name, camelCase kind, container and UTF-16 location. Queries ≤ 256 UTF-8 bytes; limits capped at 1000. Snapshot notes below. |
|
||||
| `host::publish_diagnostics(path, items)` | `diagnostics.publish` | Replaces this plugin's problems for the file (editor squiggles and the Problems view); an empty list clears them. |
|
||||
| `host::fetch(request)` → `{status, headers, body}` | `net.fetch` | See _Network_. |
|
||||
| `Project::run_process(program, args, stdin)` → `{code, stdout, stderr}` | `process.run` | Only the pinned program of a `process:` grant; runs in the project root of a trusted project; up to 16 MiB of stdout (fails if exceeded); killed when the call is cancelled or times out. |
|
||||
| `host::run_command(cmd, project, stdin)` → `{code, stdout, stderr}` | `process.run-command` | API 1.3: same limits as `run_process`, with append-only environment variables; needs the pinned `process:<program>` grant. |
|
||||
| `host::start(cmd, project)` → `Child` | `process.start` | API 1.3: `poll(max_bytes)`, `write(text)`, `close_stdin()`, `wait(timeout_ms)`, `kill()`; at most 8 handles; dropping one kills its process. |
|
||||
| `host::refresh_view(view)` | `views.refresh` | The view (or custom editors) render again; call it when the content changed. |
|
||||
| `host::reveal_view(view)` | `views.reveal` | Brings one of your declared `[[contributes.views]]` to the front in the calling window. |
|
||||
| `host::post_view_message(view, text)` | `views.post-message` | To the script of the view's `html` body. |
|
||||
| `host::open_terminal(title, program, args)` | `views.open-terminal` | A terminal tab in the invoking window running the approved `process:<program>` in the project root: output that keeps coming, interactive sessions. |
|
||||
| `host::register_run_target(project, target)` | `run-targets.register` | API 1.4: project-scoped, read-only argv wrapper; requires its process grant, executable pin and project trust; remote projects run on the server. |
|
||||
| `host::unregister_run_target(project, id)` | `run-targets.unregister` | Withdraw this plugin's target; remembered prompt answers remain. |
|
||||
| `host::start_run(config, args, debug)` | `run.start` | API 1.4: start an owned configuration in this window's Run tool window; returns an id, followed by `run-exited`. Approved `process:` program; commands/events only. |
|
||||
| `host::open_terminal_session(key, title, cmd)` | `views.open-terminal-session` | API 1.4: keyed, pinned terminal in this window, append-only env; returns its operation id and later `terminal-exited`. |
|
||||
| `host::write_terminal(key, text)` | `views.write-terminal` | API 1.4: write only to this plugin's still-running terminal in this window; trusted project, commands/events only. |
|
||||
| `host::open_content_tab(tab)` | `views.open-content-tab` | API 1.4: the shared content editor protocol, without a web view; actions arrive as `content-tab-action`. |
|
||||
| `host::reply_content_tab(request_id, reply)` | `views.reply-content-tab` | API 1.4: reply to an owned action in the originating window. No capability; commands/events only. |
|
||||
| `host::open_diff(diff, file_name)` | `views.open-diff` | API 1.7: two texts in the invoking window's diff viewer; `file_name` (at most 256 characters) picks the highlighting. Commands/events only; each side at most 1 MiB. |
|
||||
| `RunConfigurations::configurations(root)` | export `run-configurations.configurations` | API 1.4: optional `plugin-run-configurations` world; SDK registration `run_configurations`. Full project list, refreshed on configuration listing. |
|
||||
| `TestResults::parse(config, chunk)` | export `test-results.parse` | API 1.4: optional `plugin-test-results` world; SDK registration `test_results`. Bounded output batches for `framework = "plugin"`; failures use built-in parsers. |
|
||||
| `TestResults::select_tests(config, tests)` | export `test-results.select-tests` | API 1.4: full replacement argv for test-tree name paths, or `None` when unsupported (the SDK default). Enables rerunning failed tests and running tree selections. |
|
||||
| `host::open_location(path, line, column)` | `views.open-location` | Opens a file inside the project in an editor of the invoking window, the caret at `line`/`column` (0-based, UTF-16): a view's links to code. Needs `project.read`. |
|
||||
| `host::setting(key)`, `setting_bool`, `setting_string`, `setting_number` | `settings.get` | The user's value (project over global) or the schema's default. |
|
||||
|
||||
`Project::symbols` reads the current in-memory index: results may be partial
|
||||
while indexing, and locations can lag unsaved edits. Normal saves and filesystem
|
||||
@@ -867,7 +873,10 @@ Decorations are plain text, with fixed styles mapped to theme tokens:
|
||||
have `{ range, style, hover, after }`; tree items have `{ badge, style, tooltip }`,
|
||||
where a badge has at most two Unicode characters. A tree item can instead be a note
|
||||
(`set-file-note`, API 1.7): muted text after the file or folder name, one line of at
|
||||
most 200 characters, with an optional tooltip; it replaces that set's item for the file.
|
||||
most 200 characters, with an optional tooltip; or a dynamic icon (`set-file-icon` /
|
||||
`set-file-icons`, API 1.8): one of the plugin's own `[[contributes.fileIcons]]` ids
|
||||
(`patterns` may be omitted under `engines.openidea = "^1.8"`), applied to a file or
|
||||
plain folder by exact path; each replaces that set's item for the path.
|
||||
Paths must resolve inside an open project; absolute paths select that project, relative paths use the invoking
|
||||
project. Sets are isolated by plugin and project and shared by all windows of
|
||||
that project. Setting replaces a set's items for one file; clearing removes that
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
ec93db9b37437986167b4040714f5cda5cb7a2fc138e6c2dc2a9447faaf5a802
|
||||
56008cb5a36b35b8975b95da971212c95c165de378b0c8b7f7b428d9d9332ec1
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
78465a96e1e34ef3d4cf658c3b80f8388e43604dfe5abbf105f1149d040c5994
|
||||
d87e571ebe16283bbffcc7525f69755d5faffb4693ff4a5183de67418976086b
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
1bd90bca250bb06ab3b0f27b045110c722685d894ce79d23648cb7c795d2c033
|
||||
254da92cdb20bdf432b5bb4e90ef9bdfaff92eca7195fbff38127f47fa231e6e
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
173a8b66a3d5969bc51b2c97fcb435e2313555608aecbe1752d4586e3508d74f
|
||||
50ea437aa2b99dc28afd51286fbac7e07d8386f826f222c517a8bd6e4f647a1a
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
c4c86cef9512ac03193e7d89d39b6ee41665c2d4721a78f9415d406289e9ece1
|
||||
28584de67687b8e34376cd391a020d4c246fd98a3b904e15f413b56a650b5f06
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
6ddc2f9166452e4473cdaa9974943cb85ad4575457d2b3ca3d35abea65cb8b30
|
||||
335fa8a14e0d517ef1f735b02c49d8d902a57895c862ca8240854bb758796928
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
e97394224db64bcefb5ac43eba55eaba38f9d33122241313fae311ef8f0957db
|
||||
d5ddd6d4b5dc7983945163188c31a844cbcbbdfda3fb893afc5a3acca2e012bb
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
ca7266484d34f16de46b97e6317135984fdc4141099e50bdd52294617c2af9c0
|
||||
cdb81586fc7bd1043659a70b73241fa3111b9c86742029f018ca3a362cc3e9c2
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
0d7731de73514b950d8579762029d98c46136f55bfdb2fa44e256cc9a9955a0f
|
||||
de843867603a4d271af1b7159c3ce927236d1ab7575c5e8f12278b97910c8611
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
959cb2ab750211bd1cc894a4d804acb2dd16e95c1e65b267e7eccbb64cbdd8da
|
||||
9a79d41299571be7165a264b6733ae9974ef76af928d51097ac85238940cfd9c
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
d5ec377869ded4ec0bf533d07d51610aec7a6813d43ead98a261b3f54e7d0655
|
||||
bb6539e78a31a830edd6c59286c0f01d9b95418a20c8f492c962739e9b5f414a
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
ebbd24e08c19953895464567cbaf1335b9847e98e8a8da8a4e0b8c2886c1abd6
|
||||
84fce12270a15e21be616d264bd9dd6d515bffd38ec37e992232843c80e7a439
|
||||
BIN
Binary file not shown.
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user