mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 17:02:17 +00:00
Go settings, execution, debugging and test reporting were disconnected, leaving Go projects without editable build/test fields and workspace maintenance actions. - Add project build constraints that update gopls in place, and notices on excluded files. - Add editable Go Build and Go Test configurations with directory, package and file targets, separate tool and program arguments, modules, environment, output selection and build-only execution, and pass the same settings to Delve. - Decode Go JSON test output into the results view, with subtests, failures, skips and rerun failed. Add gutter targets for benchmarks and generate directives. - Add module environment and vendoring settings, read go.work members, and offer tidy, download, sync and use from file menus. - Add file and package generate actions, and format and organize imports through gopls on Save and Reformat. - Show what a Go program prints in the debug console: Delve's debuggee shares its stdio, which no DAP event carries. - Keep the default build output out of version control, and keep configurations produced by an earlier run out of the gutter choices. Fixes #507 Fixes #237
349 lines
12 KiB
TypeScript
349 lines
12 KiB
TypeScript
/**
|
|
* `languages.*` (interpreters / toolchains) and `projectStructure.*` settings:
|
|
* shape, defaults and normalisation (mirrors `ide-core::settings`).
|
|
*/
|
|
import { useMemo } from "react";
|
|
import { useProject } from "./project";
|
|
|
|
export interface GoBuildTags {
|
|
os: string;
|
|
arch: string;
|
|
version: string;
|
|
compiler: string;
|
|
cgo: string;
|
|
custom: string;
|
|
experiments: string;
|
|
}
|
|
|
|
const DEFAULT_GO_BUILD_TAGS: GoBuildTags = {
|
|
os: "",
|
|
arch: "",
|
|
version: "",
|
|
compiler: "",
|
|
cgo: "",
|
|
custom: "",
|
|
experiments: "",
|
|
};
|
|
|
|
export interface LanguagesValid {
|
|
python: { interpreter: string; activateVirtualenv: boolean };
|
|
node: { interpreter: string; packageManager: "" | "npm" | "pnpm" | "yarn" | "bun" };
|
|
rust: {
|
|
toolchain: string;
|
|
stdlib: string;
|
|
checkOnSave: boolean;
|
|
checkCommand: "check" | "clippy";
|
|
rustfmtArgs: string;
|
|
};
|
|
go: {
|
|
goroot: string;
|
|
gopath: string;
|
|
buildTags: GoBuildTags;
|
|
modules: { env: Record<string, string>; autoVendor: boolean };
|
|
};
|
|
typescript: { version: string };
|
|
java: {
|
|
jdk: string;
|
|
jdtlsJdk: string;
|
|
jdtlsVmArgs: string;
|
|
gradle: { jvm: string; distribution: "wrapper" | "local"; home: string; userHome: string };
|
|
maven: { home: string; userSettings: string; localRepository: string; unlinkedProjects: string[] };
|
|
};
|
|
}
|
|
|
|
export interface ProjectStructureValid {
|
|
sourceRoots: string[];
|
|
testRoots: string[];
|
|
resourceRoots: string[];
|
|
testResourceRoots: string[];
|
|
excluded: string[];
|
|
/** Auto-detected roots the user unmarked. */
|
|
unmarked: string[];
|
|
/** Content roots besides the project folder (absolute paths). */
|
|
contentRoots: string[];
|
|
/** "Exclude files": `;`-separated name patterns. */
|
|
excludeFiles: string;
|
|
}
|
|
|
|
/** The folder lists of `ProjectStructureValid` (marks). */
|
|
export type FolderListKey =
|
|
| "sourceRoots"
|
|
| "testRoots"
|
|
| "resourceRoots"
|
|
| "testResourceRoots"
|
|
| "excluded"
|
|
| "unmarked";
|
|
|
|
export const DEFAULT_LANGUAGES: LanguagesValid = {
|
|
python: { interpreter: "", activateVirtualenv: true },
|
|
node: { interpreter: "", packageManager: "" },
|
|
rust: { toolchain: "", stdlib: "", checkOnSave: true, checkCommand: "check", rustfmtArgs: "" },
|
|
go: { goroot: "", gopath: "", buildTags: DEFAULT_GO_BUILD_TAGS, modules: { env: {}, autoVendor: true } },
|
|
typescript: { version: "bundled" },
|
|
java: {
|
|
jdk: "",
|
|
jdtlsJdk: "",
|
|
jdtlsVmArgs: "",
|
|
gradle: { jvm: "", distribution: "wrapper", home: "", userHome: "" },
|
|
maven: { home: "", userSettings: "", localRepository: "", unlinkedProjects: [] },
|
|
},
|
|
};
|
|
|
|
export const DEFAULT_PROJECT_STRUCTURE: ProjectStructureValid = {
|
|
sourceRoots: [],
|
|
testRoots: [],
|
|
resourceRoots: [],
|
|
testResourceRoots: [],
|
|
excluded: [],
|
|
unmarked: [],
|
|
contentRoots: [],
|
|
excludeFiles: "",
|
|
};
|
|
|
|
type Obj = Record<string, unknown>;
|
|
const obj = (v: unknown): Obj => (v && typeof v === "object" && !Array.isArray(v) ? (v as Obj) : {});
|
|
const text = (v: unknown, d: string) => (typeof v === "string" ? v.trim() : d);
|
|
const bool = (v: unknown, d: boolean) => (typeof v === "boolean" ? v : d);
|
|
const oneOf = <T extends string>(v: unknown, allowed: readonly T[], d: T): T =>
|
|
typeof v === "string" && (allowed as readonly string[]).includes(v) ? (v as T) : d;
|
|
|
|
/** Project-relative folder: `/`-separated, no leading `./` or `/`, no trailing `/`. */
|
|
export function normalizeFolder(path: string): string {
|
|
return path
|
|
.replace(/\\/g, "/")
|
|
.trim()
|
|
.replace(/^(\.\/)+/, "")
|
|
.replace(/^\/+|\/+$/g, "");
|
|
}
|
|
|
|
const folders = (v: unknown): string[] =>
|
|
Array.isArray(v)
|
|
? [
|
|
...new Set(
|
|
v
|
|
.filter((x): x is string => typeof x === "string")
|
|
.map(normalizeFolder)
|
|
.filter((x) => x),
|
|
),
|
|
]
|
|
: [];
|
|
|
|
/** Settings store the content root itself as `.`; roots use `""`. */
|
|
const folderKey = (stored: string) => (stored === "." ? "" : stored);
|
|
const storedFolder = (rel: string) => normalizeFolder(rel) || ".";
|
|
|
|
export function validateLanguages(raw: unknown): LanguagesValid {
|
|
const d = DEFAULT_LANGUAGES;
|
|
const l = obj(raw);
|
|
const py = obj(l.python);
|
|
const node = obj(l.node);
|
|
const rust = obj(l.rust);
|
|
const go = obj(l.go);
|
|
const tags = obj(go.buildTags);
|
|
const modules = obj(go.modules);
|
|
const ts = obj(l.typescript);
|
|
const java = obj(l.java);
|
|
const maven = obj(java.maven);
|
|
const gradle = obj(java.gradle);
|
|
return {
|
|
python: {
|
|
interpreter: text(py.interpreter, d.python.interpreter),
|
|
activateVirtualenv: bool(py.activateVirtualenv, d.python.activateVirtualenv),
|
|
},
|
|
node: {
|
|
interpreter: text(node.interpreter, d.node.interpreter),
|
|
packageManager: oneOf(node.packageManager, ["", "npm", "pnpm", "yarn", "bun"] as const, d.node.packageManager),
|
|
},
|
|
rust: {
|
|
toolchain: text(rust.toolchain, d.rust.toolchain),
|
|
stdlib: text(rust.stdlib, d.rust.stdlib),
|
|
checkOnSave: bool(rust.checkOnSave, d.rust.checkOnSave),
|
|
checkCommand: oneOf(rust.checkCommand, ["check", "clippy"] as const, d.rust.checkCommand),
|
|
rustfmtArgs: typeof rust.rustfmtArgs === "string" ? rust.rustfmtArgs : d.rust.rustfmtArgs,
|
|
},
|
|
go: {
|
|
goroot: text(go.goroot, d.go.goroot),
|
|
gopath: text(go.gopath, d.go.gopath),
|
|
buildTags: Object.fromEntries(
|
|
Object.entries(DEFAULT_GO_BUILD_TAGS).map(([key, value]) => [key, text(tags[key], value)]),
|
|
) as unknown as GoBuildTags,
|
|
modules: {
|
|
autoVendor: bool(modules.autoVendor, true),
|
|
env: Object.fromEntries(
|
|
Object.entries(obj(modules.env)).filter(
|
|
([key, value]) => !!key && !/[=\s\0]/.test(key) && typeof value === "string" && !value.includes("\0"),
|
|
),
|
|
) as Record<string, string>,
|
|
},
|
|
},
|
|
typescript: { version: text(ts.version, d.typescript.version) || d.typescript.version },
|
|
java: {
|
|
jdk: text(java.jdk, d.java.jdk),
|
|
jdtlsJdk: text(java.jdtlsJdk, d.java.jdtlsJdk),
|
|
jdtlsVmArgs: text(java.jdtlsVmArgs, d.java.jdtlsVmArgs),
|
|
gradle: {
|
|
jvm: text(gradle.jvm, d.java.gradle.jvm),
|
|
distribution: oneOf(gradle.distribution, ["wrapper", "local"] as const, d.java.gradle.distribution),
|
|
home: text(gradle.home, d.java.gradle.home),
|
|
userHome: text(gradle.userHome, d.java.gradle.userHome),
|
|
},
|
|
maven: {
|
|
home: text(maven.home, d.java.maven.home),
|
|
userSettings: text(maven.userSettings, d.java.maven.userSettings),
|
|
localRepository: text(maven.localRepository, d.java.maven.localRepository),
|
|
unlinkedProjects: folders(maven.unlinkedProjects),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function validateProjectStructure(raw: unknown): ProjectStructureValid {
|
|
const p = obj(raw);
|
|
return {
|
|
sourceRoots: folders(p.sourceRoots),
|
|
testRoots: folders(p.testRoots),
|
|
resourceRoots: folders(p.resourceRoots),
|
|
testResourceRoots: folders(p.testResourceRoots),
|
|
excluded: folders(p.excluded),
|
|
unmarked: folders(p.unmarked),
|
|
contentRoots: Array.isArray(p.contentRoots)
|
|
? [...new Set(p.contentRoots.filter((x): x is string => typeof x === "string" && !!x.trim()))]
|
|
: [],
|
|
excludeFiles: typeof p.excludeFiles === "string" ? p.excludeFiles : "",
|
|
};
|
|
}
|
|
|
|
/** Root kinds, as in IDEA's Project Structure (`ide-workspace::roots::RootKind`). */
|
|
export type FolderRole = "source" | "test" | "resources" | "testResources" | "excluded";
|
|
|
|
export const FOLDER_ROLES: readonly FolderRole[] = ["source", "test", "resources", "testResources", "excluded"];
|
|
|
|
const ROLE_KEYS: Record<FolderRole, Exclude<FolderListKey, "unmarked">> = {
|
|
source: "sourceRoots",
|
|
test: "testRoots",
|
|
resources: "resourceRoots",
|
|
testResources: "testResourceRoots",
|
|
excluded: "excluded",
|
|
};
|
|
|
|
/** A root of the project: user-marked (`detectedFrom` null) or detected from a build file. */
|
|
export interface RootFolder {
|
|
/** Project-relative, `/`-separated; `""` is the project root. */
|
|
path: string;
|
|
kind: FolderRole;
|
|
detectedFrom: string | null;
|
|
}
|
|
|
|
/** User-marked role of a project-relative folder (exact match), or null. */
|
|
export function folderRole(ps: ProjectStructureValid, rel: string): FolderRole | null {
|
|
const r = storedFolder(rel);
|
|
for (const role of FOLDER_ROLES) {
|
|
if (ps[ROLE_KEYS[role]].includes(r)) return role;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Effective roots: user marks win, `unmarked` hides detected roots
|
|
* (mirrors `ProjectRoots::compute`), sorted by path.
|
|
*/
|
|
export function effectiveRoots(detected: readonly RootFolder[], ps: ProjectStructureValid): RootFolder[] {
|
|
const out: RootFolder[] = [];
|
|
for (const role of FOLDER_ROLES)
|
|
for (const path of ps[ROLE_KEYS[role]].map(folderKey))
|
|
if (!out.some((r) => r.path === path)) out.push({ path, kind: role, detectedFrom: null });
|
|
const unmarked = ps.unmarked.map(folderKey);
|
|
for (const d of detected) if (!unmarked.includes(d.path) && !out.some((r) => r.path === d.path)) out.push({ ...d });
|
|
return out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
}
|
|
|
|
/** Effective role of a folder (exact match): what its icon shows. */
|
|
export function rootRole(roots: readonly RootFolder[], rel: string): FolderRole | null {
|
|
const r = normalizeFolder(rel);
|
|
return roots.find((x) => x.path === r)?.kind ?? null;
|
|
}
|
|
|
|
const inside = (rel: string, root: string) => !root || rel === root || rel.startsWith(`${root}/`);
|
|
|
|
/** The innermost root containing the project-relative path `rel`. */
|
|
export function rootFor(roots: readonly RootFolder[], rel: string): RootFolder | null {
|
|
const r = normalizeFolder(rel);
|
|
let best: RootFolder | null = null;
|
|
for (const root of roots) if (inside(r, root.path) && (!best || root.path.length > best.path.length)) best = root;
|
|
return best;
|
|
}
|
|
|
|
/** Test files recognised by name (mirrors `ide-workspace::roots::is_test_file_name`). */
|
|
export function isTestFileName(name: string): boolean {
|
|
if (name.endsWith("_test.go")) return true;
|
|
if (name.endsWith(".py")) {
|
|
const stem = name.slice(0, -3);
|
|
return stem.startsWith("test_") || stem.endsWith("_test") || stem === "conftest";
|
|
}
|
|
return /^.+\.(test|spec)\.(js|jsx|ts|tsx|mjs|cjs|mts|cts)$/.test(name);
|
|
}
|
|
|
|
/** IDEA's "Project Test Files": under a test (resource) root, or a test file by name. */
|
|
export function isTestPath(roots: readonly RootFolder[], rel: string): boolean {
|
|
const root = rootFor(roots, rel);
|
|
if (root?.kind === "test" || root?.kind === "testResources") return true;
|
|
if (root?.kind === "excluded") return false;
|
|
return isTestFileName(normalizeFolder(rel).split("/").pop() ?? "");
|
|
}
|
|
|
|
/**
|
|
* Gives `rel` the `role` (null = Unmark). Marking a detected root with its
|
|
* detected role needs no override; unmarking a detected root records it in `unmarked`.
|
|
*/
|
|
export function markFolder(
|
|
ps: ProjectStructureValid,
|
|
rel: string,
|
|
role: FolderRole | null,
|
|
detected: readonly RootFolder[] = [],
|
|
): ProjectStructureValid {
|
|
const r = storedFolder(rel);
|
|
const next: ProjectStructureValid = {
|
|
...ps,
|
|
sourceRoots: ps.sourceRoots.filter((x) => x !== r),
|
|
testRoots: ps.testRoots.filter((x) => x !== r),
|
|
resourceRoots: ps.resourceRoots.filter((x) => x !== r),
|
|
testResourceRoots: ps.testResourceRoots.filter((x) => x !== r),
|
|
excluded: ps.excluded.filter((x) => x !== r),
|
|
unmarked: ps.unmarked.filter((x) => x !== r),
|
|
};
|
|
const auto = detected.find((d) => d.path === folderKey(r))?.kind ?? null;
|
|
if (role === null) {
|
|
if (auto) next.unmarked = [...next.unmarked, r].sort();
|
|
} else if (role !== auto) {
|
|
next[ROLE_KEYS[role]] = [...next[ROLE_KEYS[role]], r].sort();
|
|
}
|
|
return next;
|
|
}
|
|
|
|
/** Detected roots the user unmarked. */
|
|
export function unmarkedRoots(detected: readonly RootFolder[], ps: ProjectStructureValid): RootFolder[] {
|
|
const unmarked = ps.unmarked.map(folderKey);
|
|
return detected.filter((d) => unmarked.includes(d.path));
|
|
}
|
|
|
|
/** Brings back every unmarked detected root. */
|
|
export function restoreDetected(ps: ProjectStructureValid, detected: readonly RootFolder[]): ProjectStructureValid {
|
|
return { ...ps, unmarked: ps.unmarked.filter((p) => !detected.some((d) => d.path === folderKey(p))) };
|
|
}
|
|
|
|
/** Settings keys of `ProjectStructureValid`, in a stable order. */
|
|
export const PROJECT_STRUCTURE_KEYS = [
|
|
"sourceRoots",
|
|
"testRoots",
|
|
"resourceRoots",
|
|
"testResourceRoots",
|
|
"excluded",
|
|
"unmarked",
|
|
] as const satisfies ReadonlyArray<FolderListKey>;
|
|
|
|
/** Effective Project Structure of the open project (reactive). */
|
|
export function useProjectStructure(): ProjectStructureValid {
|
|
const raw = useProject((s) => (s.settings as { projectStructure?: unknown } | null)?.projectStructure);
|
|
return useMemo(() => validateProjectStructure(raw), [raw]);
|
|
}
|