From 61ea6fab6bb47fcc4c984c7680e6b4a2bfcfbf02 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sun, 22 Jun 2025 16:48:18 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feature:=20Add=20webview=20to=20displa?= =?UTF-8?q?y=20the=20checklist=20in=20an=20editor=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +-- src/extension.ts | 45 ++++++----------------- src/roadmapTree.ts | 89 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 7aa3378..9f910ba 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "contributes": { "commands": [ { - "command": "vscode-project-roadmap.helloWorld", - "title": "Hello World" + "command": "roadmapChecklist.openTab", + "title": "Roadmap: Open Tab" } ], "viewsContainers": { diff --git a/src/extension.ts b/src/extension.ts index de3b958..a661d86 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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 }); diff --git a/src/roadmapTree.ts b/src/roadmapTree.ts index 22873f4..957476d 100644 --- a/src/roadmapTree.ts +++ b/src/roadmapTree.ts @@ -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 readonly onDidChangeTreeData: vscode.Event = 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 this.refresh(); } }); + + vscode.commands.registerCommand('roadmapChecklist.openTab', (item?: RoadmapItem) => { + this.showWebview(); + }); } refresh(): void { @@ -77,6 +95,10 @@ export class RoadmapTreeProvider implements vscode.TreeDataProvider 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 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 `
    ` + items.map(item => { + const icon = item.checked ? '✅' : '⬜'; + return `
  • ${icon} ${item.label}${this.renderTasks(item.children)}
  • `; + }).join('') + `
`; + } + + private buildWebviewHtml(items: RoadmapItem[], webview: vscode.Webview): string { + const rows = items.map(phase => { + const taskList = this.renderTasks(phase.children); + + return ` +

${phase.label}

+

${phase.description}

+
    ${taskList}
+ `; + }).join(''); + + return ` + + + + + + + + + ${rows} + + + `; + } + + private renderWebview() { + if (this.panel) { + this.panel.webview.html = this.buildWebviewHtml(this.items, this.panel.webview); + } + } } function findParentRecursive(parent: RoadmapItem, child: RoadmapItem): RoadmapItem | null {