mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:08:16 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
837a7aec24 |
No files matched your search
@@ -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(),
|
||||
|
||||
@@ -5554,5 +5554,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