Файли
openidea/src/editor/lsp/outsideProject.ts
T
l0ng-ai 52360c72d2 fix(editor): describe a Java file outside every project accurately
jdtls still completes and navigates JDK classes in a non-project file such
as a scratch; it only skips semantic checks and project classes. The banner
said navigation and completion don't work, and in a Maven or Gradle project
it offered a build reload that can't bring a file outside the project in.

- The banner now says only syntax errors are reported and only JDK classes
  resolve.
- The Maven / Gradle reload is offered only for files inside the open
  project (or one of its workspace projects).
2026-11-04 21:09:12 +08:00

96 рядки
3.9 KiB
TypeScript

/**
* 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 { isSameOrInside } from "@/platform/paths";
import { useProject } from "@/platform/project";
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); offered
* only for files inside the open project.
*/
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". jdtls still completes
// and navigates JDK classes in such a file (a scratch, a file opened from outside the project).
{
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 only JDK classes resolve.",
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),
);
const inProject = useProject(({ project }) =>
[project?.root, ...(project?.workspace?.projects ?? [])].some((root) => !!root && isSameOrInside(path, root)),
);
if (!notice) return null;
const actions =
notice.reload && inProject
? 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 };
},
};