mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-13 04:34:21 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d369f8ac10 |
No files matched your search
@@ -1680,15 +1680,6 @@ pub enum PomGotoKind {
|
||||
ManagedDependency,
|
||||
/// A `<pluginManagement>` entry providing the plugin's version.
|
||||
ManagedPlugin,
|
||||
/// The POM of the artifact itself: a project module's, else the local repository's.
|
||||
ArtifactPom,
|
||||
}
|
||||
|
||||
/// A Maven project of the workspace, for going to a dependency on it.
|
||||
pub struct ModulePom {
|
||||
pub group_id: String,
|
||||
pub artifact_id: String,
|
||||
pub pom: PathBuf,
|
||||
}
|
||||
|
||||
/// Byte offset of `pos` (0-based line / UTF-16 column), clamped to `text`.
|
||||
@@ -1768,20 +1759,6 @@ fn coordinates(e: &Element, props: &HashMap<String, String>) -> Option<(String,
|
||||
/// the local repository when it is not in the project). `None` when there is no
|
||||
/// resolvable declaration here.
|
||||
pub fn goto_declaration(text: &str, path: &Path, pos: Pos, local_repo: Option<PathBuf>) -> Option<PomGoto> {
|
||||
goto_declaration_in(text, path, pos, local_repo, Vec::new)
|
||||
}
|
||||
|
||||
/// [`goto_declaration`], where a `<parent>` and a dependency or plugin whose version no
|
||||
/// management entry provides (or a management entry itself) go to the artifact's own
|
||||
/// POM: the project module among `modules` with its coordinates, else the local
|
||||
/// repository's POM of its version.
|
||||
pub fn goto_declaration_in(
|
||||
text: &str,
|
||||
path: &Path,
|
||||
pos: Pos,
|
||||
local_repo: Option<PathBuf>,
|
||||
modules: impl FnOnce() -> Vec<ModulePom>,
|
||||
) -> Option<PomGoto> {
|
||||
let root = xml::parse(text).filter(|e| e.name == "project")?;
|
||||
let at = offset_at(text, pos);
|
||||
if let Some((name, _, _)) = property_ref_at(text, at)
|
||||
@@ -1792,56 +1769,15 @@ pub fn goto_declaration_in(
|
||||
// A `<dependency>` / `<plugin>` in a declaration position: its version may come
|
||||
// from a management entry of this POM or an ancestor's.
|
||||
let elements = element_path(&root, at);
|
||||
if elements.get(1).is_some_and(|e| e.name == "parent") {
|
||||
let chain = parent_chain(path, text, local_repo);
|
||||
let (pom, parent_text) = chain.get(1)?;
|
||||
let label = elements[1].text_of("artifactId").unwrap_or_default();
|
||||
return Some(pom_goto(pom.clone(), parent_text, label));
|
||||
}
|
||||
let e = elements.iter().rev().find(|e| e.name == "dependency" || e.name == "plugin")?;
|
||||
// A management entry declares the version: it goes to the artifact (a BOM import, say).
|
||||
if !elements.iter().any(|a| a.name == "dependencyManagement" || a.name == "pluginManagement") {
|
||||
let kind = match e.name.as_str() {
|
||||
"dependency" => PomGotoKind::ManagedDependency,
|
||||
_ => PomGotoKind::ManagedPlugin,
|
||||
};
|
||||
if let Some(found) = managed_target(text, path, e, kind, local_repo.clone()) {
|
||||
return Some(found);
|
||||
}
|
||||
if elements.iter().any(|a| a.name == "dependencyManagement" || a.name == "pluginManagement") {
|
||||
return None; // Management entries are declarations; there is nothing to go to.
|
||||
}
|
||||
artifact_target(text, path, e, local_repo, modules)
|
||||
}
|
||||
|
||||
/// The artifact's own POM for a dependency or plugin element `e`.
|
||||
fn artifact_target(
|
||||
text: &str,
|
||||
path: &Path,
|
||||
e: &Element,
|
||||
local_repo: Option<PathBuf>,
|
||||
modules: impl FnOnce() -> Vec<ModulePom>,
|
||||
) -> Option<PomGoto> {
|
||||
let mut resolver = Resolver::new(local_repo.clone(), &[]);
|
||||
let chain = parent_chain(path, text, local_repo);
|
||||
let props = properties_at(&mut resolver, &chain, 0);
|
||||
let (group, artifact) = coordinates(e, &props)?;
|
||||
let label = format!("{group}:{artifact}");
|
||||
if let Some(module) = modules().into_iter().find(|m| m.group_id == group && m.artifact_id == artifact) {
|
||||
let module_text = xml::read_text(&module.pom)?;
|
||||
return Some(pom_goto(module.pom, &module_text, label));
|
||||
}
|
||||
let version = interpolate(&e.text_of("version")?, &props);
|
||||
let pom = resolver.repo_pom(&group, &artifact, &version)?;
|
||||
let pom_text = xml::read_text(&pom)?;
|
||||
Some(pom_goto(pom, &pom_text, label))
|
||||
}
|
||||
|
||||
/// The POM at `path` as a target, at its `<artifactId>`.
|
||||
fn pom_goto(path: PathBuf, text: &str, label: String) -> PomGoto {
|
||||
let range = xml::parse(text)
|
||||
.filter(|e| e.name == "project")
|
||||
.and_then(|root| root.child("artifactId").map(|a| range_of(text, a)))
|
||||
.unwrap_or_default();
|
||||
PomGoto { path, range, kind: PomGotoKind::ArtifactPom, label }
|
||||
let kind = match e.name.as_str() {
|
||||
"dependency" => PomGotoKind::ManagedDependency,
|
||||
_ => PomGotoKind::ManagedPlugin,
|
||||
};
|
||||
managed_target(text, path, e, kind, local_repo)
|
||||
}
|
||||
|
||||
/// The parent POMs of the POM in `text` at `path`, child-first (its `<parent>` refs,
|
||||
@@ -3151,9 +3087,9 @@ mod tests {
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
// The property is declared nowhere along the chain.
|
||||
assert_eq!(goto_declaration(&text, &path, at(&text, "${missing.key}"), None), None);
|
||||
// The dependency names its own version and no local repository has its POM.
|
||||
// The dependency names its own version: it is the declaration.
|
||||
assert_eq!(goto_declaration(&text, &path, at(&text, "<artifactId>other-lib"), None), None);
|
||||
// A management entry declares the version; its artifact's POM is not at hand either.
|
||||
// A management entry is a declaration itself, not a reference.
|
||||
let parent_path = tmp.path().join("pom.xml");
|
||||
let parent_text = std::fs::read_to_string(&parent_path).unwrap();
|
||||
assert_eq!(
|
||||
@@ -3165,72 +3101,6 @@ mod tests {
|
||||
assert_eq!(goto_declaration("<project></project>", &path, Pos::new(0, 3), None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artifacts_go_to_their_own_pom_in_the_project_or_the_local_repository() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let repo = tmp.path().join("repo");
|
||||
let bom_dir = repo.join("com/alibaba/cloud/spring-cloud-alibaba-dependencies/2.2.6.RELEASE");
|
||||
std::fs::create_dir_all(&bom_dir).unwrap();
|
||||
let bom = "<project>\n <groupId>com.alibaba.cloud</groupId>\n <artifactId>spring-cloud-alibaba-dependencies</artifactId>\n</project>";
|
||||
std::fs::write(bom_dir.join("spring-cloud-alibaba-dependencies-2.2.6.RELEASE.pom"), bom).unwrap();
|
||||
let parent = format!(
|
||||
r#"{PARENT_HEADER}<properties><alibaba.version>2.2.6.RELEASE</alibaba.version></properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
|
||||
<version>${{alibaba.version}}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
</project>"#
|
||||
);
|
||||
let child = format!(
|
||||
r#"{CHILD_HEADER}<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>"#
|
||||
);
|
||||
write_parent_child(tmp.path(), &parent, &child);
|
||||
let core = tmp.path().join("core/pom.xml");
|
||||
std::fs::create_dir_all(core.parent().unwrap()).unwrap();
|
||||
std::fs::write(
|
||||
&core,
|
||||
"<project>\n <groupId>com.example</groupId>\n <artifactId>core</artifactId>\n</project>",
|
||||
)
|
||||
.unwrap();
|
||||
let modules =
|
||||
|| vec![ModulePom { group_id: "com.example".into(), artifact_id: "core".into(), pom: core.clone() }];
|
||||
|
||||
// A BOM import in <dependencyManagement> opens the BOM from the local repository.
|
||||
let parent_path = tmp.path().join("pom.xml");
|
||||
let parent_text = std::fs::read_to_string(&parent_path).unwrap();
|
||||
let at_bom = at(&parent_text, "spring-cloud-alibaba-dependencies<");
|
||||
let goto = goto_declaration_in(&parent_text, &parent_path, at_bom, Some(repo.clone()), modules).unwrap();
|
||||
assert_eq!(goto.path, bom_dir.join("spring-cloud-alibaba-dependencies-2.2.6.RELEASE.pom"));
|
||||
assert_eq!(goto.kind, PomGotoKind::ArtifactPom);
|
||||
assert_eq!(goto.range.start, pos_at(bom, bom.find("<artifactId>").unwrap()));
|
||||
|
||||
// A dependency on a project module opens the module's POM.
|
||||
let path = tmp.path().join("app/pom.xml");
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
let goto =
|
||||
goto_declaration_in(&text, &path, at(&text, "core</artifactId>"), Some(repo.clone()), modules).unwrap();
|
||||
assert_eq!((goto.path.as_path(), goto.label.as_str()), (core.as_path(), "com.example:core"));
|
||||
|
||||
// The <parent> section opens the parent POM.
|
||||
let goto = goto_declaration_in(&text, &path, at(&text, "parent</artifactId>"), Some(repo), modules).unwrap();
|
||||
assert_eq!(goto.path, canon(&parent_path));
|
||||
assert_eq!(goto.kind, PomGotoKind::ArtifactPom);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_property_reference_after_non_ascii_text_is_still_found() {
|
||||
let text = format!(
|
||||
|
||||
@@ -27,13 +27,6 @@ pub fn set_text(text: &str, owner: usize) -> std::io::Result<()> {
|
||||
imp::set_text(text, owner)
|
||||
}
|
||||
|
||||
/// The clipboard's plain text, None when it holds none. macOS only (main thread);
|
||||
/// Linux reads it asynchronously through GTK in the app, Windows through the WebView.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn text() -> Option<String> {
|
||||
imp::text()
|
||||
}
|
||||
|
||||
/// Size of `DROPFILES`: `pFiles`, `pt` (two `LONG`s), `fNC`, `fWide`.
|
||||
const HEADER: usize = 20;
|
||||
|
||||
@@ -380,14 +373,6 @@ mod imp {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn text() -> Option<String> {
|
||||
debug_assert!(MainThreadMarker::new().is_some(), "NSPasteboard access requires the main thread");
|
||||
autoreleasepool(|_| {
|
||||
// SAFETY: the type is an AppKit constant, valid for the process' lifetime.
|
||||
NSPasteboard::generalPasteboard().stringForType(unsafe { NSPasteboardTypeString }).map(|s| s.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_text(text: &str, _owner: usize) -> std::io::Result<()> {
|
||||
debug_assert!(MainThreadMarker::new().is_some(), "NSPasteboard access requires the main thread");
|
||||
autoreleasepool(|_| {
|
||||
|
||||
@@ -76,8 +76,7 @@ fn decode_tool_output_with_code_page(bytes: &[u8], code_page: u32) -> String {
|
||||
text
|
||||
}
|
||||
|
||||
/// The `encoding_rs` encoding of a Windows code page, when it has one.
|
||||
pub fn encoding_for_code_page(cp: u32) -> Option<&'static encoding_rs::Encoding> {
|
||||
fn encoding_for_code_page(cp: u32) -> Option<&'static encoding_rs::Encoding> {
|
||||
Some(match cp {
|
||||
65001 => encoding_rs::UTF_8,
|
||||
936 => encoding_rs::GBK,
|
||||
@@ -102,56 +101,6 @@ pub fn encoding_for_code_page(cp: u32) -> Option<&'static encoding_rs::Encoding>
|
||||
})
|
||||
}
|
||||
|
||||
/// The code page Windows console programs write to a pipe in: the OEM code page (GBK on a
|
||||
/// Chinese system, Shift_JIS on a Japanese one, EUC-KR on a Korean one, 437 / 850 / 866…
|
||||
/// elsewhere). `None` when it is UTF-8 (Windows' "Use Unicode UTF-8" option) or off Windows.
|
||||
pub fn console_code_page() -> Option<u32> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// SAFETY: GetOEMCP is an argument-less query with no preconditions.
|
||||
let cp = unsafe { windows_sys::Win32::Globalization::GetOEMCP() };
|
||||
(cp != 65001).then_some(cp)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether every character of code page `cp` is one byte, so text in it can be decoded
|
||||
/// piece by piece.
|
||||
#[cfg(windows)]
|
||||
pub fn is_single_byte_code_page(cp: u32) -> bool {
|
||||
use windows_sys::Win32::Globalization::{CPINFO, GetCPInfo};
|
||||
// SAFETY: CPINFO is plain data, which GetCPInfo fills in.
|
||||
unsafe {
|
||||
let mut info: CPINFO = std::mem::zeroed();
|
||||
GetCPInfo(cp, &mut info) != 0 && info.MaxCharSize == 1
|
||||
}
|
||||
}
|
||||
|
||||
/// `bytes` in code page `cp`, decoded by Windows (for the code pages `encoding_rs` lacks,
|
||||
/// like the OEM pages 437, 850 or 852).
|
||||
#[cfg(windows)]
|
||||
pub fn decode_code_page(bytes: &[u8], cp: u32) -> String {
|
||||
use windows_sys::Win32::Globalization::MultiByteToWideChar;
|
||||
let Ok(len) = i32::try_from(bytes.len()) else {
|
||||
return String::from_utf8_lossy(bytes).into_owned();
|
||||
};
|
||||
if len == 0 {
|
||||
return String::new();
|
||||
}
|
||||
// SAFETY: `bytes` holds `len` bytes; no output buffer asks for the length needed.
|
||||
let needed = unsafe { MultiByteToWideChar(cp, 0, bytes.as_ptr(), len, std::ptr::null_mut(), 0) };
|
||||
if needed <= 0 {
|
||||
return String::from_utf8_lossy(bytes).into_owned();
|
||||
}
|
||||
let mut wide = vec![0u16; needed as usize];
|
||||
// SAFETY: `wide` holds `needed` units.
|
||||
let written = unsafe { MultiByteToWideChar(cp, 0, bytes.as_ptr(), len, wide.as_mut_ptr(), needed) };
|
||||
String::from_utf16_lossy(&wide[..written.max(0) as usize])
|
||||
}
|
||||
|
||||
const STD_PROPERTIES: &[&str] = &["stdout.encoding", "stderr.encoding", "sun.stdout.encoding", "sun.stderr.encoding"];
|
||||
const FILE_ENCODING: &str = "file.encoding";
|
||||
const TOOL_OPTIONS: &str = "JAVA_TOOL_OPTIONS";
|
||||
|
||||
@@ -143,12 +143,9 @@ mod tests {
|
||||
std::fs::create_dir_all(root.join(".openidea")).unwrap();
|
||||
let (count, on_change) = counting();
|
||||
let _watch = SourcesWatch::start(&root, None, on_change).unwrap();
|
||||
// FSEvents can deliver the creation of `.openidea/` above after the watch started.
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
let seen = count.load(Ordering::SeqCst);
|
||||
std::fs::write(root.join(".openidea/settings.toml"), "").unwrap();
|
||||
std::fs::write(root.join("main.py"), "").unwrap();
|
||||
std::thread::sleep(SETTLE * 3);
|
||||
assert_eq!(count.load(Ordering::SeqCst), seen);
|
||||
assert_eq!(count.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ use std::path::{Component, Path, PathBuf};
|
||||
|
||||
pub const MANIFEST_FILE: &str = "plugin.toml";
|
||||
/// The plugin API (engine) version this host implements.
|
||||
pub const ENGINE_VERSION: &str = "1.7.0";
|
||||
pub const ENGINE_VERSION: &str = "1.6.0";
|
||||
/// The 0.1 API, still accepted for contribution-only packages and 0.1 components.
|
||||
pub const LEGACY_API: &str = "0.1";
|
||||
/// Marketplace categories, in the order they are shown.
|
||||
|
||||
@@ -434,23 +434,10 @@ fn cwd_of(root: &Path, c: &RunConfig) -> String {
|
||||
cwd.trim_end_matches('/').to_owned()
|
||||
}
|
||||
|
||||
/// The directory a config covers: a Cargo workspace member's own directory (its `cargo test -p`
|
||||
/// runs from the workspace root), else its working directory.
|
||||
fn scope_of(root: &Path, c: &RunConfig) -> String {
|
||||
match c.extras.module.as_deref() {
|
||||
Some(module) if c.kind == "cargo" => {
|
||||
let module = module.replace('\\', "/");
|
||||
let module = module.strip_prefix("./").unwrap_or(&module);
|
||||
if module == "." { String::new() } else { module.trim_end_matches('/').to_owned() }
|
||||
}
|
||||
_ => cwd_of(root, c),
|
||||
}
|
||||
}
|
||||
|
||||
/// Configs whose scope ([`scope_of`]) most specifically contains `rel` (ties in their original order).
|
||||
/// Configs whose working directory most specifically contains `rel` (ties in their original order).
|
||||
fn all_closest<'a>(root: &Path, configs: &[&'a RunConfig], rel: &str) -> Vec<&'a RunConfig> {
|
||||
let score = |c: &RunConfig| -> Option<usize> {
|
||||
let cwd = scope_of(root, c);
|
||||
let cwd = cwd_of(root, c);
|
||||
if cwd.is_empty() { Some(0) } else { (rel == cwd || rel.starts_with(&format!("{cwd}/"))).then_some(cwd.len()) }
|
||||
};
|
||||
let scored: Vec<(&RunConfig, usize)> = configs.iter().filter_map(|c| score(c).map(|s| (*c, s))).collect();
|
||||
@@ -935,33 +922,6 @@ mod tests {
|
||||
assert_eq!(c.args, ["test", "--", "math::tests::"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_workspace_member_runs_its_own_tests() {
|
||||
let root = Path::new("/p");
|
||||
let member = |pkg: &str, dir: &str| {
|
||||
let mut c = blank(
|
||||
format!("cargo:test:{pkg}"),
|
||||
"test",
|
||||
"cargo",
|
||||
"cargo".into(),
|
||||
vec!["test".into(), "-p".into(), pkg.into()],
|
||||
);
|
||||
c.is_test = true;
|
||||
c.extras.module = Some(dir.into());
|
||||
c
|
||||
};
|
||||
let mut workspace = blank("cargo:test".into(), "test", "cargo", "cargo".into(), vec!["test".into()]);
|
||||
workspace.is_test = true;
|
||||
let configs = vec![member("ide-agent", "crates/ide-agent"), member("ide-mcp", "crates/ide-mcp"), workspace];
|
||||
let src = "#[cfg(test)]\nmod tests {\n #[test]\n fn adds() {}\n}\n";
|
||||
let rel = "crates/ide-mcp/src/stdio.rs";
|
||||
let ts = detect(src, rel);
|
||||
let c = target_config(root, &ts[1], rel, &configs, src).unwrap();
|
||||
assert_eq!(c.args, ["test", "-p", "ide-mcp", "--", "stdio::tests::adds", "--exact"]);
|
||||
let c = target_config(root, &ts[1], "src/lib.rs", &configs, src).unwrap();
|
||||
assert!(!c.args.iter().any(|a| a == "-p"), "outside every member: the workspace's tests, {:?}", c.args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cargo_test_args_get_one_separator() {
|
||||
let root = Path::new("/p");
|
||||
|
||||
@@ -455,8 +455,7 @@ pub async fn maven_dependency_declaration(
|
||||
/// absolute for a parent POM opened from the local repository): the definition of the
|
||||
/// `${property}` at the caret, or the `<dependencyManagement>` / `<pluginManagement>`
|
||||
/// entry providing the dependency's / plugin's version, along the parent chain (into
|
||||
/// the local repository when a parent is not in the project), else the artifact's own
|
||||
/// POM (a project module's, or the local repository's). `None` when nothing
|
||||
/// the local repository when a parent is not in the project). `None` when nothing
|
||||
/// resolvable is under the caret.
|
||||
pub async fn maven_goto_declaration(host: &Host, window: &str, pom: String, pos: Pos) -> IdeResult<Option<PomGoto>> {
|
||||
let project = host.project(window)?;
|
||||
@@ -467,19 +466,7 @@ pub async fn maven_goto_declaration(host: &Host, window: &str, pom: String, pos:
|
||||
let settings = project.settings.effective();
|
||||
let repo = ide_sdk::apply::maven_local_repository(&project.root, &settings)
|
||||
.or_else(ide_build::maven::default_local_repo);
|
||||
let modules = || {
|
||||
let model = load(&project.root, &project.build_cache_dir(), &settings).maven.unwrap_or_default();
|
||||
model
|
||||
.projects
|
||||
.into_iter()
|
||||
.map(|p| ide_build::maven::ModulePom {
|
||||
group_id: p.group_id,
|
||||
artifact_id: p.artifact_id,
|
||||
pom: project.root.join(&p.pom),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
Ok(ide_build::maven::goto_declaration_in(&text, &path, pos, repo, modules))
|
||||
Ok(ide_build::maven::goto_declaration(&text, &path, pos, repo))
|
||||
})
|
||||
.await
|
||||
.map_err(join_err)?
|
||||
|
||||
@@ -114,28 +114,14 @@ pub fn create_file(host: &Host, window: &str, path: PathBuf) -> IdeResult<()> {
|
||||
if empty && !bom.is_empty() {
|
||||
std::fs::write(&path, bom)?;
|
||||
}
|
||||
// A checkout backend opens the file for add; Git's hook is a no-op.
|
||||
if let Err(e) = p.with_backend_of(&path, |r| r.files_added(std::slice::from_ref(&path))) {
|
||||
tracing::debug!("vcs files_added for {}: {e}", path.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => ide_vfs::create_file(&path),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_file_bytes(host: &Host, window: &str, path: PathBuf, data: Vec<u8>) -> IdeResult<()> {
|
||||
let project = host.project(window).ok();
|
||||
host.blocking(move || {
|
||||
ide_vfs::create_file_bytes(&path, &data)?;
|
||||
if let Some(p) = project
|
||||
&& let Err(e) = p.with_backend_of(&path, |r| r.files_added(std::slice::from_ref(&path)))
|
||||
{
|
||||
tracing::debug!("vcs files_added for {}: {e}", path.display());
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
pub async fn create_file_bytes(host: &Host, path: PathBuf, data: Vec<u8>) -> IdeResult<()> {
|
||||
host.blocking(move || ide_vfs::create_file_bytes(&path, &data)).await
|
||||
}
|
||||
|
||||
/// Toggle Read-only Attribute: sets the writable bit (and the open document's
|
||||
@@ -149,54 +135,25 @@ pub fn set_writable(host: &Host, window: &str, path: PathBuf, writable: bool) ->
|
||||
|
||||
/// Moves to the trash; Local History keeps the contents so deleted files can be restored.
|
||||
pub async fn delete(host: &Host, window: &str, path: PathBuf) -> IdeResult<()> {
|
||||
let project = host.project(window).ok();
|
||||
let history = project.as_ref().map(|p| p.history.clone());
|
||||
let history = host.project(window).ok().map(|p| p.history.clone());
|
||||
host.blocking(move || {
|
||||
let paths = ide_workspace::file_ops::deletion_paths(&path)?;
|
||||
if let Some(h) = history {
|
||||
for p in &paths {
|
||||
h.record_deletion(p, DELETE_SNAPSHOT_LIMIT);
|
||||
for p in paths {
|
||||
h.record_deletion(&p, DELETE_SNAPSHOT_LIMIT);
|
||||
}
|
||||
}
|
||||
ide_workspace::file_ops::delete(&path)?;
|
||||
// A checkout backend opens the paths for delete; Git's hook is a no-op.
|
||||
if let Some(p) = project {
|
||||
let _ = p.with_backend_of(&path, |r| r.files_deleted(&paths));
|
||||
}
|
||||
Ok(())
|
||||
ide_workspace::file_ops::delete(&path)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn copy(host: &Host, window: &str, from: PathBuf, to: PathBuf) -> IdeResult<()> {
|
||||
let project = host.project(window).ok();
|
||||
host.blocking(move || {
|
||||
ide_workspace::file_ops::copy(&from, &to)?;
|
||||
// A copy is an add at the destination; Git's hook is a no-op.
|
||||
if let Some(p) = project
|
||||
&& let Err(e) = p.with_backend_of(&to, |r| r.files_added(std::slice::from_ref(&to)))
|
||||
{
|
||||
tracing::debug!("vcs files_added for {}: {e}", to.display());
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
pub async fn copy(host: &Host, from: PathBuf, to: PathBuf) -> IdeResult<()> {
|
||||
host.blocking(move || ide_workspace::file_ops::copy(&from, &to)).await
|
||||
}
|
||||
|
||||
pub async fn rename(host: &Host, window: &str, from: PathBuf, to: PathBuf) -> IdeResult<()> {
|
||||
let project = host.project(window).ok();
|
||||
host.blocking(move || {
|
||||
ide_workspace::file_ops::rename(&from, &to)?;
|
||||
// A checkout backend records the rename so history follows the file;
|
||||
// Git's hook is a no-op.
|
||||
if let Some(p) = project
|
||||
&& let Err(e) = p.with_backend_of(&to, |r| r.file_moved(&from, &to))
|
||||
{
|
||||
tracing::debug!("vcs file_moved {} → {}: {e}", from.display(), to.display());
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
pub async fn rename(host: &Host, from: PathBuf, to: PathBuf) -> IdeResult<()> {
|
||||
host.blocking(move || ide_workspace::file_ops::rename(&from, &to)).await
|
||||
}
|
||||
|
||||
/// Reads a text file without opening it as a document; prefers the unsaved buffer.
|
||||
|
||||
@@ -138,11 +138,6 @@ impl LogAccess {
|
||||
.ok_or_else(|| ide_core::IdeError::unavailable("external Git log is not open"))?
|
||||
.with_log_repo_at(at, f)
|
||||
}
|
||||
|
||||
/// `with_log_repo_at` through the backend-agnostic Changes-view surface.
|
||||
fn with_log_backend_at<T>(&self, at: Option<&Path>, f: impl FnOnce(&dyn Backend) -> IdeResult<T>) -> IdeResult<T> {
|
||||
self.with_log_repo_at(at, |r| f(r as &dyn Backend))
|
||||
}
|
||||
}
|
||||
|
||||
/// A repository of the project (IDEA's VCS root).
|
||||
@@ -285,16 +280,6 @@ fn per_repo(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `per_repo` through the backend-agnostic Changes-view surface.
|
||||
fn per_backend(
|
||||
project: &Project,
|
||||
paths: &[PathBuf],
|
||||
recorded: Option<&str>,
|
||||
f: impl Fn(&dyn Backend, &[PathBuf]) -> IdeResult<()>,
|
||||
) -> IdeResult<()> {
|
||||
per_repo(project, paths, recorded, |r, paths| f(r as &dyn Backend, paths))
|
||||
}
|
||||
|
||||
/// A shelf is one patch for one owning repository. Validate the whole selection before reverting files.
|
||||
fn shelf_repository(project: &Project, paths: &[PathBuf]) -> IdeResult<PathBuf> {
|
||||
let repos = project.repos();
|
||||
@@ -363,7 +348,7 @@ pub async fn vcs_line_hunks(host: &Host, window: &str, path: PathBuf) -> IdeResu
|
||||
let project = host.project(window)?;
|
||||
blocking(move || {
|
||||
let text = project.text_of(&path)?;
|
||||
project.with_backend_of(&path, |r| r.line_hunks(&path, &text, false, project.vcs_encoding(&path, None)?))
|
||||
project.with_repo_of(&path, |r| r.line_hunks(&path, &text, false, project.vcs_encoding(&path, None)?))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -375,7 +360,7 @@ pub async fn vcs_file_at(host: &Host, window: &str, path: PathBuf, rev: Option<S
|
||||
});
|
||||
blocking(move || {
|
||||
if let Some(project) = project {
|
||||
return project.with_log_backend_of(&path, |r| {
|
||||
return project.with_log_repo_of(&path, |r| {
|
||||
r.file_at(&path, rev.as_deref(), project.vcs_encoding(&path, rev.as_deref())?)
|
||||
});
|
||||
}
|
||||
@@ -398,7 +383,7 @@ pub async fn vcs_file_mode(host: &Host, window: &str, path: PathBuf, rev: Option
|
||||
});
|
||||
blocking(move || {
|
||||
if let Some(project) = project {
|
||||
return project.with_log_backend_of(&path, |r| r.file_mode_at(&path, rev.as_deref()));
|
||||
return project.with_log_repo_of(&path, |r| r.file_mode_at(&path, rev.as_deref()));
|
||||
}
|
||||
external
|
||||
.ok_or_else(|| ide_core::IdeError::unavailable("external Git log is not open"))?
|
||||
@@ -418,7 +403,7 @@ pub async fn vcs_blame(
|
||||
// An explicit text wins (merge view annotates its side panes, not the worktree).
|
||||
blocking(move || {
|
||||
let text = text.or_else(|| project.documents.text_of(&path));
|
||||
project.with_backend_of(&path, |r| {
|
||||
project.with_repo_of(&path, |r| {
|
||||
r.blame(&path, text.as_deref(), revision.as_deref(), project.vcs_encoding(&path, revision.as_deref())?)
|
||||
})
|
||||
})
|
||||
@@ -439,7 +424,7 @@ pub async fn vcs_block_history(
|
||||
let cancel = host.begin_request(request);
|
||||
let result = blocking(move || {
|
||||
let text = text.or_else(|| project.documents.text_of(&path));
|
||||
project.with_backend_of(&path, |r| {
|
||||
project.with_repo_of(&path, |r| {
|
||||
r.block_history(&path, text.as_deref(), lines, project.vcs_encoding(&path, None)?, &cancel)
|
||||
})
|
||||
})
|
||||
@@ -456,7 +441,7 @@ pub async fn vcs_log(host: &Host, window: &str, repo: Option<PathBuf>, query: Lo
|
||||
let result = blocking(move || {
|
||||
project.with_log_repo_at(repo.as_deref(), |r| {
|
||||
r.enable_log_index(&project.cache);
|
||||
(r as &dyn Backend).log(&query, &cancel)
|
||||
r.log_cancellable(&query, &cancel)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
@@ -555,7 +540,7 @@ pub async fn vcs_commit_files(
|
||||
commit: String,
|
||||
) -> IdeResult<Vec<ChangedFile>> {
|
||||
let project = LogAccess::new(host, window, repo.as_deref());
|
||||
blocking(move || project.with_log_backend_at(repo.as_deref(), |r| r.commit_files(&commit))).await
|
||||
blocking(move || project.with_log_repo_at(repo.as_deref(), |r| r.commit_files(&commit))).await
|
||||
}
|
||||
|
||||
/// Combined changes of several commits, `commits` oldest first.
|
||||
@@ -566,7 +551,7 @@ pub async fn vcs_commits_changes(
|
||||
commits: Vec<String>,
|
||||
) -> IdeResult<Vec<RevisionChange>> {
|
||||
let project = LogAccess::new(host, window, repo.as_deref());
|
||||
blocking(move || project.with_log_backend_at(repo.as_deref(), |r| r.commits_changes(&commits))).await
|
||||
blocking(move || project.with_log_repo_at(repo.as_deref(), |r| r.commits_changes(&commits))).await
|
||||
}
|
||||
|
||||
pub async fn vcs_branches(host: &Host, window: &str, repo: Option<PathBuf>) -> IdeResult<Vec<Branch>> {
|
||||
@@ -582,7 +567,7 @@ pub async fn vcs_stashes(host: &Host, window: &str, repo: Option<PathBuf>) -> Id
|
||||
pub async fn vcs_stage(host: &Host, window: &str, paths: Vec<PathBuf>) -> IdeResult<()> {
|
||||
host.require_trust(window)?;
|
||||
let project = host.project(window)?;
|
||||
blocking(move || per_backend(&project, &paths, None, |r, paths| r.stage(paths))).await
|
||||
blocking(move || per_repo(&project, &paths, None, |r, paths| r.stage(paths))).await
|
||||
}
|
||||
|
||||
/// Project-host operation (also registered with the remote dispatcher).
|
||||
@@ -617,7 +602,7 @@ pub async fn vcs_add_to_ignore(
|
||||
pub async fn vcs_unstage(host: &Host, window: &str, paths: Vec<PathBuf>) -> IdeResult<()> {
|
||||
host.require_trust(window)?;
|
||||
let project = host.project(window)?;
|
||||
blocking(move || per_backend(&project, &paths, None, |r, paths| r.unstage(paths))).await
|
||||
blocking(move || per_repo(&project, &paths, None, |r, paths| r.unstage(paths))).await
|
||||
}
|
||||
|
||||
pub async fn vcs_stage_patch(host: &Host, window: &str, patch: String) -> IdeResult<()> {
|
||||
@@ -629,7 +614,7 @@ pub async fn vcs_stage_patch(host: &Host, window: &str, patch: String) -> IdeRes
|
||||
pub async fn vcs_discard(host: &Host, window: &str, paths: Vec<PathBuf>) -> IdeResult<()> {
|
||||
host.require_trust(window)?;
|
||||
let project = host.project(window)?;
|
||||
blocking(move || per_backend(&project, &paths, Some("Rollback"), |r, paths| r.discard(paths))).await
|
||||
blocking(move || per_repo(&project, &paths, Some("Rollback"), |r, paths| r.discard(paths))).await
|
||||
}
|
||||
|
||||
pub async fn vcs_commit(
|
||||
@@ -643,8 +628,8 @@ pub async fn vcs_commit(
|
||||
// Commit hooks may run for a long time.
|
||||
let project = host.project(window)?;
|
||||
blocking(move || {
|
||||
project.with_backend_recorded(repo.as_deref(), None, |r| {
|
||||
r.commit(&message, &options, &|path| project.vcs_encoding(path, None))
|
||||
project.with_repo_recorded(repo.as_deref(), None, |r| {
|
||||
r.commit_with(&message, &options, &|path| project.vcs_encoding(path, None))
|
||||
})
|
||||
})
|
||||
.await
|
||||
@@ -901,7 +886,7 @@ pub async fn vcs_conflict_versions(host: &Host, window: &str, path: PathBuf) ->
|
||||
|
||||
pub async fn vcs_diff_text(host: &Host, window: &str, path: PathBuf, staged: bool) -> IdeResult<String> {
|
||||
let project = host.project(window)?;
|
||||
blocking(move || project.with_backend_of(&path, |r| r.diff_text(&path, staged, project.vcs_encoding(&path, None)?)))
|
||||
blocking(move || project.with_repo_of(&path, |r| r.diff_text(&path, staged, project.vcs_encoding(&path, None)?)))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1216,7 +1201,7 @@ pub async fn vcs_diff_with_worktree(
|
||||
rev: String,
|
||||
) -> IdeResult<Vec<ChangedFile>> {
|
||||
let project = LogAccess::new(host, window, repo.as_deref());
|
||||
blocking(move || project.with_log_backend_at(repo.as_deref(), |r| r.diff_with_worktree(&rev))).await
|
||||
blocking(move || project.with_log_repo_at(repo.as_deref(), |r| r.diff_with_worktree(&rev))).await
|
||||
}
|
||||
|
||||
/// Files that differ between two revisions (the "N files updated" of Update Project).
|
||||
@@ -1228,7 +1213,7 @@ pub async fn vcs_diff_revisions(
|
||||
to: String,
|
||||
) -> IdeResult<Vec<ChangedFile>> {
|
||||
let project = LogAccess::new(host, window, repo.as_deref());
|
||||
blocking(move || project.with_log_backend_at(repo.as_deref(), |r| r.diff_revisions(&from, &to))).await
|
||||
blocking(move || project.with_log_repo_at(repo.as_deref(), |r| r.diff_revisions(&from, &to))).await
|
||||
}
|
||||
|
||||
/// Added and deleted lines between two revisions. `paths` limits the diff; empty diffs every file.
|
||||
|
||||
@@ -1276,13 +1276,12 @@ pub(super) fn commit_draft(
|
||||
};
|
||||
// Both callers have checked that the window's project is trusted.
|
||||
r.set_execution_allowed(true);
|
||||
let backend = &r as &dyn ide_vcs::Backend;
|
||||
let pending = backend
|
||||
.pending_changes(&repo.options, budget, &|path| Ok(workspace.vcs_encoding(path)))
|
||||
.map_err(|e| match e.kind {
|
||||
let pending = r.pending_changes(&repo.options, budget, &|path| Ok(workspace.vcs_encoding(path))).map_err(
|
||||
|e| match e.kind {
|
||||
ide_core::error::ErrorKind::InvalidInput => HostError::Invalid(e.message),
|
||||
_ => HostError::Failed(e.message),
|
||||
})?;
|
||||
},
|
||||
)?;
|
||||
let root = to_ipc(r.root());
|
||||
for c in pending {
|
||||
budget -= c.diff.as_ref().map_or(0, String::len).min(budget);
|
||||
@@ -1531,19 +1530,6 @@ impl HostApi for PluginApi {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn open_diff(
|
||||
&self,
|
||||
client: &str,
|
||||
diff: ide_plugin::ContentDiff,
|
||||
file_name: Option<String>,
|
||||
) -> Result<(), HostError> {
|
||||
super::execution::validate_diff(&diff)?;
|
||||
if let Some(name) = &file_name {
|
||||
super::execution::label(name, 256)?;
|
||||
}
|
||||
self.shared.sink.emit(Some(client), PluginEvent::OpenDiff { plugin_id: self.id().into(), diff, file_name });
|
||||
Ok(())
|
||||
}
|
||||
fn reply_content_tab(
|
||||
&self,
|
||||
client: &str,
|
||||
|
||||
@@ -72,19 +72,14 @@ pub(super) fn validate_reply(reply: &ContentTabReply) -> Result<(), HostError> {
|
||||
return Err(HostError::Invalid("content status or error too long".into()));
|
||||
}
|
||||
if let Some(diff) = &reply.diff {
|
||||
validate_diff(diff)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn validate_diff(diff: &ide_plugin::ContentDiff) -> Result<(), HostError> {
|
||||
body(&diff.left)?;
|
||||
body(&diff.right)?;
|
||||
for value in [&diff.left_label, &diff.right_label].into_iter().flatten() {
|
||||
label(value, 128)?;
|
||||
}
|
||||
if let Some(title) = &diff.title {
|
||||
label(title, 256)?;
|
||||
body(&diff.left)?;
|
||||
body(&diff.right)?;
|
||||
for value in [&diff.left_label, &diff.right_label].into_iter().flatten() {
|
||||
label(value, 128)?;
|
||||
}
|
||||
if let Some(title) = &diff.title {
|
||||
label(title, 256)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -47,48 +47,6 @@ pub const SECRET_REQUEST: &str = "plugin_secret";
|
||||
pub const SECRET_WRITE: &str = "plugin_secret_write";
|
||||
/// OAuth listeners bound on the desktop, never the project server.
|
||||
pub const AUTH_REQUEST: &str = "plugin_auth";
|
||||
/// What [`RemotePlugins::register`] adds to a server's commands.
|
||||
pub const COMMANDS: &[&str] = &[
|
||||
SYNC_PACKAGES,
|
||||
"plugin_run_command",
|
||||
"plugin_set_ui_locale",
|
||||
"plugin_select_tests",
|
||||
"plugin_parse_test_results",
|
||||
"plugin_terminal_session_close",
|
||||
"plugin_terminal_session_spawn",
|
||||
"plugin_prepare_debug_run",
|
||||
"plugin_finish_operation",
|
||||
"plugin_content_tab_action",
|
||||
"plugin_post_events",
|
||||
"plugin_event_kinds",
|
||||
"plugin_status_items",
|
||||
"plugin_context_keys",
|
||||
"plugin_cancel_task",
|
||||
"plugin_answer_prompt",
|
||||
"plugin_choose_program",
|
||||
"plugin_program",
|
||||
"plugin_answer_command",
|
||||
"plugin_answer_commit",
|
||||
"plugin_commit_checks",
|
||||
"mcp_set_open_editors",
|
||||
"mcp_set_focused_editor",
|
||||
"mcp_report_problems",
|
||||
"plugin_decoration_sets",
|
||||
"plugin_set_active_file",
|
||||
"plugin_view_render",
|
||||
"plugin_view_event",
|
||||
"plugin_service_detail",
|
||||
"plugin_service_log",
|
||||
"plugin_navigation_files",
|
||||
"plugin_navigation_invalidate",
|
||||
"plugin_navigation_index",
|
||||
"plugin_navigation_goto",
|
||||
"plugin_navigation_markers",
|
||||
"plugin_endpoints",
|
||||
"plugin_run_targets",
|
||||
"plugin_answer_run_target",
|
||||
"plugin_line_markers",
|
||||
];
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SecretWrite {
|
||||
@@ -176,25 +134,20 @@ pub fn package_digest(dir: &Path, files: &[PathBuf]) -> IdeResult<String> {
|
||||
}
|
||||
|
||||
/// Whether a plugin event means something to a remote window (the desktop
|
||||
/// owns the plugin list, themes and consent). The desktop checks it again on
|
||||
/// arrival: a server may send anything, and only what acts on the remote project
|
||||
/// passes. A command may only be another plugin command, which runs on the server.
|
||||
/// owns the plugin list, themes and consent).
|
||||
pub fn forwarded(event: &PluginEvent) -> bool {
|
||||
if let PluginEvent::ExecuteCommand { id, .. } = event {
|
||||
return plugin_command(id);
|
||||
}
|
||||
matches!(
|
||||
event,
|
||||
PluginEvent::RunTargetsChanged
|
||||
| PluginEvent::WindowRequest { .. }
|
||||
| PluginEvent::ContentTabReply { .. }
|
||||
| PluginEvent::OpenDiff { .. }
|
||||
| PluginEvent::DecorationsChanged { .. }
|
||||
| PluginEvent::EditorOpen { .. }
|
||||
| PluginEvent::EditorRanges { .. }
|
||||
| PluginEvent::Notification { .. }
|
||||
| PluginEvent::StatusItemChanged { .. }
|
||||
| PluginEvent::WindowStatusItemChanged { .. }
|
||||
| PluginEvent::ExecuteCommand { .. }
|
||||
| PluginEvent::OutputChanged { .. }
|
||||
| PluginEvent::RevealOutput { .. }
|
||||
| PluginEvent::ContextChanged { .. }
|
||||
@@ -210,6 +163,7 @@ pub fn forwarded(event: &PluginEvent) -> bool {
|
||||
| PluginEvent::OpenLogTab { .. }
|
||||
| PluginEvent::CopyToClipboard { .. }
|
||||
| PluginEvent::OpenUrl { .. }
|
||||
| PluginEvent::OpenProject { .. }
|
||||
| PluginEvent::PromptRequested { .. }
|
||||
| PluginEvent::PromptClosed { .. }
|
||||
| PluginEvent::PromptWaiting { .. }
|
||||
@@ -219,13 +173,6 @@ pub fn forwarded(event: &PluginEvent) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// `plugin.<plugin id>.<command id>`, as the workbench names plugin commands.
|
||||
fn plugin_command(id: &str) -> bool {
|
||||
id.strip_prefix("plugin.")
|
||||
.and_then(|rest| rest.split_once('.'))
|
||||
.is_some_and(|(plugin, command)| !plugin.is_empty() && !command.is_empty() && !command.ends_with('.'))
|
||||
}
|
||||
|
||||
// ---- desktop side -----------------------------------------------------------
|
||||
|
||||
impl PluginHost {
|
||||
@@ -1004,43 +951,4 @@ mod tests {
|
||||
assert!(valid_id("ai-completion"));
|
||||
assert!(!valid_id("../x") && !valid_id(".data") && !valid_id(""));
|
||||
}
|
||||
|
||||
fn execute(id: &str) -> PluginEvent {
|
||||
PluginEvent::ExecuteCommand {
|
||||
request_id: "1".into(),
|
||||
id: id.into(),
|
||||
arg: Some(r"\\host.example\share\pkg".into()),
|
||||
chain: vec!["tools".into()],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_server_runs_only_plugin_commands_in_the_desktop_window() {
|
||||
assert!(forwarded(&execute("plugin.tools.build")));
|
||||
assert!(forwarded(&execute("plugin.acme.tools.build.all")));
|
||||
for id in [
|
||||
"plugins.installFromDisk",
|
||||
"project.revokeTrust",
|
||||
"project.removeTrustedLocation",
|
||||
"ssh.startSession",
|
||||
"file.openProjectPath",
|
||||
"terminal.runCommand",
|
||||
"plugin.",
|
||||
"plugin.tools",
|
||||
"plugin..build",
|
||||
"plugin.tools.",
|
||||
] {
|
||||
assert!(!forwarded(&execute(id)), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_server_does_not_open_desktop_folders() {
|
||||
let open = PluginEvent::OpenProject {
|
||||
plugin_id: "tools".into(),
|
||||
path: r"\\host.example\share\project".into(),
|
||||
new_window: true,
|
||||
};
|
||||
assert!(!forwarded(&open));
|
||||
}
|
||||
}
|
||||
@@ -218,9 +218,6 @@ pub enum PluginEvent {
|
||||
/// Reply to a content action in this window.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContentTabReply { plugin_id: String, request_id: String, reply: ide_plugin::ContentTabReply },
|
||||
/// Open two texts in this window's diff viewer.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
OpenDiff { plugin_id: String, diff: ide_plugin::ContentDiff, file_name: Option<String> },
|
||||
|
||||
/// Plugins, their status or their contributions changed: re-fetch `plugin_list`.
|
||||
CommandsChanged,
|
||||
|
||||
@@ -8,7 +8,7 @@ use ide_language::{FileTypes, LanguageRegistry, SyntaxCache};
|
||||
use ide_lsp::LspManager;
|
||||
use ide_syntax::StructureService;
|
||||
use ide_text::{AppliedChange, ByteEdit};
|
||||
use ide_vcs::{Backend, Repo, Repos};
|
||||
use ide_vcs::{Repo, Repos};
|
||||
use ide_vfs::Watcher;
|
||||
use ide_workspace::documents::DocumentObserver;
|
||||
use ide_workspace::{DocumentInfo, Documents, FileMoves, LocalHistory, ProjectRoots, SettingsStore, SourceFolder};
|
||||
@@ -971,11 +971,6 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
/// `with_repo_of` through the backend-agnostic Changes-view surface.
|
||||
pub fn with_backend_of<T>(&self, path: &Path, f: impl FnOnce(&dyn Backend) -> IdeResult<T>) -> IdeResult<T> {
|
||||
self.with_repo_of(path, |r| f(r as &dyn Backend))
|
||||
}
|
||||
|
||||
fn run_in<T>(&self, repo: &Repo, f: impl FnOnce(&Repo) -> IdeResult<T>) -> IdeResult<T> {
|
||||
let settings = self.settings.effective();
|
||||
if self.is_trusted() {
|
||||
@@ -985,40 +980,6 @@ impl Project {
|
||||
f(repo)
|
||||
}
|
||||
|
||||
/// `with_repo_at` through the backend-agnostic Changes-view surface.
|
||||
pub fn with_backend_at<T>(&self, at: Option<&Path>, f: impl FnOnce(&dyn Backend) -> IdeResult<T>) -> IdeResult<T> {
|
||||
self.with_repo_at(at, |r| f(r as &dyn Backend))
|
||||
}
|
||||
|
||||
/// `with_log_repo_at` through the backend-agnostic Changes-view surface.
|
||||
pub fn with_log_backend_at<T>(
|
||||
&self,
|
||||
at: Option<&Path>,
|
||||
f: impl FnOnce(&dyn Backend) -> IdeResult<T>,
|
||||
) -> IdeResult<T> {
|
||||
self.with_log_repo_at(at, |r| f(r as &dyn Backend))
|
||||
}
|
||||
|
||||
/// `with_log_repo_of` through the backend-agnostic Changes-view surface.
|
||||
pub fn with_log_backend_of<T>(&self, path: &Path, f: impl FnOnce(&dyn Backend) -> IdeResult<T>) -> IdeResult<T> {
|
||||
self.with_log_repo_of(path, |r| f(r as &dyn Backend))
|
||||
}
|
||||
|
||||
/// `with_repo` through the backend-agnostic Changes-view surface.
|
||||
pub fn with_backend<T>(&self, f: impl FnOnce(&dyn Backend) -> IdeResult<T>) -> IdeResult<T> {
|
||||
self.with_backend_at(None, f)
|
||||
}
|
||||
|
||||
/// `with_repo_recorded` through the backend-agnostic Changes-view surface.
|
||||
pub fn with_backend_recorded<T>(
|
||||
&self,
|
||||
at: Option<&Path>,
|
||||
operation: Option<&str>,
|
||||
f: impl FnOnce(&dyn Backend) -> IdeResult<T>,
|
||||
) -> IdeResult<T> {
|
||||
self.with_repo_recorded(at, operation, |r| f(r as &dyn Backend))
|
||||
}
|
||||
|
||||
/// Runs `f` with the project's primary git repository, or fails when the project is not under git.
|
||||
pub fn with_repo<T>(&self, f: impl FnOnce(&Repo) -> IdeResult<T>) -> IdeResult<T> {
|
||||
self.with_repo_at(None, f)
|
||||
@@ -1043,7 +1004,7 @@ impl Project {
|
||||
) -> IdeResult<T> {
|
||||
let Some(name) = operation else { return self.with_disk_sync(|| self.with_repo_at(at, f)) };
|
||||
self.history_label(&format!("Before {name}"));
|
||||
if let Ok(changed) = self.with_backend_at(at, |r| r.status()) {
|
||||
if let Ok(changed) = self.with_repo_at(at, |r| r.status()) {
|
||||
for file in changed.iter().take(HISTORY_BATCH_LIMIT) {
|
||||
if let Err(e) = self.history.record_file(&file.path, &format!("Before {name}"), false) {
|
||||
tracing::debug!("local history: {e}");
|
||||
|
||||
+3
-106
@@ -185,15 +185,15 @@ impl Dispatcher {
|
||||
.add("fs_entry_kind", |c| async move { ops::fs::entry_kind(&c.host, c.arg("path")?).await })
|
||||
.add("fs_create_file", |c| async move { ops::fs::create_file(&c.host, &c.window, c.arg("path")?) })
|
||||
.add("fs_create_file_bytes", |c| async move {
|
||||
ops::fs::create_file_bytes(&c.host, &c.window, c.arg("path")?, c.arg("data")?).await
|
||||
ops::fs::create_file_bytes(&c.host, c.arg("path")?, c.arg("data")?).await
|
||||
})
|
||||
.add("fs_create_dir", |c| async move { ide_vfs::create_dir(&c.arg::<PathBuf>("path")?) })
|
||||
.add("fs_rename", |c| async move { ops::fs::rename(&c.host, &c.window, c.arg("from")?, c.arg("to")?).await })
|
||||
.add("fs_rename", |c| async move { ops::fs::rename(&c.host, c.arg("from")?, c.arg("to")?).await })
|
||||
.add("fs_set_writable", |c| async move {
|
||||
ops::fs::set_writable(&c.host, &c.window, c.arg("path")?, c.arg("writable")?)
|
||||
})
|
||||
.add("fs_delete", |c| async move { ops::fs::delete(&c.host, &c.window, c.arg("path")?).await })
|
||||
.add("fs_copy", |c| async move { ops::fs::copy(&c.host, &c.window, c.arg("from")?, c.arg("to")?).await })
|
||||
.add("fs_copy", |c| async move { ops::fs::copy(&c.host, c.arg("from")?, c.arg("to")?).await })
|
||||
.add("fs_copy_merge", |c| async move {
|
||||
ops::fs::copy_merge(
|
||||
&c.host,
|
||||
@@ -298,56 +298,6 @@ pub fn event_sink(peer: Arc<OnceLock<Peer>>, window: Option<String>) -> ide_core
|
||||
Arc::new(PeerSink { peer, window })
|
||||
}
|
||||
|
||||
/// Whether a desktop shows an `event` notification of its server in the window. A server reports
|
||||
/// on its project; what this machine answers or owns itself (MCP approvals, app updates, window
|
||||
/// groups) never comes from it.
|
||||
pub fn client_accepts(event: &IdeEvent) -> bool {
|
||||
match event {
|
||||
IdeEvent::McpRequest { .. }
|
||||
| IdeEvent::McpRequestClosed { .. }
|
||||
| IdeEvent::WindowGroupsChanged
|
||||
| IdeEvent::WindowFullscreenChanged
|
||||
| IdeEvent::UpdateStatusChanged { .. }
|
||||
| IdeEvent::UpdateAvailable { .. } => false,
|
||||
IdeEvent::DbSourcesChanged
|
||||
| IdeEvent::ProjectTrustChanged { .. }
|
||||
| IdeEvent::FsChanged { .. }
|
||||
| IdeEvent::ExcludedDirsChanged { .. }
|
||||
| IdeEvent::DocPatched { .. }
|
||||
| IdeEvent::DocDirty { .. }
|
||||
| IdeEvent::WorkspaceEditApplied { .. }
|
||||
| IdeEvent::DocLanguage { .. }
|
||||
| IdeEvent::Diagnostics { .. }
|
||||
| IdeEvent::Task { .. }
|
||||
| IdeEvent::IndexState { .. }
|
||||
| IdeEvent::VcsChanged { .. }
|
||||
| IdeEvent::LanguageServer { .. }
|
||||
| IdeEvent::LanguageServerWarning { .. }
|
||||
| IdeEvent::LanguageServerRemoved { .. }
|
||||
| IdeEvent::LspRefresh { .. }
|
||||
| IdeEvent::TerminalExited { .. }
|
||||
| IdeEvent::RunExited { .. }
|
||||
| IdeEvent::Debug { .. }
|
||||
| IdeEvent::Notify { .. }
|
||||
| IdeEvent::SettingsChanged { .. }
|
||||
| IdeEvent::ProjectRootsChanged { .. }
|
||||
| IdeEvent::Askpass { .. }
|
||||
| IdeEvent::AskpassDone { .. }
|
||||
| IdeEvent::NotebookKernel { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The commands a desktop routes to its server: what `openidea-server` of this build answers,
|
||||
/// fixed here rather than taken from the server, so a server cannot move a command to this machine.
|
||||
pub fn client_commands() -> Vec<&'static str> {
|
||||
let mut out = Dispatcher::standard().commands();
|
||||
out.extend(crate::plugins::remote_host::COMMANDS);
|
||||
out.push(crate::plugins::remote::SYNC_COMMAND);
|
||||
out.sort_unstable();
|
||||
out.dedup();
|
||||
out
|
||||
}
|
||||
|
||||
struct PeerSink {
|
||||
peer: Arc<OnceLock<Peer>>,
|
||||
window: Option<String>,
|
||||
@@ -414,56 +364,3 @@ pub fn error_from_reply(e: ide_jsonrpc::RpcError) -> IdeError {
|
||||
other => other.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ide_core::event::{AskpassKind, McpUiRequest, NotifyLevel};
|
||||
|
||||
#[test]
|
||||
fn a_server_cannot_raise_desktop_prompts() {
|
||||
let confirm = McpUiRequest::Confirm {
|
||||
client: "agent".into(),
|
||||
tool: "run".into(),
|
||||
subject: "rm -rf".into(),
|
||||
project: "/srv/app".into(),
|
||||
detail: None,
|
||||
};
|
||||
let update = ide_core::updates::UpdateInfo {
|
||||
version: "9.9.9".into(),
|
||||
current_version: "0.1.0".into(),
|
||||
notes: None,
|
||||
date: None,
|
||||
};
|
||||
for event in [
|
||||
IdeEvent::McpRequest { id: 1, request: confirm },
|
||||
IdeEvent::McpRequestClosed { id: 1 },
|
||||
IdeEvent::UpdateAvailable { update },
|
||||
IdeEvent::UpdateStatusChanged {
|
||||
status: ide_core::updates::UpdateStatus { last_checked: None, checking: true },
|
||||
},
|
||||
IdeEvent::WindowGroupsChanged,
|
||||
IdeEvent::WindowFullscreenChanged,
|
||||
] {
|
||||
assert!(!client_accepts(&event), "{event:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_server_reports_its_project_to_the_desktop() {
|
||||
for event in [
|
||||
IdeEvent::FsChanged {
|
||||
created: vec!["/srv/app/a".into()],
|
||||
modified: vec![],
|
||||
removed: vec![],
|
||||
ignore_rules: vec![],
|
||||
},
|
||||
IdeEvent::VcsChanged { root: "/srv/app".into() },
|
||||
IdeEvent::Notify { level: NotifyLevel::Info, title: "Build".into(), body: None },
|
||||
IdeEvent::Askpass { id: 1, prompt: "Password".into(), kind: AskpassKind::Secret, remember: false },
|
||||
IdeEvent::ProjectTrustChanged { root: "/srv/app".into(), trusted: true },
|
||||
] {
|
||||
assert!(client_accepts(&event), "{event:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,15 +18,6 @@ pub struct TrustStore {
|
||||
state: Mutex<IdeResult<TrustDecisions>>,
|
||||
/// Folders trusted for this process only, by the client that started it ([`TRUSTED_ROOT_ENV`]).
|
||||
inherited: Mutex<Vec<PathBuf>>,
|
||||
remote_file: PathBuf,
|
||||
remote: Mutex<IdeResult<RemoteDecisions>>,
|
||||
}
|
||||
|
||||
/// This machine's decisions about remote projects, by location (see [`TrustStore::remote_decision`]).
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
struct RemoteDecisions {
|
||||
trusted: Vec<String>,
|
||||
restricted: Vec<String>,
|
||||
}
|
||||
|
||||
/// Set by the desktop when it starts the server in a dev container of a folder the user trusts
|
||||
@@ -49,37 +40,7 @@ impl TrustStore {
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
};
|
||||
let remote_file = data.join("remote-project-trust.json");
|
||||
let remote = match std::fs::read(&remote_file) {
|
||||
Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| IdeError::invalid(e.to_string())),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(RemoteDecisions::default()),
|
||||
Err(e) => Err(e.into()),
|
||||
};
|
||||
Self { file, state: Mutex::new(state), inherited: Mutex::default(), remote_file, remote: Mutex::new(remote) }
|
||||
}
|
||||
|
||||
/// The decision made on this machine for the remote project at `location` (`None`: none yet).
|
||||
/// Commands a remote window runs here follow it, not the server's own trust.
|
||||
pub fn remote_decision(&self, location: &str) -> IdeResult<Option<bool>> {
|
||||
let remote = self.remote.lock().clone()?;
|
||||
Ok(if remote.restricted.iter().any(|l| l == location) {
|
||||
Some(false)
|
||||
} else if remote.trusted.iter().any(|l| l == location) {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decide_remote(&self, location: &str, trusted: bool) -> IdeResult<()> {
|
||||
let mut state = self.remote.lock();
|
||||
let mut next = state.clone()?;
|
||||
next.trusted.retain(|l| l != location);
|
||||
next.restricted.retain(|l| l != location);
|
||||
if trusted { &mut next.trusted } else { &mut next.restricted }.push(location.to_owned());
|
||||
persist(&self.remote_file, &next)?;
|
||||
*state = Ok(next);
|
||||
Ok(())
|
||||
Self { file, state: Mutex::new(state), inherited: Mutex::default() }
|
||||
}
|
||||
|
||||
pub fn decisions(&self) -> IdeResult<TrustDecisions> {
|
||||
@@ -172,7 +133,7 @@ impl TrustStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn persist<T: Serialize>(file: &Path, state: &T) -> IdeResult<()> {
|
||||
fn persist(file: &Path, state: &TrustDecisions) -> IdeResult<()> {
|
||||
if let Some(parent) = file.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
@@ -411,8 +372,6 @@ pub fn restricted_command_allowed(cmd: &str) -> bool {
|
||||
| "remote_current"
|
||||
| "remote_disconnect"
|
||||
| "remote_connect"
|
||||
| "remote_trust_status"
|
||||
| "remote_trust_set"
|
||||
| "remote_bootstrap_list"
|
||||
| "wsl_distros"
|
||||
| "wsl_resolve"
|
||||
@@ -567,18 +526,4 @@ mod tests {
|
||||
fn restricted_mode_allows_clipboard_write_text() {
|
||||
assert!(super::restricted_command_allowed("clipboard_write_text"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_project_decisions_are_kept_on_this_machine() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let load = || super::TrustStore::load(&tmp.path().join("config"), &tmp.path().join("data"));
|
||||
let store = load();
|
||||
let here = r#"[{"kind":"ssh","config":"box"},"/srv/app"]"#;
|
||||
assert_eq!(store.remote_decision(here).unwrap(), None);
|
||||
store.decide_remote(here, true).unwrap();
|
||||
assert_eq!(store.remote_decision(here).unwrap(), Some(true));
|
||||
assert_eq!(store.remote_decision(r#"[{"kind":"ssh","config":"box"},"/srv/other"]"#).unwrap(), None);
|
||||
store.decide_remote(here, false).unwrap();
|
||||
assert_eq!(load().remote_decision(here).unwrap(), Some(false));
|
||||
}
|
||||
}
|
||||
@@ -6352,26 +6352,6 @@ fn api14_stage3_configurations_test_results_and_content_actions_use_real_exports
|
||||
assert!(f.host.parse_test_results("main", "plugin:execution:dynamic", "trap".into()).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn a_plugin_command_opens_a_diff_in_its_window() {
|
||||
let f = execution_fixture();
|
||||
f.take_events();
|
||||
f.run("execution", "diff", None).unwrap();
|
||||
f.host.flush_events_at(Instant::now() + Duration::from_secs(3));
|
||||
assert!(eventually(|| {
|
||||
f.events.lock().iter().any(|(client, event)| {
|
||||
client.as_deref() == Some("main")
|
||||
&& matches!(event, PluginEvent::OpenDiff { plugin_id, diff, file_name }
|
||||
if plugin_id == "execution"
|
||||
&& diff.left == "before\n"
|
||||
&& diff.right == "after\n"
|
||||
&& diff.title.as_deref() == Some("Review")
|
||||
&& file_name.as_deref() == Some("src/review.rs"))
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api14_fixture_exports_test_selection() {
|
||||
let f = fixture(Limits::default());
|
||||
|
||||
@@ -455,29 +455,3 @@ async fn properties_conversion_through_remote_dispatch_without_a_remote_host() {
|
||||
invoke(&peer, "close_project", json!({})).await.unwrap();
|
||||
peer.kill();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn the_desktop_routes_exactly_what_a_server_of_this_build_answers() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let base = tmp.path().join("server");
|
||||
let peer_slot = Arc::new(OnceLock::new());
|
||||
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::default(),
|
||||
sink_for: Box::new(|_| Arc::new(ide_core::NullSink)),
|
||||
owner: |w| w,
|
||||
runtime: tokio::runtime::Handle::current(),
|
||||
project_environment: false,
|
||||
}));
|
||||
// As `openidea-server` assembles its commands.
|
||||
let plugins = ide_host::plugins::remote_host::RemotePlugins::start(&base.join("data"), &host, peer_slot).unwrap();
|
||||
let mut dispatcher = Dispatcher::standard();
|
||||
plugins.register(&mut dispatcher);
|
||||
dispatcher.add(ide_host::plugins::remote::SYNC_COMMAND, |_| async { Ok(()) });
|
||||
assert_eq!(rpc::client_commands(), dispatcher.commands());
|
||||
assert!(rpc::client_commands().contains(&"term_spawn"));
|
||||
plugins.host().shutdown();
|
||||
}
|
||||
@@ -83,10 +83,10 @@ async fn opens_unity_without_generated_solutions_and_excludes_caches_from_the_in
|
||||
};
|
||||
let original_metadata = "fileFormatVersion: 2\nguid: 11111111111111111111111111111111\n";
|
||||
std::fs::write(meta(&original), original_metadata).unwrap();
|
||||
ops::fs::rename(&host, "main", original.clone(), renamed.clone()).await.unwrap();
|
||||
ops::fs::rename(&host, original.clone(), renamed.clone()).await.unwrap();
|
||||
assert!(!original.exists() && !meta(&original).exists());
|
||||
assert_eq!(std::fs::read_to_string(meta(&renamed)).unwrap(), original_metadata);
|
||||
ops::fs::copy(&host, "main", renamed.clone(), copied.clone()).await.unwrap();
|
||||
ops::fs::copy(&host, renamed.clone(), copied.clone()).await.unwrap();
|
||||
assert!(copied.exists());
|
||||
assert_ne!(std::fs::read_to_string(meta(&copied)).unwrap(), original_metadata);
|
||||
assert_eq!(std::fs::read_to_string(meta(&renamed)).unwrap(), original_metadata);
|
||||
|
||||
@@ -21,9 +21,6 @@ ignore.workspace = true
|
||||
globset.workspace = true
|
||||
postcard = { version = "1.1", default-features = false, features = ["use-std"] }
|
||||
|
||||
[build-dependencies]
|
||||
blake3.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
criterion = { version = "0.5", default-features = false }
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
//! Hashes this crate's sources into `INDEX_SOURCE_HASH`: what is extracted and stored for a
|
||||
//! file (ignored state, size limit, record layout) is decided here as well as in
|
||||
//! `ide-language`, so the persistent index is rebuilt when either changes.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let src = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("src");
|
||||
println!("cargo:rerun-if-changed={}", src.display());
|
||||
let mut files = Vec::new();
|
||||
collect(&src, &mut files);
|
||||
files.sort();
|
||||
let mut h = blake3::Hasher::new();
|
||||
for file in files {
|
||||
h.update(file.strip_prefix(&src).unwrap_or(&file).to_string_lossy().replace('\\', "/").as_bytes());
|
||||
h.update(b"\0");
|
||||
h.update(&std::fs::read(&file).unwrap());
|
||||
h.update(b"\0");
|
||||
}
|
||||
println!("cargo:rustc-env=INDEX_SOURCE_HASH={}", &h.finalize().to_hex()[..16]);
|
||||
}
|
||||
|
||||
fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else { return };
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
collect(&path, out);
|
||||
} else {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ impl Store {
|
||||
}
|
||||
};
|
||||
let store = Self { db: RwLock::new(db) };
|
||||
let stamp = format!("{SCHEMA}\n{}\n{fingerprint}\n{}", env!("INDEX_SOURCE_HASH"), root.display());
|
||||
let stamp = format!("{SCHEMA}\n{fingerprint}\n{}", root.display());
|
||||
match store.meta("stamp") {
|
||||
Ok(Some(v)) if v == stamp.as_bytes() => {}
|
||||
_ => {
|
||||
|
||||
@@ -58,9 +58,6 @@ tree-sitter-graphql = "0.3.0"
|
||||
tree-sitter-prisma-io = "1.6.0"
|
||||
tree-sitter-gdscript = "6.1.0"
|
||||
|
||||
[build-dependencies]
|
||||
blake3.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", default-features = false }
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
//! Hashes everything that decides what `Syntax::tags` extracts (this crate's
|
||||
//! sources, queries, language definitions and the locked tree-sitter crates)
|
||||
//! into `SYNTAX_SOURCE_HASH`, so persistent caches survive releases that leave
|
||||
//! extraction untouched.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let workspace = manifest.join("../..");
|
||||
let lock = workspace.join("Cargo.lock");
|
||||
let inputs = [manifest.join("src"), manifest.join("queries"), workspace.join("languages")];
|
||||
|
||||
let mut h = blake3::Hasher::new();
|
||||
for dir in &inputs {
|
||||
println!("cargo:rerun-if-changed={}", dir.display());
|
||||
let mut files = Vec::new();
|
||||
collect(dir, &mut files);
|
||||
files.sort();
|
||||
for file in files {
|
||||
let rel = file.strip_prefix(&workspace).unwrap_or(&file);
|
||||
h.update(rel.to_string_lossy().replace('\\', "/").as_bytes());
|
||||
h.update(b"\0");
|
||||
h.update(&std::fs::read(&file).unwrap());
|
||||
h.update(b"\0");
|
||||
}
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", lock.display());
|
||||
match std::fs::read_to_string(&lock) {
|
||||
Ok(text) => h.update(tree_sitter_packages(&text).as_bytes()),
|
||||
// Built outside the workspace: fall back to the crate version.
|
||||
Err(_) => h.update(std::env::var("CARGO_PKG_VERSION").unwrap().as_bytes()),
|
||||
};
|
||||
println!("cargo:rustc-env=SYNTAX_SOURCE_HASH={}", &h.finalize().to_hex()[..16]);
|
||||
}
|
||||
|
||||
fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else { return };
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
collect(&path, out);
|
||||
} else {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `name version checksum` of every locked `tree-sitter*` package.
|
||||
fn tree_sitter_packages(lock: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for package in lock.split("[[package]]") {
|
||||
let field = |key: &str| {
|
||||
package.lines().find_map(|l| l.strip_prefix(key)?.strip_prefix(" = ")).unwrap_or("").trim_matches('"')
|
||||
};
|
||||
if field("name").starts_with("tree-sitter") {
|
||||
out.push_str(&format!("{} {} {}\n", field("name"), field("version"), field("checksum")));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -132,11 +132,29 @@ impl Syntax {
|
||||
SPECS.iter().map(|s| s.name)
|
||||
}
|
||||
|
||||
/// Changes whenever extraction output may change (this crate's sources,
|
||||
/// queries, language definitions, tree-sitter crate versions; see
|
||||
/// `build.rs`); persistent caches of [`Syntax::tags`] key on it.
|
||||
/// Changes whenever extraction output may change (grammar set, queries,
|
||||
/// crate version); persistent caches of [`Syntax::tags`] key on it.
|
||||
pub fn fingerprint(&self) -> String {
|
||||
env!("SYNTAX_SOURCE_HASH").to_owned()
|
||||
static FP: OnceLock<String> = OnceLock::new();
|
||||
FP.get_or_init(|| {
|
||||
let mut h = blake3::Hasher::new();
|
||||
h.update(env!("CARGO_PKG_VERSION").as_bytes());
|
||||
// Bump when web definitions (`.class`, `#id`, `--var` tags) change.
|
||||
h.update(b"web-definitions-1");
|
||||
for s in SPECS {
|
||||
h.update(s.name.as_bytes());
|
||||
h.update(&Language::new(s.language).abi_version().to_le_bytes());
|
||||
h.update(s.outline.as_bytes());
|
||||
for list in [s.fold_nodes, s.fold_bodies, s.imports, s.constructors] {
|
||||
for k in list {
|
||||
h.update(k.as_bytes());
|
||||
h.update(b"\0");
|
||||
}
|
||||
}
|
||||
}
|
||||
h.finalize().to_hex()[..16].to_owned()
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Whether a tree-sitter grammar is bundled for this language.
|
||||
|
||||
@@ -55,7 +55,7 @@ use views_bindings::exports::openidea::plugin::view_provider as wit_view;
|
||||
use views_bindings::openidea::plugin::view_types as wit_vt;
|
||||
|
||||
/// The API version of the world above, `major.minor` of [`host_api`].
|
||||
pub const API_VERSION: &str = "1.7";
|
||||
pub const API_VERSION: &str = "1.6";
|
||||
const EPOCH_TICK: Duration = Duration::from_millis(10);
|
||||
pub(crate) const MAX_TEXT: usize = 1 << 20;
|
||||
pub(crate) const MAX_TITLE: usize = 256;
|
||||
@@ -170,11 +170,6 @@ pub trait HostApi: Send + Sync {
|
||||
let _ = (client, tab, cancel, timeout);
|
||||
Err(HostError::Failed("content tabs unavailable".into()))
|
||||
}
|
||||
/// Opens a diff of two texts in the invoking window.
|
||||
fn open_diff(&self, client: &str, diff: crate::ContentDiff, file_name: Option<String>) -> Result<(), HostError> {
|
||||
let _ = (client, diff, file_name);
|
||||
Err(HostError::Failed("diffs unavailable".into()))
|
||||
}
|
||||
/// Answers a content action in the invoking window.
|
||||
fn reply_content_tab(&self, client: &str, request: &str, reply: crate::ContentTabReply) -> Result<(), HostError> {
|
||||
let _ = (client, request, reply);
|
||||
|
||||
@@ -1770,12 +1770,6 @@ impl views::Host for StoreData {
|
||||
)
|
||||
.map_err(to_wit))
|
||||
}
|
||||
fn open_diff(&mut self, diff: views::ContentDiff, file_name: Option<String>) -> Fallible<()> {
|
||||
if let Err(e) = self.require_window() {
|
||||
return Ok(Err(e));
|
||||
}
|
||||
Ok(self.host.open_diff(self.invocation.client.as_deref().unwrap(), diff.into(), file_name).map_err(to_wit))
|
||||
}
|
||||
fn reply_content_tab(&mut self, request_id: String, reply: views::ContentTabReply) -> Fallible<()> {
|
||||
if let Err(e) = self.require_window() {
|
||||
return Ok(Err(e));
|
||||
|
||||
@@ -91,15 +91,15 @@ fn the_sdk_carries_the_hosts_wit() {
|
||||
|
||||
#[test]
|
||||
fn the_version_check_runs_compatible_components_and_explains_the_rest() {
|
||||
let host = v("1.7.0");
|
||||
for ok in ["1.0.0", "1.3.0", "1.4.7", "1.5.0", "1.6.0", "1.7.0", "1.0.0-rc.1"] {
|
||||
let host = v("1.6.0");
|
||||
for ok in ["1.0.0", "1.3.0", "1.4.7", "1.5.0", "1.6.0", "1.0.0-rc.1"] {
|
||||
assert_eq!(check_api(&v(ok), &host), Ok(()), "{ok}");
|
||||
}
|
||||
let newer = check_api(&v("1.8.0"), &host).unwrap_err();
|
||||
assert!(newer.contains("1.8") && newer.contains("update OpenIDEA"), "{newer}");
|
||||
let newer = check_api(&v("1.7.0"), &host).unwrap_err();
|
||||
assert!(newer.contains("1.7") && newer.contains("update OpenIDEA"), "{newer}");
|
||||
assert!(check_api(&v("2.0.0"), &host).unwrap_err().contains("update OpenIDEA"));
|
||||
let older = check_api(&v("0.2.0"), &host).unwrap_err();
|
||||
assert!(older.contains("rebuild it with the 1.7 SDK"), "{older}");
|
||||
assert!(older.contains("rebuild it with the 1.6 SDK"), "{older}");
|
||||
// 0.x: every minor is its own API.
|
||||
assert_eq!(check_api(&v("0.2.0"), &v("0.2.5")), Ok(()));
|
||||
assert!(check_api(&v("0.1.0"), &v("0.2.0")).is_err());
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
3c7f35facd15ee8380ccb6475b7c051d500e38346b711e1805077becd0c75f75
|
||||
005d9da29eb7d713a97ff872051e408f653030434d837835843298553c9e12e0
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
3879822d975695c6258e95286b0e64c5ee1e3265876501efaf0e577b38f8222b
|
||||
6359ef4271f38374889412bd926d9aa735f59a8ce047ffcb06c47976444196cf
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
228ae888bf556ce5cedc7843eb20e92f221d9e1f640f3c6c595bed3fd6be6051
|
||||
6f560f3d0d7737150d2dfa09c3b801bd3b4c3a52cef9893ed8fc045809fe33e1
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
41de239b6f723020b38eb2b67f642b7c88cc88c79117c3072ba122633afc418c
|
||||
4024af2d55d7fe4e8c456efd7e0c497ade4b2c5959e9108ff218f4a669e920f4
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
78c5ed3c756498c7619b46c3622ae7a59461c61c747b45caecd79b341ee654de
|
||||
4ca977f53a18e9e8f869d34e027a15abece9e97b9331e23d4f71af721b104611
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
ee848321e53cb181536e6158570dbb4ab02a6fd2a1a064b068803ac7da7f3448
|
||||
3d12a00f81e0db1b0902c2ddebfd6454a056f15b97add80b22a0333722568dc8
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
b587de945875a98f1d4ca20fab7cf94fb4dc6d848136aa19ab0e0e5319155a80
|
||||
95d89e411216f50d0b34bee110c6bfc9b8fbcb2ffaae2e912c3609f7bf678b2c
|
||||
@@ -40,10 +40,6 @@ title = "write"
|
||||
id = "content"
|
||||
title = "content"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "diff"
|
||||
title = "diff"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "events"
|
||||
title = "events"
|
||||
|
||||
@@ -78,18 +78,6 @@ impl Plugin for Execution {
|
||||
})?;
|
||||
Ok(None)
|
||||
}
|
||||
"diff" => {
|
||||
host::open_diff(
|
||||
&ContentDiff {
|
||||
left: "before\n".into(),
|
||||
right: "after\n".into(),
|
||||
title: Some("Review".into()),
|
||||
..Default::default()
|
||||
},
|
||||
Some("src/review.rs"),
|
||||
)?;
|
||||
Ok(None)
|
||||
}
|
||||
"events" => host::storage::get("events"),
|
||||
"validation" => host::storage::get("validation"),
|
||||
_ => Err(Error::NotFound(id.into())),
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
1c8776dcae35d2d49dd6c1e32b0cdd354de1eb0f72a473245a2c47368b89ec30
|
||||
dd4b6c5153892a4f3facb3f23cb1d92da39a3977985b8abf4eaa9558decfa4a9
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
2a517cbffd6d3cdda37117d8ca8815978a09995923fc3d5aa5af7020afdac56a
|
||||
325bc15cd28933d9687bc3c316008f921f48a4a69d7528c6b367fa0ff22489b8
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
dbd912b1f8488ecd446af07b5dcaef55d44bce57c1a55be5c0b0fb33e2697903
|
||||
abe706392e04ef5a14f9feab88e0c0d6a9e96bee7de771b61deeb65e7d9e0a7c
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
efdc84cd2bedb3989b03d19b569d09958f26e502db65db4dd786eb3438a7c407
|
||||
fb3677946021b30d226e2e3d422c352645c2065c93057c0a645246520860fc90
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
be6bc15ee42a5e21781ba54f548007351a46b669882ee4491da20ff556927a06
|
||||
a01852ead9004ac1558028da19a1ae96ccd035775484089fd39f14263082daf1
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
e5c3a3beda750dff27d7b823676d53f15b7c76b8ad539594cd913bd178d750da
|
||||
bf00050b7bb9911680a94fd36aaeb83fc96d94030e8ceef51d1723c3950253f9
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
322de3299f0413101c02358836e7df7e06e58cbb22c11897664564abbb8af090
|
||||
d00a07d9d51e7ca117cec59070d2812d4d1efb2730b60a6f171c8fd12903d055
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
d768ff6a4a466cc6f1d72499df3ccb4ae76872436b8eddff27f62aa156331a37
|
||||
0aa0ee56e8bacd4d218bafecdec7f4b6249381fad6e31f8b2ee89fd88b16d710
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
57aea107b2c10e4d30fa3785ac4c6566f1b40908a18a3859aca8be047fac9518
|
||||
e06955818969808e6695fba9178727239e343bc4542efbf56262dd407175fd8a
|
||||
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
c98bcc89750e26e738d7115a7c0454fde25367a0a23bdc2ff95fb5a5dacc9f98
|
||||
7af67673471d49f97aa2cc131b5deddfc78ee7fc203901eba12f4970a5b13f75
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
a4c23b93a14eda705cb18c88bbc2373d1ef3f96031ff2df21b8b092b50633996
|
||||
e81d2454a61977ab69dc8b5c3c617627c559c4e340337a3d43309daeb9b1c12d
|
||||
+3
-3
@@ -5,9 +5,9 @@ wit_bindgen::generate!({
|
||||
inline: "
|
||||
package fixture:misbehave;
|
||||
world misbehave {
|
||||
include openidea:plugin/plugin-syntax@1.7.0;
|
||||
export openidea:plugin/providers@1.7.0;
|
||||
export openidea:plugin/file-actions@1.7.0;
|
||||
include openidea:plugin/plugin-syntax@1.6.0;
|
||||
export openidea:plugin/providers@1.6.0;
|
||||
export openidea:plugin/file-actions@1.6.0;
|
||||
}
|
||||
",
|
||||
path: "../../../../../wit",
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
aec002b3dd310068e69a6ae67a49d30655bf8baea9153a110a407e9d408e6880
|
||||
123ae68df6baa3b5983349ba05fb441ec67fddcb32cab566b0d02e443bb4800e
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
08ece563c3b55c2b86e68fbb0815ddcbb63beb6f1f6c32b8dbdbee558a4a66c1
|
||||
7b26a4ec36b496f9a4f5e029ef557707961a281e31508863c989a5a5abd5e225
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
450845edf234a1cc78cd6cfc0b812a89f3a2e41484689cc1e1b05be66873d4eb
|
||||
67f278ade12d11b1445ac3d448853ec46282f2f3e2b2299c6c8fe0dfb4fc86cd
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
61b735de5ef61dcd0ea8e640c2ad004e84a63fe4dfd7872ffb5e04d6d81dc9a9
|
||||
8cf9210844a1968f485e6e747f6dff4a1fdf0a628e2945f731d8289dd01f8385
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
36776367782eaa4b8b8121f4a665dd8e68525a4836bff1c92bc16770cce14929
|
||||
263e6ab4b54593f09ae2cf71e9a191acb95ed744cc16bbaf9a9263e41614e99a
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
f6311d232a99356b02a398b2ce3db0865c425f435a8d201157719178ba2b97c7
|
||||
682a712b6c75ad0c0da24476f87c77a76b248c201d4d31923a79fda053b76b90
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
fe1d26665e3c816ed43fc720acabad03a37b7924bccd4dd8c357fd156947b3c7
|
||||
c5718b3978f21291cfff7a00233ba68d545a5977bc3e90c546b4380528295446
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
76bab8e48235f9f066f5b9bbfdd9398916b6c3ed73c072c543d80595130519f7
|
||||
186b30c785fae23d18c5efe8fa63f6526d23d9d141580deef7ee7009310a88c3
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
f9ed2eac306229f76b05d0c4a4fb2f0ed8d81a1dfc5eb50d044bbe10c9206a5f
|
||||
cb3a1c4a0518aa3f3e689337e8f4249180e5b3753b0e7bfaba8c974f89f6ef7a
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
f2d83d9f56da072d38a1552195af8a9d4edc11888c645627d5dd1e4a46afc4df
|
||||
fa97682ca30cb0ad1392ff1bab4d99ea6d9ccb2654feedfdc130d11adb7df93b
|
||||
@@ -498,8 +498,7 @@ impl ProcessManager {
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
let decoder = || OutputDecoder::for_pipes(options.encoding.as_deref());
|
||||
spawn_pump(rx, on_output.clone(), entry.clone(), sink, [decoder(), decoder()], notice)?;
|
||||
spawn_pump(rx, on_output.clone(), entry.clone(), sink, options.encoding.clone(), notice)?;
|
||||
Ok(())
|
||||
})();
|
||||
if let Err(e) = result {
|
||||
@@ -630,10 +629,7 @@ impl ProcessManager {
|
||||
#[cfg(unix)]
|
||||
Some(Box::new(wait_readable)),
|
||||
)
|
||||
.and_then(|_| {
|
||||
let decoder = || OutputDecoder::for_label(options.encoding.as_deref());
|
||||
spawn_pump(rx, on_output, entry.clone(), sink, [decoder(), decoder()], None)
|
||||
});
|
||||
.and_then(|_| spawn_pump(rx, on_output, entry.clone(), sink, options.encoding.clone(), None));
|
||||
if let Err(e) = result {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
@@ -1124,35 +1120,18 @@ fn chunk(stream: Stream, text: String) -> OutputChunk {
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends what `decode` writes to `pending`, through the notice filter when there is one.
|
||||
fn emit(
|
||||
decode: impl FnOnce(&mut String),
|
||||
notice: &mut Option<ide_core::output_encoding::NoticeFilter>,
|
||||
decoded: &mut String,
|
||||
pending: &mut String,
|
||||
) {
|
||||
match notice {
|
||||
Some(filter) => {
|
||||
decode(decoded);
|
||||
filter.filter(decoded, pending);
|
||||
decoded.clear();
|
||||
}
|
||||
None => decode(pending),
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges both streams in arrival order, batching consecutive output of the same stream.
|
||||
/// `decoders` decode stdout and stderr.
|
||||
fn spawn_pump(
|
||||
rx: mpsc::Receiver<Msg>,
|
||||
on_output: OutputFn,
|
||||
entry: Arc<Entry>,
|
||||
sink: SharedSink,
|
||||
decoders: [OutputDecoder; 2],
|
||||
encoding: Option<String>,
|
||||
notice: Option<String>,
|
||||
) -> std::io::Result<()> {
|
||||
std::thread::Builder::new().name(format!("openidea-run-pump-{}", entry.id.0)).spawn(move || {
|
||||
let [mut out_dec, mut err_dec] = decoders;
|
||||
let mut out_dec = OutputDecoder::for_label(encoding.as_deref());
|
||||
let mut err_dec = OutputDecoder::for_label(encoding.as_deref());
|
||||
// "Picked up JAVA_TOOL_OPTIONS: …" from every JVM given the console encoding, on
|
||||
// either stream (`spring-boot:run` copies its fork's stderr to Maven's stdout).
|
||||
let filter = || notice.clone().map(ide_core::output_encoding::NoticeFilter::new);
|
||||
@@ -1161,8 +1140,6 @@ fn spawn_pump(
|
||||
let mut pending = String::new();
|
||||
let mut pending_stream = Stream::Stdout;
|
||||
let mut batch_started: Option<Instant> = None;
|
||||
// Since when a decoder holds part of a line back to tell its encoding.
|
||||
let mut held_since: Option<Instant> = None;
|
||||
let mut status: Option<Exit> = None;
|
||||
let mut drain_deadline: Option<Instant> = None;
|
||||
|
||||
@@ -1177,7 +1154,6 @@ fn spawn_pump(
|
||||
let now = Instant::now();
|
||||
let wait = [
|
||||
batch_started.map(|t| (t + BATCH_INTERVAL).saturating_duration_since(now)),
|
||||
held_since.map(|t| (t + BATCH_INTERVAL).saturating_duration_since(now)),
|
||||
drain_deadline.map(|t| t.saturating_duration_since(now)),
|
||||
]
|
||||
.into_iter()
|
||||
@@ -1194,8 +1170,14 @@ fn spawn_pump(
|
||||
Stream::Stdout => (&mut out_dec, &mut out_notice),
|
||||
Stream::Stderr => (&mut err_dec, &mut err_notice),
|
||||
};
|
||||
emit(|out| dec.decode_into(&bytes, out), notice, &mut decoded, &mut pending);
|
||||
held_since = if out_dec.holds() || err_dec.holds() { held_since.or(Some(now)) } else { None };
|
||||
match notice {
|
||||
Some(filter) => {
|
||||
dec.decode_into(&bytes, &mut decoded);
|
||||
filter.filter(&decoded, &mut pending);
|
||||
decoded.clear();
|
||||
}
|
||||
None => dec.decode_into(&bytes, &mut pending),
|
||||
}
|
||||
if !pending.is_empty() && batch_started.is_none() {
|
||||
batch_started = Some(Instant::now());
|
||||
}
|
||||
@@ -1211,25 +1193,6 @@ fn spawn_pump(
|
||||
if drain_deadline.is_some_and(|d| Instant::now() >= d) {
|
||||
break;
|
||||
}
|
||||
// Nothing more came: show a held prompt.
|
||||
if held_since.is_some_and(|t| t.elapsed() >= BATCH_INTERVAL) {
|
||||
held_since = None;
|
||||
for (stream, dec, notice) in [
|
||||
(Stream::Stdout, &mut out_dec, &mut out_notice),
|
||||
(Stream::Stderr, &mut err_dec, &mut err_notice),
|
||||
] {
|
||||
if dec.holds() {
|
||||
if stream != pending_stream {
|
||||
flush(&mut pending, pending_stream, &mut batch_started);
|
||||
pending_stream = stream;
|
||||
}
|
||||
emit(|out| dec.release(out), notice, &mut decoded, &mut pending);
|
||||
}
|
||||
}
|
||||
if !pending.is_empty() && batch_started.is_none() {
|
||||
batch_started = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
if batch_started.is_some_and(|t| t.elapsed() >= BATCH_INTERVAL) {
|
||||
flush(&mut pending, pending_stream, &mut batch_started);
|
||||
}
|
||||
|
||||
+1
-390
@@ -136,7 +136,6 @@ mod tests {
|
||||
pub enum OutputDecoder {
|
||||
Utf8(Utf8Decoder),
|
||||
Other(Box<encoding_rs::Decoder>),
|
||||
Mixed(Box<MixedDecoder>),
|
||||
}
|
||||
|
||||
impl OutputDecoder {
|
||||
@@ -148,36 +147,10 @@ impl OutputDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::for_label`] for output read from pipes. On Windows a UTF-8 console also decodes
|
||||
/// what Windows' own programs write: the lines that are not UTF-8 in the system's console
|
||||
/// code page (`ping`, `dir`, `cmd`'s messages), whatever the system's language, and UTF-16
|
||||
/// (`wsl.exe`, `cmd /u`).
|
||||
pub fn for_pipes(label: Option<&str>) -> Self {
|
||||
match Self::for_label(label) {
|
||||
Self::Utf8(_) if cfg!(windows) => Self::Mixed(Box::new(MixedDecoder::new(
|
||||
ide_core::output_encoding::console_code_page().and_then(NativeDecoder::for_code_page),
|
||||
))),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_into(&mut self, bytes: &[u8], out: &mut String) {
|
||||
match self {
|
||||
Self::Utf8(d) => d.decode_into(bytes, out),
|
||||
Self::Other(d) => decode_with(d, bytes, out, false),
|
||||
Self::Mixed(d) => d.decode_into(bytes, out),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether part of a line is held back until it shows its encoding.
|
||||
pub fn holds(&self) -> bool {
|
||||
matches!(self, Self::Mixed(d) if d.holds())
|
||||
}
|
||||
|
||||
/// Decodes the held part of a line now (no more output came for a while).
|
||||
pub fn release(&mut self, out: &mut String) {
|
||||
if let Self::Mixed(d) = self {
|
||||
d.release(out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,218 +162,10 @@ impl OutputDecoder {
|
||||
decode_with(d, &[], &mut out, true);
|
||||
out
|
||||
}
|
||||
Self::Mixed(d) => d.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decoder for a Windows code page: `encoding_rs`'s, or Windows' own for the single-byte
|
||||
/// pages `encoding_rs` lacks (the OEM pages 437, 850, 852…).
|
||||
pub enum NativeDecoder {
|
||||
Encoding(Box<encoding_rs::Decoder>),
|
||||
#[cfg(windows)]
|
||||
SingleByte(u32),
|
||||
}
|
||||
|
||||
impl NativeDecoder {
|
||||
/// `None` for UTF-8 and for code pages neither can decode piece by piece.
|
||||
pub fn for_code_page(cp: u32) -> Option<Self> {
|
||||
if let Some(enc) = ide_core::output_encoding::encoding_for_code_page(cp) {
|
||||
return (enc != encoding_rs::UTF_8)
|
||||
.then(|| Self::Encoding(Box::new(enc.new_decoder_without_bom_handling())));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
if ide_core::output_encoding::is_single_byte_code_page(cp) {
|
||||
return Some(Self::SingleByte(cp));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn decode_into(&mut self, bytes: &[u8], out: &mut String, last: bool) {
|
||||
match self {
|
||||
Self::Encoding(d) => decode_with(d, bytes, out, last),
|
||||
#[cfg(windows)]
|
||||
Self::SingleByte(cp) => out.push_str(&ide_core::output_encoding::decode_code_page(bytes, *cp)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Line {
|
||||
/// Only ASCII so far, which reads the same in both.
|
||||
Open,
|
||||
Utf8,
|
||||
Native,
|
||||
}
|
||||
|
||||
/// UTF-8, the system code page for the lines that are not UTF-8, and UTF-16: on pipes, cargo,
|
||||
/// git or node write UTF-8, Windows' own programs the console code page (GBK, Shift_JIS,
|
||||
/// EUC-KR, 866…) and a few of them (`wsl.exe`, `cmd /u`) UTF-16LE, even within one command.
|
||||
///
|
||||
/// Each line is decoded as UTF-8 or the code page (UTF-8 with replacement characters without
|
||||
/// one). The part of a line after its first non-ASCII byte is held until the line ends or
|
||||
/// turns out not to be UTF-8; [`Self::release`] decodes a held prompt when no more output comes.
|
||||
///
|
||||
/// A read with NUL bytes, which text in the others never has, is UTF-16LE (ASCII characters
|
||||
/// and line ends in it have them); UTF-16 goes on until one of its lines has ended and a read
|
||||
/// without NUL bytes comes.
|
||||
pub struct MixedDecoder {
|
||||
utf8: Utf8Decoder,
|
||||
native: Option<NativeDecoder>,
|
||||
line: Line,
|
||||
held: Vec<u8>,
|
||||
utf16: Option<Utf16>,
|
||||
}
|
||||
|
||||
impl MixedDecoder {
|
||||
pub fn new(native: Option<NativeDecoder>) -> Self {
|
||||
Self { utf8: Utf8Decoder::new(), native, line: Line::Open, held: Vec::new(), utf16: None }
|
||||
}
|
||||
|
||||
pub fn decode_into(&mut self, bytes: &[u8], out: &mut String) {
|
||||
if bytes.is_empty() {
|
||||
return;
|
||||
}
|
||||
let nul = bytes.contains(&0);
|
||||
match self.utf16.as_ref().map(|u| u.open || nul) {
|
||||
Some(true) => {
|
||||
if let Some(u) = &mut self.utf16 {
|
||||
u.decode(bytes, out);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Some(false) => self.utf16 = None,
|
||||
None if nul => {
|
||||
self.end_line(out);
|
||||
let mut u = Utf16::new();
|
||||
u.decode(utf16_aligned(bytes), out);
|
||||
self.utf16 = Some(u);
|
||||
return;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
for piece in bytes.split_inclusive(|b| *b == b'\n') {
|
||||
let ends = piece.last() == Some(&b'\n');
|
||||
match self.line {
|
||||
Line::Utf8 => self.utf8.decode_into(piece, out),
|
||||
Line::Native => self.decode_native(piece, out),
|
||||
Line::Open => {
|
||||
let mut rest = piece;
|
||||
if self.held.is_empty() {
|
||||
let ascii = piece.iter().take_while(|b| b.is_ascii()).count();
|
||||
out.push_str(std::str::from_utf8(&piece[..ascii]).unwrap_or_default());
|
||||
rest = &piece[ascii..];
|
||||
}
|
||||
self.held.extend_from_slice(rest);
|
||||
let decided = match std::str::from_utf8(&self.held) {
|
||||
Err(e) if e.error_len().is_some() => Some(Line::Native),
|
||||
Ok(_) if ends => Some(Line::Utf8),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(line) = decided {
|
||||
self.line = line;
|
||||
self.flush_held(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ends {
|
||||
self.line = Line::Open;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_held(&mut self, out: &mut String) {
|
||||
let held = std::mem::take(&mut self.held);
|
||||
match self.line {
|
||||
Line::Native => self.decode_native(&held, out),
|
||||
Line::Open | Line::Utf8 => self.utf8.decode_into(&held, out),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_native(&mut self, bytes: &[u8], out: &mut String) {
|
||||
match &mut self.native {
|
||||
Some(native) => native.decode_into(bytes, out, false),
|
||||
None => self.utf8.decode_into(bytes, out),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes the rest of the current line as it is.
|
||||
fn end_line(&mut self, out: &mut String) {
|
||||
if self.holds() {
|
||||
self.line = if std::str::from_utf8(&self.held).is_ok() { Line::Utf8 } else { Line::Native };
|
||||
self.flush_held(out);
|
||||
}
|
||||
out.push_str(&self.utf8.finish());
|
||||
self.line = Line::Open;
|
||||
}
|
||||
|
||||
pub fn holds(&self) -> bool {
|
||||
!self.held.is_empty()
|
||||
}
|
||||
|
||||
/// Decodes the held part of the line as UTF-8: held text has been UTF-8 so far.
|
||||
pub fn release(&mut self, out: &mut String) {
|
||||
if self.holds() {
|
||||
self.line = Line::Utf8;
|
||||
self.flush_held(out);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(&mut self) -> String {
|
||||
let mut out = String::new();
|
||||
if let Some(mut u) = self.utf16.take() {
|
||||
decode_with(&mut u.decoder, &[], &mut out, true);
|
||||
}
|
||||
self.end_line(&mut out);
|
||||
if let Some(native) = &mut self.native {
|
||||
native.decode_into(&[], &mut out, true);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
struct Utf16 {
|
||||
decoder: encoding_rs::Decoder,
|
||||
/// Inside a line or a code unit: the next read goes on in UTF-16 whatever it holds.
|
||||
open: bool,
|
||||
/// An odd number of bytes so far.
|
||||
odd: bool,
|
||||
}
|
||||
|
||||
impl Utf16 {
|
||||
fn new() -> Self {
|
||||
Self { decoder: encoding_rs::UTF_16LE.new_decoder_with_bom_removal(), open: false, odd: false }
|
||||
}
|
||||
|
||||
fn decode(&mut self, bytes: &[u8], out: &mut String) {
|
||||
let start = out.len();
|
||||
decode_with(&mut self.decoder, bytes, out, false);
|
||||
self.odd ^= bytes.len() % 2 == 1;
|
||||
if self.odd {
|
||||
self.open = true;
|
||||
} else if out.len() > start {
|
||||
self.open = !out.ends_with('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `bytes` from the first byte of a UTF-16LE code unit. NUL bytes are mostly the high bytes of
|
||||
/// ASCII characters, at odd offsets from it; at even ones, the first byte is the high byte of a
|
||||
/// character whose low byte came before (and was shown as ASCII).
|
||||
fn utf16_aligned(bytes: &[u8]) -> &[u8] {
|
||||
let (mut even, mut odd) = (0usize, 0usize);
|
||||
for (i, b) in bytes.iter().enumerate() {
|
||||
if *b == 0 {
|
||||
if i % 2 == 0 {
|
||||
even += 1;
|
||||
} else {
|
||||
odd += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if even > odd { &bytes[1..] } else { bytes }
|
||||
}
|
||||
|
||||
fn decode_with(d: &mut encoding_rs::Decoder, bytes: &[u8], out: &mut String, last: bool) {
|
||||
let mut src = bytes;
|
||||
loop {
|
||||
@@ -415,8 +180,7 @@ fn decode_with(d: &mut encoding_rs::Decoder, bytes: &[u8], out: &mut String, las
|
||||
|
||||
#[cfg(test)]
|
||||
mod encoding_tests {
|
||||
use super::{MixedDecoder, NativeDecoder, OutputDecoder};
|
||||
use ide_core::output_encoding::encoding_for_code_page;
|
||||
use super::OutputDecoder;
|
||||
|
||||
#[test]
|
||||
fn gbk_split_across_chunks() {
|
||||
@@ -438,157 +202,4 @@ mod encoding_tests {
|
||||
assert_eq!(out, "héllo");
|
||||
assert!(matches!(d, OutputDecoder::Utf8(_)));
|
||||
}
|
||||
|
||||
fn mixed(cp: u32) -> MixedDecoder {
|
||||
MixedDecoder::new(Some(NativeDecoder::for_code_page(cp).expect("code page")))
|
||||
}
|
||||
|
||||
fn encode(cp: u32, text: &str) -> Vec<u8> {
|
||||
let (bytes, _, unmappable) = encoding_for_code_page(cp).expect("code page").encode(text);
|
||||
assert!(!unmappable, "{text} in {cp}");
|
||||
bytes.into_owned()
|
||||
}
|
||||
|
||||
/// Simplified and Traditional Chinese, Korean, Japanese, Cyrillic (OEM and ANSI), Western.
|
||||
const SAMPLES: &[(u32, &str)] = &[
|
||||
(936, "正在 Ping 127.0.0.1 具有 32 字节的数据:"),
|
||||
(950, "系統找不到指定的路徑。"),
|
||||
(949, "지정된 경로를 찾을 수 없습니다."),
|
||||
(932, "指定されたパスが見つかりません。"),
|
||||
(866, "Системе не удается найти указанный путь."),
|
||||
(1251, "Системе не удается найти указанный путь."),
|
||||
(1252, "Le chemin d'accès spécifié est introuvable."),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn utf8_and_code_page_lines_mix_whatever_the_chunks() {
|
||||
for &(cp, native) in SAMPLES {
|
||||
let mut bytes = " Compiling 编译 ü\n".as_bytes().to_vec();
|
||||
bytes.extend(encode(cp, native));
|
||||
bytes.extend_from_slice(b"\r\n");
|
||||
bytes.extend_from_slice("ok ✓\n".as_bytes());
|
||||
bytes.extend(encode(cp, native));
|
||||
let expected = format!(" Compiling 编译 ü\n{native}\r\nok ✓\n{native}");
|
||||
for split in 0..=bytes.len() {
|
||||
let mut d = mixed(cp);
|
||||
let mut out = String::new();
|
||||
d.decode_into(&bytes[..split], &mut out);
|
||||
d.decode_into(&bytes[split..], &mut out);
|
||||
out.push_str(&d.finish());
|
||||
assert_eq!(out, expected, "code page {cp}, split at {split}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_by_byte_code_page_output() {
|
||||
for &(cp, native) in SAMPLES {
|
||||
let bytes = [encode(cp, native), b"\n".to_vec()].concat();
|
||||
let mut d = mixed(cp);
|
||||
let mut out = String::new();
|
||||
for b in &bytes {
|
||||
d.decode_into(std::slice::from_ref(b), &mut out);
|
||||
}
|
||||
assert!(!d.holds());
|
||||
assert_eq!(out, format!("{native}\n"), "code page {cp}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii_and_code_page_prompts_show_at_once_and_utf8_ones_on_release() {
|
||||
let mut d = mixed(936);
|
||||
let mut out = String::new();
|
||||
d.decode_into(b"Password: ", &mut out);
|
||||
assert_eq!(out, "Password: ");
|
||||
assert!(!d.holds());
|
||||
|
||||
out.clear();
|
||||
d.decode_into(&encode(936, "请按任意键继续. . ."), &mut out);
|
||||
assert_eq!(out, "请按任意键继续. . .");
|
||||
assert!(!d.holds());
|
||||
|
||||
let mut d = mixed(936);
|
||||
out.clear();
|
||||
d.decode_into("Continue? 继续 ".as_bytes(), &mut out);
|
||||
assert_eq!(out, "Continue? ");
|
||||
assert!(d.holds());
|
||||
d.release(&mut out);
|
||||
assert_eq!(out, "Continue? 继续 ");
|
||||
d.decode_into("[y/N]\n".as_bytes(), &mut out);
|
||||
assert_eq!(out, "Continue? 继续 [y/N]\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_released_line_split_inside_a_character_completes() {
|
||||
let mut d = mixed(949);
|
||||
let mut out = String::new();
|
||||
let bytes = "é中".as_bytes();
|
||||
d.decode_into(&bytes[..3], &mut out);
|
||||
d.release(&mut out);
|
||||
d.decode_into(&bytes[3..], &mut out);
|
||||
out.push_str(&d.finish());
|
||||
assert_eq!(out, "é中");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_code_page_has_no_mixed_decoder() {
|
||||
assert!(NativeDecoder::for_code_page(65001).is_none());
|
||||
}
|
||||
|
||||
fn utf16(text: &str) -> Vec<u8> {
|
||||
text.encode_utf16().flat_map(u16::to_le_bytes).collect()
|
||||
}
|
||||
|
||||
/// What `wsl.exe -l -v` writes, on a Chinese system.
|
||||
const WSL: &str = " NAME STATE VERSION\r\n* Ubuntu-22.04 Running 2\r\n\
|
||||
适用于 Linux 的 Windows 子系统: 默认\r\n";
|
||||
|
||||
#[test]
|
||||
fn utf16_output_between_utf8_and_code_page_lines_whatever_the_chunks() {
|
||||
let before = " Compiling 编译\n".as_bytes();
|
||||
let wsl = utf16(WSL);
|
||||
let after = [encode(936, "系统找不到指定的路径。"), "\r\ndone ✓\n".as_bytes().to_vec()].concat();
|
||||
let expected = format!(" Compiling 编译\n{WSL}系统找不到指定的路径。\r\ndone ✓\n");
|
||||
for split in 0..=wsl.len() {
|
||||
let mut d = mixed(936);
|
||||
let mut out = String::new();
|
||||
for chunk in [before, &wsl[..split], &wsl[split..], &after] {
|
||||
d.decode_into(chunk, &mut out);
|
||||
}
|
||||
out.push_str(&d.finish());
|
||||
assert_eq!(out, expected, "split at {split}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16_with_a_byte_order_mark_or_starting_with_cjk() {
|
||||
let mut d = mixed(949);
|
||||
let mut out = String::new();
|
||||
d.decode_into(&[&[0xFF, 0xFE][..], &utf16("한국어 OK\r\n")].concat(), &mut out);
|
||||
d.decode_into(&utf16("中文 OK\r\n"), &mut out);
|
||||
out.push_str(&d.finish());
|
||||
assert_eq!(out, "한국어 OK\r\n中文 OK\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_code_page_other_lines_are_lossy_utf8_and_utf16_still_reads() {
|
||||
let mut d = MixedDecoder::new(None);
|
||||
let mut out = String::new();
|
||||
d.decode_into(b"caf\xe9\n", &mut out);
|
||||
d.decode_into(&utf16("wsl 子系统\r\n"), &mut out);
|
||||
d.decode_into("ok ✓\n".as_bytes(), &mut out);
|
||||
out.push_str(&d.finish());
|
||||
assert_eq!(out, "caf\u{FFFD}\nwsl 子系统\r\nok ✓\n");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn oem_code_pages_without_an_encoding_rs_table_decode_through_windows() {
|
||||
for cp in [437, 850, 852] {
|
||||
let mut d = mixed(cp);
|
||||
let mut out = String::new();
|
||||
d.decode_into(b"caf\x82\r\n", &mut out);
|
||||
assert_eq!(out, "caf\u{e9}\r\n", "code page {cp}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
#![cfg(windows)]
|
||||
|
||||
use ide_core::{IdeEvent, SharedSink};
|
||||
use ide_run::{OutputChunk, ProcessManager, RunConfig, StartOptions};
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Stdout and stderr of a shell command line run on pipes in a UTF-8 console.
|
||||
fn run(command: &str) -> String {
|
||||
let (tx, events) = mpsc::channel();
|
||||
let tx = Mutex::new(tx);
|
||||
let sink: SharedSink = Arc::new(move |e: IdeEvent| {
|
||||
let _ = tx.lock().send(e);
|
||||
});
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let config = RunConfig {
|
||||
id: "t:out".into(),
|
||||
name: "t:out".into(),
|
||||
kind: "shell".into(),
|
||||
command: command.into(),
|
||||
args: Vec::new(),
|
||||
cwd: None,
|
||||
env: BTreeMap::new(),
|
||||
detected: false,
|
||||
is_test: false,
|
||||
debug_adapter: None,
|
||||
allow_parallel: false,
|
||||
extras: Default::default(),
|
||||
};
|
||||
let chunks: Arc<Mutex<Vec<OutputChunk>>> = Arc::default();
|
||||
let out = chunks.clone();
|
||||
let pm = ProcessManager::new();
|
||||
let info = pm
|
||||
.start(root.path(), &config, &StartOptions::default(), sink, Box::new(move |c| out.lock().push(c)))
|
||||
.expect("start");
|
||||
loop {
|
||||
match events.recv_timeout(Duration::from_secs(20)).expect("RunExited event") {
|
||||
IdeEvent::RunExited { run, .. } if run == info.id => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
chunks
|
||||
.lock()
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
OutputChunk::Stdout { text } | OutputChunk::Stderr { text } => Some(text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `cmd`'s own messages reach the pipe in the console code page (GBK, Shift_JIS, EUC-KR,
|
||||
/// 437…, by the system's language), and still read right in a UTF-8 console.
|
||||
#[test]
|
||||
fn native_messages_in_the_console_code_page_are_decoded() {
|
||||
let text = run(r"dir C:\openidea-no-such-dir");
|
||||
assert!(text.trim().lines().count() >= 2, "{text:?}");
|
||||
assert!(!text.contains('\u{FFFD}'), "{text:?}");
|
||||
}
|
||||
|
||||
/// `cmd /u` writes UTF-16LE, as `wsl.exe` does; it reads right whatever the system's language.
|
||||
#[test]
|
||||
fn utf16_output_is_decoded() {
|
||||
let text = run("cmd /u /c echo 中文 한국어 OK");
|
||||
assert_eq!(text, "中文 한국어 OK\r\n");
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
//! The version-control backend abstraction (#743).
|
||||
//!
|
||||
//! `ide-vcs` grew up Git-only: [`Repo`] talks to git through gitoxide and the
|
||||
//! `git` CLI. A second backend (Perforce first, maybe SVN later) must serve the
|
||||
//! same UI — the Commit / Local Changes tool window, the diff viewer, the log
|
||||
//! and Annotate — without that UI learning backend specifics.
|
||||
//!
|
||||
//! [`Backend`] is that surface: status of the working area, the text and mode
|
||||
//! of a file at a revision, unified diffs, line hunks and annotate, the change
|
||||
//! log, and the writes a user makes from the Changes view — stage ("open for
|
||||
//! edit/add" in Perforce), discard ("revert") and commit ("submit").
|
||||
//!
|
||||
//! Deliberately Git-only concepts stay on [`Repo`]: branches and remotes,
|
||||
//! stash, rebase/merge/cherry-pick, tags, worktrees, the index's staged /
|
||||
//! unstaged split beyond what [`FileStatus`] already carries, and Restricted
|
||||
//! Mode. A caller that needs them downcasts through [`Backend::as_git`]; a
|
||||
//! caller that must work for every backend uses only the trait. As Perforce
|
||||
//! support lands, methods that turn out to be needed generically move here.
|
||||
|
||||
use crate::{
|
||||
BlameLine, BlockHistory, ChangedFile, Commit, CommitOptions, FileStatus, LineHunk, LogQuery, PendingChange, Repo,
|
||||
RevisionChange, TextEncoding,
|
||||
};
|
||||
use ide_core::{CancellationToken, IdeResult};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The version-control surface the Changes view, diff viewer, annotate and log
|
||||
/// need from a repository. Implemented by every supported backend; today that
|
||||
/// is Git via [`Repo`].
|
||||
///
|
||||
/// Revision strings follow the backend's own syntax: git revisions (`HEAD`,
|
||||
/// `:0` for the index, ids, `<id>^`…) for the Git backend; a Perforce backend
|
||||
/// takes `#have`, `@label` or a change number, and its "index" side is the
|
||||
/// opened-file state.
|
||||
pub trait Backend: Send + Sync {
|
||||
/// The repository's working root.
|
||||
fn root(&self) -> &Path;
|
||||
|
||||
/// Working-area changes, in the same shape `git status` gives: staged and
|
||||
/// unstaged kinds per path. A backend without an index reports everything
|
||||
/// as `unstaged`.
|
||||
fn status(&self) -> IdeResult<Vec<FileStatus>>;
|
||||
|
||||
/// Text of `path` at `rev`; `None` when absent there.
|
||||
fn file_at(&self, path: &Path, rev: Option<&str>, encoding: TextEncoding) -> IdeResult<Option<String>>;
|
||||
|
||||
/// The file's mode at `rev` in the backend's own notation (git's `100644`,
|
||||
/// `100755`, `120000`…; a Perforce backend reports the filetype, where
|
||||
/// `+x` is the executable case). `None` when absent there. The diff viewer
|
||||
/// shows "File mode changed" when the texts compare equal but the modes
|
||||
/// differ. `rev` follows [`file_at`](Self::file_at)'s rules, `WT` reads the
|
||||
/// worktree.
|
||||
fn file_mode_at(&self, path: &Path, rev: Option<&str>) -> IdeResult<Option<String>>;
|
||||
|
||||
/// Unified diff text of `path`'s working change (against the depot/HEAD
|
||||
/// side), empty when unchanged.
|
||||
fn diff_text(&self, path: &Path, staged: bool, encoding: TextEncoding) -> IdeResult<String>;
|
||||
|
||||
/// Working tree against `against` (or the base revision) as a patch,
|
||||
/// optionally restricted to a file.
|
||||
fn working_tree_patch(
|
||||
&self,
|
||||
path: Option<&Path>,
|
||||
against: Option<&str>,
|
||||
max_bytes: usize,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<String>;
|
||||
|
||||
/// Gutter markers: `current_text`'s hunks against the base.
|
||||
fn line_hunks(
|
||||
&self,
|
||||
path: &Path,
|
||||
current_text: &str,
|
||||
against_index: bool,
|
||||
encoding: TextEncoding,
|
||||
) -> IdeResult<Vec<LineHunk>>;
|
||||
|
||||
/// Annotate: who last changed each line of `path` (`text` when given is the
|
||||
/// buffer, not the disk file).
|
||||
fn blame(
|
||||
&self,
|
||||
path: &Path,
|
||||
text: Option<&str>,
|
||||
revision: Option<&str>,
|
||||
encoding: TextEncoding,
|
||||
) -> IdeResult<Vec<BlameLine>>;
|
||||
|
||||
/// History for Selection: the changes that touched `lines` (0-based) of
|
||||
/// `text` (the buffer, or the disk file when `None`).
|
||||
fn block_history(
|
||||
&self,
|
||||
path: &Path,
|
||||
text: Option<&str>,
|
||||
lines: (u32, u32),
|
||||
encoding: TextEncoding,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<BlockHistory>;
|
||||
|
||||
/// The change log, newest first.
|
||||
fn log(&self, query: &LogQuery, cancel: &CancellationToken) -> IdeResult<Vec<Commit>>;
|
||||
|
||||
/// Files `commit` changed.
|
||||
fn commit_files(&self, commit: &str) -> IdeResult<Vec<ChangedFile>>;
|
||||
|
||||
/// The per-file changes a set of commits made, for a combined diff.
|
||||
fn commits_changes(&self, commits: &[String]) -> IdeResult<Vec<RevisionChange>>;
|
||||
|
||||
/// Changes between two revisions.
|
||||
fn diff_revisions(&self, from: &str, to: &str) -> IdeResult<Vec<ChangedFile>>;
|
||||
|
||||
/// Files differing between `rev` and the worktree.
|
||||
fn diff_with_worktree(&self, rev: &str) -> IdeResult<Vec<ChangedFile>>;
|
||||
|
||||
/// Per-file line counts of `diff_with_worktree`.
|
||||
fn numstat_with_worktree(&self, rev: &str) -> IdeResult<Vec<(String, u32, u32)>>;
|
||||
|
||||
// ---- working-area writes ----
|
||||
|
||||
/// Marks `paths` for commit: `git add`, `p4 edit`/`p4 add`. For a file a
|
||||
/// backend stores read-only until opened, this is checkout-on-edit too.
|
||||
fn stage(&self, paths: &[PathBuf]) -> IdeResult<()>;
|
||||
|
||||
/// Reverses [`stage`](Self::stage) without touching the worktree
|
||||
/// (`git restore --staged`, `p4 revert -k` equivalent).
|
||||
fn unstage(&self, paths: &[PathBuf]) -> IdeResult<()>;
|
||||
|
||||
/// Discards working-area changes: tracked paths return to the base,
|
||||
/// untracked ones are deleted (`git restore`/`clean`, `p4 revert`).
|
||||
fn discard(&self, paths: &[PathBuf]) -> IdeResult<()>;
|
||||
|
||||
/// Records the staged/opened changes as a new change (`git commit`,
|
||||
/// `p4 submit`); returns its id. `encoding` resolves each file's text
|
||||
/// encoding (the caller knows the open documents' settings).
|
||||
fn commit(
|
||||
&self,
|
||||
message: &str,
|
||||
options: &CommitOptions,
|
||||
encoding: &dyn Fn(&Path) -> IdeResult<TextEncoding>,
|
||||
) -> IdeResult<String>;
|
||||
|
||||
/// What [`commit`](Self::commit) would record, without committing.
|
||||
fn pending_changes(
|
||||
&self,
|
||||
options: &CommitOptions,
|
||||
max_bytes: usize,
|
||||
encoding: &dyn Fn(&Path) -> IdeResult<TextEncoding>,
|
||||
) -> IdeResult<Vec<PendingChange>>;
|
||||
|
||||
// ---- IDE file operations ----
|
||||
|
||||
/// Files or folders the IDE created (project tree New File, refactorings).
|
||||
/// A checkout backend opens them for add (`p4 add`); Git tracks new files
|
||||
/// by content, so this is a no-op. Called after the files exist on disk;
|
||||
/// a failure leaves them as ordinary untracked files.
|
||||
fn files_added(&self, _paths: &[PathBuf]) -> IdeResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Files or folders the IDE deleted (project tree Delete, refactorings).
|
||||
/// A checkout backend opens them for delete (`p4 delete`); Git records the
|
||||
/// deletion at commit time, so this is a no-op. Called after they are gone
|
||||
/// from disk.
|
||||
fn files_deleted(&self, _paths: &[PathBuf]) -> IdeResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A file or folder the IDE moved or renamed (project tree Rename/Move,
|
||||
/// refactorings). A checkout backend records the rename (`p4 move`) so
|
||||
/// history follows the file; Git detects renames from content, so this is
|
||||
/// a no-op. Called after `from` is at `to` on disk.
|
||||
fn file_moved(&self, _from: &Path, _to: &Path) -> IdeResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The concrete Git repository, when this backend is one — for the
|
||||
/// Git-only surface (branches, remotes, stash, rebase…). `None` for other
|
||||
/// backends; callers should degrade to hiding Git-only UI.
|
||||
fn as_git(&self) -> Option<&Repo> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for Repo {
|
||||
fn root(&self) -> &Path {
|
||||
Repo::root(self)
|
||||
}
|
||||
|
||||
fn status(&self) -> IdeResult<Vec<FileStatus>> {
|
||||
Repo::status(self)
|
||||
}
|
||||
|
||||
fn file_at(&self, path: &Path, rev: Option<&str>, encoding: TextEncoding) -> IdeResult<Option<String>> {
|
||||
Repo::file_at(self, path, rev, encoding)
|
||||
}
|
||||
|
||||
fn file_mode_at(&self, path: &Path, rev: Option<&str>) -> IdeResult<Option<String>> {
|
||||
Repo::file_mode_at(self, path, rev)
|
||||
}
|
||||
|
||||
fn diff_text(&self, path: &Path, staged: bool, encoding: TextEncoding) -> IdeResult<String> {
|
||||
Repo::diff_text(self, path, staged, encoding)
|
||||
}
|
||||
|
||||
fn working_tree_patch(
|
||||
&self,
|
||||
path: Option<&Path>,
|
||||
against: Option<&str>,
|
||||
max_bytes: usize,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<String> {
|
||||
Repo::working_tree_patch(self, path, against, max_bytes, cancel)
|
||||
}
|
||||
|
||||
fn line_hunks(
|
||||
&self,
|
||||
path: &Path,
|
||||
current_text: &str,
|
||||
against_index: bool,
|
||||
encoding: TextEncoding,
|
||||
) -> IdeResult<Vec<LineHunk>> {
|
||||
Repo::line_hunks(self, path, current_text, against_index, encoding)
|
||||
}
|
||||
|
||||
fn blame(
|
||||
&self,
|
||||
path: &Path,
|
||||
text: Option<&str>,
|
||||
revision: Option<&str>,
|
||||
encoding: TextEncoding,
|
||||
) -> IdeResult<Vec<BlameLine>> {
|
||||
Repo::blame(self, path, text, revision, encoding)
|
||||
}
|
||||
|
||||
fn block_history(
|
||||
&self,
|
||||
path: &Path,
|
||||
text: Option<&str>,
|
||||
lines: (u32, u32),
|
||||
encoding: TextEncoding,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<BlockHistory> {
|
||||
Repo::block_history(self, path, text, lines, encoding, cancel)
|
||||
}
|
||||
|
||||
fn log(&self, query: &LogQuery, cancel: &CancellationToken) -> IdeResult<Vec<Commit>> {
|
||||
Repo::log_cancellable(self, query, cancel)
|
||||
}
|
||||
|
||||
fn commit_files(&self, commit: &str) -> IdeResult<Vec<ChangedFile>> {
|
||||
Repo::commit_files(self, commit)
|
||||
}
|
||||
|
||||
fn commits_changes(&self, commits: &[String]) -> IdeResult<Vec<RevisionChange>> {
|
||||
Repo::commits_changes(self, commits)
|
||||
}
|
||||
|
||||
fn diff_revisions(&self, from: &str, to: &str) -> IdeResult<Vec<ChangedFile>> {
|
||||
Repo::diff_revisions(self, from, to)
|
||||
}
|
||||
|
||||
fn diff_with_worktree(&self, rev: &str) -> IdeResult<Vec<ChangedFile>> {
|
||||
Repo::diff_with_worktree(self, rev)
|
||||
}
|
||||
|
||||
fn numstat_with_worktree(&self, rev: &str) -> IdeResult<Vec<(String, u32, u32)>> {
|
||||
Repo::numstat_with_worktree(self, rev)
|
||||
}
|
||||
|
||||
fn stage(&self, paths: &[PathBuf]) -> IdeResult<()> {
|
||||
Repo::stage(self, paths)
|
||||
}
|
||||
|
||||
fn unstage(&self, paths: &[PathBuf]) -> IdeResult<()> {
|
||||
Repo::unstage(self, paths)
|
||||
}
|
||||
|
||||
fn discard(&self, paths: &[PathBuf]) -> IdeResult<()> {
|
||||
Repo::discard(self, paths)
|
||||
}
|
||||
|
||||
fn commit(
|
||||
&self,
|
||||
message: &str,
|
||||
options: &CommitOptions,
|
||||
encoding: &dyn Fn(&Path) -> IdeResult<TextEncoding>,
|
||||
) -> IdeResult<String> {
|
||||
Repo::commit_with(self, message, options, encoding)
|
||||
}
|
||||
|
||||
fn pending_changes(
|
||||
&self,
|
||||
options: &CommitOptions,
|
||||
max_bytes: usize,
|
||||
encoding: &dyn Fn(&Path) -> IdeResult<TextEncoding>,
|
||||
) -> IdeResult<Vec<PendingChange>> {
|
||||
Repo::pending_changes(self, options, max_bytes, encoding)
|
||||
}
|
||||
|
||||
fn as_git(&self) -> Option<&Repo> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
@@ -610,7 +610,6 @@ pub struct ChangelistState {
|
||||
pub files: std::collections::BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
mod backend;
|
||||
mod bek;
|
||||
mod blame;
|
||||
mod block_history;
|
||||
@@ -638,7 +637,6 @@ pub use index_writes::is_own_index_write;
|
||||
mod text_encoding;
|
||||
mod write;
|
||||
|
||||
pub use backend::Backend;
|
||||
pub use cli::{
|
||||
GitCommandObserver, detected_git_executable, filter_overrides_from, git_version, restrict, set_askpass_env,
|
||||
set_git_command_observer, set_git_executable,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The repositories of a project (IDEA's VCS roots): the one containing the project
|
||||
//! root plus those nested below it (separate clones side by side, submodules).
|
||||
|
||||
use crate::{Backend, FileStatus, Repo};
|
||||
use crate::{FileStatus, Repo};
|
||||
use ide_core::IdeResult;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
@@ -142,9 +142,6 @@ impl Repos {
|
||||
/// Changes of every repository. A nested repository reports its own files, so the
|
||||
/// enclosing one's "untracked folder" entry for it is dropped. A repository that cannot
|
||||
/// be read (e.g. deleted since the last scan) is left out; fails only when all fail.
|
||||
///
|
||||
/// The reads go through [`Backend`], the Changes-view surface — repositories of a
|
||||
/// future second backend slot in here once `Repos` learns to hold them.
|
||||
pub fn status(&self) -> IdeResult<Vec<FileStatus>> {
|
||||
let mut out = Vec::new();
|
||||
let mut first_error = None;
|
||||
@@ -156,7 +153,7 @@ impl Repos {
|
||||
.map(|r| r.root())
|
||||
.filter(|r| *r != repo.root() && r.starts_with(repo.root()))
|
||||
.collect();
|
||||
let status = match (repo.as_ref() as &dyn Backend).status() {
|
||||
let status = match repo.status() {
|
||||
Ok(status) => {
|
||||
read += 1;
|
||||
status
|
||||
|
||||
@@ -108,9 +108,7 @@ fn discover_with_limit(
|
||||
if name.starts_with('.') || SKIP_DIRS.contains(&name.as_ref()) {
|
||||
continue;
|
||||
}
|
||||
if entry.file_type().is_ok_and(|kind| kind.is_dir())
|
||||
&& crate::roots::virtual_env_marker(&entry.path()).is_none()
|
||||
{
|
||||
if entry.file_type().is_ok_and(|kind| kind.is_dir()) {
|
||||
children.push(entry.path());
|
||||
}
|
||||
}
|
||||
@@ -172,19 +170,6 @@ mod tests {
|
||||
assert_eq!(result.direct[0], root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_virtual_environments_whatever_their_name() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
project(root, "code10", "pyvenv.cfg");
|
||||
project(root, "code10/Lib/site-packages/pkg", "pyproject.toml");
|
||||
fs::create_dir_all(root.join("conda/conda-meta")).unwrap();
|
||||
project(root, "conda/pkgs/dep", "setup.py");
|
||||
let app = project(root, "app", "pyproject.toml");
|
||||
let result = discover(vec![root.into()], &CancellationToken::new()).unwrap();
|
||||
assert_eq!(result.projects.iter().map(|p| p.path.to_path_buf()).collect::<Vec<_>>(), [app]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_depth_four_but_not_five_and_retains_empty_folders() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -17,19 +17,6 @@ const PYTHON_FILES: &[&str] = &["pyproject.toml", "setup.py", "setup.cfg", "Pipf
|
||||
const SKIP_DIRS: &[&str] =
|
||||
&["node_modules", "target", "build", "dist", "out", "venv", "__pycache__", "vendor", "bower_components"];
|
||||
|
||||
/// A Python virtual environment (`python -m venv`, virtualenv, uv) or conda environment,
|
||||
/// whatever it is named: its packages are dependencies, not modules of the project.
|
||||
/// Returns the file that marks it.
|
||||
pub(crate) fn virtual_env_marker(dir: &Path) -> Option<&'static str> {
|
||||
if dir.join("pyvenv.cfg").is_file() {
|
||||
Some("pyvenv.cfg")
|
||||
} else if dir.join("conda-meta").is_dir() {
|
||||
Some("conda-meta")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Build files whose changes can change the detected roots.
|
||||
pub(crate) const BUILD_FILES: &[&str] = &[
|
||||
"pom.xml",
|
||||
@@ -78,10 +65,7 @@ pub fn affects_roots(rel: &Path) -> bool {
|
||||
let Some(name) = names.next() else { return false };
|
||||
BUILD_FILES.contains(&name)
|
||||
|| ROOT_NAMES.contains(&name)
|
||||
|| matches!(
|
||||
name,
|
||||
"Assets" | "Packages" | "ProjectSettings" | "ProjectVersion.txt" | "pyvenv.cfg" | "conda-meta"
|
||||
)
|
||||
|| matches!(name, "Assets" | "Packages" | "ProjectSettings" | "ProjectVersion.txt")
|
||||
|| names.next() == Some("src")
|
||||
}
|
||||
|
||||
@@ -147,13 +131,6 @@ pub fn detect(root: &Path) -> Vec<SourceFolder> {
|
||||
if found.iter().any(|f| *f == child || f.is_empty() && name == "src") {
|
||||
continue;
|
||||
}
|
||||
if let Some(marker) = virtual_env_marker(&root.join(&child)) {
|
||||
if !out.iter().any(|r| r.path == child) {
|
||||
let from = join(&child, marker);
|
||||
out.push(SourceFolder { path: child, kind: RootKind::Excluded, detected_from: Some(from) });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
queue.push_back((child, depth + 1));
|
||||
}
|
||||
}
|
||||
@@ -528,35 +505,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn virtual_environments_are_excluded_whatever_their_name() {
|
||||
// A package in the environment's site-packages has build files of its own; as a
|
||||
// source root it would be put on PYTHONPATH and shadow the standard library.
|
||||
let dir = tree(&[
|
||||
"requirements.txt",
|
||||
"app.py",
|
||||
"code10/pyvenv.cfg",
|
||||
"code10/Lib/site-packages/pkg/pyproject.toml",
|
||||
"code10/Lib/site-packages/pkg/io/__init__.py",
|
||||
"envs/conda/conda-meta/",
|
||||
"envs/conda/lib/pkg/setup.py",
|
||||
"svc/pyproject.toml",
|
||||
"svc/env/pyvenv.cfg",
|
||||
"svc/env/lib/python3.12/site-packages/dep/setup.py",
|
||||
]);
|
||||
assert_eq!(
|
||||
summary(&detect(dir.path())),
|
||||
vec![
|
||||
"[email protected]",
|
||||
"code10=Excluded@code10/pyvenv.cfg",
|
||||
"envs/conda=Excluded@envs/conda/conda-meta",
|
||||
"svc=Source@svc/pyproject.toml",
|
||||
"svc/env=Excluded@svc/env/pyvenv.cfg",
|
||||
]
|
||||
);
|
||||
assert!(affects_roots(Path::new("code10/pyvenv.cfg")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_ecosystem_wins_for_a_shared_folder() {
|
||||
// A Gradle build with a frontend: `build` stays Gradle's, `src` is not a Node root.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! project-relative and `/`-separated; `""` is the project root itself.
|
||||
|
||||
mod detect;
|
||||
pub(crate) use detect::{BUILD_FILES, virtual_env_marker};
|
||||
pub(crate) use detect::BUILD_FILES;
|
||||
mod gradle;
|
||||
mod java;
|
||||
mod maven;
|
||||
|
||||
@@ -19,7 +19,7 @@ The plugin host runs **where the project is**: in the desktop app for local proj
|
||||
|
||||
## The interface
|
||||
|
||||
The API is defined in WIT, in `wit/` (package `openidea:plugin@1.7.0`). A plugin is a component targeting the `plugin` world (or `plugin-syntax`). The guest SDK `sdk/rust/openidea-plugin` wraps it in a typed Rust API and carries a copy of `wit/`; `crates/ide-plugin/tests/api_compat.rs` checks the copies are identical.
|
||||
The API is defined in WIT, in `wit/` (package `openidea:plugin@1.6.0`). A plugin is a component targeting the `plugin` world (or `plugin-syntax`). The guest SDK `sdk/rust/openidea-plugin` wraps it in a typed Rust API and carries a copy of `wit/`; `crates/ide-plugin/tests/api_compat.rs` checks the copies are identical.
|
||||
|
||||
Released API versions are frozen. wasmtime resolves interface names by semver, but function types must match exactly, so a released 1.x only grows: new interfaces and new optional exports, never changed records or variants. See _Evolving the plugin API_ in `plugins/README.md` before touching `wit/`.
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ On your own messages you can **Edit** (the messages after it are replaced; files
|
||||
|
||||
When an agent wants to run a command or call a tool that needs approval, the task shows the request and is listed as **Needs Input**. Choose one of the options the agent offers, such as **Allow Once**, **Allow for This Session**, **Always Allow**, **Decline**, **Always Decline** or **Decline and Stop**. Many agents also have a mode picker that approves edits or everything automatically; use it with care.
|
||||
|
||||
Once an agent has run a task, new tasks start in its mode that approves everything (Claude Code's bypass permissions, Codex's Full Access), and command permissions don't apply there. To start them in a mode that asks, set **Settings › Agents › Chat › New tasks start** to **Asking for approval**. Only your own settings choose this, never a project's.
|
||||
Once an agent has run a task, new tasks start in its mode that approves everything (Claude Code's bypass permissions, Codex's Full Access), and the Command Execution Policy doesn't apply there. To start them in a mode that asks, set **Settings › Agents › Chat › New tasks start** to **Asking for approval**. Only your own settings choose this, never a project's.
|
||||
|
||||
### Review the changes
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: "API reference"
|
||||
description: "Every interface of the openidea:plugin WIT package, with a one-line summary and a link to its source."
|
||||
---
|
||||
|
||||
The plugin API is the WIT package `openidea:plugin@1.7.0` in [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit). The WIT files are the reference: each interface, type and function is documented in comments there. 1.7.0 is the version on `main`; the latest released version is 1.6.0, whose WIT is kept in [`sdk/rust/openidea-plugin/tests/api/released/`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin/tests/api/released).
|
||||
The plugin API is the WIT package `openidea:plugin@1.6.0` in [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit). The WIT files are the reference: each interface, type and function is documented in comments there. 1.6.0 is the version on `main`; the latest released version is 1.4.0, whose WIT is kept in [`sdk/rust/openidea-plugin/tests/api/released/`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin/tests/api/released).
|
||||
|
||||
<Info>
|
||||
A generated API reference is planned. Until then, use the WIT files below, `cargo doc` for the Rust SDK
|
||||
|
||||
@@ -9,7 +9,7 @@ A code plugin is a WebAssembly **component** (component model, WASI preview 2) t
|
||||
|
||||
| Part | Where | Role |
|
||||
| ---------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Plugin API | [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) | The contract: the WIT package `openidea:plugin@1.7.0`, its interfaces and worlds |
|
||||
| Plugin API | [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) | The contract: the WIT package `openidea:plugin@1.6.0`, its interfaces and worlds |
|
||||
| Rust SDK | [`sdk/rust/openidea-plugin`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin) | Typed wrappers over the WIT, the `Plugin` trait, `register_plugin!`, and a mock host for `cargo test` |
|
||||
| Packages | [`crates/ide-extension`](https://github.com/l0ng-ai/openidea/tree/main/crates/ide-extension) | The manifest, capabilities and their risk, the grant ledger, contribution points, `.oiplugin` packages and the marketplace index |
|
||||
| Runtime | [`crates/ide-plugin`](https://github.com/l0ng-ai/openidea/tree/main/crates/ide-plugin) | The wasmtime engine, one `Store` per plugin, limits, and one actor thread per active plugin |
|
||||
|
||||
Loaded 100 of 263 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user