✨feature: Add webview to display the checklist in an editor tab
This commit is contained in:
@@ -17,8 +17,8 @@
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "vscode-project-roadmap.helloWorld",
|
||||
"title": "Hello World"
|
||||
"command": "roadmapChecklist.openTab",
|
||||
"title": "Roadmap: Open Tab"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user