mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
feat(plugins): expose indexed workspace symbol search (#2040)
Closes #1944 API 1.3 lets a plugin search the IDE's workspace symbol index. Results are limited to paths the plugin may read under its `project.read` grant (external content roots and symlink aliases excluded), the search honours the call's cancellation and deadline including lock waits, and ranges use UTF-16. Incremental index updates no longer publish symlink-ancestor aliases. Verified on macOS with the test fixture. Co-authored-by: arnaud-ritti <[email protected]>
This commit is contained in:
1 parent
0bee9b7aab
commit
7cedf0b9be
71 files changed
+1157
-40
No files matched your search
@@ -51,6 +51,17 @@ pub trait PluginWorkspace: Send + Sync {
|
||||
fn indexed_files(&self, _root: &Path, _matches: &dyn Fn(&Path) -> bool, _limit: usize) -> Option<Vec<PathBuf>> {
|
||||
None
|
||||
}
|
||||
/// Current index of the project rooted at `root`; never reads disk or calls LSP.
|
||||
fn symbols(
|
||||
&self,
|
||||
_root: &Path,
|
||||
_query: &str,
|
||||
_limit: usize,
|
||||
_cancel: &CancellationToken,
|
||||
_deadline: Instant,
|
||||
) -> Result<Vec<ide_plugin::SymbolInfo>, HostError> {
|
||||
Err(HostError::NotFound("no project index is available".into()))
|
||||
}
|
||||
/// Version and text of the open document at `path`.
|
||||
fn document(&self, path: &Path) -> Option<(u64, String)>;
|
||||
/// Applies `edits` to the open document at `path` if it is still at
|
||||
@@ -1297,6 +1308,26 @@ impl HostApi for PluginApi {
|
||||
}
|
||||
}
|
||||
|
||||
fn symbols(
|
||||
&self,
|
||||
root: &Path,
|
||||
query: &str,
|
||||
limit: usize,
|
||||
cancel: &CancellationToken,
|
||||
deadline: Instant,
|
||||
) -> Result<Vec<ide_plugin::SymbolInfo>, HostError> {
|
||||
if query.len() > 256 {
|
||||
return Err(HostError::Invalid("symbol query exceeds 256 UTF-8 bytes".into()));
|
||||
}
|
||||
self.shared.workspace().ok_or_else(|| HostError::NotFound("no project index is available".into()))?.symbols(
|
||||
root,
|
||||
query,
|
||||
limit.min(1000),
|
||||
cancel,
|
||||
deadline,
|
||||
)
|
||||
}
|
||||
|
||||
fn find_files(&self, root: &Path, glob: &str, limit: usize) -> Result<Vec<String>, HostError> {
|
||||
let matcher = globset::GlobBuilder::new(glob)
|
||||
.literal_separator(true)
|
||||
@@ -1872,6 +1903,57 @@ impl PluginWorkspace for HostWorkspace {
|
||||
project.index.files_matching(root, matches, limit)
|
||||
}
|
||||
|
||||
fn symbols(
|
||||
&self,
|
||||
root: &Path,
|
||||
query: &str,
|
||||
limit: usize,
|
||||
cancel: &CancellationToken,
|
||||
deadline: Instant,
|
||||
) -> Result<Vec<ide_plugin::SymbolInfo>, HostError> {
|
||||
let project = HostWorkspace::projects(self)
|
||||
.into_iter()
|
||||
.find(|p| p.root == root)
|
||||
.ok_or_else(|| HostError::NotFound("the project is no longer open".into()))?;
|
||||
let stopped = || cancel.is_cancelled() || Instant::now() >= deadline;
|
||||
let interrupted = || {
|
||||
if cancel.is_cancelled() {
|
||||
HostError::Cancelled
|
||||
} else {
|
||||
HostError::Failed("symbol search timed out".into())
|
||||
}
|
||||
};
|
||||
let symbols = project
|
||||
.index
|
||||
.find_symbols_until(
|
||||
query,
|
||||
ide_index::SymbolFilter::All,
|
||||
limit.min(1000),
|
||||
&|path| path.starts_with(root),
|
||||
&stopped,
|
||||
)
|
||||
.ok_or_else(interrupted)?;
|
||||
let mut out = Vec::new();
|
||||
for s in symbols {
|
||||
if stopped() {
|
||||
return Err(interrupted());
|
||||
}
|
||||
out.push(ide_plugin::SymbolInfo {
|
||||
name: s.name,
|
||||
kind: serde_json::to_value(s.kind)
|
||||
.ok()
|
||||
.and_then(|k| k.as_str().map(str::to_owned))
|
||||
.unwrap_or_else(|| "unknown".into()),
|
||||
container: s.container,
|
||||
location: ide_plugin::Location { path: s.path.0, range: s.range },
|
||||
});
|
||||
}
|
||||
if stopped() {
|
||||
return Err(interrupted());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn document(&self, path: &Path) -> Option<(u64, String)> {
|
||||
let (project, doc) = self.open_document(path)?;
|
||||
let snap = project.documents.snapshot(doc).ok()?;
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
//! The synchronous workspace import uses the real project index, also from a provider.
|
||||
use ide_core::CancellationToken;
|
||||
use ide_core::{NullSink, Pos, Range};
|
||||
use ide_host::plugins::{Consent, HostWorkspace, PluginHost, PluginWorkspace};
|
||||
use ide_host::{Host, HostConfig};
|
||||
use ide_plugin::{Invocation, Limits, ProviderDocument};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const WAIT: Duration = Duration::from_secs(30);
|
||||
|
||||
struct Fixture {
|
||||
_tmp: tempfile::TempDir,
|
||||
_runtime: tokio::runtime::Runtime,
|
||||
host: Arc<Host>,
|
||||
plugins: PluginHost,
|
||||
root: PathBuf,
|
||||
other: PathBuf,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn new(capability: bool) -> Self {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let base = dunce::canonicalize(tmp.path()).unwrap();
|
||||
let (root, other) = (base.join("project"), base.join("other"));
|
||||
for dir in [&root, &other] {
|
||||
std::fs::create_dir_all(dir).unwrap();
|
||||
}
|
||||
std::fs::write(root.join("Order.php"), "<?php class OrderController { public function item() {} }\n").unwrap();
|
||||
std::fs::write(root.join("Main.java"), "class OrderService { void serve() {} }\n").unwrap();
|
||||
std::fs::write(other.join("Order.php"), "<?php class OrderElsewhere {}\n").unwrap();
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap();
|
||||
let syntax = OnceLock::new();
|
||||
syntax.set(Arc::new(ide_language::Syntax::new())).ok().unwrap();
|
||||
let host = Arc::new(Host::new(HostConfig {
|
||||
config_dir: base.join("config"),
|
||||
data_dir: base.join("data"),
|
||||
local_data_dir: base.join("data"),
|
||||
languages: Arc::new(ide_language::LanguageRegistry::load(None).unwrap()),
|
||||
syntax: Arc::new(syntax),
|
||||
sink_for: Box::new(|_| Arc::new(NullSink)),
|
||||
owner: |w| w,
|
||||
runtime: runtime.handle().clone(),
|
||||
project_environment: false,
|
||||
}));
|
||||
// Restricted projects need no LSP/process startup for index queries.
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
for (window, dir) in [("main", &root), ("other", &other)] {
|
||||
let project = host.open_project(window, dir).unwrap();
|
||||
assert!(project.index.wait_idle(WAIT));
|
||||
}
|
||||
}
|
||||
let plugins = PluginHost::new(
|
||||
base.join("plugins"),
|
||||
Arc::new(|_: Option<&str>, _| {}),
|
||||
Limits { edit_soft: Duration::from_secs(5), edit_hard: Duration::from_secs(10), ..Limits::default() },
|
||||
)
|
||||
.unwrap();
|
||||
plugins.set_workspace(Some(Arc::new(HostWorkspace(Arc::downgrade(&host)))));
|
||||
plugins.set_project_policy(Arc::new(|_| true));
|
||||
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/symbols");
|
||||
let package = base.join("fixture");
|
||||
std::fs::create_dir_all(package.join("dist")).unwrap();
|
||||
std::fs::copy(source.join("dist/plugin.wasm"), package.join("dist/plugin.wasm")).unwrap();
|
||||
let manifest = std::fs::read_to_string(source.join("plugin.toml")).unwrap();
|
||||
let manifest =
|
||||
if capability { manifest } else { manifest.replace("required = [\"project.read\"]", "required = []") };
|
||||
std::fs::write(package.join("plugin.toml"), manifest).unwrap();
|
||||
plugins.install_from_dir(&package, &Consent::all()).unwrap();
|
||||
Self { _tmp: tmp, _runtime: runtime, host, plugins, root, other }
|
||||
}
|
||||
|
||||
fn invoke(&self, command: &str, root: &Path, query: &str) -> ide_core::IdeResult<Option<String>> {
|
||||
self.plugins.run_command(
|
||||
"test.symbols",
|
||||
command,
|
||||
Some(query),
|
||||
Invocation { workspace_root: Some(root.to_path_buf()), ..Default::default() },
|
||||
)
|
||||
}
|
||||
|
||||
fn search(&self, query: &str) -> String {
|
||||
self.invoke("symbols", &self.root, query).unwrap().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Fixture {
|
||||
fn drop(&mut self) {
|
||||
self.host.close_project("main");
|
||||
self.host.close_project("other");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbols_reach_commands_and_providers_without_a_language_server() {
|
||||
let f = Fixture::new(true);
|
||||
let all = f.search("Order|10");
|
||||
assert!(all.contains("OrderController\tclass\t"), "{all}");
|
||||
assert!(all.contains("OrderService\tclass\t"), "{all}");
|
||||
assert!(!all.contains("OrderElsewhere"));
|
||||
assert_eq!(f.search("OCo|1").lines().count(), 1);
|
||||
assert!(f.search("OrderController.item|1").starts_with("item\tmethod\tOrderController\t"));
|
||||
assert!(f.search("|10").is_empty());
|
||||
assert!(f.search("Order|0").is_empty());
|
||||
assert!(f.search("missing|10").is_empty());
|
||||
assert!(f.search("Order|4294967295").contains("OrderController"));
|
||||
assert!(
|
||||
f.invoke("symbols", &f.root, &format!("{}|1", "é".repeat(129)))
|
||||
.unwrap_err()
|
||||
.message
|
||||
.contains("256 UTF-8 bytes")
|
||||
);
|
||||
let other = f.invoke("symbols", &f.other, "Order|10").unwrap().unwrap();
|
||||
assert!(other.contains("OrderElsewhere") && !other.contains("OrderController"));
|
||||
for language in ["php", "java"] {
|
||||
let targets = f.plugins.provide_definition(
|
||||
&ProviderDocument {
|
||||
path: f.root.join(if language == "php" { "Usage.php" } else { "Usage.java" }),
|
||||
language: language.into(),
|
||||
version: 1,
|
||||
text: "OCo".into(),
|
||||
},
|
||||
Pos::new(0, 3),
|
||||
);
|
||||
assert_eq!(targets.len(), 1);
|
||||
assert_eq!(targets[0].path, f.root.join("Order.php"));
|
||||
let indexed = HostWorkspace(Arc::downgrade(&f.host))
|
||||
.symbols(&f.root, "OCo", 1, &CancellationToken::new(), Instant::now() + WAIT)
|
||||
.unwrap();
|
||||
assert_eq!(targets[0].range, indexed[0].location.range);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reindex_and_closed_project_replace_results_and_invalidate_retained_handles() {
|
||||
let f = Fixture::new(true);
|
||||
f.invoke("remember", &f.root, "").unwrap();
|
||||
let p = f.host.project("main").unwrap();
|
||||
let path = f.root.join("Order.php");
|
||||
std::fs::write(&path, "<?php /*😀*/ class UpdatedController {}\n").unwrap();
|
||||
p.index.update(std::slice::from_ref(&path), &[]);
|
||||
assert!(p.index.wait_idle(WAIT));
|
||||
assert!(!f.search("Order|10").contains("OrderController"));
|
||||
let symbols = HostWorkspace(Arc::downgrade(&f.host))
|
||||
.symbols(&f.root, "UpdatedController", 1, &CancellationToken::new(), Instant::now() + WAIT)
|
||||
.unwrap();
|
||||
assert_eq!(symbols.len(), 1);
|
||||
assert_eq!(symbols[0].location.range, Range::new(Pos::new(0, 19), Pos::new(0, 36)));
|
||||
assert!(f.search("Updated|10").contains("0:19-0:36"));
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
p.index.update(&[], &[path]);
|
||||
assert!(p.index.wait_idle(WAIT));
|
||||
assert!(f.search("Updated|10").is_empty());
|
||||
f.host.close_project("main");
|
||||
let error = f.invoke("remembered", &f.root, "Order|10").unwrap_err();
|
||||
assert!(error.message.contains("no longer open"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_limits_are_capped_before_searching_the_index() {
|
||||
let f = Fixture::new(true);
|
||||
let project = f.host.project("main").unwrap();
|
||||
let path = f.root.join("Bulk.java");
|
||||
let text = (0..1005).map(|i| format!("class BulkSymbol{i} {{}}\n")).collect::<String>();
|
||||
std::fs::write(&path, text).unwrap();
|
||||
project.index.update(&[path], &[]);
|
||||
assert!(project.index.wait_idle(WAIT));
|
||||
assert_eq!(f.search("BulkSymbol|4294967295").lines().count(), 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_queries_require_project_read_even_with_an_invocation_project() {
|
||||
let f = Fixture::new(false);
|
||||
let error = f.invoke("symbols", &f.root, "Order|10").unwrap_err();
|
||||
assert!(error.message.contains("project.read"), "{error}");
|
||||
assert!(
|
||||
f.plugins
|
||||
.provide_definition(
|
||||
&ProviderDocument {
|
||||
path: f.root.join("Usage.php"),
|
||||
language: "php".into(),
|
||||
version: 1,
|
||||
text: "OCo".into(),
|
||||
},
|
||||
Pos::new(0, 3)
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_search_excludes_external_content_roots_before_ranking() {
|
||||
let f = Fixture::new(true);
|
||||
let project = f.host.project("main").unwrap();
|
||||
std::fs::write(f.other.join("Exact.php"), "<?php class Order {}\n").unwrap();
|
||||
project.index.set_scope(vec![f.other.clone()], Vec::new());
|
||||
assert!(project.index.wait_idle(WAIT));
|
||||
assert_eq!(project.index.find_symbols("OrderElsewhere", ide_index::SymbolFilter::All, 1).len(), 1);
|
||||
assert!(f.search("OrderElsewhere|10").is_empty());
|
||||
// A better external match must not displace a readable fuzzy match.
|
||||
let result = f.search("Order|1");
|
||||
assert!(result.contains("OrderController") || result.contains("OrderService"), "{result}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_symbol_search_honours_cancellation_and_deadlines() {
|
||||
let f = Fixture::new(true);
|
||||
let workspace = HostWorkspace(Arc::downgrade(&f.host));
|
||||
let cancel = CancellationToken::new();
|
||||
cancel.cancel();
|
||||
assert_eq!(
|
||||
workspace.symbols(&f.root, "Order", 10, &cancel, Instant::now() + WAIT),
|
||||
Err(ide_plugin::HostError::Cancelled)
|
||||
);
|
||||
let error = workspace.symbols(&f.root, "Order", 10, &CancellationToken::new(), Instant::now()).unwrap_err();
|
||||
assert!(error.to_string().contains("timed out"), "{error}");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn symbol_search_excludes_updates_through_escaping_symlink_ancestors() {
|
||||
let f = Fixture::new(true);
|
||||
let project = f.host.project("main").unwrap();
|
||||
std::fs::write(f.other.join("Secret.php"), "<?php class SecretSymbol {}\n").unwrap();
|
||||
let dir = f.root.join("linked");
|
||||
std::os::unix::fs::symlink(&f.other, &dir).unwrap();
|
||||
project.index.update(&[dir.join("Secret.php")], &[]);
|
||||
assert!(project.index.wait_idle(WAIT));
|
||||
assert!(f.search("Secret|1").is_empty());
|
||||
}
|
||||
@@ -268,6 +268,43 @@ impl ProjectIndex {
|
||||
matching::find_symbols(&d, &matching::Recents { paths: &recent }, query, filter, limit)
|
||||
}
|
||||
|
||||
/// Scoped symbol search with cooperative interruption, including lock waits.
|
||||
/// `None` means interrupted; no partial results are returned.
|
||||
pub fn find_symbols_until(
|
||||
&self,
|
||||
query: &str,
|
||||
filter: SymbolFilter,
|
||||
limit: usize,
|
||||
include: &(impl Fn(&Path) -> bool + Sync),
|
||||
stopped: &(impl Fn() -> bool + Sync),
|
||||
) -> Option<Vec<SymbolMatch>> {
|
||||
let recent = loop {
|
||||
if stopped() {
|
||||
return None;
|
||||
}
|
||||
if let Some(recent) = self.shared.recent.try_lock_for(Duration::from_millis(1)) {
|
||||
break recent.clone();
|
||||
}
|
||||
};
|
||||
let data = loop {
|
||||
if stopped() {
|
||||
return None;
|
||||
}
|
||||
if let Some(data) = self.shared.data.try_read_for(Duration::from_millis(1)) {
|
||||
break data;
|
||||
}
|
||||
};
|
||||
matching::find_symbols_until(
|
||||
&data,
|
||||
&matching::Recents { paths: &recent },
|
||||
query,
|
||||
filter,
|
||||
limit,
|
||||
include,
|
||||
stopped,
|
||||
)
|
||||
}
|
||||
|
||||
/// Symbols named exactly `name` (of `kind`, if given): `.card` selectors for `class="card"`.
|
||||
pub fn symbols_named(&self, name: &str, kind: Option<SymbolKind>, limit: usize) -> Vec<SymbolMatch> {
|
||||
matching::symbols_named(&self.shared.data.read(), name, kind, limit)
|
||||
|
||||
@@ -121,22 +121,35 @@ fn scan(
|
||||
tiebreak: &(impl Fn(&Cand, &Cand) -> Ordering + Sync),
|
||||
per_file: impl Fn(u32, &FileEntry, &mut Vec<Cand>) + Sync,
|
||||
) -> Vec<Cand> {
|
||||
scan_until(data, pool, tiebreak, &|| false, per_file).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn scan_until(
|
||||
data: &IndexData,
|
||||
pool: usize,
|
||||
tiebreak: &(impl Fn(&Cand, &Cand) -> Ordering + Sync),
|
||||
stopped: &(impl Fn() -> bool + Sync),
|
||||
per_file: impl Fn(u32, &FileEntry, &mut Vec<Cand>) + Sync,
|
||||
) -> Option<Vec<Cand>> {
|
||||
data.files
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.with_min_len(256)
|
||||
.fold(Vec::new, |mut acc, (slot, e)| {
|
||||
.try_fold(Vec::new, |mut acc, (slot, e)| {
|
||||
if stopped() {
|
||||
return None;
|
||||
}
|
||||
if let Some(e) = e {
|
||||
per_file(slot as u32, e, &mut acc);
|
||||
if acc.len() >= pool * 2 {
|
||||
acc = keep_best(acc, pool, tiebreak);
|
||||
}
|
||||
}
|
||||
acc
|
||||
Some(acc)
|
||||
})
|
||||
.reduce(Vec::new, |mut a, b| {
|
||||
.try_reduce(Vec::new, |mut a, b| {
|
||||
a.extend(b);
|
||||
if a.len() >= pool * 2 { keep_best(a, pool, tiebreak) } else { a }
|
||||
Some(if a.len() >= pool * 2 { keep_best(a, pool, tiebreak) } else { a })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -423,9 +436,21 @@ pub(crate) fn find_symbols(
|
||||
filter: SymbolFilter,
|
||||
limit: usize,
|
||||
) -> Vec<SymbolMatch> {
|
||||
find_symbols_until(data, recents, query, filter, limit, &|_| true, &|| false).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn find_symbols_until(
|
||||
data: &IndexData,
|
||||
recents: &Recents,
|
||||
query: &str,
|
||||
filter: SymbolFilter,
|
||||
limit: usize,
|
||||
include: &(impl Fn(&Path) -> bool + Sync),
|
||||
stopped: &(impl Fn() -> bool + Sync),
|
||||
) -> Option<Vec<SymbolMatch>> {
|
||||
let q: String = query.trim().chars().filter(|c| !c.is_whitespace()).collect();
|
||||
if q.is_empty() || limit == 0 {
|
||||
return Vec::new();
|
||||
return Some(Vec::new());
|
||||
}
|
||||
let (container_q, name_q) = split_symbol_query(&q);
|
||||
let name = Needle::new(name_q);
|
||||
@@ -436,13 +461,16 @@ pub(crate) fn find_symbols(
|
||||
sa.name.len().cmp(&sb.name.len()).then_with(|| file(data, a.slot).rel.cmp(&file(data, b.slot).rel))
|
||||
};
|
||||
let pool = limit * 4 + 64;
|
||||
let cands = scan(data, pool, &tiebreak, |slot, e, acc| {
|
||||
if e.symbols.is_empty() {
|
||||
let cands = scan_until(data, pool, &tiebreak, stopped, |slot, e, acc| {
|
||||
if e.symbols.is_empty() || !include(&e.path) {
|
||||
return;
|
||||
}
|
||||
with_matcher(|m| {
|
||||
let mut buf = Vec::new();
|
||||
for (i, s) in e.symbols.iter().enumerate() {
|
||||
if stopped() {
|
||||
break;
|
||||
}
|
||||
if filter == SymbolFilter::Types && !s.kind.is_type_like() {
|
||||
continue;
|
||||
}
|
||||
@@ -451,11 +479,15 @@ pub(crate) fn find_symbols(
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})?;
|
||||
if stopped() {
|
||||
return None;
|
||||
}
|
||||
let mut out: Vec<(u32, Cand, Vec<u32>)> = with_matcher(|m| {
|
||||
let mut buf = Vec::new();
|
||||
cands
|
||||
.into_iter()
|
||||
.take_while(|_| !stopped())
|
||||
.filter_map(|c| {
|
||||
let s = sym(&c);
|
||||
let e = file(data, c.slot);
|
||||
@@ -472,7 +504,8 @@ pub(crate) fn find_symbols(
|
||||
b.0.cmp(&a.0).then_with(|| tiebreak(&a.1, &b.1)).then_with(|| sym(&a.1).range.cmp(&sym(&b.1).range))
|
||||
});
|
||||
out.truncate(limit);
|
||||
out.into_iter()
|
||||
let results = out
|
||||
.into_iter()
|
||||
.map(|(score, c, indices)| {
|
||||
let e = file(data, c.slot);
|
||||
let s = &e.symbols[c.sym as usize];
|
||||
@@ -487,7 +520,8 @@ pub(crate) fn find_symbols(
|
||||
indices,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
(!stopped()).then_some(results)
|
||||
}
|
||||
|
||||
/// Symbols named exactly `name` (and of `kind`, when given), by path then position.
|
||||
|
||||
@@ -652,3 +652,80 @@ fn plugin_inventory_respects_excludes_and_symbols_require_matching_text() {
|
||||
assert!(symbols.iter().any(|s| s.name == "find"));
|
||||
assert!(index.symbols_for_text(&path, &java.replace("find", "renamed")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_search_can_stop_during_scanning_and_lock_waits() {
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
let fx = sample();
|
||||
let idx = fx.open();
|
||||
let calls = AtomicUsize::new(0);
|
||||
let stopped = || calls.fetch_add(1, Ordering::Relaxed) >= 5;
|
||||
assert!(idx.find_symbols_until("a", SymbolFilter::All, 20, &|_| true, &stopped).is_none());
|
||||
assert!(calls.load(Ordering::Relaxed) > 5);
|
||||
for recent in [false, true] {
|
||||
let _data = (!recent).then(|| idx.shared.data.write());
|
||||
let _recent = recent.then(|| idx.shared.recent.lock());
|
||||
let deadline = std::time::Instant::now() + Duration::from_millis(10);
|
||||
assert!(
|
||||
idx.find_symbols_until("a", SymbolFilter::All, 20, &|_| true, &|| std::time::Instant::now() >= deadline)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scoped_symbol_search_preserves_fuzzy_ranking_within_allowed_paths() {
|
||||
let fx = sample();
|
||||
let idx = fx.open();
|
||||
let allowed = fx.root.join("src/order");
|
||||
let symbols =
|
||||
idx.find_symbols_until("OCo", SymbolFilter::All, 1, &|path| path.starts_with(&allowed), &|| false).unwrap();
|
||||
assert_eq!(symbols.len(), 1);
|
||||
assert_eq!(symbols[0].name, "OrderController");
|
||||
assert!(
|
||||
idx.find_symbols_until("OtherCollection", SymbolFilter::All, 1, &|path| path.starts_with(&allowed), &|| false)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn incremental_symbols_resolve_symlink_ancestors_before_scope_checks() {
|
||||
let fx = Fixture::new(&[("src/inside.rs", "fn inside_symbol() {}\n")]);
|
||||
let outside = fx._dir.path().join("outside");
|
||||
std::fs::create_dir(&outside).unwrap();
|
||||
std::fs::write(outside.join("secret.rs"), "fn secret_symbol() {}\n").unwrap();
|
||||
std::os::unix::fs::symlink(&outside, fx.root.join("escape")).unwrap();
|
||||
std::os::unix::fs::symlink(fx.root.join("src"), fx.root.join("alias")).unwrap();
|
||||
let idx = fx.open();
|
||||
idx.update(&[fx.root.join("escape/secret.rs"), fx.root.join("alias/inside.rs")], &[]);
|
||||
assert!(idx.wait_idle(WAIT));
|
||||
assert!(symbols(&idx, "secret_symbol", SymbolFilter::All).is_empty());
|
||||
let inside = idx.find_symbols("inside_symbol", SymbolFilter::All, 10);
|
||||
assert_eq!(inside.len(), 1);
|
||||
assert_eq!(inside[0].path.0, fx.root.join("src/inside.rs"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn persisted_symbols_do_not_load_through_escaping_symlink_ancestors() {
|
||||
let fx = Fixture::new(&[("src/old.rs", "fn old_symbol() {}\n")]);
|
||||
drop(fx.open());
|
||||
let outside = fx._dir.path().join("outside");
|
||||
std::fs::rename(fx.root.join("src"), &outside).unwrap();
|
||||
std::os::unix::fs::symlink(outside, fx.root.join("src")).unwrap();
|
||||
let initial = Arc::new(Mutex::new(Vec::new()));
|
||||
let events = initial.clone();
|
||||
let sink: SharedSink = Arc::new(move |event: IdeEvent| {
|
||||
if let IdeEvent::IndexState { symbols, .. } = event {
|
||||
events.lock().push(symbols);
|
||||
}
|
||||
});
|
||||
let languages = Arc::new(LanguageRegistry::load(None).unwrap());
|
||||
let idx = ProjectIndex::open(&fx.root, &fx.cache, Vec::new(), Vec::new(), languages, Arc::new(Syntax::new()), sink)
|
||||
.unwrap();
|
||||
assert!(idx.wait_idle(WAIT));
|
||||
assert!(symbols(&idx, "old_symbol", SymbolFilter::All).is_empty());
|
||||
assert_eq!(initial.lock()[0], 0, "unsafe cached symbols must never be published");
|
||||
}
|
||||
@@ -174,7 +174,10 @@ impl Shared {
|
||||
Ok(records) => {
|
||||
let entries: Vec<FileEntry> = records
|
||||
.into_par_iter()
|
||||
.filter(|(rel, _)| !filter.is_excluded(&root.join(rel), false))
|
||||
.filter(|(rel, _)| {
|
||||
let path = root.join(rel);
|
||||
!filter.is_excluded(&path, false) && dunce::canonicalize(&path).is_ok_and(|real| real == path)
|
||||
})
|
||||
.map(|(rel, rec)| FileEntry::new(root.join(&rel), rel, rec))
|
||||
.collect();
|
||||
let mut d = self.data.write();
|
||||
@@ -420,7 +423,10 @@ impl Shared {
|
||||
let mut seen = HashSet::new();
|
||||
let mut by_dir: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
|
||||
for path in changed {
|
||||
if !seen.insert(path.clone()) {
|
||||
// A watcher path may have a symlink ancestor even when its spelling
|
||||
// is inside the root. Resolve it on the worker before reading it.
|
||||
let Ok(path) = dunce::canonicalize(&path) else { continue };
|
||||
if !filter.contains(&path) || !seen.insert(path.clone()) {
|
||||
continue;
|
||||
}
|
||||
let Ok(meta) = std::fs::symlink_metadata(&path) else { continue };
|
||||
|
||||
@@ -122,6 +122,19 @@ pub trait HostApi: Send + Sync {
|
||||
.filter(|root| real.starts_with(crate::resolve_real(root)))
|
||||
.max_by_key(|root| root.components().count())
|
||||
}
|
||||
|
||||
/// Searches the current in-memory index of an open project.
|
||||
fn symbols(
|
||||
&self,
|
||||
root: &Path,
|
||||
query: &str,
|
||||
limit: usize,
|
||||
cancel: &CancellationToken,
|
||||
deadline: Instant,
|
||||
) -> Result<Vec<crate::SymbolInfo>, HostError> {
|
||||
let _ = (root, query, limit, cancel, deadline);
|
||||
Err(HostError::NotFound("no project index is available".into()))
|
||||
}
|
||||
/// Unsaved text of an open editor's file.
|
||||
fn buffer_text(&self, path: &Path) -> Option<String>;
|
||||
/// Version and text of the open document at `path`.
|
||||
|
||||
@@ -433,6 +433,15 @@ pub struct Location {
|
||||
pub range: ide_core::Range,
|
||||
}
|
||||
|
||||
/// A symbol in the current project index. Ranges use UTF-16 columns.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SymbolInfo {
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub container: Option<String>,
|
||||
pub location: Location,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CompletionItem {
|
||||
pub label: String,
|
||||
|
||||
@@ -511,6 +511,39 @@ impl workspace::Host for StoreData {
|
||||
self.host.project_of(Path::new(&path)).map(|root| self.push(ProjectRes { root })).transpose()
|
||||
}
|
||||
|
||||
fn symbols(
|
||||
&mut self,
|
||||
project: Resource<ProjectRes>,
|
||||
query: String,
|
||||
limit: u32,
|
||||
) -> Fallible<Vec<workspace::SymbolInfo>> {
|
||||
if let Err(e) = self.require(Capability::ProjectRead) {
|
||||
return Ok(Err(e));
|
||||
}
|
||||
if query.len() > 256 {
|
||||
return Ok(Err(wit::Error::Invalid("symbol query exceeds 256 UTF-8 bytes".into())));
|
||||
}
|
||||
let root = &self.table.get(&project)?.root;
|
||||
Ok(self
|
||||
.host
|
||||
.symbols(root, &query, limit.min(1000) as usize, &self.cancel.token(), self.deadline)
|
||||
.map(|symbols| {
|
||||
symbols
|
||||
.into_iter()
|
||||
.map(|s| workspace::SymbolInfo {
|
||||
name: s.name,
|
||||
kind: s.kind,
|
||||
container: s.container,
|
||||
location: wit::Location {
|
||||
path: to_ipc(&s.location.path),
|
||||
range: range_to_wit(&s.location.range),
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.map_err(to_wit))
|
||||
}
|
||||
|
||||
fn projects(&mut self) -> Fallible<Vec<Resource<ProjectRes>>> {
|
||||
if let Err(e) = self.require(Capability::ProjectRead) {
|
||||
return Ok(Err(e));
|
||||
|
||||
@@ -31,6 +31,7 @@ fn components() -> Vec<PathBuf> {
|
||||
repo().join("crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm"),
|
||||
repo().join("crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm"),
|
||||
repo().join("crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm"),
|
||||
repo().join("crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm"),
|
||||
];
|
||||
let official = repo().join("plugins/official");
|
||||
let mut dirs: Vec<_> = std::fs::read_dir(&official).unwrap().map(|e| e.unwrap().path()).collect();
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
13db3ba9045429fef5973e83075a51a315076408a03ec3c1b837c0229365fcbe
|
||||
3f444f1cc3b7f495f26378594fd9f98418146cd8b2275310e53d4905d0bb5c30
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
6c2a59bf732acebd74ad4d05824be3aafbd501964be01205ca1e0d5a6135d113
|
||||
26ae74bb9df6f88bca87a31d20fb00d52cd9464047df3525d0e47b5bf6b2b2ca
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
183bf50a6ee479128440ef4bd86939f24b036b699a24842a6efd1d3f43c6cd10
|
||||
24b3c81c74355174aa2fd484677bbac38020ecd119aff419d0479a942e2e4dbd
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
97f7f6fe965e95510d5556feb40329c312e5430e631479be6c9646046a888478
|
||||
0bb069928979ba22a2d6bd977108d905ff870ef981aad6ef4930834ad5c35c35
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
f73de3fdfe5b343f26ff4dd1ae150c975273ce398d518f6f2100b0baa8074ffc
|
||||
da7c5e58d2d26b7613a8ba5b50d4ec7788438f186b4abcfda90a9bf0c34ab591
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
a985162e44066c6bf6c93d01af1b01d40d305c6be43072d4f33d4f1f19a4dee0
|
||||
65583ded79c9fb4de0ed856d04aa08e58c69368d3a9235a57afa50ea757eb45f
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
5063a997e11258386e88ba129d42262f5050bfba41c8ea069152b2dfba007fa7
|
||||
0c0e4f3611971307268cc8e2cc6fb96734923f10dbccbd8950639d86036c682c
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
9a3d097559f6685e82024bd84cb0396875762b7136636fdc9bd11cb41875373a
|
||||
c09efb8c9d0be262b7a25099639213e304fbfd5b0b91ad3d80672fb8148c0e4a
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
6ca0c03e31e0cd67192bb86c93b1feb294fec79cf432ceb57b9cdc3e42eb6b02
|
||||
7c728e98b1e1d4ea3d03831c263298fc4792599a81138e40dfd4c48ce6c4a47e
|
||||
@@ -1 +1 @@
|
||||
a3b813b172441c5fad462b249c69dadda2b8fb1e2b387bda2487f7df3f729338
|
||||
d8626558aba036e4d476df9929c6e16089dd0a1e43101b48dd978946889c7616
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
848356d2e0fbaef341436ea70a4698915cce705d3ccea4dd4e50900b12c97d33
|
||||
512b7d41868034322fccc15f100c85e9f53335c22aec4d7492a14723b54cd874
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
07001ff22cd33d5ae6f96b584707e66d31f2f93423cf72a993e6f11c9f0ec319
|
||||
de7a217d3931c62387718b231a2c895438a210cc2e2b2cbb374312a1cf86a68d
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
ae9cb4a106aa8fd46662dba07acd0daba2927b7fe53e544cd95348954505bc36
|
||||
f76dd5cbd131af57c371c2f44146274426731a3e9609cc6f5aaabbacaecb17a7
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
|
||||
|
||||
[[package]]
|
||||
name = "macro-string"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "openidea-plugin"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openidea-test-symbols"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"openidea-plugin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.262.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.262.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.262.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.63.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.63.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.63.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.63.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"macro-string",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.262.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.262.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hashbrown",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-ident",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "openidea-test-symbols"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
openidea-plugin = { path = "../../../../../sdk/rust/openidea-plugin" }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
ffe6e931db4d0232005de934fce7090ae9098e9cdfd1f9e8b7f6243abe3d3ae9
|
||||
@@ -0,0 +1,23 @@
|
||||
id = "test.symbols"
|
||||
name = "Indexed symbols fixture"
|
||||
version = "0.1.0"
|
||||
engines.openidea = "^1.3"
|
||||
wasm = "dist/plugin.wasm"
|
||||
[capabilities]
|
||||
required = ["project.read"]
|
||||
|
||||
[[contributes.providers]]
|
||||
kinds = ["definition"]
|
||||
languages = ["php", "java"]
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "symbols"
|
||||
title = "Query indexed symbols"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "remember"
|
||||
title = "Remember project"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "remembered"
|
||||
title = "Query remembered project"
|
||||
@@ -0,0 +1,55 @@
|
||||
use openidea_plugin::host::{self, Project};
|
||||
use openidea_plugin::providers::{Document, Providers};
|
||||
use openidea_plugin::{Error, Invocation, Location, Plugin, Position, register_plugin};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Fixture {
|
||||
project: Option<Project>,
|
||||
}
|
||||
|
||||
impl Plugin for Fixture {
|
||||
fn run_command(&mut self, id: &str, ctx: &Invocation, arg: Option<&str>) -> Result<Option<String>, Error> {
|
||||
if id == "remember" {
|
||||
self.project = ctx.project();
|
||||
return Ok(None);
|
||||
}
|
||||
let current = ctx.project();
|
||||
let project = if id == "remembered" { self.project.as_ref() } else { current.as_ref() }
|
||||
.ok_or_else(|| Error::NotFound("no project".into()))?;
|
||||
let (query, limit) = arg.unwrap_or("Order|10").rsplit_once('|').ok_or("expected query|limit")?;
|
||||
let limit = limit.parse().map_err(|_| Error::Invalid("invalid limit".into()))?;
|
||||
let symbols = project.symbols(query, limit)?;
|
||||
Ok(Some(
|
||||
symbols
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
format!(
|
||||
"{}\t{}\t{}\t{}\t{}:{}-{}:{}",
|
||||
s.name,
|
||||
s.kind,
|
||||
s.container.unwrap_or_default(),
|
||||
s.location.path,
|
||||
s.location.range.start.line,
|
||||
s.location.range.start.character,
|
||||
s.location.range.end.line,
|
||||
s.location.range.end.character
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Providers for Fixture {
|
||||
fn definition(&mut self, doc: &Document, _: Position) -> Vec<Location> {
|
||||
host::project_of(&doc.path)
|
||||
.and_then(|project| project.symbols(doc.text.trim(), 10).ok())
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|s| s.location)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
register_plugin!(Fixture, providers);
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
8a79f2899d88773d7c33175136960727b5ad0aefc5ce5eb363fd20dc4990c60e
|
||||
8d42a25cd250041ceb6aaafed66ac0bd38ff1717cc450d6175517cd3d01738c9
|
||||
+16
-1
@@ -281,7 +281,7 @@ machine. Users can revoke optional capabilities in Settings › Plugins.
|
||||
|
||||
| Capability | Risk | Unlocks today |
|
||||
| ------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `project.read` | low | `Project::root`, `read_file`, `find_files`, `host::projects`, `publish_diagnostics` |
|
||||
| `project.read` | low | `Project::root`, `read_file`, `find_files`, `symbols`, `host::projects`, `publish_diagnostics` |
|
||||
| `project.write` | medium | `Project::write_file` — creates and replaces text files inside the open projects |
|
||||
| `fs.read:<path>` | high | `host::fs::read_file`, `read_bytes`, `stat`, `read_dir` — files outside the projects, under the grant's directory |
|
||||
| `fs.write:<path>` | high | `host::fs::write_file`, `write_bytes` — files under that path; doesn't imply reading |
|
||||
@@ -439,6 +439,7 @@ then run `openidea-plugin check`.
|
||||
| `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; 4 MiB of output; killed when the call is cancelled or times out. |
|
||||
@@ -451,6 +452,20 @@ then run `openidea-plugin check`.
|
||||
| `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
|
||||
changes update the index in the background. Results are confined to the project's
|
||||
root, with normal exclusions; external content roots and updates through escaping
|
||||
symlinks are excluded. Search honours the current call's cancellation and deadline.
|
||||
Empty queries and zero limits return no symbols; a closed project returns
|
||||
`Error::NotFound`.
|
||||
|
||||
Type resolution and member queries remain deferred to the asynchronous API 2.0
|
||||
model. Providers can select their project with `host::project_of(&doc.path)` and
|
||||
call `symbols` synchronously; `crates/ide-plugin/tests/fixtures/symbols` demonstrates
|
||||
a definition provider. `MockHost::set_symbols(root, symbols)` replaces a test
|
||||
snapshot; its mock uses case-insensitive substring matching rather than fuzzy ranking.
|
||||
|
||||
Both process APIs only add environment variables. A key already in the prepared child
|
||||
environment (including the login-shell `PATH`) fails with an error naming the key;
|
||||
variables are never replaced or removed. Names compare case-insensitively on Windows.
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
7872480dee517db617120a9327d3bbbcc007d31bbf9e6c999ce202f38f0ed478
|
||||
977835f3364613876b3f563877d7f0cf36d668226c5c6ac2280473c73d44117d
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
31f1d88c7e88553eed145fd170485c10e19967eccbf2021ec71fca862d05e952
|
||||
55e3306fc2ff9456d79f02b63712c7f83b1fe4e75fec051f5fb96e7fb766066f
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
6a2db880c49b15d7ce5ba7fda69ac8d1cac04f0412d42b6922550594ef94a7c5
|
||||
f582b640e980e4b69e292ec425d0ed8113527119d18d0fa1ea73e7c34f1f89a1
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
9299f10c517ee5582312f0f14871e21d36050ee8d621df31d7e6a4126c4ea37e
|
||||
842de029af0767864b56dc71199a823ca0ea04aec5ece8928dac69340ff7ab7a
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
1f86f15f5e864ec9a91cb31379047eafd4ae8c07ffacb5e1c85955ee07873342
|
||||
1dd92a9e139458813f513479cbc0f8187eff217f5dac2b39441537e3039e9dca
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
540e60e34b752eb201ef9cacea44066b4f59a4e36c3d1a468bf208223569a628
|
||||
5ce56abe0082fc610deec13f6907cdfd8be7346ce9a1c43c59e1828957bc3274
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
106f4a0b300fdb98fd0bd8f8bc1b3ce2c692a8a7e0107353905d7edbb7c7ce4d
|
||||
be6dd0b46a0f407f354ae2b67c6304948c71749bc3587dcf81b39dd245afdb48
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
eba583c669da994dba24a8a16546c9d2cb50fb46db53207207809b20882680e8
|
||||
4fe7aec43c3009cd0567e93f2e80c135091269aa0a2fcea3ba00f43a24c5ec56
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
d1385eb9aa4d78bbcbdf98565fdf3b550050388af9248814f5c994b54d605708
|
||||
7be4ce274fef38b5d4741646de970afd803fbf5ccffaa49236af1d15c8c870a9
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
8f4c3af7f1f0f6e2001ce161d89e2b83ef700480d6fd5517f46e90437f74639e
|
||||
0a02c2ec0444e5eccb68b2bfabeaaf7752d1bc2b22bc0f26883a2a35eeac7df7
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
2504128882832c282d627946668f10603fe4b165dca8f12a49131934d1a5c09d
|
||||
10c214d40a6b43bb8b850bfe6d97c1a360ed748b5e95645354b3317806d7a538
|
||||
@@ -310,6 +310,22 @@ mod imp {
|
||||
t.is_cancelled()
|
||||
}
|
||||
|
||||
pub fn project_symbols(p: &ProjectRes, query: &str, limit: u32) -> Result<Vec<SymbolInfo>, Error> {
|
||||
wit::workspace::symbols(p, query, limit)
|
||||
.map(|symbols| {
|
||||
symbols
|
||||
.into_iter()
|
||||
.map(|s| SymbolInfo {
|
||||
name: s.name,
|
||||
kind: s.kind,
|
||||
container: s.container,
|
||||
location: Location { path: s.location.path, range: range_from(s.location.range) },
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.map_err(error)
|
||||
}
|
||||
|
||||
pub fn project_find(p: &ProjectRes, glob: &str, limit: u32) -> Result<Vec<String>, Error> {
|
||||
p.find_files(glob, limit).map_err(error)
|
||||
}
|
||||
@@ -611,6 +627,17 @@ impl Project {
|
||||
imp::project_find(&self.inner, glob, limit)
|
||||
}
|
||||
|
||||
/// Fuzzy search of this project's current in-memory index. Needs `project.read`.
|
||||
/// Never waits for LSP or reads file contents. Only paths inside this project's
|
||||
/// root are returned, including when the index has external content roots.
|
||||
/// Search honours the current call's cancellation and deadline. Empty queries return no symbols;
|
||||
/// queries over 256 UTF-8 bytes are invalid and limits are capped at 1000.
|
||||
/// Results may be incomplete during indexing or lag unsaved edits. A closed
|
||||
/// project returns `Error::NotFound`. Locations use UTF-16 columns.
|
||||
pub fn symbols(&self, query: &str, limit: u32) -> Result<Vec<SymbolInfo>, Error> {
|
||||
imp::project_symbols(&self.inner, query, limit)
|
||||
}
|
||||
|
||||
/// Runs the approved `process:<program>` in this project's root.
|
||||
pub fn run_process(&self, program: &str, args: &[String], stdin: Option<&str>) -> Result<ProcessOutput, Error> {
|
||||
imp::run_process(program, args, Some(&self.inner), stdin)
|
||||
|
||||
@@ -80,6 +80,7 @@ struct State {
|
||||
capabilities: Vec<String>,
|
||||
root: Option<String>,
|
||||
other_projects: Vec<String>,
|
||||
symbols: BTreeMap<String, Vec<SymbolInfo>>,
|
||||
files: BTreeMap<String, String>,
|
||||
|
||||
/// Files outside every project, for `fs.read` (absolute paths).
|
||||
@@ -191,6 +192,12 @@ impl MockHost {
|
||||
self.state.borrow_mut().other_projects.push(root.to_owned());
|
||||
}
|
||||
|
||||
/// Replaces a project's indexed symbols. Filtering in the mock is a
|
||||
/// case-insensitive substring; the real index also supports fuzzy matching.
|
||||
pub fn set_symbols(&self, root: &str, symbols: Vec<SymbolInfo>) {
|
||||
self.state.borrow_mut().symbols.insert(root.into(), symbols);
|
||||
}
|
||||
|
||||
/// A project file `read_file` can return (project-relative or absolute path).
|
||||
pub fn add_file(&self, path: &str, text: &str) {
|
||||
self.state.borrow_mut().files.insert(path.to_owned(), text.to_owned());
|
||||
@@ -818,6 +825,35 @@ pub mod current {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn project_symbols(p: &ProjectRes, query: &str, limit: u32) -> Result<Vec<SymbolInfo>, Error> {
|
||||
with(|s| {
|
||||
require(s, "project.read")?;
|
||||
if query.len() > 256 {
|
||||
return Err(Error::Invalid("symbol query exceeds 256 UTF-8 bytes".into()));
|
||||
}
|
||||
if !s.root.iter().chain(&s.other_projects).any(|root| *root == p.root) {
|
||||
return Err(Error::NotFound("the project is no longer open".into()));
|
||||
}
|
||||
let query = query.trim().to_lowercase();
|
||||
Ok(s.symbols
|
||||
.get(&p.root)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|symbol| {
|
||||
!query.is_empty()
|
||||
&& symbol.name.to_lowercase().contains(&query)
|
||||
&& symbol
|
||||
.location
|
||||
.path
|
||||
.strip_prefix(p.root.trim_end_matches('/'))
|
||||
.is_some_and(|rest| rest.starts_with('/'))
|
||||
})
|
||||
.take(limit.min(1000) as usize)
|
||||
.cloned()
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn project_stat(p: &ProjectRes, path: &str) -> Result<FileStat, Error> {
|
||||
with(|s| {
|
||||
require(s, "project.read")?;
|
||||
@@ -1558,6 +1594,39 @@ mod tests {
|
||||
assert!(matches!(project.stat("x"), Err(Error::Denied(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_symbols_are_bounded_scoped_and_capability_checked() {
|
||||
let mock = MockHost::install();
|
||||
let symbol = SymbolInfo {
|
||||
name: "OrderController".into(),
|
||||
kind: "class".into(),
|
||||
container: Some("App".into()),
|
||||
location: Location {
|
||||
path: "/project/Order.php".into(),
|
||||
range: Range::new(Position::new(0, 2), Position::new(0, 17)),
|
||||
},
|
||||
};
|
||||
let mut symbols = vec![symbol.clone(); 1001];
|
||||
let mut outside = symbol.clone();
|
||||
outside.location.path = "/project-other/Order.php".into();
|
||||
symbols.insert(0, outside);
|
||||
mock.set_symbols("/project", symbols);
|
||||
mock.add_project("/other");
|
||||
let project = mock.invocation().project().unwrap();
|
||||
assert_eq!(project.symbols("order", u32::MAX).unwrap().len(), 1000);
|
||||
assert_eq!(project.symbols("order", 1).unwrap(), [symbol]);
|
||||
assert!(project.symbols("", 1).unwrap().is_empty());
|
||||
assert!(project.symbols("order", 0).unwrap().is_empty());
|
||||
assert!(matches!(project.symbols(&"é".repeat(129), 1), Err(Error::Invalid(_))));
|
||||
assert!(host::projects().unwrap()[1].symbols("order", 1).unwrap().is_empty());
|
||||
mock.set_symbols("/project", Vec::new());
|
||||
assert!(project.symbols("order", 1).unwrap().is_empty());
|
||||
mock.set_project_root(None);
|
||||
assert!(matches!(project.symbols("order", 1), Err(Error::NotFound(_))));
|
||||
let _mock = mock.with_capabilities(&[]);
|
||||
assert_eq!(project.symbols("order", 1), Err(Error::Denied("project.read".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn versioned_edits() {
|
||||
let mock = MockHost::install();
|
||||
|
||||
@@ -102,6 +102,16 @@ pub struct TextEdit {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// A symbol from the host's current in-memory project index.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SymbolInfo {
|
||||
pub name: String,
|
||||
/// Host symbol kind in camelCase (`class`, `method`, `enumMember`, …).
|
||||
pub kind: String,
|
||||
pub container: Option<String>,
|
||||
pub location: Location,
|
||||
}
|
||||
|
||||
/// An editor's document at one version.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Document {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package openidea:[email protected];
|
||||
|
||||
interface workspace {
|
||||
use types.{error};
|
||||
use types.{error, location};
|
||||
|
||||
/// Requests File > Open in this command/event's window (or a new window),
|
||||
/// including the usual project trust prompt. Needs no capability.
|
||||
@@ -30,6 +30,26 @@ interface workspace {
|
||||
modified: option<u64>,
|
||||
}
|
||||
|
||||
/// A symbol in the host's current in-memory project index (new in 1.3).
|
||||
record symbol-info {
|
||||
name: string,
|
||||
/// Host symbol kind, in camelCase (`class`, `method`, `enumMember`, …).
|
||||
kind: string,
|
||||
container: option<string>,
|
||||
location: location,
|
||||
}
|
||||
|
||||
/// Fuzzy symbol search (CamelHump and container-qualified queries) over the
|
||||
/// current index only; never starts or waits for a language server. Results
|
||||
/// may be incomplete while indexing and ranges reflect the indexed text,
|
||||
/// not necessarily unsaved edits. Empty queries return no symbols. Queries
|
||||
/// over 256 UTF-8 bytes are invalid; limit is capped at 1000 (zero is empty).
|
||||
/// Only paths readable under this project's root are returned; external
|
||||
/// content roots are excluded. Cancellation returns cancelled; exceeding the
|
||||
/// current call's deadline fails. A closed project is not-found.
|
||||
/// Requires `project.read`. New in 1.3.
|
||||
symbols: func(project: borrow<project>, query: string, limit: u32) -> result<list<symbol-info>, error>;
|
||||
|
||||
/// A project open in the IDE.
|
||||
resource project {
|
||||
/// Absolute path of the project root. Requires `project.read`.
|
||||
|
||||
+21
-1
@@ -1,7 +1,7 @@
|
||||
package openidea:[email protected];
|
||||
|
||||
interface workspace {
|
||||
use types.{error};
|
||||
use types.{error, location};
|
||||
|
||||
/// Requests File > Open in this command/event's window (or a new window),
|
||||
/// including the usual project trust prompt. Needs no capability.
|
||||
@@ -30,6 +30,26 @@ interface workspace {
|
||||
modified: option<u64>,
|
||||
}
|
||||
|
||||
/// A symbol in the host's current in-memory project index (new in 1.3).
|
||||
record symbol-info {
|
||||
name: string,
|
||||
/// Host symbol kind, in camelCase (`class`, `method`, `enumMember`, …).
|
||||
kind: string,
|
||||
container: option<string>,
|
||||
location: location,
|
||||
}
|
||||
|
||||
/// Fuzzy symbol search (CamelHump and container-qualified queries) over the
|
||||
/// current index only; never starts or waits for a language server. Results
|
||||
/// may be incomplete while indexing and ranges reflect the indexed text,
|
||||
/// not necessarily unsaved edits. Empty queries return no symbols. Queries
|
||||
/// over 256 UTF-8 bytes are invalid; limit is capped at 1000 (zero is empty).
|
||||
/// Only paths readable under this project's root are returned; external
|
||||
/// content roots are excluded. Cancellation returns cancelled; exceeding the
|
||||
/// current call's deadline fails. A closed project is not-found.
|
||||
/// Requires `project.read`. New in 1.3.
|
||||
symbols: func(project: borrow<project>, query: string, limit: u32) -> result<list<symbol-info>, error>;
|
||||
|
||||
/// A project open in the IDE.
|
||||
resource project {
|
||||
/// Absolute path of the project root. Requires `project.read`.
|
||||
|
||||
Reference in new issue
Block a user