Порівняти коміти

..
Автор SHA1 Повідомлення Дата
l0ng-ai ae14a61eec fix(python): exclude virtual environments from detected roots whatever their name
Root detection only skipped environments named `venv` / `.venv`. A venv with another
name in the project (e.g. `code10`) was walked into, and any package in its
site-packages that ships `pyproject.toml`, `setup.py` or `requirements.txt` became a
source root. Source roots go on PYTHONPATH for runs, so a package directory with an
`io` package shadowed the standard library and Python 3.10 failed at startup with
`init_sys_streams: module 'io' has no attribute 'open'`.

A directory with `pyvenv.cfg` (venv, virtualenv, uv) or `conda-meta` (conda) is now
an excluded root and not searched for modules; creating one re-runs detection.
Project discovery in the workspace folder picker skips such directories too, so
packages inside an environment are not offered as projects.
2026-11-13 12:21:07 +08:00
l0ng-ai c905f1428e fix(maven): go from a dependency, plugin or parent to its own POM (#3057)
Go to Declaration in pom.xml found nothing on a dependency that names its own
version, on a <dependencyManagement> entry (a BOM import, say) or on <parent>.
They now open the artifact's POM: the project module with those coordinates, else
the local repository's POM of that version (properties resolved). A dependency
whose version a management entry provides still goes to that entry.
2026-11-13 12:10:46 +08:00
l0ng-ai a8036ce6fa feat(plugins): open two texts in the diff viewer (API 1.7) (#3055)
Plugins could show their own content in an editor tab (open-content-tab) but
could open the diff viewer only as the reply to one of that tab's actions. A
review plugin (a merge request's files, say) had no way to show a file's
before and after.

views.open-diff opens two texts in the invoking window's diff viewer, with
optional labels and title and a file name that picks the syntax highlighting;
web views send { op: "open-diff", ... }. The SDK wraps it as host::open_diff
with a mock. The plugin API moves to 1.7.0 (1.6.0 is released) and the checked-in
components are rebuilt.
2026-11-13 12:02:36 +08:00
8 змінених файлів з 237 додано та 22 видалено

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!(
+15 -2
Переглянути файл
@@ -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)?
+16 -1
Переглянути файл
@@ -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();
+53 -1
Переглянути файл
@@ -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.
+1 -1
Переглянути файл
@@ -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;
+3 -1
Переглянути файл
@@ -8113,7 +8113,9 @@ export type PomGotoKind =
/** A `<dependencyManagement>` entry providing the dependency's version. */
"managedDependency" |
/** A `<pluginManagement>` entry providing the plugin's version. */
"managedPlugin";
"managedPlugin" |
/** The POM of the artifact itself: a project module's, else the local repository's. */
"artifactPom";
/** A 0-based line / UTF-16 column position. */
export type Pos = {
+3 -1
Переглянути файл
@@ -108,9 +108,11 @@ describe("POM artifact elements", () => {
expect(inArtifactElement(pom, pom.indexOf("example-lib</artifactId>"))).toBe(true);
expect(inArtifactElement(pom, pom.indexOf("<artifactId>maven-compiler-plugin"))).toBe(true);
expect(inArtifactElement(pom, pom.indexOf("${example.version}"))).toBe(true);
// Inside `<parent>`: its artifact is the parent POM.
expect(inArtifactElement(pom, pom.indexOf("<parent>") + "<parent>".length + 1)).toBe(true);
});
it("sees no artifact element outside dependencies and plugins", () => {
it("sees no artifact element outside dependencies, plugins and the parent", () => {
expect(inArtifactElement(pom, pom.indexOf("<properties>"))).toBe(false);
expect(inArtifactElement(pom, pom.indexOf("<parent>"))).toBe(false);
// `<plugins>` and `<dependencyManagement>` are not a `<plugin>` / `<dependency>`.
+6 -5
Переглянути файл
@@ -4,7 +4,8 @@
* along the parent chain (a parent POM outside the project opens from the local
* repository, read-only as a plain file) — and a dependency / plugin whose version a
* `<dependencyManagement>` / `<pluginManagement>` entry provides opens that managing
* declaration. The backend resolves both with the build model's POM reader and parent
* declaration; otherwise a dependency, plugin or `<parent>` opens the artifact's own POM
* (a project module's, else the local repository's). The backend resolves both with the build model's POM reader and parent
* resolver; here only cheap eligibility is computed (a reference span, an artifact
* element), so the ⌘-hover underline stays out of the rest of the file, and nothing
* resolves to no jump at all: an unresolvable property or parent never navigates
@@ -34,12 +35,12 @@ function inside(name: string, text: string, at: number): boolean {
}
/**
* Whether `at` may sit in a `<dependency>` / `<plugin>` element: an eligibility test
* only (a `<dependency>` inside a comment counts too) — whether the element's version
* actually comes from a management entry is the backend's answer.
* Whether `at` may sit in a `<dependency>` / `<plugin>` / `<parent>` element: an
* eligibility test only (a `<dependency>` inside a comment counts too) — what the
* element resolves to is the backend's answer.
*/
export function inArtifactElement(text: string, at: number): boolean {
return inside("dependency", text, at) || inside("plugin", text, at);
return inside("dependency", text, at) || inside("plugin", text, at) || inside("parent", text, at);
}
/** Go to declaration in a POM of the project (or an opened parent POM of the local repository). */