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:
Vijay Janapa Reddi
2026-01-31 19:46:54 -05:00
parent 06f9a8fbb8
commit 9e257f960f
22 changed files with 731 additions and 0 deletions

3
book/vscode-ext/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
out/
node_modules/
*.vsix

View 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
View 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"
}
}
}

View 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" }
]
}
}
}

View 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

View 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);
})
);
}
}

View 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);
}),
);
}

View 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);
})
);
}

View 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);
}),
);
}

View 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);
}),
);
}

View 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' },
];

View 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
}

View 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';
}
}

View 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 [];
}
}

View 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'),
];
}
}

View 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];
}
}

View 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,
];
}
}

View 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;
}

View 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;
}

View 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);
}

View 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],
};
}

View 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"]
}