mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(editor): say when a file is outside the language server's projects (#1046)
jdtls only checks the syntax of a Java file outside every module's source folders (or outside any imported project) and answers navigation with nothing. The editor then said only "Cannot find declaration to go to", and the server's explanation was a warning squiggle on line 1. - A banner above the editor explains it while the server reports it, for jdtls (not on the classpath of a project / non-project file), gopls (no package, build constraints — this replaces the Go-only banner) and rust-analyzer (unlinked file). For Java it offers the Maven / Gradle reload when available. - Go to Declaration / Implementation / Type Declaration that finds nothing in such a file gives the reason in its hint. Fixes #827
This commit is contained in:
@@ -7,7 +7,12 @@
|
||||
*/
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { registerEditorContribution, type EditorDocContext } from "@/editor/api";
|
||||
import { registerOverlay, registerStatusItem, registerToolWindow } from "@/workbench/contributions";
|
||||
import {
|
||||
registerEditorBanner,
|
||||
registerOverlay,
|
||||
registerStatusItem,
|
||||
registerToolWindow,
|
||||
} from "@/workbench/contributions";
|
||||
import { lspDoc } from "./lsp";
|
||||
import { hintExtension } from "./hint";
|
||||
import { completionExtension } from "./completion";
|
||||
@@ -31,6 +36,7 @@ import { spellingExtension } from "./spelling";
|
||||
import { DocumentationWindow, LspPopups, UsagesStatus } from "./Popups";
|
||||
import { DOCUMENTATION_WINDOW } from "./docs";
|
||||
import { prefetchMarkdown } from "./markdown";
|
||||
import { outsideProjectBanner } from "./outsideProject";
|
||||
import { implementationGutter, registerNavigationActions } from "@/editor/navigation";
|
||||
|
||||
export function lspExtensions(ctx: EditorDocContext): Extension[] {
|
||||
@@ -66,6 +72,7 @@ export function activate(): () => void {
|
||||
// After the templates (order 90): live template / postfix expansion keeps ⇥ first.
|
||||
registerEditorContribution({ id: "lsp.parameterTab", order: 95, extensions: () => parameterTabKeys }),
|
||||
registerOverlay({ id: "editor-lsp.popups", component: LspPopups }),
|
||||
registerEditorBanner(outsideProjectBanner),
|
||||
registerStatusItem({ id: "editor-lsp.usagesStatus", align: "left", priority: 10, component: UsagesStatus }),
|
||||
// IDEA's Documentation tool window: right side, listed once F1 moved documentation there.
|
||||
registerToolWindow({
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
type DeclarationTarget,
|
||||
} from "./syntaxFallback";
|
||||
import { showHint, showPendingHint } from "./hint";
|
||||
import { outsideProjectReason } from "./outsideProject";
|
||||
import { caretAnchor, openPopup, type ListPopupItem } from "./popupStore";
|
||||
import { readManyFileLines, targetLabel } from "./files";
|
||||
import { documentLinks, followReference, knownLinks, linkReference, referenceAt, textReference } from "./links";
|
||||
@@ -198,7 +199,16 @@ export async function gotoTargets(view: EditorView, kind: GotoKind, at?: number)
|
||||
((await gotoWebReference(view, doc, pos, offset)) || (await followReferenceAt(view, doc, offset)))
|
||||
)
|
||||
return;
|
||||
showHint(view, serverDown ? `${NOT_FOUND[kind]}: no language server is running for this file` : NOT_FOUND[kind]);
|
||||
// The server only checks this file's syntax: say why instead of just "not found".
|
||||
const reason = serverDown ? null : outsideProjectReason(doc.path);
|
||||
showHint(
|
||||
view,
|
||||
serverDown
|
||||
? `${NOT_FOUND[kind]}: no language server is running for this file`
|
||||
: reason
|
||||
? `${tx(NOT_FOUND[kind])}. ${tx(reason)}`
|
||||
: NOT_FOUND[kind],
|
||||
);
|
||||
return;
|
||||
}
|
||||
// IDEA: ⌘B on a declaration shows its usages.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { outsideProjectNotice } from "./outsideProject";
|
||||
|
||||
const text = (message: string) => outsideProjectNotice(message)?.text ?? null;
|
||||
|
||||
it("recognizes each server's report that a file is outside its projects", () => {
|
||||
const cases: [string, RegExp | null][] = [
|
||||
// jdtls: a file outside the module's source folders, and one outside every project.
|
||||
[
|
||||
"Extra.java is not on the classpath of project service, only syntax errors are reported",
|
||||
/source folders of its Java module/,
|
||||
],
|
||||
["Scratch.java is a non-project file, only syntax errors are reported", /imported Java project/],
|
||||
// gopls: build constraints win over the generic "no package".
|
||||
["No packages found for open file x.go.\nThis file may be excluded due to its build tags", /build constraints/],
|
||||
["This file may be excluded due to its GOOS/GOARCH, or other build constraints.", /build constraints/],
|
||||
["No packages found for open file x.go.", /no package of the workspace/],
|
||||
// rust-analyzer `unlinked-file`.
|
||||
[
|
||||
"This file is not included in any crates, so rust-analyzer can't offer IDE services.",
|
||||
/not included in any crate/,
|
||||
],
|
||||
// Ordinary errors are not such reports.
|
||||
["undefined: missingSymbol", null],
|
||||
["invalid build tags syntax", null],
|
||||
["ActionDTO cannot be resolved to a type", null],
|
||||
["Missing artifact demo:api:jar:1.0", null],
|
||||
];
|
||||
for (const [message, expected] of cases) {
|
||||
const got = text(message);
|
||||
if (expected) expect(got, message).toMatch(expected);
|
||||
else expect(got, message).toBeNull();
|
||||
}
|
||||
// A build reload is offered only where it can bring the file into a project.
|
||||
expect(outsideProjectNotice("A.java is a non-project file, only syntax errors are reported")?.reload).toBe(true);
|
||||
expect(outsideProjectNotice("No packages found for open file x.go.")?.reload).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows a banner while the server reports the file, with the build reloads available", async () => {
|
||||
const { renderHook, act } = await import("@testing-library/react");
|
||||
const { setFileDiagnostics } = await import("./diagnostics");
|
||||
const { registerActions } = await import("@/platform/actions");
|
||||
const { outsideProjectBanner } = await import("./outsideProject");
|
||||
const off = registerActions([{ id: "maven.reload", title: "Reload All Maven Projects", run: () => {} }]);
|
||||
const path = "/p/service/src/extra/Extra.java";
|
||||
try {
|
||||
const { result } = renderHook(() => outsideProjectBanner.useBanner(path));
|
||||
expect(result.current).toBeNull();
|
||||
const warning = {
|
||||
range: { start: { line: 0, col: 0 }, end: { line: 0, col: 1 } },
|
||||
severity: "warning" as const,
|
||||
message: "Extra.java is not on the classpath of project service, only syntax errors are reported",
|
||||
source: "Java",
|
||||
code: "32",
|
||||
};
|
||||
act(() => setFileDiagnostics(path, "jdtls", [warning]));
|
||||
expect(result.current?.level).toBe("warning");
|
||||
expect(result.current?.text).toMatch(/source folders of its Java module/);
|
||||
expect(result.current?.actions?.map((a) => a.label)).toEqual(["Reload Maven Projects"]);
|
||||
act(() => setFileDiagnostics(path, "jdtls", []));
|
||||
expect(result.current).toBeNull();
|
||||
} finally {
|
||||
off();
|
||||
setFileDiagnostics(path, "jdtls", []);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Files a language server only checks for syntax because they belong to none of
|
||||
* its projects: a Java file outside every module's source folders, a Rust file in
|
||||
* no crate, a Go file in no package of the workspace. The server says so in a
|
||||
* diagnostic; navigation, completion and usages then fail without saying why.
|
||||
*/
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { executeAction, getAction, isActionEnabled } from "@/platform/actions";
|
||||
import type { EditorBannerContribution } from "@/workbench/contributions";
|
||||
import { diagnosticsFor, subscribeDiagnostics } from "./diagnostics";
|
||||
|
||||
/** `reload`: a build reload may bring the file into a project (a module the import missed). */
|
||||
const NOTICES: readonly { test: RegExp; text: string; reload?: boolean }[] = [
|
||||
// jdtls: "Foo.java is not on the classpath of project app, only syntax errors are reported"
|
||||
{
|
||||
test: /is not on the classpath of project .*only syntax errors are reported/,
|
||||
text: "This file is outside the source folders of its Java module: only syntax errors are reported, and navigation and completion don't work.",
|
||||
reload: true,
|
||||
},
|
||||
// jdtls: "Foo.java is a non-project file, only syntax errors are reported"
|
||||
{
|
||||
test: /is a non-project file, only syntax errors are reported/,
|
||||
text: "This file does not belong to an imported Java project: only syntax errors are reported, and navigation and completion don't work.",
|
||||
reload: true,
|
||||
},
|
||||
// gopls, before its generic "No packages found" below.
|
||||
{
|
||||
test: /excluded[^\n]*(?:build|GOOS\/GOARCH)/i,
|
||||
text: "This Go file is excluded by the current build constraints.",
|
||||
},
|
||||
{
|
||||
test: /^No packages found for open file/,
|
||||
text: "This Go file belongs to no package of the workspace: code insight is limited.",
|
||||
},
|
||||
// rust-analyzer's `unlinked-file`.
|
||||
{
|
||||
test: /not included in any crates/,
|
||||
text: "This Rust file is not included in any crate: rust-analyzer can't offer code insight for it.",
|
||||
},
|
||||
];
|
||||
|
||||
type Notice = (typeof NOTICES)[number];
|
||||
|
||||
/** What a server's `message` means for its file, when it says the file is outside its projects. */
|
||||
export function outsideProjectNotice(message: string): Notice | null {
|
||||
return NOTICES.find((n) => n.test.test(message)) ?? null;
|
||||
}
|
||||
|
||||
function noticeFor(path: string): Notice | null {
|
||||
for (const d of diagnosticsFor(path)) {
|
||||
const notice = outsideProjectNotice(d.message);
|
||||
if (notice) return notice;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Why code insight is limited in `path`, from its current diagnostics, or null. */
|
||||
export function outsideProjectReason(path: string): string | null {
|
||||
return noticeFor(path)?.text ?? null;
|
||||
}
|
||||
|
||||
const RELOADS = [
|
||||
{ id: "maven.reload", label: "Reload Maven Projects" },
|
||||
{ id: "gradle.reload", label: "Reload Gradle Project" },
|
||||
];
|
||||
|
||||
/** The notice above an editor whose file a language server treats as outside its projects. */
|
||||
export const outsideProjectBanner: EditorBannerContribution = {
|
||||
id: "lsp.outsideProject",
|
||||
order: 5,
|
||||
useBanner: (path) => {
|
||||
const notice = useSyncExternalStore(
|
||||
(l) => subscribeDiagnostics(path, l),
|
||||
() => noticeFor(path),
|
||||
);
|
||||
if (!notice) return null;
|
||||
const actions = notice.reload
|
||||
? RELOADS.filter(({ id }) => {
|
||||
const action = getAction(id);
|
||||
return action && isActionEnabled(action);
|
||||
}).map(({ id, label }) => ({ label, run: () => void executeAction(id) }))
|
||||
: [];
|
||||
return { text: notice.text, level: "warning", actions };
|
||||
},
|
||||
};
|
||||
@@ -303,6 +303,29 @@ describe("navigation wiring", () => {
|
||||
expect(document.querySelector(".cm-lsp-hint")?.textContent).toBe("Cannot find declaration to go to");
|
||||
});
|
||||
|
||||
it("says why when the server only checks the file's syntax", async () => {
|
||||
const { setFileDiagnostics } = await import("./diagnostics");
|
||||
setFileDiagnostics("/p/a.ts", "jdtls", [
|
||||
{
|
||||
range: { start: { line: 0, col: 0 }, end: { line: 0, col: 1 } },
|
||||
severity: "warning",
|
||||
message: "a.ts is not on the classpath of project app, only syntax errors are reported",
|
||||
source: "Java",
|
||||
code: "32",
|
||||
},
|
||||
]);
|
||||
try {
|
||||
cmd.lspGoto.mockReturnValueOnce(ok([]));
|
||||
const view = editor("const x = 1;\nx;", "typescript", 13);
|
||||
await gotoTargets(view, "definition");
|
||||
expect(document.querySelector(".cm-lsp-hint")?.textContent).toBe(
|
||||
"Cannot find declaration to go to. This file is outside the source folders of its Java module: only syntax errors are reported, and navigation and completion don't work.",
|
||||
);
|
||||
} finally {
|
||||
setFileDiagnostics("/p/a.ts", "jdtls", []);
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to the local definition when the server is not ready", async () => {
|
||||
cmd.syntaxLocals.mockReturnValueOnce(ok(local({ references: [r(1, 0, 1)] })));
|
||||
const view = editor("const x = 1;\nx;", "typescript", 13);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { goConstraintExcluded } from "./goConstraints";
|
||||
|
||||
it("recognizes build exclusion without confusing unrelated package errors", () => {
|
||||
expect(
|
||||
goConstraintExcluded("No packages found for open file x.go.\nThis file may be excluded due to its build tags"),
|
||||
).toBe(true);
|
||||
expect(goConstraintExcluded("This file may be excluded due to its GOOS/GOARCH, or other build constraints.")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(goConstraintExcluded("No packages found for open file x.go.")).toBe(false);
|
||||
expect(goConstraintExcluded("undefined: missingSymbol")).toBe(false);
|
||||
expect(goConstraintExcluded("invalid build tags syntax")).toBe(false);
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
/** Only gopls diagnostics that explicitly identify build exclusion qualify. */
|
||||
export function goConstraintExcluded(message: string): boolean {
|
||||
return /excluded[^\n]*(?:build|GOOS\/GOARCH)/i.test(message);
|
||||
}
|
||||
@@ -5,12 +5,7 @@
|
||||
*/
|
||||
import { registerActions } from "@/platform/actions";
|
||||
import { useProject } from "@/platform/project";
|
||||
import {
|
||||
registerEditorBanner,
|
||||
registerOverlay,
|
||||
registerStatusItem,
|
||||
registerToolWindow,
|
||||
} from "@/workbench/contributions";
|
||||
import { registerOverlay, registerStatusItem, registerToolWindow } from "@/workbench/contributions";
|
||||
import { clearProblems, installProblemsEvents, useProblems } from "./problemsStore";
|
||||
import { installProblemDecorations } from "./decorations";
|
||||
import { ProblemsHeaderActions, ProblemsTabs, ProblemsView, useProblemsHeaderMenu } from "./ProblemsView";
|
||||
@@ -20,7 +15,6 @@ import { findUsages, rerunActiveUsages, resetUsages, showUsageResults, showUsage
|
||||
import { HierarchyTabs, HierarchyView } from "./HierarchyView";
|
||||
import { resetHierarchy, showHierarchy } from "./hierarchy";
|
||||
import { installServerEvents } from "./servers";
|
||||
import { goConstraintExcluded } from "./goConstraints";
|
||||
import { ServerStatus } from "./ServerStatus";
|
||||
import { installWorkspaceEditUndo } from "./workspaceUndo";
|
||||
|
||||
@@ -48,16 +42,6 @@ export function activate(): () => void {
|
||||
});
|
||||
const disposers = [
|
||||
unsubProject,
|
||||
registerEditorBanner({
|
||||
id: "go.buildConstraints",
|
||||
useBanner: (path) => {
|
||||
const excluded = useProblems(
|
||||
(s) =>
|
||||
path.endsWith(".go") && (s.map.get(path)?.get("gopls") ?? []).some((d) => goConstraintExcluded(d.message)),
|
||||
);
|
||||
return excluded ? { text: "This Go file is excluded by the current build constraints." } : null;
|
||||
},
|
||||
}),
|
||||
installProblemsEvents(),
|
||||
installProblemDecorations(),
|
||||
installServerEvents(),
|
||||
|
||||
@@ -5581,5 +5581,11 @@
|
||||
"External file changes sync may be slow": "外部文件更改同步可能较慢",
|
||||
"recursive": "递归",
|
||||
"Java project import failed": "Java 项目导入失败",
|
||||
"The Java project cannot be imported because these files use a name Windows reserves for devices: {paths}. Delete or rename them, then restart the Java language server.": "无法导入 Java 项目,因为这些文件使用了 Windows 保留的设备名:{paths}。请删除或重命名这些文件,然后重启 Java 语言服务器。"
|
||||
"The Java project cannot be imported because these files use a name Windows reserves for devices: {paths}. Delete or rename them, then restart the Java language server.": "无法导入 Java 项目,因为这些文件使用了 Windows 保留的设备名:{paths}。请删除或重命名这些文件,然后重启 Java 语言服务器。",
|
||||
"This file is outside the source folders of its Java module: only syntax errors are reported, and navigation and completion don't work.": "此文件不在所属 Java 模块的源码目录中:只报告语法错误,跳转和补全不可用。",
|
||||
"This file does not belong to an imported Java project: only syntax errors are reported, and navigation and completion don't work.": "此文件不属于已导入的 Java 项目:只报告语法错误,跳转和补全不可用。",
|
||||
"This Go file belongs to no package of the workspace: code insight is limited.": "此 Go 文件不属于工作区中的任何包:代码洞察功能受限。",
|
||||
"This Rust file is not included in any crate: rust-analyzer can't offer code insight for it.": "此 Rust 文件未包含在任何 crate 中:rust-analyzer 无法为它提供代码洞察。",
|
||||
"Reload Maven Projects": "重新加载 Maven 项目",
|
||||
"Reload Gradle Project": "重新加载 Gradle 项目"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user