mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-13 15:41:36 -10:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a05a40e0a3 | ||
|
|
3b978721e3 | ||
|
|
9793857024 | ||
|
|
8fb3b7b084 | ||
|
|
22fe1702be | ||
|
|
ad434af78c | ||
|
|
6d103df5cf | ||
|
|
e2d089a0c4 | ||
|
|
47fa0e6f79 | ||
|
|
921f1f2b2d | ||
|
|
88f73630a6 | ||
|
|
504ffac10a | ||
|
|
5ee558d1a7 |
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();
|
||||
}
|
||||
|
||||
@@ -531,3 +531,67 @@ async fn renames_a_branch_with_the_clients_argument_names() {
|
||||
invoke(&peer, "close_project", json!({})).await.unwrap();
|
||||
peer.kill();
|
||||
}
|
||||
|
||||
/// A handler reads its arguments by the names the desktop client sends: `src/bindings.ts`
|
||||
/// passes them in camelCase, and a name the client never sends reads as `null` (an
|
||||
/// `Option` silently `None`, anything else an "invalid type: null" error).
|
||||
#[test]
|
||||
fn handlers_read_the_argument_names_the_client_sends() {
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::path::Path;
|
||||
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let bindings = std::fs::read_to_string(repo.join("src/bindings.ts")).unwrap();
|
||||
let mut sent = HashMap::<&str, BTreeSet<&str>>::new();
|
||||
for part in bindings.split("__TAURI_INVOKE(\"").skip(1) {
|
||||
let (command, rest) = part.split_once('"').unwrap();
|
||||
let args = rest.strip_prefix(", {").and_then(|r| r.split_once('}')).map_or("", |(args, _)| args);
|
||||
let names = args.split(',').map(|a| a.split(':').next().unwrap().trim()).filter(|a| !a.is_empty());
|
||||
sent.insert(command, names.collect());
|
||||
}
|
||||
fn sources(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
|
||||
for entry in std::fs::read_dir(dir).unwrap() {
|
||||
let path = entry.unwrap().path();
|
||||
if path.is_dir() {
|
||||
sources(&path, out);
|
||||
} else if path.extension().is_some_and(|e| e == "rs") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut files = Vec::new();
|
||||
sources(&repo.join("crates/ide-host/src"), &mut files);
|
||||
let mut checked = 0;
|
||||
let mut wrong = Vec::new();
|
||||
for file in files {
|
||||
let src = std::fs::read_to_string(&file).unwrap();
|
||||
// `.add("name", …)`, also with the name on the next line.
|
||||
let starts: Vec<usize> = src
|
||||
.match_indices(".add(")
|
||||
.map(|(i, m)| i + m.len())
|
||||
.filter_map(|i| src[i..].trim_start().strip_prefix('"').map(|rest| src.len() - rest.len()))
|
||||
.collect();
|
||||
for (n, &start) in starts.iter().enumerate() {
|
||||
let body = &src[start..starts.get(n + 1).copied().unwrap_or(src.len())];
|
||||
let (command, body) = body.split_once('"').unwrap();
|
||||
let Some(sent) = sent.get(command) else { continue };
|
||||
checked += 1;
|
||||
for reader in ["c.arg", "c.sender", "c.channel"] {
|
||||
for (at, _) in body.match_indices(reader) {
|
||||
let rest = &body[at + reader.len()..];
|
||||
let rest = match rest.strip_prefix("::<") {
|
||||
Some(generic) => &generic[generic.find(">(").unwrap() + 1..],
|
||||
None => rest,
|
||||
};
|
||||
let Some(name) = rest.strip_prefix("(\"").and_then(|r| r.split_once('"')).map(|(n, _)| n) else {
|
||||
continue;
|
||||
};
|
||||
if !sent.contains(name) {
|
||||
wrong.push(format!("{command} reads `{name}`; the client sends {sent:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(checked > 400, "found only {checked} handlers with client bindings");
|
||||
assert!(wrong.is_empty(), "{wrong:#?}");
|
||||
}
|
||||
@@ -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
|
||||
Loaded 100 of 147 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user