import path from 'path'; import { CancellationToken, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window } from 'vscode'; import { openSetDueDateInputbox } from '../commands/setDueDate'; import { decrementCountForTask, editTask, editTaskRawText, incrementCountForTask, revealTask, startTaskAtLine, toggleDoneAtLine, toggleTaskCollapse, toggleTaskCollapseRecursive, tryToDeleteTask } from '../documentActions'; import { updateEverything } from '../events'; import { $config, $state, Global } from '../extension'; import { MessageFromWebview, MessageToWebview } from '../types'; import { getActiveOrDefaultDocument } from '../utils/extensionUtils'; import { getTaskAtLineExtension } from '../utils/taskUtils'; import { followLink } from '../utils/vscodeUtils'; import { getNonce } from './webviewUtils'; export class TasksWebviewViewProvider implements WebviewViewProvider { public static readonly viewType = 'todomd.webviewTasks'; private _view?: WebviewView; constructor( private readonly _extensionUri: Uri, ) { } public resolveWebviewView( webviewView: WebviewView, context: WebviewViewResolveContext, _token: CancellationToken, ) { this._view = webviewView; const localResourceRoots = [ this._extensionUri, ]; if ($config.webview.customCSSPath) { localResourceRoots.push(Uri.file(path.dirname($config.webview.customCSSPath))); } webviewView.webview.options = { enableScripts: true, enableCommandUris: true, localResourceRoots, }; webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); webviewView.webview.onDidReceiveMessage(async (message: MessageFromWebview) => { switch (message.type) { // ──── Needs to update everything ──────────────────────────── case 'webviewLoaded': { this.sendEverything(); break; } case 'toggleDone': { await toggleDoneAtLine(await getActiveOrDefaultDocument(), message.value); await updateEverything(); break; } case 'toggleTaskCollapse': { await toggleTaskCollapse(await getActiveOrDefaultDocument(), message.value); await updateEverything(); break; } case 'editTask': { await editTask(await getActiveOrDefaultDocument(), message.value); await updateEverything(); break; } case 'startTask': { await startTaskAtLine(message.value, await getActiveOrDefaultDocument()); await updateEverything(); break; } case 'toggleTaskCollapseRecursive': { await toggleTaskCollapseRecursive(await getActiveOrDefaultDocument(), message.value); await updateEverything(); break; } case 'incrementCount': { await incrementCountForTask(await getActiveOrDefaultDocument(), message.value, getTaskAtLineExtension(message.value)!); await updateEverything(); break; } case 'decrementCount': { await decrementCountForTask(await getActiveOrDefaultDocument(), message.value, getTaskAtLineExtension(message.value)!); await updateEverything(); break; } case 'deleteTask': { await tryToDeleteTask(await getActiveOrDefaultDocument(), message.value); await updateEverything(); break; } case 'editTaskRawText': { await editTaskRawText(await getActiveOrDefaultDocument(), message.value.lineNumber, message.value.newRawText); await updateEverything(); break; } // ──── No need to update everything ────────────────────────── case 'showNotification': { window.showInformationMessage(message.value); break; } case 'goToTask': { revealTask(message.value); break; } case 'updateWebviewTitle': { this.updateTitle(message.value); break; } case 'followLink': { followLink(message.value); break; } case 'setDueDate': { openSetDueDateInputbox(await getActiveOrDefaultDocument(), [message.value]); break; } } }); /** * Update webview on it's visibility change (only when it becomes visible). */ webviewView.onDidChangeVisibility(e => { if (webviewView.visible === true) { this.sendEverything(); } }); } /** * Send all the needed data to the webview view */ sendEverything() { if (this._view && this._view.visible === true) { this.sendMessageToWebview({ type: 'updateEverything', value: { tasksAsTree: $state.tasksAsTree, tags: $state.tags, projects: $state.projects, contexts: $state.contexts, defaultFileSpecified: Boolean($config.defaultFile), activeDocumentOpened: Boolean($state.activeDocument), config: $config.webview, }, }); this.updateTitle($state.tasksAsTree.length); } } /** * Update webview title (counter). */ updateTitle(numberOfTasks: number) { if (this._view) { this._view.title = `webview (${numberOfTasks})`; } } /** * Focus main input in webview. */ focusFilterInput() { this.sendMessageToWebview({ type: 'focusFilterInput', }); } /** * Send message. js objects that will be serialized to json. */ private sendMessageToWebview(message: MessageToWebview) { this._view?.webview.postMessage(message); } /** * Generate html template for webview. */ private _getHtmlForWebview(webview: Webview) { const JSUri = webview.asWebviewUri(Uri.joinPath(this._extensionUri, 'media', 'webview.js')); const CSSUri = webview.asWebviewUri(Uri.joinPath(this._extensionUri, 'media', 'webview.css')); const codiconCSSUri = webview.asWebviewUri(Uri.joinPath(this._extensionUri, 'media', 'vendor', 'codicon.css')); const nonce = getNonce();// Use a nonce to only allow a specific script to be run. const userCSSLink = $config.webview.customCSSPath ? `<link href="${webview.asWebviewUri(Uri.file($config.webview.customCSSPath))}" rel="stylesheet">` : ''; return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src 'unsafe-inline' ${webview.cspSource}; script-src 'nonce-${nonce}';"> <link href="${codiconCSSUri}" rel="stylesheet" /> <link href="${CSSUri}" rel="stylesheet"> ${userCSSLink} <title>Tasks</title> </head> <body> <div id="app"></div> <script defer nonce="${nonce}" src="${JSUri}"></script> </body> </html>`; } } /** * Update main webview view (tasks) */ export function updateWebviewView() { if (Global.webviewProvider) { Global.webviewProvider.sendEverything(); } }