diff --git a/.vitepress/config.mts b/.vitepress/config.mts index 1822928f..d7f0a110 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -2,10 +2,16 @@ import { defineConfig } from "vitepress"; import { withMermaid } from "vitepress-plugin-mermaid"; import footnote from "markdown-it-footnote"; import taskLists from "markdown-it-task-lists"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; import { genSidebar } from "./sidebar"; +import { loadGithubState, githubLinksPlugin } from "./github-links"; const projects = ["clice", "catter", "clore"]; +const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const githubState = await loadGithubState(rootDir); + export default withMermaid(defineConfig({ title: "Project Clice", description: "新一代 C++ 工具链", @@ -14,6 +20,7 @@ export default withMermaid(defineConfig({ config: (md) => { md.use(footnote); md.use(taskLists); + githubLinksPlugin(md, githubState); // Override caption only (display text) so repeated refs show [1] not [1:1]; leaves id/href intact. md.renderer.rules.footnote_caption = (tokens, idx) => `[${Number(tokens[idx].meta.id + 1)}]`; diff --git a/.vitepress/github-links.json b/.vitepress/github-links.json new file mode 100644 index 00000000..10110432 --- /dev/null +++ b/.vitepress/github-links.json @@ -0,0 +1,356 @@ +{ + "clangd/clangd/issues/1009": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Go to declaration even if signatures mismatch" + }, + "clangd/clangd/issues/1034": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Duplicate inlay hints in the presence of explicit function template instantiation" + }, + "clangd/clangd/issues/1067": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Refactoring: Replace `using namespace` with `using` for the individual elements" + }, + "clangd/clangd/issues/1254": { + "type": "issue", + "state": "open", + "reason": "reopened", + "title": "\"Extract to Function\" tweak often not offered where it should be" + }, + "clangd/clangd/issues/1325": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Index doesn't understand overrides of specializations" + }, + "clangd/clangd/issues/1446": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Support `source.fixAll` (batch applying quick fixes)" + }, + "clangd/clangd/issues/1455": { + "type": "issue", + "state": "open", + "reason": null, + "title": "clangd doesn't support code folding for class access specifiers" + }, + "clangd/clangd/issues/1509": { + "type": "issue", + "state": "open", + "reason": "reopened", + "title": "Semantic token type for constructors and destructor should be `method`, not `class`" + }, + "clangd/clangd/issues/1617": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Switch between header/source doesn't find correct file in trivial cases." + }, + "clangd/clangd/issues/1619": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Code action for dropping qualifiers or using aliases" + }, + "clangd/clangd/issues/1690": { + "type": "issue", + "state": "open", + "reason": null, + "title": "[Bug] It takes too long time to build preamble when open a file." + }, + "clangd/clangd/issues/1697": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Make go-to-definition go to the macro's underlying function (same for show-references)" + }, + "clangd/clangd/issues/1709": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Semantic highlighting: Support for `std::format` strings" + }, + "clangd/clangd/issues/1710": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Extract function can extract code containing types defined in the enclosing functions scope" + }, + "clangd/clangd/issues/1747": { + "type": "issue", + "state": "open", + "reason": null, + "title": "clangd throws away FixIts when the fix is not in the main file" + }, + "clangd/clangd/issues/1982": { + "type": "issue", + "state": "open", + "reason": null, + "title": "`NOLINT` directives don't work for `unused-includes`" + }, + "clangd/clangd/issues/2000": { + "type": "issue", + "state": "open", + "reason": null, + "title": "`clangd` still shows some `clang-tidy` warnings from inside a macro coming from a system header" + }, + "clangd/clangd/issues/2010": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Adding include through code-actions uses absolute paths" + }, + "clangd/clangd/issues/2254": { + "type": "issue", + "state": "open", + "reason": null, + "title": "New inlay hint type: implicit conversion hints" + }, + "clangd/clangd/issues/2303": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Inlay hints - array disgnators , should have same text length." + }, + "clangd/clangd/issues/2382": { + "type": "issue", + "state": "open", + "reason": null, + "title": "crash on ExtractVariable - prepare" + }, + "clangd/clangd/issues/2426": { + "type": "issue", + "state": "open", + "reason": null, + "title": "signature help triggers unexpectedly" + }, + "clangd/clangd/issues/2454": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Add a quick fix to generate a missing method declaration corresponding to an out-of-line method definition" + }, + "clangd/clangd/issues/2472": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Provide signature help for operator[]" + }, + "clangd/clangd/issues/2476": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Add a setting to control whether formatting is applied to changes made by a code action" + }, + "clangd/clangd/issues/2486": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Module name does not get a semantic highlighting" + }, + "clangd/clangd/issues/2510": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Implementation of onTypeFormatting assumes that the position in the request is *after* the inserted character" + }, + "clangd/clangd/issues/2569": { + "type": "issue", + "state": "open", + "reason": null, + "title": "[C++20 modules] Important features still missing for the module support of clangd" + }, + "clangd/clangd/issues/2577": { + "type": "issue", + "state": "open", + "reason": null, + "title": "[C++20 modules] Auto-insert module import statements" + }, + "clangd/clangd/issues/2580": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Add \"Remove function\" tweak" + }, + "clangd/clangd/issues/2619": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Semantic tokens for using-declarations are inconsistent" + }, + "clangd/clangd/issues/2620": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Nested macro invocations result in incorrect parameter name inlay hints" + }, + "clangd/clangd/issues/2622": { + "type": "issue", + "state": "open", + "reason": null, + "title": "[Modules] Remove module partitions of other modules from suggestions" + }, + "clangd/clangd/issues/2623": { + "type": "issue", + "state": "open", + "reason": null, + "title": "[Modules] Fix missing icons from suggested module imports" + }, + "clangd/clangd/issues/2626": { + "type": "issue", + "state": "open", + "reason": null, + "title": "[Modules] Add a new single-token completion for `import` and `module`, with type `CompletionItemKind.Keyword`" + }, + "clangd/clangd/issues/2666": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Move folding range `startLine` to the declaration line for function/class bodies" + }, + "clangd/clangd/issues/2667": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Support LSP 3.17 `collapsedText` for folding ranges" + }, + "clangd/clangd/issues/2675": { + "type": "issue", + "state": "open", + "reason": null, + "title": "\"Move function body to declaration\" not appear for functions involving cstdint types" + }, + "clangd/clangd/issues/289": { + "type": "issue", + "state": "open", + "reason": null, + "title": "signaturehelp triggers in comments" + }, + "clangd/clangd/issues/443": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Improve code completion inside templates (e.g. provide completions for container[i].^ where container is a vector>)" + }, + "clangd/clangd/issues/446": { + "type": "issue", + "state": "open", + "reason": null, + "title": "refactor: inline and extract variables" + }, + "clangd/clangd/issues/460": { + "type": "issue", + "state": "open", + "reason": null, + "title": "refactor: modify function parameters" + }, + "clangd/clangd/issues/466": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Refactor: invert if" + }, + "clangd/clangd/issues/467": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Refactor: Create from usage" + }, + "clangd/clangd/issues/601": { + "type": "issue", + "state": "open", + "reason": null, + "title": "More detailed outline/document symbol information" + }, + "clangd/clangd/issues/603": { + "type": "issue", + "state": "open", + "reason": null, + "title": "code completion: add operator suggestions" + }, + "clangd/clangd/issues/616": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Outliner not detecting variables in functions (VS Code)" + }, + "clangd/clangd/issues/696": { + "type": "issue", + "state": "open", + "reason": null, + "title": "RA similar \"auto import\" by code action" + }, + "clangd/clangd/issues/698": { + "type": "issue", + "state": "open", + "reason": null, + "title": "code action : extract highlighted text to method / function" + }, + "clangd/clangd/issues/820": { + "type": "issue", + "state": "open", + "reason": null, + "title": "\"Expand macro one level\" code action" + }, + "clangd/clangd/issues/830": { + "type": "issue", + "state": "open", + "reason": null, + "title": "\"fix all occurrences\" code action" + }, + "clangd/clangd/issues/872": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Modifiers are wrong in semantic token for destructor" + }, + "clangd/clangd/issues/89": { + "type": "issue", + "state": "open", + "reason": null, + "title": "Code completion missing some of c++ keyword" + }, + "clangd/clangd/issues/924": { + "type": "issue", + "state": "open", + "reason": null, + "title": "extract subexpression does not replace all occurrences" + }, + "clice-io/clice/issues/90": { + "type": "issue", + "state": "closed", + "reason": "completed", + "title": "To-do list for the next phase" + }, + "clice-io/clice/pull/452": { + "type": "pull", + "state": "merged", + "reason": null, + "title": "feat(hover): port clangd hover implementation" + }, + "clice-io/clice/pull/460": { + "type": "pull", + "state": "open", + "reason": null, + "title": "feat(completion): trigger import completion on space" + }, + "llvm/llvm-project/pull/66462": { + "type": "pull", + "state": "merged", + "reason": null, + "title": "[clangd] [C++20] [Modules] Introduce initial support for C++20 Modules" + }, + "microsoft/vscode/issues/67714": { + "type": "issue", + "state": "closed", + "reason": "completed", + "title": "Discuss the use of space as IntelliSense trigger character" + } +} diff --git a/.vitepress/github-links.ts b/.vitepress/github-links.ts new file mode 100644 index 00000000..6e79eeec --- /dev/null +++ b/.vitepress/github-links.ts @@ -0,0 +1,229 @@ +import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs"; +import { join, extname } from "node:path"; + +// Renders GitHub issue/PR links the way GitHub itself does: the icon leads, the +// link collapses to the `owner/repo#number` shorthand, and hovering shows a +// card with the issue/PR title + state. Title and state are resolved at build +// time and cached in github-links.json, so the published site is a static +// snapshot — no client-side API calls. + +export type RefInfo = { + type: "issue" | "pull"; + state: "open" | "closed" | "merged"; + reason: string | null; + title: string; +}; + +export type GithubState = Map; + +// https://github.com/{owner}/{repo}/{issues|pull}/{number} +const HREF_RE = /^https?:\/\/github\.com\/([^/#?]+)\/([^/#?]+)\/(issues|pull)\/(\d+)(?:[/#?].*)?$/i; +const SCAN_RE = /https?:\/\/github\.com\/([^/#?\s)]+)\/([^/#?\s)]+)\/(issues|pull)\/(\d+)/gi; + +function keyFor(owner: string, repo: string, type: string, number: string): string { + return `${owner}/${repo}/${type}/${number}`; +} + +function seedPathFor(rootDir: string): string { + return join(rootDir, ".vitepress", "github-links.json"); +} + +function scanMarkdown(dir: string, out: Set): void { + for (const name of readdirSync(dir)) { + if (name === "node_modules" || name.startsWith(".")) continue; + const full = join(dir, name); + const st = statSync(full); + if (st.isDirectory()) { + scanMarkdown(full, out); + } else if (extname(name) === ".md") { + for (const m of readFileSync(full, "utf8").matchAll(SCAN_RE)) { + out.add(keyFor(m[1], m[2], m[3], m[4])); + } + } + } +} + +async function pool(items: T[], size: number, fn: (item: T) => Promise): Promise { + const out: R[] = []; + let i = 0; + const run = async () => { + while (i < items.length) { + const idx = i++; + out[idx] = await fn(items[idx]); + } + }; + await Promise.all(Array.from({ length: Math.min(size, items.length) }, run)); + return out; +} + +async function fetchState(key: string): Promise { + const [owner, repo, type, number] = key.split("/"); + const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; + const headers: Record = { + Accept: "application/vnd.github+json", + "User-Agent": "clice-docs-build", + }; + if (token) headers.Authorization = `Bearer ${token}`; + const base = `https://api.github.com/repos/${owner}/${repo}`; + try { + if (type === "pull") { + const res = await fetch(`${base}/pulls/${number}`, { headers }); + if (!res.ok) return null; + const j: any = await res.json(); + return { type: "pull", state: j.merged ? "merged" : j.state, reason: null, title: j.title }; + } + const res = await fetch(`${base}/issues/${number}`, { headers }); + if (!res.ok) return null; + const j: any = await res.json(); + return { + type: j.pull_request ? "pull" : "issue", + state: j.state, + reason: j.state_reason ?? null, + title: j.title, + }; + } catch { + return null; + } +} + +function differs(a: RefInfo | undefined, b: RefInfo): boolean { + return !a || a.type !== b.type || a.state !== b.state || a.reason !== b.reason || a.title !== b.title; +} + +// Loads cached states from github-links.json. When a GitHub token is available +// (CI) or DOCS_REFRESH_GH=1 is set, it also refreshes live and rewrites the +// cache. Without a token it stays fully offline using the committed cache, +// keeping local dev builds deterministic and free of API rate limits. +export async function loadGithubState(rootDir: string): Promise { + const seedPath = seedPathFor(rootDir); + const map: GithubState = new Map(); + if (existsSync(seedPath)) { + try { + const seed = JSON.parse(readFileSync(seedPath, "utf8")) as Record; + for (const [k, v] of Object.entries(seed)) map.set(k, v); + } catch { + /* corrupt cache — fall through with empty/partial map */ + } + } + + const refresh = process.env.DOCS_REFRESH_GH === "1" || !!process.env.GITHUB_TOKEN || !!process.env.GH_TOKEN; + if (refresh) { + const urls = new Set(); + try { + scanMarkdown(rootDir, urls); + } catch { + /* ignore scan errors, refresh only what we already know */ + } + for (const k of map.keys()) urls.add(k); + + const keys = [...urls]; + const results = await pool(keys, 6, async (key) => [key, await fetchState(key)] as const); + let changed = false; + for (const [key, info] of results) { + if (info && differs(map.get(key), info)) { + map.set(key, info); + changed = true; + } + } + if (changed) { + const obj = Object.fromEntries([...map.entries()].sort((a, b) => a[0].localeCompare(b[0]))); + try { + writeFileSync(seedPath, JSON.stringify(obj, null, 2) + "\n"); + } catch { + /* read-only fs — in-memory map is still used for this build */ + } + } + } + + return map; +} + +function cssState(info: RefInfo): string { + if (info.type === "pull") { + if (info.state === "merged") return "merged"; + if (info.state === "closed") return "closed-pr"; + return "open"; + } + if (info.state === "closed") return info.reason === "not_planned" ? "not-planned" : "completed"; + return "open"; +} + +const PATHS: Record = { + open: + '' + + '', + completed: + '' + + '', + "not-planned": + '', + merged: + '', + "open-pr": + '', + "closed-pr": + '' + + '', +}; + +function svgFor(info: RefInfo): string { + const state = cssState(info); + const glyph = info.type === "pull" && state === "open" ? "open-pr" : state; + const path = PATHS[glyph] ?? PATHS.open; + return ``; +} + +// GitHub issue/PR titles arrive as plain text, but they commonly contain +// `backtick` code spans. GitHub itself shows the raw backticks; we do a touch +// better by rendering those spans as inline , while escaping everything +// else. We deliberately handle only code spans — running a full markdown pass +// could emit a nested inside our link. +function renderTitle(esc: (s: string) => string, title: string): string { + let out = ""; + let last = 0; + for (const m of title.matchAll(/`([^`]+)`/g)) { + out += esc(title.slice(last, m.index)) + `${esc(m[1])}`; + last = m.index + m[0].length; + } + return out + esc(title.slice(last)); +} + +// markdown-it plugin: tags GitHub issue/PR links with a state class and leads +// with the state icon. Bare/auto-linked URLs are rendered the way GitHub renders +// a pasted reference — `[icon] Title owner/repo#number`, all inline. Explicit +// links keep the author's text and only gain the leading state icon. +export function githubLinksPlugin(md: any, state: GithubState): void { + const esc = md.utils.escapeHtml; + const defaultOpen = + md.renderer.rules.link_open || + ((tokens: any, idx: number, options: any, _env: any, self: any) => + self.renderToken(tokens, idx, options)); + + md.renderer.rules.link_open = (tokens: any, idx: number, options: any, env: any, self: any) => { + const token = tokens[idx]; + const href: string = token.attrGet("href") || ""; + const m = href.match(HREF_RE); + if (!m) return defaultOpen(tokens, idx, options, env, self); + + const [, owner, repo, type, number] = m; + const info = state.get(keyFor(owner, repo, type, number)); + token.attrJoin("class", info ? `gh-ref gh-${cssState(info)}` : "gh-ref"); + + const opened = defaultOpen(tokens, idx, options, env, self); + if (!info) return opened; + + const icon = `${svgFor(info)}`; + const ref = `${owner}/${repo}#${number}`; + + // Bare/auto-linked URL: render the title and reference inline, GitHub-style. + const isAuto = token.markup === "linkify" || token.markup === "autolink"; + const next = tokens[idx + 1]; + if (isAuto && next && next.type === "text") { + next.content = ""; + return `${opened}${icon}${renderTitle(esc, info.title)} ${esc(ref)}`; + } + + // Explicit link with author-chosen text: just lead with the state icon. + return opened + icon; + }; +} diff --git a/.vitepress/theme/style.css b/.vitepress/theme/style.css index d532eb57..4d80f7be 100644 --- a/.vitepress/theme/style.css +++ b/.vitepress/theme/style.css @@ -238,3 +238,61 @@ border-color: var(--vp-c-divider); background-color: var(--vp-c-bg-soft); } + +/** + * Component: GitHub Issue/PR Links + * Bare GitHub issue/PR links render the way GitHub renders a pasted reference: + * a leading state icon, the issue/PR title, and a muted `owner/repo#number`. + * -------------------------------------------------------------------------- */ + +/* Leading state icon, sized to the surrounding text and colored by state. */ +.gh-icon { + display: inline-block; + width: 1em; + height: 1em; + margin-right: 0.25em; + vertical-align: text-bottom; + color: var(--gh-color); +} + +.gh-icon svg { + display: block; + width: 100%; + height: 100%; + fill: currentColor; +} + +/* Muted, non-breaking reference shorthand trailing the title. */ +.gh-num { + color: var(--vp-c-text-2); + white-space: nowrap; +} + +/* State colors (light). The color cascades to the inline icon. */ +.gh-open { + --gh-color: #1a7f37; +} +.gh-completed, +.gh-merged { + --gh-color: #8250df; +} +.gh-closed-pr { + --gh-color: #cf222e; +} +.gh-not-planned { + --gh-color: #59636e; +} + +.dark .gh-open { + --gh-color: #3fb950; +} +.dark .gh-completed, +.dark .gh-merged { + --gh-color: #a371f7; +} +.dark .gh-closed-pr { + --gh-color: #f85149; +} +.dark .gh-not-planned { + --gh-color: #9198a1; +}