mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-12 18:36:53 -10:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b98bddbbab | ||
|
|
21b05ab73a | ||
|
|
7d7de0b966 | ||
|
|
830f4aa802 | ||
|
|
c905f1428e | ||
|
|
a8036ce6fa | ||
|
|
28829ad756 | ||
|
|
d6c562049f | ||
|
|
100075166c | ||
|
|
d5332a2197 | ||
|
|
b0d20d7997 | ||
|
|
8df59cf5a7 | ||
|
|
3c68d2ea7f | ||
|
|
cfe1416dbd | ||
|
|
44fc0a2ebc |
No files matched your search
+140
-10
@@ -1680,6 +1680,15 @@ 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`.
|
||||
@@ -1759,6 +1768,20 @@ 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)
|
||||
@@ -1769,15 +1792,56 @@ pub fn goto_declaration(text: &str, path: &Path, pos: Pos, local_repo: Option<Pa
|
||||
// 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);
|
||||
let e = elements.iter().rev().find(|e| e.name == "dependency" || e.name == "plugin")?;
|
||||
if elements.iter().any(|a| a.name == "dependencyManagement" || a.name == "pluginManagement") {
|
||||
return None; // Management entries are declarations; there is nothing to go to.
|
||||
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 kind = match e.name.as_str() {
|
||||
"dependency" => PomGotoKind::ManagedDependency,
|
||||
_ => PomGotoKind::ManagedPlugin,
|
||||
};
|
||||
managed_target(text, path, e, kind, local_repo)
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 }
|
||||
}
|
||||
|
||||
/// The parent POMs of the POM in `text` at `path`, child-first (its `<parent>` refs,
|
||||
@@ -3087,9 +3151,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: it is the declaration.
|
||||
// The dependency names its own version and no local repository has its POM.
|
||||
assert_eq!(goto_declaration(&text, &path, at(&text, "<artifactId>other-lib"), None), None);
|
||||
// A management entry is a declaration itself, not a reference.
|
||||
// A management entry declares the version; its artifact's POM is not at hand either.
|
||||
let parent_path = tmp.path().join("pom.xml");
|
||||
let parent_text = std::fs::read_to_string(&parent_path).unwrap();
|
||||
assert_eq!(
|
||||
@@ -3101,6 +3165,72 @@ 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!(
|
||||
|
||||
@@ -143,9 +143,12 @@ 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), 0);
|
||||
assert_eq!(count.load(Ordering::SeqCst), seen);
|
||||
}
|
||||
}
|
||||
@@ -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.6.0";
|
||||
pub const ENGINE_VERSION: &str = "1.7.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.
|
||||
|
||||
@@ -455,7 +455,8 @@ 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). `None` when nothing
|
||||
/// 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
|
||||
/// 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)?;
|
||||
@@ -466,7 +467,19 @@ 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);
|
||||
Ok(ide_build::maven::goto_declaration(&text, &path, pos, 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))
|
||||
})
|
||||
.await
|
||||
.map_err(join_err)?
|
||||
|
||||
@@ -114,14 +114,28 @@ 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, path: PathBuf, data: Vec<u8>) -> IdeResult<()> {
|
||||
host.blocking(move || ide_vfs::create_file_bytes(&path, &data)).await
|
||||
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
|
||||
}
|
||||
|
||||
/// Toggle Read-only Attribute: sets the writable bit (and the open document's
|
||||
@@ -135,25 +149,54 @@ 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 history = host.project(window).ok().map(|p| p.history.clone());
|
||||
let project = host.project(window).ok();
|
||||
let history = project.as_ref().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)
|
||||
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(())
|
||||
})
|
||||
.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 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 rename(host: &Host, from: PathBuf, to: PathBuf) -> IdeResult<()> {
|
||||
host.blocking(move || ide_workspace::file_ops::rename(&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
|
||||
}
|
||||
|
||||
/// Reads a text file without opening it as a document; prefers the unsaved buffer.
|
||||
|
||||
@@ -138,6 +138,11 @@ 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).
|
||||
@@ -280,6 +285,16 @@ 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();
|
||||
@@ -348,7 +363,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_repo_of(&path, |r| r.line_hunks(&path, &text, false, project.vcs_encoding(&path, None)?))
|
||||
project.with_backend_of(&path, |r| r.line_hunks(&path, &text, false, project.vcs_encoding(&path, None)?))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -360,7 +375,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_repo_of(&path, |r| {
|
||||
return project.with_log_backend_of(&path, |r| {
|
||||
r.file_at(&path, rev.as_deref(), project.vcs_encoding(&path, rev.as_deref())?)
|
||||
});
|
||||
}
|
||||
@@ -383,7 +398,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_repo_of(&path, |r| r.file_mode_at(&path, rev.as_deref()));
|
||||
return project.with_log_backend_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"))?
|
||||
@@ -403,7 +418,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_repo_of(&path, |r| {
|
||||
project.with_backend_of(&path, |r| {
|
||||
r.blame(&path, text.as_deref(), revision.as_deref(), project.vcs_encoding(&path, revision.as_deref())?)
|
||||
})
|
||||
})
|
||||
@@ -424,7 +439,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_repo_of(&path, |r| {
|
||||
project.with_backend_of(&path, |r| {
|
||||
r.block_history(&path, text.as_deref(), lines, project.vcs_encoding(&path, None)?, &cancel)
|
||||
})
|
||||
})
|
||||
@@ -441,7 +456,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.log_cancellable(&query, &cancel)
|
||||
(r as &dyn Backend).log(&query, &cancel)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
@@ -540,7 +555,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_repo_at(repo.as_deref(), |r| r.commit_files(&commit))).await
|
||||
blocking(move || project.with_log_backend_at(repo.as_deref(), |r| r.commit_files(&commit))).await
|
||||
}
|
||||
|
||||
/// Combined changes of several commits, `commits` oldest first.
|
||||
@@ -551,7 +566,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_repo_at(repo.as_deref(), |r| r.commits_changes(&commits))).await
|
||||
blocking(move || project.with_log_backend_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>> {
|
||||
@@ -567,7 +582,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_repo(&project, &paths, None, |r, paths| r.stage(paths))).await
|
||||
blocking(move || per_backend(&project, &paths, None, |r, paths| r.stage(paths))).await
|
||||
}
|
||||
|
||||
/// Project-host operation (also registered with the remote dispatcher).
|
||||
@@ -602,7 +617,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_repo(&project, &paths, None, |r, paths| r.unstage(paths))).await
|
||||
blocking(move || per_backend(&project, &paths, None, |r, paths| r.unstage(paths))).await
|
||||
}
|
||||
|
||||
pub async fn vcs_stage_patch(host: &Host, window: &str, patch: String) -> IdeResult<()> {
|
||||
@@ -614,7 +629,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_repo(&project, &paths, Some("Rollback"), |r, paths| r.discard(paths))).await
|
||||
blocking(move || per_backend(&project, &paths, Some("Rollback"), |r, paths| r.discard(paths))).await
|
||||
}
|
||||
|
||||
pub async fn vcs_commit(
|
||||
@@ -628,8 +643,8 @@ pub async fn vcs_commit(
|
||||
// Commit hooks may run for a long time.
|
||||
let project = host.project(window)?;
|
||||
blocking(move || {
|
||||
project.with_repo_recorded(repo.as_deref(), None, |r| {
|
||||
r.commit_with(&message, &options, &|path| project.vcs_encoding(path, None))
|
||||
project.with_backend_recorded(repo.as_deref(), None, |r| {
|
||||
r.commit(&message, &options, &|path| project.vcs_encoding(path, None))
|
||||
})
|
||||
})
|
||||
.await
|
||||
@@ -886,7 +901,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_repo_of(&path, |r| r.diff_text(&path, staged, project.vcs_encoding(&path, None)?)))
|
||||
blocking(move || project.with_backend_of(&path, |r| r.diff_text(&path, staged, project.vcs_encoding(&path, None)?)))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1201,7 +1216,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_repo_at(repo.as_deref(), |r| r.diff_with_worktree(&rev))).await
|
||||
blocking(move || project.with_log_backend_at(repo.as_deref(), |r| r.diff_with_worktree(&rev))).await
|
||||
}
|
||||
|
||||
/// Files that differ between two revisions (the "N files updated" of Update Project).
|
||||
@@ -1213,7 +1228,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_repo_at(repo.as_deref(), |r| r.diff_revisions(&from, &to))).await
|
||||
blocking(move || project.with_log_backend_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,12 +1276,13 @@ pub(super) fn commit_draft(
|
||||
};
|
||||
// Both callers have checked that the window's project is trusted.
|
||||
r.set_execution_allowed(true);
|
||||
let pending = r.pending_changes(&repo.options, budget, &|path| Ok(workspace.vcs_encoding(path))).map_err(
|
||||
|e| match e.kind {
|
||||
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 {
|
||||
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);
|
||||
@@ -1530,6 +1531,19 @@ 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,14 +72,19 @@ 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 {
|
||||
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)?;
|
||||
}
|
||||
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)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -47,6 +47,48 @@ 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 {
|
||||
@@ -134,20 +176,25 @@ 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).
|
||||
/// 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.
|
||||
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 { .. }
|
||||
@@ -163,7 +210,6 @@ pub fn forwarded(event: &PluginEvent) -> bool {
|
||||
| PluginEvent::OpenLogTab { .. }
|
||||
| PluginEvent::CopyToClipboard { .. }
|
||||
| PluginEvent::OpenUrl { .. }
|
||||
| PluginEvent::OpenProject { .. }
|
||||
| PluginEvent::PromptRequested { .. }
|
||||
| PluginEvent::PromptClosed { .. }
|
||||
| PluginEvent::PromptWaiting { .. }
|
||||
@@ -173,6 +219,13 @@ 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 {
|
||||
@@ -951,4 +1004,43 @@ 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,6 +218,9 @@ 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::{Repo, Repos};
|
||||
use ide_vcs::{Backend, Repo, Repos};
|
||||
use ide_vfs::Watcher;
|
||||
use ide_workspace::documents::DocumentObserver;
|
||||
use ide_workspace::{DocumentInfo, Documents, FileMoves, LocalHistory, ProjectRoots, SettingsStore, SourceFolder};
|
||||
@@ -971,6 +971,11 @@ 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() {
|
||||
@@ -980,6 +985,40 @@ 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)
|
||||
@@ -1004,7 +1043,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_repo_at(at, |r| r.status()) {
|
||||
if let Ok(changed) = self.with_backend_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}");
|
||||
|
||||
+106
-3
@@ -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.arg("path")?, c.arg("data")?).await
|
||||
ops::fs::create_file_bytes(&c.host, &c.window, 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.arg("from")?, c.arg("to")?).await })
|
||||
.add("fs_rename", |c| async move { ops::fs::rename(&c.host, &c.window, 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.arg("from")?, c.arg("to")?).await })
|
||||
.add("fs_copy", |c| async move { ops::fs::copy(&c.host, &c.window, c.arg("from")?, c.arg("to")?).await })
|
||||
.add("fs_copy_merge", |c| async move {
|
||||
ops::fs::copy_merge(
|
||||
&c.host,
|
||||
@@ -298,6 +298,56 @@ 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>,
|
||||
@@ -364,3 +414,56 @@ 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,6 +18,15 @@ 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
|
||||
@@ -40,7 +49,37 @@ impl TrustStore {
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
};
|
||||
Self { file, state: Mutex::new(state), inherited: Mutex::default() }
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn decisions(&self) -> IdeResult<TrustDecisions> {
|
||||
@@ -133,7 +172,7 @@ impl TrustStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn persist(file: &Path, state: &TrustDecisions) -> IdeResult<()> {
|
||||
fn persist<T: Serialize>(file: &Path, state: &T) -> IdeResult<()> {
|
||||
if let Some(parent) = file.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
@@ -372,6 +411,8 @@ 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"
|
||||
@@ -526,4 +567,18 @@ 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,6 +6352,26 @@ 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,3 +455,29 @@ 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, original.clone(), renamed.clone()).await.unwrap();
|
||||
ops::fs::rename(&host, "main", 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, renamed.clone(), copied.clone()).await.unwrap();
|
||||
ops::fs::copy(&host, "main", 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,6 +21,9 @@ 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 }
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//! 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{fingerprint}\n{}", root.display());
|
||||
let stamp = format!("{SCHEMA}\n{}\n{fingerprint}\n{}", env!("INDEX_SOURCE_HASH"), root.display());
|
||||
match store.meta("stamp") {
|
||||
Ok(Some(v)) if v == stamp.as_bytes() => {}
|
||||
_ => {
|
||||
|
||||
@@ -58,6 +58,9 @@ 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
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//! 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,29 +132,11 @@ impl Syntax {
|
||||
SPECS.iter().map(|s| s.name)
|
||||
}
|
||||
|
||||
/// Changes whenever extraction output may change (grammar set, queries,
|
||||
/// crate version); persistent caches of [`Syntax::tags`] key on it.
|
||||
/// 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.
|
||||
pub fn fingerprint(&self) -> String {
|
||||
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()
|
||||
env!("SYNTAX_SOURCE_HASH").to_owned()
|
||||
}
|
||||
|
||||
/// 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.6";
|
||||
pub const API_VERSION: &str = "1.7";
|
||||
const EPOCH_TICK: Duration = Duration::from_millis(10);
|
||||
pub(crate) const MAX_TEXT: usize = 1 << 20;
|
||||
pub(crate) const MAX_TITLE: usize = 256;
|
||||
@@ -170,6 +170,11 @@ 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,6 +1770,12 @@ 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.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"] {
|
||||
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"] {
|
||||
assert_eq!(check_api(&v(ok), &host), Ok(()), "{ok}");
|
||||
}
|
||||
let newer = check_api(&v("1.7.0"), &host).unwrap_err();
|
||||
assert!(newer.contains("1.7") && newer.contains("update OpenIDEA"), "{newer}");
|
||||
let newer = check_api(&v("1.8.0"), &host).unwrap_err();
|
||||
assert!(newer.contains("1.8") && 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.6 SDK"), "{older}");
|
||||
assert!(older.contains("rebuild it with the 1.7 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 @@
|
||||
005d9da29eb7d713a97ff872051e408f653030434d837835843298553c9e12e0
|
||||
3c7f35facd15ee8380ccb6475b7c051d500e38346b711e1805077becd0c75f75
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
6359ef4271f38374889412bd926d9aa735f59a8ce047ffcb06c47976444196cf
|
||||
3879822d975695c6258e95286b0e64c5ee1e3265876501efaf0e577b38f8222b
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
6f560f3d0d7737150d2dfa09c3b801bd3b4c3a52cef9893ed8fc045809fe33e1
|
||||
228ae888bf556ce5cedc7843eb20e92f221d9e1f640f3c6c595bed3fd6be6051
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
4024af2d55d7fe4e8c456efd7e0c497ade4b2c5959e9108ff218f4a669e920f4
|
||||
41de239b6f723020b38eb2b67f642b7c88cc88c79117c3072ba122633afc418c
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
4ca977f53a18e9e8f869d34e027a15abece9e97b9331e23d4f71af721b104611
|
||||
78c5ed3c756498c7619b46c3622ae7a59461c61c747b45caecd79b341ee654de
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
3d12a00f81e0db1b0902c2ddebfd6454a056f15b97add80b22a0333722568dc8
|
||||
ee848321e53cb181536e6158570dbb4ab02a6fd2a1a064b068803ac7da7f3448
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
95d89e411216f50d0b34bee110c6bfc9b8fbcb2ffaae2e912c3609f7bf678b2c
|
||||
b587de945875a98f1d4ca20fab7cf94fb4dc6d848136aa19ab0e0e5319155a80
|
||||
@@ -40,6 +40,10 @@ title = "write"
|
||||
id = "content"
|
||||
title = "content"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "diff"
|
||||
title = "diff"
|
||||
|
||||
[[contributes.commands]]
|
||||
id = "events"
|
||||
title = "events"
|
||||
|
||||
@@ -78,6 +78,18 @@ 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 @@
|
||||
dd4b6c5153892a4f3facb3f23cb1d92da39a3977985b8abf4eaa9558decfa4a9
|
||||
1c8776dcae35d2d49dd6c1e32b0cdd354de1eb0f72a473245a2c47368b89ec30
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
325bc15cd28933d9687bc3c316008f921f48a4a69d7528c6b367fa0ff22489b8
|
||||
2a517cbffd6d3cdda37117d8ca8815978a09995923fc3d5aa5af7020afdac56a
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
abe706392e04ef5a14f9feab88e0c0d6a9e96bee7de771b61deeb65e7d9e0a7c
|
||||
dbd912b1f8488ecd446af07b5dcaef55d44bce57c1a55be5c0b0fb33e2697903
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
fb3677946021b30d226e2e3d422c352645c2065c93057c0a645246520860fc90
|
||||
efdc84cd2bedb3989b03d19b569d09958f26e502db65db4dd786eb3438a7c407
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
a01852ead9004ac1558028da19a1ae96ccd035775484089fd39f14263082daf1
|
||||
be6bc15ee42a5e21781ba54f548007351a46b669882ee4491da20ff556927a06
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
bf00050b7bb9911680a94fd36aaeb83fc96d94030e8ceef51d1723c3950253f9
|
||||
e5c3a3beda750dff27d7b823676d53f15b7c76b8ad539594cd913bd178d750da
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
d00a07d9d51e7ca117cec59070d2812d4d1efb2730b60a6f171c8fd12903d055
|
||||
322de3299f0413101c02358836e7df7e06e58cbb22c11897664564abbb8af090
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
0aa0ee56e8bacd4d218bafecdec7f4b6249381fad6e31f8b2ee89fd88b16d710
|
||||
d768ff6a4a466cc6f1d72499df3ccb4ae76872436b8eddff27f62aa156331a37
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
e06955818969808e6695fba9178727239e343bc4542efbf56262dd407175fd8a
|
||||
57aea107b2c10e4d30fa3785ac4c6566f1b40908a18a3859aca8be047fac9518
|
||||
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
7af67673471d49f97aa2cc131b5deddfc78ee7fc203901eba12f4970a5b13f75
|
||||
c98bcc89750e26e738d7115a7c0454fde25367a0a23bdc2ff95fb5a5dacc9f98
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
e81d2454a61977ab69dc8b5c3c617627c559c4e340337a3d43309daeb9b1c12d
|
||||
a4c23b93a14eda705cb18c88bbc2373d1ef3f96031ff2df21b8b092b50633996
|
||||
+3
-3
@@ -5,9 +5,9 @@ wit_bindgen::generate!({
|
||||
inline: "
|
||||
package fixture:misbehave;
|
||||
world misbehave {
|
||||
include openidea:plugin/plugin-syntax@1.6.0;
|
||||
export openidea:plugin/providers@1.6.0;
|
||||
export openidea:plugin/file-actions@1.6.0;
|
||||
include openidea:plugin/plugin-syntax@1.7.0;
|
||||
export openidea:plugin/providers@1.7.0;
|
||||
export openidea:plugin/file-actions@1.7.0;
|
||||
}
|
||||
",
|
||||
path: "../../../../../wit",
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
123ae68df6baa3b5983349ba05fb441ec67fddcb32cab566b0d02e443bb4800e
|
||||
aec002b3dd310068e69a6ae67a49d30655bf8baea9153a110a407e9d408e6880
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
7b26a4ec36b496f9a4f5e029ef557707961a281e31508863c989a5a5abd5e225
|
||||
08ece563c3b55c2b86e68fbb0815ddcbb63beb6f1f6c32b8dbdbee558a4a66c1
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
67f278ade12d11b1445ac3d448853ec46282f2f3e2b2299c6c8fe0dfb4fc86cd
|
||||
450845edf234a1cc78cd6cfc0b812a89f3a2e41484689cc1e1b05be66873d4eb
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
8cf9210844a1968f485e6e747f6dff4a1fdf0a628e2945f731d8289dd01f8385
|
||||
61b735de5ef61dcd0ea8e640c2ad004e84a63fe4dfd7872ffb5e04d6d81dc9a9
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
263e6ab4b54593f09ae2cf71e9a191acb95ed744cc16bbaf9a9263e41614e99a
|
||||
36776367782eaa4b8b8121f4a665dd8e68525a4836bff1c92bc16770cce14929
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
682a712b6c75ad0c0da24476f87c77a76b248c201d4d31923a79fda053b76b90
|
||||
f6311d232a99356b02a398b2ce3db0865c425f435a8d201157719178ba2b97c7
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
c5718b3978f21291cfff7a00233ba68d545a5977bc3e90c546b4380528295446
|
||||
fe1d26665e3c816ed43fc720acabad03a37b7924bccd4dd8c357fd156947b3c7
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
186b30c785fae23d18c5efe8fa63f6526d23d9d141580deef7ee7009310a88c3
|
||||
76bab8e48235f9f066f5b9bbfdd9398916b6c3ed73c072c543d80595130519f7
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
cb3a1c4a0518aa3f3e689337e8f4249180e5b3753b0e7bfaba8c974f89f6ef7a
|
||||
f9ed2eac306229f76b05d0c4a4fb2f0ed8d81a1dfc5eb50d044bbe10c9206a5f
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
fa97682ca30cb0ad1392ff1bab4d99ea6d9ccb2654feedfdc130d11adb7df93b
|
||||
f2d83d9f56da072d38a1552195af8a9d4edc11888c645627d5dd1e4a46afc4df
|
||||
@@ -0,0 +1,302 @@
|
||||
//! 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,6 +610,7 @@ pub struct ChangelistState {
|
||||
pub files: std::collections::BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
mod backend;
|
||||
mod bek;
|
||||
mod blame;
|
||||
mod block_history;
|
||||
@@ -637,6 +638,7 @@ 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::{FileStatus, Repo};
|
||||
use crate::{Backend, FileStatus, Repo};
|
||||
use ide_core::IdeResult;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
@@ -142,6 +142,9 @@ 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;
|
||||
@@ -153,7 +156,7 @@ impl Repos {
|
||||
.map(|r| r.root())
|
||||
.filter(|r| *r != repo.root() && r.starts_with(repo.root()))
|
||||
.collect();
|
||||
let status = match repo.status() {
|
||||
let status = match (repo.as_ref() as &dyn Backend).status() {
|
||||
Ok(status) => {
|
||||
read += 1;
|
||||
status
|
||||
|
||||
@@ -108,7 +108,9 @@ 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()) {
|
||||
if entry.file_type().is_ok_and(|kind| kind.is_dir())
|
||||
&& crate::roots::virtual_env_marker(&entry.path()).is_none()
|
||||
{
|
||||
children.push(entry.path());
|
||||
}
|
||||
}
|
||||
@@ -170,6 +172,19 @@ 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,6 +17,19 @@ 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",
|
||||
@@ -65,7 +78,10 @@ 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")
|
||||
|| matches!(
|
||||
name,
|
||||
"Assets" | "Packages" | "ProjectSettings" | "ProjectVersion.txt" | "pyvenv.cfg" | "conda-meta"
|
||||
)
|
||||
|| names.next() == Some("src")
|
||||
}
|
||||
|
||||
@@ -131,6 +147,13 @@ 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));
|
||||
}
|
||||
}
|
||||
@@ -505,6 +528,35 @@ 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;
|
||||
pub(crate) use detect::{BUILD_FILES, virtual_env_marker};
|
||||
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.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.
|
||||
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.
|
||||
|
||||
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/`.
|
||||
|
||||
|
||||
@@ -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.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).
|
||||
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).
|
||||
|
||||
<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.6.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.7.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 |
|
||||
|
||||
@@ -281,6 +281,7 @@ With `engines.openidea = "^1.3"`, a page can send JSON messages through
|
||||
| `pty-spawn`, `pty-input`, `pty-resize`, `pty-kill`, `pty-ack` | An interactive PTY owned by the frame; output arrives as `pty-data`, `pty-exit`, `pty-error`. At most 8 sessions per plugin. |
|
||||
| `open-log-tab` | A read-only editor tab following a program's output, with argument toggles. |
|
||||
| `open-content-tab` | An editor tab on content the plugin supplies, with toolbar actions answered by `content-tab-reply`. |
|
||||
| `open-diff` | API 1.7: the diff viewer on two texts (`left`, `right`, optional labels, `title` and `fileName` for highlighting). |
|
||||
|
||||
The message fields and limits are in the
|
||||
[plugin guide](https://github.com/l0ng-ai/openidea/blob/main/plugins/README.md#web-frame-ptys-api-13);
|
||||
@@ -338,15 +339,16 @@ divider. The host asks after the item's `select` event and again after `refresh_
|
||||
|
||||
These are called from a command or event of a window:
|
||||
|
||||
| Function | Opens |
|
||||
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `host::open_location(path, line, column)` | A project file in an editor, caret at the position (`project.read`; a file in the plugin's private directory also works). |
|
||||
| `host::open_editor(path, selection)` | API 1.4: open a project file and select a range. |
|
||||
| `host::open_terminal(title, program, args)` | A Terminal tab running an approved `process:<program>` in the project root. |
|
||||
| `host::open_terminal_session(key, title, &cmd)` | API 1.4: a keyed terminal; reopening the key focuses it. Write to it with `host::write_terminal(key, text)`; its exit arrives as a `terminal-exited` event. |
|
||||
| `host::open_log_tab(key, title, &log)` | API 1.3: the read-only log tab, from a `ProcessLog`. At most 32 per window; not yet in remote projects. |
|
||||
| `host::open_content_tab(&ContentTab { .. })` | API 1.4: a content editor tab without a web view; actions arrive as `content-tab-action` events, answered with `host::reply_content_tab`. |
|
||||
| `host::output_append(channel, text)` | API 1.4: a read-only **Output** tab per channel (32 channels, 1 MiB each); `output_clear`, `output_reveal`. |
|
||||
| Function | Opens |
|
||||
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `host::open_location(path, line, column)` | A project file in an editor, caret at the position (`project.read`; a file in the plugin's private directory also works). |
|
||||
| `host::open_editor(path, selection)` | API 1.4: open a project file and select a range. |
|
||||
| `host::open_terminal(title, program, args)` | A Terminal tab running an approved `process:<program>` in the project root. |
|
||||
| `host::open_terminal_session(key, title, &cmd)` | API 1.4: a keyed terminal; reopening the key focuses it. Write to it with `host::write_terminal(key, text)`; its exit arrives as a `terminal-exited` event. |
|
||||
| `host::open_log_tab(key, title, &log)` | API 1.3: the read-only log tab, from a `ProcessLog`. At most 32 per window; not yet in remote projects. |
|
||||
| `host::open_content_tab(&ContentTab { .. })` | API 1.4: a content editor tab without a web view; actions arrive as `content-tab-action` events, answered with `host::reply_content_tab`. |
|
||||
| `host::open_diff(&ContentDiff { .. }, file_name)` | API 1.7: the diff viewer on two texts, such as a review's file before and after; `file_name` picks the syntax highlighting. |
|
||||
| `host::output_append(channel, text)` | API 1.4: a read-only **Output** tab per channel (32 channels, 1 MiB each); `output_clear`, `output_reveal`. |
|
||||
|
||||
```rust
|
||||
use openidea_plugin::{ContentAction, ContentTab, host};
|
||||
|
||||
@@ -36,8 +36,8 @@ Code plugins are WebAssembly components targeting the `plugin` world in [`wit/`]
|
||||
|
||||
| | |
|
||||
| ------------------------- | ------------------------------------------------------------ |
|
||||
| Latest released API | **1.4.0**, shipped with OpenIDEA 0.1.40 |
|
||||
| In development | **1.6.0** on the `main` branch, planned for the next release |
|
||||
| Latest released API | **1.6.0**, shipped with OpenIDEA 0.1.43 |
|
||||
| In development | **1.7.0** on the `main` branch, planned for the next release |
|
||||
| Format of the WIT package | `openidea:plugin@<version>` |
|
||||
|
||||
Declare the API your plugin needs with `engines.openidea` in the manifest, for example `engines.openidea = "^1.4"`.
|
||||
|
||||
@@ -3,7 +3,7 @@ title: "API versions and compatibility"
|
||||
description: "engines.openidea, the plugin API versions from 1.0 to 1.6, and the rules that keep released plugins loading in newer IDEs."
|
||||
---
|
||||
|
||||
The plugin API has one version number: the version of the WIT package in [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) (`openidea:plugin@1.6.0` on `main`). The host's engine version and the SDK's `API_VERSION` always agree with it, and the SDK carries an identical copy of the WIT files.
|
||||
The plugin API has one version number: the version of the WIT package in [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) (`openidea:plugin@1.7.0` on `main`). The host's engine version and the SDK's `API_VERSION` always agree with it, and the SDK carries an identical copy of the WIT files.
|
||||
|
||||
<Note>
|
||||
The plugin system is a preview. The compatibility rules below hold for released 1.x versions; what is still unreleased
|
||||
@@ -12,15 +12,16 @@ The plugin API has one version number: the version of the WIT package in [`wit/`
|
||||
|
||||
## Versions
|
||||
|
||||
| API | Shipped with | Highlights |
|
||||
| ----- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1.0.0 | OpenIDEA 0.1.36 | The base API: commands, events, storage, tasks, project and editor access, VCS, diagnostics, network and secrets, processes, declarative views and custom editors, settings, editor providers, syntax provider |
|
||||
| 1.1.0 | OpenIDEA 0.1.37 | `vcs.remotes`, `ui.copy-to-clipboard`, `ui.open-url`, opening a project file at a line from a view |
|
||||
| 1.2.0 | OpenIDEA 0.1.38 | Background document indexers, pickers, input boxes and confirmations, plugin context keys for `when` clauses, the files selected in the Project view, a manifest `icon`, the `viewItem.context` menu |
|
||||
| 1.3.0 | OpenIDEA 0.1.39 | Commit message and before-commit checks, Services detail panes, diagnostics while typing, hover, inlay hints, semantic tokens, file-changing code actions, HTTP endpoints, files outside the project (`fs.read`/`fs.write`, `project.write`, private plugin directory), batched `files-changed` events and path filters, workspace symbols, long-running processes, notifications with buttons, translations, multi-page settings, file template variables, web view PTYs, log tabs and content tabs |
|
||||
| 1.4.0 | OpenIDEA 0.1.40 | `net.http:`, `ipc:`, `ide.commands` and `database.sources` capabilities; editor and project snapshots, decorations, text search, Problems, Git log/blame/diff; output channels; OAuth loopback; runs, terminal sessions, run configurations, test result parsing and run targets; formatting and other language features; embedded languages; snippets; more menu locations; database sources; MCP tools |
|
||||
| 1.5.0 | Planned for the next release | `Project::notify` (a notification in one project's window), the `net:ask` capability, run target validation, per-command run target working-directory arguments |
|
||||
| 1.6.0 | In development on `main` | Bounded, project-bound background WebSockets for event-stream plugins |
|
||||
| API | Shipped with | Highlights |
|
||||
| ----- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1.0.0 | OpenIDEA 0.1.36 | The base API: commands, events, storage, tasks, project and editor access, VCS, diagnostics, network and secrets, processes, declarative views and custom editors, settings, editor providers, syntax provider |
|
||||
| 1.1.0 | OpenIDEA 0.1.37 | `vcs.remotes`, `ui.copy-to-clipboard`, `ui.open-url`, opening a project file at a line from a view |
|
||||
| 1.2.0 | OpenIDEA 0.1.38 | Background document indexers, pickers, input boxes and confirmations, plugin context keys for `when` clauses, the files selected in the Project view, a manifest `icon`, the `viewItem.context` menu |
|
||||
| 1.3.0 | OpenIDEA 0.1.39 | Commit message and before-commit checks, Services detail panes, diagnostics while typing, hover, inlay hints, semantic tokens, file-changing code actions, HTTP endpoints, files outside the project (`fs.read`/`fs.write`, `project.write`, private plugin directory), batched `files-changed` events and path filters, workspace symbols, long-running processes, notifications with buttons, translations, multi-page settings, file template variables, web view PTYs, log tabs and content tabs |
|
||||
| 1.4.0 | OpenIDEA 0.1.40 | `net.http:`, `ipc:`, `ide.commands` and `database.sources` capabilities; editor and project snapshots, decorations, text search, Problems, Git log/blame/diff; output channels; OAuth loopback; runs, terminal sessions, run configurations, test result parsing and run targets; formatting and other language features; embedded languages; snippets; more menu locations; database sources; MCP tools |
|
||||
| 1.5.0 | OpenIDEA 0.1.41 | `Project::notify` (a notification in one project's window), the `net:ask` capability, run target validation, per-command run target working-directory arguments |
|
||||
| 1.6.0 | OpenIDEA 0.1.43 | Bounded, project-bound background WebSockets for event-stream plugins |
|
||||
| 1.7.0 | In development on `main` | `views.open-diff`: two texts in the diff viewer, such as a review's file before and after |
|
||||
|
||||
The full list of what each version added is in [`plugins/README.md`](https://github.com/l0ng-ai/openidea/blob/main/plugins/README.md).
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ description: "插件系统的内部实现:扩展内核、每个插件一个 ac
|
||||
|
||||
## 接口
|
||||
|
||||
API 用 WIT 定义,位于 `wit/`(包名 `openidea:plugin@1.6.0`)。插件是面向 `plugin` world(或 `plugin-syntax`)的组件。插件端 SDK `sdk/rust/openidea-plugin` 把它封装成类型化的 Rust API,并带有一份 `wit/` 副本;`crates/ide-plugin/tests/api_compat.rs` 检查两份是否一致。
|
||||
API 用 WIT 定义,位于 `wit/`(包名 `openidea:plugin@1.7.0`)。插件是面向 `plugin` world(或 `plugin-syntax`)的组件。插件端 SDK `sdk/rust/openidea-plugin` 把它封装成类型化的 Rust API,并带有一份 `wit/` 副本;`crates/ide-plugin/tests/api_compat.rs` 检查两份是否一致。
|
||||
|
||||
已发布的 API 版本是冻结的。wasmtime 按 semver 解析接口名,但函数类型必须完全一致,所以已发布的 1.x 只能增加:新的接口和新的可选导出,不能修改已有的 record 或 variant。改 `wit/` 之前请先读 `plugins/README.md` 里的 _Evolving the plugin API_。
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: "API 参考"
|
||||
description: "openidea:plugin WIT 包中的每个 interface:一句话说明及源码链接。"
|
||||
---
|
||||
|
||||
插件 API 是 [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) 中的 WIT 包 `openidea:plugin@1.6.0`。WIT 文件就是参考文档:每个 interface、类型和函数都在注释中有说明。1.6.0 是 `main` 上的版本;最新已发布版本是 1.4.0,其 WIT 保存在 [`sdk/rust/openidea-plugin/tests/api/released/`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin/tests/api/released)。
|
||||
插件 API 是 [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) 中的 WIT 包 `openidea:plugin@1.7.0`。WIT 文件就是参考文档:每个 interface、类型和函数都在注释中有说明。1.7.0 是 `main` 上的版本;最新已发布版本是 1.6.0,其 WIT 保存在 [`sdk/rust/openidea-plugin/tests/api/released/`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin/tests/api/released)。
|
||||
|
||||
<Info>
|
||||
自动生成的 API 参考正在计划中。在此之前,请参考下面的 WIT 文件、Rust SDK 的 `cargo
|
||||
|
||||
@@ -9,7 +9,7 @@ description: "插件代码如何运行:WebAssembly 组件、WIT world、宿主
|
||||
|
||||
| 部分 | 位置 | 作用 |
|
||||
| -------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
||||
| 插件 API | [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) | 契约:WIT 包 `openidea:plugin@1.6.0`、其中的 interface 和 world |
|
||||
| 插件 API | [`wit/`](https://github.com/l0ng-ai/openidea/tree/main/wit) | 契约:WIT 包 `openidea:plugin@1.7.0`、其中的 interface 和 world |
|
||||
| Rust SDK | [`sdk/rust/openidea-plugin`](https://github.com/l0ng-ai/openidea/tree/main/sdk/rust/openidea-plugin) | WIT 的类型化封装、`Plugin` trait、`register_plugin!`,以及给 `cargo test` 用的模拟宿主 |
|
||||
| 插件包 | [`crates/ide-extension`](https://github.com/l0ng-ai/openidea/tree/main/crates/ide-extension) | 清单、能力及其风险等级、授权记录、贡献点、`.oiplugin` 包和插件市场索引 |
|
||||
| 运行时 | [`crates/ide-plugin`](https://github.com/l0ng-ai/openidea/tree/main/crates/ide-plugin) | wasmtime 引擎、每个插件一个 `Store`、各项限制、每个活跃插件一个 actor 线程 |
|
||||
|
||||
@@ -248,11 +248,12 @@ Vite + React 页面由 `plugins/build.sh official/graphviz` 构建到 `web/`,
|
||||
声明 `engines.openidea = "^1.3"` 后,页面可以通过 `openidea.postMessage` 发送 JSON 消息,请 IDE 打开更多东西。
|
||||
这些协议只运行通过 `process:<program>` 授权的程序,工作目录是窗口所在的受信任项目根目录,从不经过 shell:
|
||||
|
||||
| `op` | 打开什么 |
|
||||
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `pty-spawn`、`pty-input`、`pty-resize`、`pty-kill`、`pty-ack` | 归该 frame 所有的交互式 PTY;输出以 `pty-data`、`pty-exit`、`pty-error` 返回。每个插件最多 8 个会话。 |
|
||||
| `open-log-tab` | 只读编辑器标签页,持续显示程序输出,可带参数开关。 |
|
||||
| `open-content-tab` | 内容由插件提供的编辑器标签页,带工具栏操作,用 `content-tab-reply` 回复。 |
|
||||
| `op` | 打开什么 |
|
||||
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| `pty-spawn`、`pty-input`、`pty-resize`、`pty-kill`、`pty-ack` | 归该 frame 所有的交互式 PTY;输出以 `pty-data`、`pty-exit`、`pty-error` 返回。每个插件最多 8 个会话。 |
|
||||
| `open-log-tab` | 只读编辑器标签页,持续显示程序输出,可带参数开关。 |
|
||||
| `open-content-tab` | 内容由插件提供的编辑器标签页,带工具栏操作,用 `content-tab-reply` 回复。 |
|
||||
| `open-diff` | API 1.7:在差异查看器中对比两段文本(`left`、`right`,可选标签、`title`,以及用于高亮的 `fileName`)。 |
|
||||
|
||||
消息字段和限制见 [插件指南](https://github.com/l0ng-ai/openidea/blob/main/plugins/README.md#web-frame-ptys-api-13);
|
||||
`crates/ide-plugin/tests/fixtures/` 下已构建的 `web-pty`、`web-log`、`web-content` 测试插件演示了这些协议。
|
||||
@@ -303,15 +304,16 @@ impl Details for MyPlugin {
|
||||
|
||||
以下函数需在窗口的命令或事件中调用:
|
||||
|
||||
| 函数 | 打开什么 |
|
||||
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `host::open_location(path, line, column)` | 在编辑器中打开项目文件并定位光标(`project.read`;插件私有目录中的文件也可以)。 |
|
||||
| `host::open_editor(path, selection)` | API 1.4:打开项目文件并选中一个范围。 |
|
||||
| `host::open_terminal(title, program, args)` | 在项目根目录运行已批准 `process:<program>` 的终端标签页。 |
|
||||
| `host::open_terminal_session(key, title, &cmd)` | API 1.4:带 key 的终端;再次使用同一 key 会聚焦它。用 `host::write_terminal(key, text)` 写入;退出时收到 `terminal-exited` 事件。 |
|
||||
| `host::open_log_tab(key, title, &log)` | API 1.3:基于 `ProcessLog` 的只读日志标签页。每个窗口最多 32 个;暂不支持远程项目。 |
|
||||
| `host::open_content_tab(&ContentTab { .. })` | API 1.4:不依赖网页视图的内容编辑器标签页;操作以 `content-tab-action` 事件送达,用 `host::reply_content_tab` 回复。 |
|
||||
| `host::output_append(channel, text)` | API 1.4:每个通道一个只读 **输出** 标签页(32 个通道,各 1 MiB);另有 `output_clear`、`output_reveal`。 |
|
||||
| 函数 | 打开什么 |
|
||||
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `host::open_location(path, line, column)` | 在编辑器中打开项目文件并定位光标(`project.read`;插件私有目录中的文件也可以)。 |
|
||||
| `host::open_editor(path, selection)` | API 1.4:打开项目文件并选中一个范围。 |
|
||||
| `host::open_terminal(title, program, args)` | 在项目根目录运行已批准 `process:<program>` 的终端标签页。 |
|
||||
| `host::open_terminal_session(key, title, &cmd)` | API 1.4:带 key 的终端;再次使用同一 key 会聚焦它。用 `host::write_terminal(key, text)` 写入;退出时收到 `terminal-exited` 事件。 |
|
||||
| `host::open_log_tab(key, title, &log)` | API 1.3:基于 `ProcessLog` 的只读日志标签页。每个窗口最多 32 个;暂不支持远程项目。 |
|
||||
| `host::open_content_tab(&ContentTab { .. })` | API 1.4:不依赖网页视图的内容编辑器标签页;操作以 `content-tab-action` 事件送达,用 `host::reply_content_tab` 回复。 |
|
||||
| `host::open_diff(&ContentDiff { .. }, file_name)` | API 1.7:在差异查看器中对比两段文本,例如评审中某个文件的修改前后;`file_name` 决定语法高亮。 |
|
||||
| `host::output_append(channel, text)` | API 1.4:每个通道一个只读 **输出** 标签页(32 个通道,各 1 MiB);另有 `output_clear`、`output_reveal`。 |
|
||||
|
||||
```rust
|
||||
use openidea_plugin::{ContentAction, ContentTab, host};
|
||||
|
||||
Loaded 100 of 227 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user