feature: Add webview to display the checklist in an editor tab

This commit is contained in:
Keith Solomon
2025-06-22 16:48:18 -05:00
parent 6e0405b192
commit 61ea6fab6b
3 changed files with 101 additions and 37 deletions

View File

@@ -17,8 +17,8 @@
"contributes": {
"commands": [
{
"command": "vscode-project-roadmap.helloWorld",
"title": "Hello World"
"command": "roadmapChecklist.openTab",
"title": "Roadmap: Open Tab"
}
],
"viewsContainers": {

View File

@@ -17,44 +17,21 @@ export function activate(context: vscode.ExtensionContext) {
const doc = vscode.workspace.textDocuments.find(d => d.uri.fsPath === checklistFile);
if (!doc) { return; }
const editor = await vscode.window.showTextDocument(doc, { preview: false });
const lines = doc.getText().split('\n');
const labelRegex = new RegExp(`[-*]\\s+\\[(${item.checked ? 'x' : ' '})\\]\\s+${escapeRegex(item.label)}$`);
const matchIndex = lines.findIndex(line => labelRegex.test(line.trim()));
if (matchIndex === -1) {
vscode.window.showWarningMessage(`Could not find task "${item.label}" in the file.`);
return;
}
const line = lines[matchIndex];
const newCheck = item.checked ? '[ ]' : '[x]';
const newLine = line.replace(/\[( |x)\]/, newCheck);
const edit = new vscode.WorkspaceEdit();
const uri = doc.uri;
const lines = doc.getText().split('\n');
const index = lines.findIndex(line =>
line.trim().match(/^[-*]\s+\[[ xX]\]/) &&
line.includes(item.label)
);
if (index === -1) { return; }
edit.replace(uri, doc.lineAt(matchIndex).range, newLine);
// If parent, toggle all children too
if (item.children.length > 0) {
const indentLevel = line.match(/^(\s*)/)?.[1].length || 0;
for (let i = matchIndex + 1; i < lines.length; i++) {
const thisLine = lines[i];
const currentIndent = thisLine.match(/^(\s*)/)?.[1].length || 0;
if (currentIndent <= indentLevel) { break; }
if (/^\s*[-*]\s+\[( |x)\]/.test(thisLine)) {
const toggled = thisLine.replace(/\[( |x)\]/, newCheck);
edit.replace(uri, doc.lineAt(i).range, toggled);
}
}
}
const line = lines[index];
const toggledLine = line.replace(/\[(x| )\]/i, item.checked ? '[ ]' : '[x]');
const range = new vscode.Range(new vscode.Position(index, 0), new vscode.Position(index, line.length));
edit.replace(doc.uri, range, toggledLine);
await vscode.workspace.applyEdit(edit);
await doc.save();
await doc.save(); // ✅ Triggers the existing onDidSaveTextDocument → refresh()
}),
vscode.commands.registerCommand('roadmapChecklist.reveal', async (item: RoadmapItem) => {
treeView.reveal(item, { expand: true });

View File

@@ -7,15 +7,28 @@ export class RoadmapItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public collapsibleState: vscode.TreeItemCollapsibleState,
checked: boolean = false
) {
super(label, collapsibleState);
this.checked = checked;
if (collapsibleState === vscode.TreeItemCollapsibleState.None) {
// this is a task
this.description = checked ? '✅ Done' : '';
this.iconPath = new vscode.ThemeIcon(checked ? 'check' : 'circle-large-outline');
this.command = {
command: 'roadmap.toggleCheckbox',
title: 'Toggle Task',
arguments: [this]
};
} else {
// this is a phase
this.command = {
command: 'roadmapChecklist.openTab',
title: 'Open Tab View',
arguments: [this]
};
}
}
@@ -51,6 +64,7 @@ export class RoadmapTreeProvider implements vscode.TreeDataProvider<RoadmapItem>
readonly onDidChangeTreeData: vscode.Event<RoadmapItem | undefined | void> = this._onDidChangeTreeData.event;
private items: RoadmapItem[] = [];
private panel: vscode.WebviewPanel | null = null;
constructor(
private readonly checklistPath: string,
@@ -62,6 +76,10 @@ export class RoadmapTreeProvider implements vscode.TreeDataProvider<RoadmapItem>
this.refresh();
}
});
vscode.commands.registerCommand('roadmapChecklist.openTab', (item?: RoadmapItem) => {
this.showWebview();
});
}
refresh(): void {
@@ -77,6 +95,10 @@ export class RoadmapTreeProvider implements vscode.TreeDataProvider<RoadmapItem>
vscode.commands.executeCommand('roadmapChecklist.reveal', firstOpenPhase, { expand: true });
}, 100);
}
if (this.panel) {
this.renderWebview();
}
}
getTreeItem(element: RoadmapItem): vscode.TreeItem {
@@ -167,6 +189,71 @@ export class RoadmapTreeProvider implements vscode.TreeDataProvider<RoadmapItem>
return { items, firstOpenPhase };
}
private showWebview() {
if (this.panel) {
this.panel.reveal(vscode.ViewColumn.One);
return;
}
this.panel = vscode.window.createWebviewPanel(
'roadmapTab',
'Roadmap Overview',
vscode.ViewColumn.One,
{ enableScripts: true }
);
this.renderWebview();
this.panel.onDidDispose(() => {
this.panel = null;
});
}
private renderTasks(items: RoadmapItem[]): string {
if (!items.length) {return '';}
return `<ul>` + items.map(item => {
const icon = item.checked ? '✅' : '⬜';
return `<li>${icon} ${item.label}${this.renderTasks(item.children)}</li>`;
}).join('') + `</ul>`;
}
private buildWebviewHtml(items: RoadmapItem[], webview: vscode.Webview): string {
const rows = items.map(phase => {
const taskList = this.renderTasks(phase.children);
return `
<h2>${phase.label}</h2>
<p>${phase.description}</p>
<ul>${taskList}</ul>
`;
}).join('');
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src ${webview.cspSource};">
<style>
body { font-family: sans-serif; padding: 1em; }
h2 { margin-top: 1em; }
ul { padding-left: 1em; list-style-type: none; }
li { list-style-type: none; }
</style>
</head>
<body>
${rows}
</body>
</html>
`;
}
private renderWebview() {
if (this.panel) {
this.panel.webview.html = this.buildWebviewHtml(this.items, this.panel.webview);
}
}
}
function findParentRecursive(parent: RoadmapItem, child: RoadmapItem): RoadmapItem | null {