mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(maven): unlink modules of a reactor so jdtls stops importing them (#1059)
Only root Maven projects could be unlinked, and jdtls imports an aggregator with all of its modules whatever java.import.exclusions say. A module of a reactor can now be unlinked from the Maven tool window; when one is, jdtls gets initializationOptions.projectConfigurations listing the linked POMs except the aggregators above the unlinked module (plus the Gradle build file if there is one), and a Maven sync only names the POMs jdtls imported. Unlinking whole roots keeps using exclusions. Fixes #767
This commit is contained in:
@@ -1208,6 +1208,40 @@ impl UnlinkedProjects {
|
||||
self.poms.iter().map(String::as_str)
|
||||
}
|
||||
|
||||
/// The POMs the Java language server should import when an unlinked POM is a module of a
|
||||
/// linked reactor, else `None` (excluding the unlinked directories is enough then).
|
||||
///
|
||||
/// jdtls imports an aggregator together with all of its modules, whatever its import
|
||||
/// exclusions say, so a reactor keeping an unlinked module is left out; its other linked
|
||||
/// modules are listed on their own. Such an aggregator (`pom` packaging) has no sources.
|
||||
pub fn import_poms(&self, model: &MavenModel) -> Option<Vec<String>> {
|
||||
let linked: Vec<&MavenProject> = model.projects.iter().filter(|p| !self.contains(&p.pom)).collect();
|
||||
if !linked.iter().any(|p| p.modules.iter().any(|m| self.contains(m))) {
|
||||
return None;
|
||||
}
|
||||
let by_pom: std::collections::HashMap<String, &MavenProject> =
|
||||
model.projects.iter().map(|p| (normalized_pom(&p.pom), p)).collect();
|
||||
// Whether `pom`'s module tree reaches an unlinked POM.
|
||||
let reaches_unlinked = |pom: &str| {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut pending = vec![normalized_pom(pom)];
|
||||
while let Some(pom) = pending.pop() {
|
||||
if !seen.insert(pom.clone()) {
|
||||
continue;
|
||||
}
|
||||
let Some(project) = by_pom.get(&pom) else { continue };
|
||||
for module in &project.modules {
|
||||
if self.contains(module) {
|
||||
return true;
|
||||
}
|
||||
pending.push(normalized_pom(module));
|
||||
}
|
||||
}
|
||||
false
|
||||
};
|
||||
Some(linked.into_iter().filter(|p| !reaches_unlinked(&p.pom)).map(|p| normalized_pom(&p.pom)).collect())
|
||||
}
|
||||
|
||||
pub fn filter(&self, model: &mut MavenModel) {
|
||||
model.projects.retain(|p| !self.contains(&p.pom));
|
||||
for project in &mut model.projects {
|
||||
@@ -1993,4 +2027,51 @@ mod linkage_tests {
|
||||
UnlinkedProjects::from_model(&original, &MavenSettings::default()).filter(&mut restored);
|
||||
assert_eq!(restored, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_server_imports_reactor_modules_individually_once_one_is_unlinked() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
// r ─┬─ x
|
||||
// ├─ y ─┬─ y/y1
|
||||
// │ └─ y/y2
|
||||
// └─ z other/ is a separate root.
|
||||
for (dir, modules) in [
|
||||
("r", "<modules><module>x</module><module>y</module><module>z</module></modules>"),
|
||||
("r/x", ""),
|
||||
("r/y", "<modules><module>y1</module><module>y2</module></modules>"),
|
||||
("r/y/y1", ""),
|
||||
("r/y/y2", ""),
|
||||
("r/z", ""),
|
||||
("other", ""),
|
||||
] {
|
||||
std::fs::create_dir_all(root.join(dir)).unwrap();
|
||||
std::fs::write(
|
||||
root.join(dir).join("pom.xml"),
|
||||
format!("<project><artifactId>{dir}</artifactId>{modules}</project>"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let model = load_with_repo(root, None);
|
||||
let cases: [(&[&str], Option<&[&str]>); 5] = [
|
||||
// Nothing unlinked, or whole roots: the import exclusions do it.
|
||||
(&[], None),
|
||||
(&["other/pom.xml"], None),
|
||||
(&["r/pom.xml"], None),
|
||||
// A module of a linked reactor: every other linked POM, minus the aggregators above it.
|
||||
(&["r/y/y1/pom.xml"], Some(&["other/pom.xml", "r/x/pom.xml", "r/y/y2/pom.xml", "r/z/pom.xml"])),
|
||||
(&["./r/y/pom.xml"], Some(&["other/pom.xml", "r/x/pom.xml", "r/z/pom.xml"])),
|
||||
];
|
||||
for (unlinked, expected) in cases {
|
||||
let settings = MavenSettings {
|
||||
unlinked_projects: unlinked.iter().map(|s| s.to_string()).collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut poms = UnlinkedProjects::from_model(&model, &settings).import_poms(&model);
|
||||
if let Some(poms) = &mut poms {
|
||||
poms.sort();
|
||||
}
|
||||
assert_eq!(poms, expected.map(|e| e.iter().map(|s| s.to_string()).collect::<Vec<_>>()), "{unlinked:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,16 @@ pub async fn build_sync(
|
||||
}
|
||||
};
|
||||
let lsp = project.lsp.clone();
|
||||
let uris = build_files.iter().map(|f| ide_lsp::path_to_uri_string(&project.root.join(f))).collect();
|
||||
// With a module of a reactor unlinked, jdtls imports the other modules one by one and
|
||||
// not the reactor: naming the reactor here would import it again, all modules included.
|
||||
let imported = match tool {
|
||||
BuildTool::Maven => crate::project::jdtls_maven_import(&project.root, &project.settings.effective()).1,
|
||||
BuildTool::Gradle => None,
|
||||
};
|
||||
let uris = match imported {
|
||||
Some(files) => files.into_iter().filter(|f| f.ends_with("/pom.xml")).collect(),
|
||||
None => build_files.iter().map(|f| ide_lsp::path_to_uri_string(&project.root.join(f))).collect(),
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
if profiles_changed {
|
||||
// jdtls takes the profiles from its Maven settings (see `ide_sdk::maven_profiles`).
|
||||
|
||||
@@ -218,8 +218,10 @@ impl Host {
|
||||
ide_sdk::jdtls::launch_directly(cfg);
|
||||
}
|
||||
ide_sdk::apply::server_protocol_overrides(cfg, &effective);
|
||||
let excluded_poms = unlinked_maven_poms(root, &effective);
|
||||
ide_sdk::apply::configure_maven_import(cfg, root, &excluded_poms);
|
||||
if cfg.name == "jdtls" {
|
||||
let (excluded_poms, build_files) = jdtls_maven_import(root, &effective);
|
||||
ide_sdk::apply::configure_maven_import(cfg, root, &excluded_poms, build_files.as_deref());
|
||||
}
|
||||
}));
|
||||
}
|
||||
{
|
||||
@@ -1035,6 +1037,31 @@ pub(crate) fn unlinked_maven_poms(root: &Path, settings: &ide_core::settings::Se
|
||||
ide_build::maven::UnlinkedProjects::load(root, &settings.languages.java.maven).poms().map(PathBuf::from).collect()
|
||||
}
|
||||
|
||||
/// The unlinked Maven POMs jdtls leaves out, and the build files (`file:` URIs) it is to
|
||||
/// import when that takes more than exclusions: an unlinked module of a linked reactor.
|
||||
/// Those then include the Gradle build, which jdtls would otherwise find by itself.
|
||||
pub(crate) fn jdtls_maven_import(
|
||||
root: &Path,
|
||||
settings: &ide_core::settings::Settings,
|
||||
) -> (Vec<PathBuf>, Option<Vec<String>>) {
|
||||
let maven = &settings.languages.java.maven;
|
||||
if maven.unlinked_projects.is_empty() {
|
||||
return (Vec::new(), None);
|
||||
}
|
||||
let model = ide_build::maven::load(root);
|
||||
let unlinked = ide_build::maven::UnlinkedProjects::from_model(&model, maven);
|
||||
let excluded = unlinked.poms().map(PathBuf::from).collect();
|
||||
let build_files = unlinked.import_poms(&model).map(|poms| {
|
||||
let gradle = ide_build::gradle::load(root).and_then(|g| {
|
||||
let dir = root.join(&g.dir);
|
||||
let names = ide_build::gradle::SETTINGS_FILES.iter().chain(ide_build::gradle::BUILD_FILES);
|
||||
names.map(|n| dir.join(n)).find(|f| f.is_file())
|
||||
});
|
||||
poms.iter().map(|pom| root.join(pom)).chain(gradle).map(|f| ide_lsp::path_to_uri_string(&f)).collect()
|
||||
});
|
||||
(excluded, build_files)
|
||||
}
|
||||
|
||||
/// The detected roots with the last Gradle sync's source sets first: Gradle evaluated what
|
||||
/// static detection guesses. Folders outside the project keep their absolute path, which
|
||||
/// `normalize` turns into the same form the UI compares (`Users/me/svc/src/main/java`).
|
||||
@@ -1212,6 +1239,27 @@ pub fn apply_git_executable(settings: &ide_core::settings::Settings) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod maven_linkage_tests {
|
||||
#[test]
|
||||
fn jdtls_imports_the_linked_modules_and_the_gradle_build_once_a_reactor_module_is_unlinked() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
for (dir, body) in [("", "<modules><module>x</module><module>y</module></modules>"), ("x", ""), ("y", "")] {
|
||||
std::fs::create_dir_all(root.join(dir)).unwrap();
|
||||
std::fs::write(root.join(dir).join("pom.xml"), format!("<project>{body}</project>")).unwrap();
|
||||
}
|
||||
std::fs::write(root.join("settings.gradle"), "rootProject.name = 'g'").unwrap();
|
||||
let mut settings = ide_core::settings::Settings::default();
|
||||
assert_eq!(super::jdtls_maven_import(root, &settings), (Vec::new(), None));
|
||||
settings.languages.java.maven.unlinked_projects.push("y/pom.xml".into());
|
||||
let (excluded, files) = super::jdtls_maven_import(root, &settings);
|
||||
assert_eq!(excluded, vec![std::path::PathBuf::from("y/pom.xml")]);
|
||||
let uri = |f: &str| ide_lsp::path_to_uri_string(&root.join(f));
|
||||
assert_eq!(files, Some(vec![uri("x/pom.xml"), uri("settings.gradle")]));
|
||||
// The whole reactor unlinked: the import exclusions are enough.
|
||||
settings.languages.java.maven.unlinked_projects = vec!["pom.xml".into()];
|
||||
assert_eq!(super::jdtls_maven_import(root, &settings).1, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlinked_roots_remain_plain_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -1240,7 +1288,7 @@ mod maven_linkage_tests {
|
||||
serde_json::json!({"name": "jdtls", "command": "jdtls"}),
|
||||
)
|
||||
.unwrap();
|
||||
ide_sdk::apply::configure_maven_import(&mut server, tmp.path(), &excluded_poms);
|
||||
ide_sdk::apply::configure_maven_import(&mut server, tmp.path(), &excluded_poms, None);
|
||||
let options = serde_json::to_value(server.initialization_options).unwrap();
|
||||
let exclusions = options.pointer("/settings/java/import/exclusions").unwrap().as_array().unwrap();
|
||||
assert!(exclusions.contains(&serde_json::json!(tmp.path().join("a").to_string_lossy().replace('\\', "/"))));
|
||||
|
||||
@@ -236,7 +236,14 @@ fn merge(base: &mut Value, layer: Value) {
|
||||
}
|
||||
|
||||
/// Excludes project-relative Maven POM paths from JDTLS import, preserving user exclusions.
|
||||
pub fn configure_maven_import(cfg: &mut LanguageServerConfig, root: &Path, excluded_poms: &[PathBuf]) {
|
||||
/// `build_files` (`file:` URIs), when given, are the only build files jdtls imports: the
|
||||
/// exclusions cannot leave out a module of a reactor jdtls imports.
|
||||
pub fn configure_maven_import(
|
||||
cfg: &mut LanguageServerConfig,
|
||||
root: &Path,
|
||||
excluded_poms: &[PathBuf],
|
||||
build_files: Option<&[String]>,
|
||||
) {
|
||||
if cfg.name != "jdtls" {
|
||||
return;
|
||||
}
|
||||
@@ -268,6 +275,9 @@ pub fn configure_maven_import(cfg: &mut LanguageServerConfig, root: &Path, exclu
|
||||
}
|
||||
if !excluded_poms.is_empty() {
|
||||
merge_init_options(cfg, json!({"settings": {"java": {"import": {"exclusions": exclusions}}}}));
|
||||
if let Some(files) = build_files {
|
||||
merge_init_options(cfg, json!({"projectConfigurations": files}));
|
||||
}
|
||||
if let Some(settings) = &mut cfg.settings {
|
||||
merge(
|
||||
settings,
|
||||
@@ -948,12 +958,27 @@ mod maven_linkage_tests {
|
||||
let mut cfg = custom_server("jdtls", &LanguageServerSettings::default());
|
||||
cfg.args = vec!["-data".into(), "/cache/jdtls".into()];
|
||||
merge_init_options(&mut cfg, json!({"settings":{"java":{"import":{"exclusions":["**/custom"]}}}}));
|
||||
configure_maven_import(&mut cfg, tmp.path(), &["a/child/pom.xml".into(), "a/pom.xml".into()]);
|
||||
configure_maven_import(&mut cfg, tmp.path(), &["a/child/pom.xml".into(), "a/pom.xml".into()], None);
|
||||
let options = serde_json::to_value(cfg.initialization_options).unwrap();
|
||||
let exclusions = options.pointer("/settings/java/import/exclusions").unwrap().as_array().unwrap();
|
||||
assert!(exclusions.contains(&json!("**/custom")));
|
||||
assert!(exclusions.contains(&json!(tmp.path().join("a").to_string_lossy().replace('\\', "/"))));
|
||||
assert!(exclusions.contains(&json!(tmp.path().join("a/child").to_string_lossy().replace('\\', "/"))));
|
||||
assert!(cfg.args[1].starts_with("/cache/jdtls-maven-"));
|
||||
assert!(options.get("projectConfigurations").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_lists_the_build_files_when_given() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let mut cfg = custom_server("jdtls", &LanguageServerSettings::default());
|
||||
let files = ["file:///p/r/x/pom.xml".to_owned(), "file:///p/build.gradle".to_owned()];
|
||||
configure_maven_import(&mut cfg, tmp.path(), &["r/y/pom.xml".into()], Some(&files));
|
||||
let options = serde_json::to_value(cfg.initialization_options).unwrap();
|
||||
assert_eq!(options["projectConfigurations"], json!(files));
|
||||
// Nothing unlinked: jdtls finds the build files itself.
|
||||
let mut cfg = custom_server("jdtls", &LanguageServerSettings::default());
|
||||
configure_maven_import(&mut cfg, tmp.path(), &[], Some(&files));
|
||||
assert!(serde_json::to_value(cfg.initialization_options).unwrap().get("projectConfigurations").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,8 +306,7 @@ function BuildToolView({ tool }: { tool: BuildTool }) {
|
||||
else items.push({ label: "Create Run Configuration…", icon: "edit", run: () => void createRunConfig(config) });
|
||||
}
|
||||
if (node.kind === "mavenProject") {
|
||||
if (!node.project.aggregated)
|
||||
items.push({ kind: "action", action: "maven.unlinkProjects", arg: { pom: node.project.pom } });
|
||||
items.push({ kind: "action", action: "maven.unlinkProjects", arg: { pom: node.project.pom } });
|
||||
items.push(
|
||||
{
|
||||
label: "Open pom.xml",
|
||||
|
||||
@@ -34,7 +34,10 @@ vi.mock("@/platform/ipc", async (original) => {
|
||||
});
|
||||
vi.mock("@tauri-apps/plugin-dialog", () => ({ open: mocks.open }));
|
||||
vi.mock("@/workbench/messages", () => ({ confirm: mocks.confirm }));
|
||||
vi.mock("@/ui", async (original) => ({ ...(await original<typeof import("@/ui")>()), showContextMenu: mocks.menu }));
|
||||
vi.mock("@/ui", async (original) => ({
|
||||
...(await original<typeof import("@/ui")>()),
|
||||
showContextMenu: mocks.menu,
|
||||
}));
|
||||
|
||||
const model: BuildModel = {
|
||||
gradle: null,
|
||||
@@ -69,7 +72,9 @@ const model: BuildModel = {
|
||||
let dispose: () => void;
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
mocks.get.mockResolvedValue({ languages: { java: { maven: { unlinkedProjects: [] } } } });
|
||||
mocks.get.mockResolvedValue({
|
||||
languages: { java: { maven: { unlinkedProjects: [] } } },
|
||||
});
|
||||
mocks.set.mockResolvedValue({});
|
||||
mocks.model.mockResolvedValue(model);
|
||||
mocks.configs.mockResolvedValue([]);
|
||||
@@ -91,7 +96,12 @@ it("offers unlink on a root's context menu and persists a confirmed project sett
|
||||
fireEvent.contextMenu(screen.getByText("project-a"));
|
||||
expect(mocks.menu).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([expect.objectContaining({ action: "maven.unlinkProjects", arg: { pom: "a/pom.xml" } })]),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
action: "maven.unlinkProjects",
|
||||
arg: { pom: "a/pom.xml" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
await act(async () => {
|
||||
await executeAction("maven.unlinkProjects", { pom: "a/pom.xml" });
|
||||
@@ -101,24 +111,83 @@ it("offers unlink on a root's context menu and persists a confirmed project sett
|
||||
expect(mocks.model).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not unlink modules or write after cancelled confirmation", async () => {
|
||||
it("does not write after cancelled confirmation", async () => {
|
||||
mocks.confirm.mockResolvedValue(false);
|
||||
await executeAction("maven.unlinkProjects", { pom: "a/pom.xml" });
|
||||
expect(mocks.confirm).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.set).not.toHaveBeenCalled();
|
||||
act(() =>
|
||||
useBuild.setState({
|
||||
model: { ...model, maven: { ...model.maven!, projects: [{ ...model.maven!.projects[0]!, aggregated: true }] } },
|
||||
});
|
||||
|
||||
it("unlinks a module of a linked reactor so only the modules in use are imported", async () => {
|
||||
const root = model.maven!.projects[0]!;
|
||||
const child = {
|
||||
...root,
|
||||
pom: "a/child/pom.xml",
|
||||
dir: "a/child",
|
||||
artifactId: "child",
|
||||
displayName: "child-module",
|
||||
packaging: "jar",
|
||||
parent: "a/pom.xml",
|
||||
aggregated: true,
|
||||
};
|
||||
const reactor: BuildModel = {
|
||||
...model,
|
||||
maven: {
|
||||
...model.maven!,
|
||||
projects: [{ ...root, modules: ["a/child/pom.xml"] }, child],
|
||||
},
|
||||
};
|
||||
mocks.model.mockResolvedValue(reactor);
|
||||
mocks.get.mockResolvedValue({
|
||||
languages: { java: { maven: { unlinkedProjects: ["b/pom.xml"] } } },
|
||||
});
|
||||
await act(async () => {
|
||||
useBuild.setState({ model: reactor });
|
||||
});
|
||||
render(<MavenView />);
|
||||
fireEvent.contextMenu(await screen.findByText("child-module"));
|
||||
expect(mocks.menu).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
action: "maven.unlinkProjects",
|
||||
arg: { pom: "a/child/pom.xml" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
await act(async () => {
|
||||
await executeAction("maven.unlinkProjects", { pom: "a/child/pom.xml" });
|
||||
});
|
||||
expect(mocks.confirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("Java language server stops importing them"),
|
||||
}),
|
||||
);
|
||||
render(<MavenView />);
|
||||
await executeAction("maven.unlinkProjects", { pom: "a/pom.xml" });
|
||||
expect(mocks.confirm).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.set).toHaveBeenCalledWith(
|
||||
"project",
|
||||
"languages.java.maven.unlinkedProjects",
|
||||
'["b/pom.xml","a/child/pom.xml"]',
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores a POM that is not in the Maven model", async () => {
|
||||
await executeAction("maven.unlinkProjects", { pom: "missing/pom.xml" });
|
||||
expect(mocks.confirm).not.toHaveBeenCalled();
|
||||
expect(mocks.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("re-adds a picked POM using the plus button and preserves other unlinked roots", async () => {
|
||||
mocks.get.mockResolvedValue({ languages: { java: { maven: { unlinkedProjects: ["a/pom.xml", "b/pom.xml"] } } } });
|
||||
mocks.get.mockResolvedValue({
|
||||
languages: {
|
||||
java: { maven: { unlinkedProjects: ["a/pom.xml", "b/pom.xml"] } },
|
||||
},
|
||||
});
|
||||
mocks.open.mockResolvedValue("/p/a/pom.xml");
|
||||
act(() => useBuild.setState({ model: { ...model, maven: { ...model.maven!, projects: [] } } }));
|
||||
act(() =>
|
||||
useBuild.setState({
|
||||
model: { ...model, maven: { ...model.maven!, projects: [] } },
|
||||
}),
|
||||
);
|
||||
render(<MavenView />);
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add Maven Projects" }));
|
||||
|
||||
@@ -255,11 +255,14 @@ async function setLinked(root: string, pom: string, linked: boolean) {
|
||||
|
||||
export async function unlinkProjects(pom: string | null) {
|
||||
const { root, model } = useBuild.getState();
|
||||
if (!root || !pom || !model?.maven?.projects.some((p) => p.pom === pom && !p.aggregated)) return;
|
||||
const project = model?.maven?.projects.find((p) => p.pom === pom);
|
||||
if (!root || !pom || !project) return;
|
||||
if (
|
||||
!(await confirm({
|
||||
title: "Unlink Maven projects?",
|
||||
message: "Remove this project and its modules from Maven? Files will remain in the Project view.",
|
||||
message: project.aggregated
|
||||
? "Remove this module and its submodules from Maven? The Java language server stops importing them, so they no longer use memory. Files will remain in the Project view; add the pom.xml again to restore them."
|
||||
: "Remove this project and its modules from Maven? Files will remain in the Project view.",
|
||||
ok: "Unlink",
|
||||
}))
|
||||
)
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
"Unlink Maven Projects": "取消链接 Maven 项目",
|
||||
"Unlink Maven projects?": "取消链接 Maven 项目?",
|
||||
"Remove this project and its modules from Maven? Files will remain in the Project view.": "从 Maven 中移除此项目及其模块?文件将保留在项目视图中。",
|
||||
"Remove this module and its submodules from Maven? The Java language server stops importing them, so they no longer use memory. Files will remain in the Project view; add the pom.xml again to restore them.": "从 Maven 中移除此模块及其子模块?Java 语言服务器将不再导入它们,也就不再为它们占用内存。文件将保留在项目视图中;重新添加其 pom.xml 即可恢复。",
|
||||
"Unlink": "取消链接",
|
||||
"(does not exist)": "(不存在)",
|
||||
", e.g.": ",例如",
|
||||
|
||||
Reference in New Issue
Block a user