mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-03-11 17:49:25 -05:00
Add VSCode extension for book build and debug tooling
Sidebar extension with tree views for build, debug, precommit, and publish workflows. Integrates with the book CLI for chapter-level builds, cross-reference checks, and MIT Press release packaging.
This commit is contained in:
3
book/vscode-ext/.gitignore
vendored
Normal file
3
book/vscode-ext/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
out/
|
||||
node_modules/
|
||||
*.vsix
|
||||
8
book/vscode-ext/.vscodeignore
Normal file
8
book/vscode-ext/.vscodeignore
Normal file
@@ -0,0 +1,8 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
src/**
|
||||
.gitignore
|
||||
tsconfig.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
node_modules/**
|
||||
58
book/vscode-ext/package-lock.json
generated
Normal file
58
book/vscode-ext/package-lock.json
generated
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "mlsysbook-workbench",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mlsysbook-workbench",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/vscode": "^1.85.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
||||
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/vscode": {
|
||||
"version": "1.108.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.108.1.tgz",
|
||||
"integrity": "sha512-DerV0BbSzt87TbrqmZ7lRDIYaMiqvP8tmJTzW2p49ZBVtGUnGAu2RGQd1Wv4XMzEVUpaHbsemVM5nfuQJj7H6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
81
book/vscode-ext/package.json
Normal file
81
book/vscode-ext/package.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "mlsysbook-workbench",
|
||||
"displayName": "MLSysBook Workbench",
|
||||
"description": "Build, debug, validate, and publish the ML Systems textbook",
|
||||
"version": "0.1.0",
|
||||
"publisher": "mlsysbook",
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
},
|
||||
"categories": ["Other"],
|
||||
"activationEvents": [
|
||||
"workspaceContains:book/binder"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.85.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"contributes": {
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "mlsysbook",
|
||||
"title": "MLSysBook",
|
||||
"icon": "resources/icon.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
"views": {
|
||||
"mlsysbook": [
|
||||
{ "id": "mlsysbook.build", "name": "Build" },
|
||||
{ "id": "mlsysbook.debug", "name": "Debug" },
|
||||
{ "id": "mlsysbook.precommit", "name": "Pre-commit / Validate" },
|
||||
{ "id": "mlsysbook.publish", "name": "Publish / Maintenance" }
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{ "command": "mlsysbook.buildChapterHtml", "title": "MLSysBook: Build Chapter (HTML)" },
|
||||
{ "command": "mlsysbook.buildChapterPdf", "title": "MLSysBook: Build Chapter (PDF)" },
|
||||
{ "command": "mlsysbook.buildChapterEpub", "title": "MLSysBook: Build Chapter (EPUB)" },
|
||||
{ "command": "mlsysbook.previewChapter", "title": "MLSysBook: Preview Chapter" },
|
||||
{ "command": "mlsysbook.buildVolumeHtml", "title": "MLSysBook: Build Full Volume (HTML)" },
|
||||
{ "command": "mlsysbook.buildVolumePdf", "title": "MLSysBook: Build Full Volume (PDF)" },
|
||||
{ "command": "mlsysbook.buildVolumeEpub", "title": "MLSysBook: Build Full Volume (EPUB)" },
|
||||
{ "command": "mlsysbook.debugVolumePdf", "title": "MLSysBook: Debug Volume PDF" },
|
||||
{ "command": "mlsysbook.debugChapterSections", "title": "MLSysBook: Debug Chapter Sections" },
|
||||
{ "command": "mlsysbook.precommitRunAll", "title": "MLSysBook: Run All Pre-commit Hooks" },
|
||||
{ "command": "mlsysbook.precommitRunHook", "title": "MLSysBook: Run Pre-commit Hook" },
|
||||
{ "command": "mlsysbook.runAction", "title": "MLSysBook: Run Action" },
|
||||
{ "command": "mlsysbook.cleanArtifacts", "title": "MLSysBook: Clean Build Artifacts" },
|
||||
{ "command": "mlsysbook.doctor", "title": "MLSysBook: Doctor (Health Check)" },
|
||||
{ "command": "mlsysbook.buildGlossary", "title": "MLSysBook: Build Global Glossary" },
|
||||
{ "command": "mlsysbook.compressImages", "title": "MLSysBook: Compress Images" },
|
||||
{ "command": "mlsysbook.repoHealth", "title": "MLSysBook: Repo Health Check" },
|
||||
{ "command": "mlsysbook.contextBuildHtml", "title": "MLSysBook: Build as HTML" },
|
||||
{ "command": "mlsysbook.contextBuildPdf", "title": "MLSysBook: Build as PDF" },
|
||||
{ "command": "mlsysbook.contextBuildEpub", "title": "MLSysBook: Build as EPUB" },
|
||||
{ "command": "mlsysbook.contextPreview", "title": "MLSysBook: Preview" },
|
||||
{ "command": "mlsysbook.contextDebugSections", "title": "MLSysBook: Debug Sections" },
|
||||
{ "command": "mlsysbook.refreshBuildTree", "title": "Refresh", "icon": "$(refresh)" }
|
||||
],
|
||||
"menus": {
|
||||
"explorer/context": [
|
||||
{ "command": "mlsysbook.contextBuildHtml", "when": "resourceExtname == .qmd", "group": "mlsysbook@1" },
|
||||
{ "command": "mlsysbook.contextBuildPdf", "when": "resourceExtname == .qmd", "group": "mlsysbook@2" },
|
||||
{ "command": "mlsysbook.contextBuildEpub", "when": "resourceExtname == .qmd", "group": "mlsysbook@3" },
|
||||
{ "command": "mlsysbook.contextPreview", "when": "resourceExtname == .qmd", "group": "mlsysbook@4" },
|
||||
{ "command": "mlsysbook.contextDebugSections", "when": "resourceExtname == .qmd", "group": "mlsysbook@5" }
|
||||
],
|
||||
"view/title": [
|
||||
{ "command": "mlsysbook.refreshBuildTree", "when": "view == mlsysbook.build", "group": "navigation" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
6
book/vscode-ext/resources/icon.svg
Normal file
6
book/vscode-ext/resources/icon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
|
||||
<path d="M8 7h8"/>
|
||||
<path d="M8 11h6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 332 B |
36
book/vscode-ext/src/commands/buildCommands.ts
Normal file
36
book/vscode-ext/src/commands/buildCommands.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { VolumeId, BuildFormat } from '../types';
|
||||
import { getRepoRoot } from '../utils/workspace';
|
||||
import { runInTerminal } from '../utils/terminal';
|
||||
|
||||
export function registerBuildCommands(context: vscode.ExtensionContext): void {
|
||||
const root = getRepoRoot();
|
||||
if (!root) { return; }
|
||||
|
||||
// Chapter-level builds
|
||||
for (const fmt of ['Html', 'Pdf', 'Epub'] as const) {
|
||||
const fmtLower = fmt.toLowerCase() as BuildFormat;
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(`mlsysbook.buildChapter${fmt}`, (vol: VolumeId, chapter: string) => {
|
||||
runInTerminal(`./book/binder ${fmtLower} ${chapter} --${vol} -v`, root);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Preview
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('mlsysbook.previewChapter', (vol: VolumeId, chapter: string) => {
|
||||
runInTerminal(`./book/binder preview ${chapter}`, root);
|
||||
})
|
||||
);
|
||||
|
||||
// Volume-level builds
|
||||
for (const fmt of ['Html', 'Pdf', 'Epub'] as const) {
|
||||
const fmtLower = fmt.toLowerCase() as BuildFormat;
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(`mlsysbook.buildVolume${fmt}`, (vol: VolumeId) => {
|
||||
runInTerminal(`./book/binder ${fmtLower} --${vol} -v`, root);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
43
book/vscode-ext/src/commands/contextMenuCommands.ts
Normal file
43
book/vscode-ext/src/commands/contextMenuCommands.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { getRepoRoot, parseQmdFile } from '../utils/workspace';
|
||||
import { runInTerminal } from '../utils/terminal';
|
||||
|
||||
export function registerContextMenuCommands(context: vscode.ExtensionContext): void {
|
||||
const root = getRepoRoot();
|
||||
if (!root) { return; }
|
||||
|
||||
const makeHandler = (format: string) => {
|
||||
return (uri: vscode.Uri) => {
|
||||
const ctx = parseQmdFile(uri);
|
||||
if (!ctx) {
|
||||
vscode.window.showWarningMessage('Could not determine volume/chapter from file path.');
|
||||
return;
|
||||
}
|
||||
runInTerminal(`./book/binder ${format} ${ctx.chapter} --${ctx.volume} -v`, root);
|
||||
};
|
||||
};
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('mlsysbook.contextBuildHtml', makeHandler('html')),
|
||||
vscode.commands.registerCommand('mlsysbook.contextBuildPdf', makeHandler('pdf')),
|
||||
vscode.commands.registerCommand('mlsysbook.contextBuildEpub', makeHandler('epub')),
|
||||
|
||||
vscode.commands.registerCommand('mlsysbook.contextPreview', (uri: vscode.Uri) => {
|
||||
const ctx = parseQmdFile(uri);
|
||||
if (!ctx) {
|
||||
vscode.window.showWarningMessage('Could not determine volume/chapter from file path.');
|
||||
return;
|
||||
}
|
||||
runInTerminal(`./book/binder preview ${ctx.chapter}`, root);
|
||||
}),
|
||||
|
||||
vscode.commands.registerCommand('mlsysbook.contextDebugSections', (uri: vscode.Uri) => {
|
||||
const ctx = parseQmdFile(uri);
|
||||
if (!ctx) {
|
||||
vscode.window.showWarningMessage('Could not determine volume/chapter from file path.');
|
||||
return;
|
||||
}
|
||||
runInTerminal(`./book/binder debug pdf --${ctx.volume} --chapter ${ctx.chapter}`, root);
|
||||
}),
|
||||
);
|
||||
}
|
||||
49
book/vscode-ext/src/commands/debugCommands.ts
Normal file
49
book/vscode-ext/src/commands/debugCommands.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { VolumeId } from '../types';
|
||||
import { getRepoRoot } from '../utils/workspace';
|
||||
import { runInTerminal } from '../utils/terminal';
|
||||
import { discoverChapters } from '../utils/chapters';
|
||||
|
||||
export function registerDebugCommands(context: vscode.ExtensionContext): void {
|
||||
const root = getRepoRoot();
|
||||
if (!root) { return; }
|
||||
|
||||
// Debug full volume
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('mlsysbook.debugVolumePdf', (vol: VolumeId) => {
|
||||
runInTerminal(`./book/binder debug pdf --${vol}`, root);
|
||||
})
|
||||
);
|
||||
|
||||
// Debug chapter sections (interactive QuickPick)
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('mlsysbook.debugChapterSections', async () => {
|
||||
const volPick = await vscode.window.showQuickPick(
|
||||
[
|
||||
{ label: 'Volume I', id: 'vol1' as VolumeId },
|
||||
{ label: 'Volume II', id: 'vol2' as VolumeId },
|
||||
],
|
||||
{ placeHolder: 'Select volume' },
|
||||
);
|
||||
if (!volPick) { return; }
|
||||
|
||||
const volumes = discoverChapters(root);
|
||||
const volume = volumes.find(v => v.id === volPick.id);
|
||||
if (!volume) { return; }
|
||||
|
||||
const chapterPick = await vscode.window.showQuickPick(
|
||||
volume.chapters.map(ch => ({ label: ch.displayName, description: ch.name, id: ch.name })),
|
||||
{ placeHolder: 'Select chapter to debug' },
|
||||
);
|
||||
if (!chapterPick) { return; }
|
||||
|
||||
const fmtPick = await vscode.window.showQuickPick(
|
||||
['pdf', 'html', 'epub'],
|
||||
{ placeHolder: 'Select format (default: pdf)' },
|
||||
);
|
||||
const fmt = fmtPick ?? 'pdf';
|
||||
|
||||
runInTerminal(`./book/binder debug ${fmt} --${volPick.id} --chapter ${chapterPick.id}`, root);
|
||||
})
|
||||
);
|
||||
}
|
||||
18
book/vscode-ext/src/commands/precommitCommands.ts
Normal file
18
book/vscode-ext/src/commands/precommitCommands.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { getRepoRoot } from '../utils/workspace';
|
||||
import { runInTerminal } from '../utils/terminal';
|
||||
|
||||
export function registerPrecommitCommands(context: vscode.ExtensionContext): void {
|
||||
const root = getRepoRoot();
|
||||
if (!root) { return; }
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('mlsysbook.precommitRunAll', () => {
|
||||
runInTerminal('pre-commit run --all-files', root);
|
||||
}),
|
||||
|
||||
vscode.commands.registerCommand('mlsysbook.precommitRunHook', (command: string) => {
|
||||
runInTerminal(command, root);
|
||||
}),
|
||||
);
|
||||
}
|
||||
34
book/vscode-ext/src/commands/publishCommands.ts
Normal file
34
book/vscode-ext/src/commands/publishCommands.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { getRepoRoot } from '../utils/workspace';
|
||||
import { runInTerminal } from '../utils/terminal';
|
||||
|
||||
export function registerPublishCommands(context: vscode.ExtensionContext): void {
|
||||
const root = getRepoRoot();
|
||||
if (!root) { return; }
|
||||
|
||||
// Generic action runner (used by tree items that pass a command string)
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('mlsysbook.runAction', (command: string) => {
|
||||
runInTerminal(command, root);
|
||||
})
|
||||
);
|
||||
|
||||
// Named aliases for command palette discoverability
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('mlsysbook.cleanArtifacts', () => {
|
||||
runInTerminal('./book/binder clean', root);
|
||||
}),
|
||||
vscode.commands.registerCommand('mlsysbook.doctor', () => {
|
||||
runInTerminal('./book/binder doctor', root);
|
||||
}),
|
||||
vscode.commands.registerCommand('mlsysbook.buildGlossary', () => {
|
||||
runInTerminal('python3 book/tools/scripts/glossary/build_global_glossary.py', root);
|
||||
}),
|
||||
vscode.commands.registerCommand('mlsysbook.compressImages', () => {
|
||||
runInTerminal('python3 book/tools/scripts/images/compress_images.py', root);
|
||||
}),
|
||||
vscode.commands.registerCommand('mlsysbook.repoHealth', () => {
|
||||
runInTerminal('python3 book/tools/scripts/maintenance/repo_health_check.py --health-check', root);
|
||||
}),
|
||||
);
|
||||
}
|
||||
34
book/vscode-ext/src/constants.ts
Normal file
34
book/vscode-ext/src/constants.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { PrecommitHook, ActionDef } from './types';
|
||||
|
||||
export const PRECOMMIT_HOOKS: PrecommitHook[] = [
|
||||
{ id: 'book-check-duplicate-labels', label: 'Check Duplicate Labels', command: 'pre-commit run book-check-duplicate-labels --all-files' },
|
||||
{ id: 'book-check-unreferenced-labels', label: 'Check Unreferenced Labels', command: 'pre-commit run book-check-unreferenced-labels --all-files' },
|
||||
{ id: 'book-validate-citations', label: 'Check Citations', command: 'pre-commit run book-validate-citations --all-files' },
|
||||
{ id: 'book-check-references', label: 'Check References', command: 'pre-commit run book-check-references --all-files' },
|
||||
{ id: 'book-validate-image-references', label: 'Check Image References', command: 'pre-commit run book-validate-image-references --all-files' },
|
||||
{ id: 'book-validate-footnotes', label: 'Check Footnotes', command: 'pre-commit run book-validate-footnotes --all-files' },
|
||||
{ id: 'book-check-forbidden-footnotes', label: 'Check Forbidden Footnotes', command: 'pre-commit run book-check-forbidden-footnotes --all-files' },
|
||||
{ id: 'book-check-table-formatting', label: 'Check Table Formatting', command: 'pre-commit run book-check-table-formatting --all-files' },
|
||||
{ id: 'book-check-unclosed-divs', label: 'Check Unclosed Divs', command: 'pre-commit run book-check-unclosed-divs --all-files' },
|
||||
{ id: 'book-check-purpose-unnumbered', label: 'Check Purpose Unnumbered', command: 'pre-commit run book-check-purpose-unnumbered --all-files' },
|
||||
{ id: 'book-format-python', label: 'Format Python in QMD', command: 'pre-commit run book-format-python --all-files' },
|
||||
{ id: 'book-collapse-blank-lines', label: 'Collapse Blank Lines', command: 'pre-commit run book-collapse-blank-lines --all-files' },
|
||||
{ id: 'codespell', label: 'Codespell', command: 'pre-commit run codespell --all-files' },
|
||||
{ id: 'mdformat', label: 'Format Markdown', command: 'pre-commit run mdformat --all-files' },
|
||||
{ id: 'bibtex-tidy', label: 'Tidy BibTeX', command: 'pre-commit run bibtex-tidy --all-files' },
|
||||
];
|
||||
|
||||
export const PUBLISH_ACTIONS: ActionDef[] = [
|
||||
{ id: 'mit-press-vol1', label: 'MIT Press Vol1', command: 'bash book/tools/scripts/publish/mit-press-release.sh --vol1', icon: 'rocket' },
|
||||
{ id: 'mit-press-vol2', label: 'MIT Press Vol2', command: 'bash book/tools/scripts/publish/mit-press-release.sh --vol2', icon: 'rocket' },
|
||||
{ id: 'mit-press-vol1-copyedit', label: 'MIT Press Vol1 (Copy-edit)', command: 'bash book/tools/scripts/publish/mit-press-release.sh --vol1 --copyedit', icon: 'rocket' },
|
||||
{ id: 'extract-figures-vol1', label: 'Extract Figures Vol1', command: 'python3 book/tools/scripts/publish/extract_figures.py --vol 1', icon: 'file-media' },
|
||||
];
|
||||
|
||||
export const MAINTENANCE_ACTIONS: ActionDef[] = [
|
||||
{ id: 'clean', label: 'Clean Build Artifacts', command: './book/binder clean', icon: 'trash' },
|
||||
{ id: 'doctor', label: 'Doctor (Health Check)', command: './book/binder doctor', icon: 'heart' },
|
||||
{ id: 'glossary', label: 'Build Global Glossary', command: 'python3 book/tools/scripts/glossary/build_global_glossary.py', icon: 'book' },
|
||||
{ id: 'compress-images', label: 'Compress Images', command: 'python3 book/tools/scripts/images/compress_images.py', icon: 'file-media' },
|
||||
{ id: 'repo-health', label: 'Repo Health Check', command: 'python3 book/tools/scripts/maintenance/repo_health_check.py --health-check', icon: 'pulse' },
|
||||
];
|
||||
48
book/vscode-ext/src/extension.ts
Normal file
48
book/vscode-ext/src/extension.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { getRepoRoot } from './utils/workspace';
|
||||
import { BuildTreeProvider } from './providers/buildTreeProvider';
|
||||
import { DebugTreeProvider } from './providers/debugTreeProvider';
|
||||
import { PrecommitTreeProvider } from './providers/precommitTreeProvider';
|
||||
import { PublishTreeProvider } from './providers/publishTreeProvider';
|
||||
import { registerBuildCommands } from './commands/buildCommands';
|
||||
import { registerDebugCommands } from './commands/debugCommands';
|
||||
import { registerPrecommitCommands } from './commands/precommitCommands';
|
||||
import { registerPublishCommands } from './commands/publishCommands';
|
||||
import { registerContextMenuCommands } from './commands/contextMenuCommands';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext): void {
|
||||
const root = getRepoRoot();
|
||||
if (!root) {
|
||||
vscode.window.showWarningMessage('MLSysBook Workbench: could not find repo root (book/binder not found).');
|
||||
return;
|
||||
}
|
||||
|
||||
// Tree view providers
|
||||
const buildProvider = new BuildTreeProvider(root);
|
||||
const debugProvider = new DebugTreeProvider();
|
||||
const precommitProvider = new PrecommitTreeProvider();
|
||||
const publishProvider = new PublishTreeProvider();
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.createTreeView('mlsysbook.build', { treeDataProvider: buildProvider }),
|
||||
vscode.window.createTreeView('mlsysbook.debug', { treeDataProvider: debugProvider }),
|
||||
vscode.window.createTreeView('mlsysbook.precommit', { treeDataProvider: precommitProvider }),
|
||||
vscode.window.createTreeView('mlsysbook.publish', { treeDataProvider: publishProvider }),
|
||||
);
|
||||
|
||||
// Refresh command for build tree
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('mlsysbook.refreshBuildTree', () => buildProvider.refresh()),
|
||||
);
|
||||
|
||||
// Register all command groups
|
||||
registerBuildCommands(context);
|
||||
registerDebugCommands(context);
|
||||
registerPrecommitCommands(context);
|
||||
registerPublishCommands(context);
|
||||
registerContextMenuCommands(context);
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
// nothing to clean up
|
||||
}
|
||||
42
book/vscode-ext/src/models/treeItems.ts
Normal file
42
book/vscode-ext/src/models/treeItems.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { ChapterInfo, VolumeId } from '../types';
|
||||
|
||||
export class VolumeTreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly volumeId: VolumeId,
|
||||
label: string,
|
||||
chapterCount: number,
|
||||
) {
|
||||
super(label, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
this.description = `${chapterCount} chapters`;
|
||||
this.contextValue = 'volume';
|
||||
this.iconPath = new vscode.ThemeIcon('book');
|
||||
}
|
||||
}
|
||||
|
||||
export class ChapterTreeItem extends vscode.TreeItem {
|
||||
constructor(public readonly chapter: ChapterInfo) {
|
||||
super(chapter.displayName, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
this.contextValue = 'chapter';
|
||||
this.iconPath = new vscode.ThemeIcon('file-text');
|
||||
this.tooltip = `${chapter.volume}/${chapter.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionTreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
label: string,
|
||||
commandId: string,
|
||||
commandArgs: unknown[],
|
||||
icon?: string,
|
||||
) {
|
||||
super(label, vscode.TreeItemCollapsibleState.None);
|
||||
this.command = {
|
||||
command: commandId,
|
||||
title: label,
|
||||
arguments: commandArgs,
|
||||
};
|
||||
this.iconPath = new vscode.ThemeIcon(icon ?? 'play');
|
||||
this.contextValue = 'action';
|
||||
}
|
||||
}
|
||||
65
book/vscode-ext/src/providers/buildTreeProvider.ts
Normal file
65
book/vscode-ext/src/providers/buildTreeProvider.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { VolumeInfo, BuildFormat } from '../types';
|
||||
import { discoverChapters } from '../utils/chapters';
|
||||
import { VolumeTreeItem, ChapterTreeItem, ActionTreeItem } from '../models/treeItems';
|
||||
|
||||
type TreeNode = VolumeTreeItem | ChapterTreeItem | ActionTreeItem;
|
||||
|
||||
export class BuildTreeProvider implements vscode.TreeDataProvider<TreeNode> {
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode | undefined>();
|
||||
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
private volumes: VolumeInfo[] = [];
|
||||
|
||||
constructor(private repoRoot: string) {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.volumes = discoverChapters(this.repoRoot);
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
|
||||
getTreeItem(element: TreeNode): vscode.TreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
getChildren(element?: TreeNode): TreeNode[] {
|
||||
if (!element) {
|
||||
return this.volumes.map(v =>
|
||||
new VolumeTreeItem(v.id, v.label, v.chapters.length)
|
||||
);
|
||||
}
|
||||
|
||||
if (element instanceof VolumeTreeItem) {
|
||||
const vol = this.volumes.find(v => v.id === element.volumeId);
|
||||
if (!vol) { return []; }
|
||||
|
||||
const chapterItems = vol.chapters.map(ch => new ChapterTreeItem(ch));
|
||||
|
||||
const formats: BuildFormat[] = ['html', 'pdf', 'epub'];
|
||||
const volumeActions = formats.map(fmt =>
|
||||
new ActionTreeItem(
|
||||
`Build Full ${vol.label} (${fmt.toUpperCase()})`,
|
||||
`mlsysbook.buildVolume${fmt.charAt(0).toUpperCase() + fmt.slice(1)}`,
|
||||
[vol.id],
|
||||
'package',
|
||||
)
|
||||
);
|
||||
|
||||
return [...chapterItems, ...volumeActions];
|
||||
}
|
||||
|
||||
if (element instanceof ChapterTreeItem) {
|
||||
const ch = element.chapter;
|
||||
return [
|
||||
new ActionTreeItem('Build HTML', 'mlsysbook.buildChapterHtml', [ch.volume, ch.name], 'globe'),
|
||||
new ActionTreeItem('Build PDF', 'mlsysbook.buildChapterPdf', [ch.volume, ch.name], 'file-pdf'),
|
||||
new ActionTreeItem('Build EPUB', 'mlsysbook.buildChapterEpub', [ch.volume, ch.name], 'book'),
|
||||
new ActionTreeItem('Preview', 'mlsysbook.previewChapter', [ch.volume, ch.name], 'eye'),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
19
book/vscode-ext/src/providers/debugTreeProvider.ts
Normal file
19
book/vscode-ext/src/providers/debugTreeProvider.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { ActionTreeItem } from '../models/treeItems';
|
||||
|
||||
export class DebugTreeProvider implements vscode.TreeDataProvider<ActionTreeItem> {
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<ActionTreeItem | undefined>();
|
||||
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
getTreeItem(element: ActionTreeItem): vscode.TreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
getChildren(): ActionTreeItem[] {
|
||||
return [
|
||||
new ActionTreeItem('Debug Vol1 PDF', 'mlsysbook.debugVolumePdf', ['vol1'], 'bug'),
|
||||
new ActionTreeItem('Debug Vol2 PDF', 'mlsysbook.debugVolumePdf', ['vol2'], 'bug'),
|
||||
new ActionTreeItem('Debug Chapter Sections...', 'mlsysbook.debugChapterSections', [], 'search'),
|
||||
];
|
||||
}
|
||||
}
|
||||
27
book/vscode-ext/src/providers/precommitTreeProvider.ts
Normal file
27
book/vscode-ext/src/providers/precommitTreeProvider.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { PRECOMMIT_HOOKS } from '../constants';
|
||||
import { ActionTreeItem } from '../models/treeItems';
|
||||
|
||||
export class PrecommitTreeProvider implements vscode.TreeDataProvider<ActionTreeItem> {
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<ActionTreeItem | undefined>();
|
||||
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
getTreeItem(element: ActionTreeItem): vscode.TreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
getChildren(): ActionTreeItem[] {
|
||||
const runAll = new ActionTreeItem(
|
||||
'Run ALL Hooks',
|
||||
'mlsysbook.precommitRunAll',
|
||||
[],
|
||||
'checklist',
|
||||
);
|
||||
|
||||
const hookItems = PRECOMMIT_HOOKS.map(h =>
|
||||
new ActionTreeItem(h.label, 'mlsysbook.precommitRunHook', [h.command], 'play')
|
||||
);
|
||||
|
||||
return [runAll, ...hookItems];
|
||||
}
|
||||
}
|
||||
38
book/vscode-ext/src/providers/publishTreeProvider.ts
Normal file
38
book/vscode-ext/src/providers/publishTreeProvider.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { PUBLISH_ACTIONS, MAINTENANCE_ACTIONS } from '../constants';
|
||||
import { ActionTreeItem } from '../models/treeItems';
|
||||
|
||||
type TreeNode = ActionTreeItem | SeparatorItem;
|
||||
|
||||
class SeparatorItem extends vscode.TreeItem {
|
||||
constructor(label: string) {
|
||||
super(label, vscode.TreeItemCollapsibleState.None);
|
||||
this.description = '';
|
||||
this.contextValue = 'separator';
|
||||
}
|
||||
}
|
||||
|
||||
export class PublishTreeProvider implements vscode.TreeDataProvider<TreeNode> {
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode | undefined>();
|
||||
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
getTreeItem(element: TreeNode): vscode.TreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
getChildren(): TreeNode[] {
|
||||
const publishItems = PUBLISH_ACTIONS.map(a =>
|
||||
new ActionTreeItem(a.label, 'mlsysbook.runAction', [a.command], a.icon ?? 'rocket')
|
||||
);
|
||||
|
||||
const maintenanceItems = MAINTENANCE_ACTIONS.map(a =>
|
||||
new ActionTreeItem(a.label, 'mlsysbook.runAction', [a.command], a.icon ?? 'tools')
|
||||
);
|
||||
|
||||
return [
|
||||
...publishItems,
|
||||
new SeparatorItem('--- Maintenance ---'),
|
||||
...maintenanceItems,
|
||||
];
|
||||
}
|
||||
}
|
||||
33
book/vscode-ext/src/types.ts
Normal file
33
book/vscode-ext/src/types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export type VolumeId = 'vol1' | 'vol2';
|
||||
export type BuildFormat = 'html' | 'pdf' | 'epub';
|
||||
|
||||
export interface ChapterInfo {
|
||||
name: string;
|
||||
volume: VolumeId;
|
||||
dirPath: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface VolumeInfo {
|
||||
id: VolumeId;
|
||||
label: string;
|
||||
chapters: ChapterInfo[];
|
||||
}
|
||||
|
||||
export interface PrecommitHook {
|
||||
id: string;
|
||||
label: string;
|
||||
command: string;
|
||||
}
|
||||
|
||||
export interface ActionDef {
|
||||
id: string;
|
||||
label: string;
|
||||
command: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface QmdFileContext {
|
||||
volume: VolumeId;
|
||||
chapter: string;
|
||||
}
|
||||
45
book/vscode-ext/src/utils/chapters.ts
Normal file
45
book/vscode-ext/src/utils/chapters.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { ChapterInfo, VolumeId, VolumeInfo } from '../types';
|
||||
|
||||
const EXCLUDED_DIRS = new Set(['frontmatter', 'backmatter', 'parts', 'glossary']);
|
||||
|
||||
function toDisplayName(dirName: string): string {
|
||||
return dirName
|
||||
.split('_')
|
||||
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
export function discoverChapters(repoRoot: string): VolumeInfo[] {
|
||||
const volumes: VolumeInfo[] = [];
|
||||
|
||||
for (const vol of ['vol1', 'vol2'] as VolumeId[]) {
|
||||
const contentsDir = path.join(repoRoot, 'book', 'quarto', 'contents', vol);
|
||||
if (!fs.existsSync(contentsDir)) { continue; }
|
||||
|
||||
const entries = fs.readdirSync(contentsDir, { withFileTypes: true });
|
||||
const chapters: ChapterInfo[] = entries
|
||||
.filter(e => e.isDirectory() && !EXCLUDED_DIRS.has(e.name))
|
||||
.filter(e => {
|
||||
// Must contain a .qmd file to count as a chapter
|
||||
const dirPath = path.join(contentsDir, e.name);
|
||||
return fs.readdirSync(dirPath).some(f => f.endsWith('.qmd'));
|
||||
})
|
||||
.map(e => ({
|
||||
name: e.name,
|
||||
volume: vol,
|
||||
dirPath: path.join(contentsDir, e.name),
|
||||
displayName: toDisplayName(e.name),
|
||||
}))
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
|
||||
volumes.push({
|
||||
id: vol,
|
||||
label: vol === 'vol1' ? 'Volume I' : 'Volume II',
|
||||
chapters,
|
||||
});
|
||||
}
|
||||
|
||||
return volumes;
|
||||
}
|
||||
12
book/vscode-ext/src/utils/terminal.ts
Normal file
12
book/vscode-ext/src/utils/terminal.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const TERMINAL_NAME = 'MLSysBook';
|
||||
|
||||
export function runInTerminal(command: string, cwd: string): void {
|
||||
let terminal = vscode.window.terminals.find(t => t.name === TERMINAL_NAME);
|
||||
if (!terminal) {
|
||||
terminal = vscode.window.createTerminal({ name: TERMINAL_NAME, cwd });
|
||||
}
|
||||
terminal.show(false);
|
||||
terminal.sendText(command);
|
||||
}
|
||||
17
book/vscode-ext/src/utils/workspace.ts
Normal file
17
book/vscode-ext/src/utils/workspace.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { QmdFileContext, VolumeId } from '../types';
|
||||
|
||||
export function getRepoRoot(): string | undefined {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!folders || folders.length === 0) { return undefined; }
|
||||
return folders[0].uri.fsPath;
|
||||
}
|
||||
|
||||
export function parseQmdFile(uri: vscode.Uri): QmdFileContext | undefined {
|
||||
const match = uri.fsPath.match(/contents\/(vol[12])\/([^/]+)\//);
|
||||
if (!match) { return undefined; }
|
||||
return {
|
||||
volume: match[1] as VolumeId,
|
||||
chapter: match[2],
|
||||
};
|
||||
}
|
||||
15
book/vscode-ext/tsconfig.json
Normal file
15
book/vscode-ext/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2020",
|
||||
"outDir": "out",
|
||||
"lib": ["ES2020"],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
||||
Reference in New Issue
Block a user