From 8ea5ad7f9a498fd282a25c71748a303d8d1995f4 Mon Sep 17 00:00:00 2001 From: Enguerrand de Ribaucourt Date: Tue, 7 Jan 2025 17:55:26 +0100 Subject: [PATCH 1/8] Template: Add DependsDotView based on the webview view sample Import the base VS Code sample from https://github.com/microsoft/vscode-extension-samples/tree/main/webview-view-sample We'll use this new view to display the dependency graph of recipes. --- client/src/extension.ts | 4 ++ client/src/ui/DependsDotView.ts | 110 ++++++++++++++++++++++++++++++ client/web/depends-dot/main.css | 3 + client/web/depends-dot/main.html | 29 ++++++++ client/web/depends-dot/main.js | 98 ++++++++++++++++++++++++++ client/web/depends-dot/reset.css | 31 +++++++++ client/web/depends-dot/vscode.css | 92 +++++++++++++++++++++++++ package.json | 8 +++ 8 files changed, 375 insertions(+) create mode 100644 client/src/ui/DependsDotView.ts create mode 100644 client/web/depends-dot/main.css create mode 100644 client/web/depends-dot/main.html create mode 100644 client/web/depends-dot/main.js create mode 100644 client/web/depends-dot/reset.css create mode 100644 client/web/depends-dot/vscode.css diff --git a/client/src/extension.ts b/client/src/extension.ts index 32be5f3d..e5364ad8 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -28,6 +28,7 @@ import { type BitbakeScanResult, scanContainsData } from './lib/src/types/Bitbak import { reviewDiagnostics } from './language/diagnosticsSupport' import { embeddedLanguageDocsManager } from './language/EmbeddedLanguageDocsManager' import { NotificationMethod } from './lib/src/types/notifications' +import { DependsDotView } from './ui/DependsDotView' let client: LanguageClient const bitbakeDriver: BitbakeDriver = new BitbakeDriver() @@ -37,6 +38,7 @@ const bitbakeWorkspace: BitbakeWorkspace = new BitbakeWorkspace() let bitbakeRecipesView: BitbakeRecipesView | undefined let devtoolWorkspacesView: DevtoolWorkspacesView | undefined let terminalProvider: BitbakeTerminalProfileProvider | undefined +let dependsDotWebview: DependsDotView | undefined export function canModifyConfig (): boolean { const disableConfigModification = vscode.workspace.getConfiguration('bitbake').get('disableConfigModification') @@ -160,6 +162,8 @@ export async function activate (context: vscode.ExtensionContext): Promise bitbakeRecipesView.registerView(context) devtoolWorkspacesView = new DevtoolWorkspacesView(bitBakeProjectScanner) devtoolWorkspacesView.registerView(context) + dependsDotWebview = new DependsDotView(bitbakeDriver, context.extensionUri) + dependsDotWebview.registerView(context) void vscode.commands.executeCommand('setContext', 'bitbake.active', true) const bitbakeStatusBar = new BitbakeStatusBar(bitBakeProjectScanner) context.subscriptions.push(bitbakeStatusBar.statusBarItem) diff --git a/client/src/ui/DependsDotView.ts b/client/src/ui/DependsDotView.ts new file mode 100644 index 00000000..570007ef --- /dev/null +++ b/client/src/ui/DependsDotView.ts @@ -0,0 +1,110 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2023 Savoir-faire Linux. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as vscode from 'vscode' +import { BitbakeDriver } from '../driver/BitbakeDriver' + +export class DependsDotView { + private readonly provider: DependsDotViewProvider + + constructor (bitbakeDriver: BitbakeDriver, extensionUri: vscode.Uri) { + this.provider = new DependsDotViewProvider(bitbakeDriver, extensionUri) + } + + registerView (context: vscode.ExtensionContext): void { + context.subscriptions.push(vscode.window.registerWebviewViewProvider( + DependsDotViewProvider.viewType, + this.provider)) + } +} + +class DependsDotViewProvider implements vscode.WebviewViewProvider { + private readonly bitbakeDriver: BitbakeDriver + public static readonly viewType = "bitbake.oeDependsDot" + private view?: vscode.WebviewView; + private extensionUri: vscode.Uri + + constructor (bitbakeDriver: BitbakeDriver, extensionUri: vscode.Uri) { + this.bitbakeDriver = bitbakeDriver + this.extensionUri = extensionUri + } + + resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken): Thenable | void { + this.view = webviewView; + + webviewView.webview.options = { + // Allow scripts in the webview + enableScripts: true, + + localResourceRoots: [ + this.extensionUri + ] + }; + + webviewView.webview.html = this.getHtmlForWebview(webviewView.webview); + + webviewView.webview.onDidReceiveMessage(data => { + switch (data.type) { + case 'colorSelected': + { + vscode.window.activeTextEditor?.insertSnippet(new vscode.SnippetString(`#${data.value}`)); + break; + } + } + }); + } + + private getHtmlForWebview(webview: vscode.Webview) { + // Get the local path to main script run in the webview, then convert it to a uri we can use in the webview. + const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'main.js')); + + // Do the same for the stylesheet. + const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'reset.css')); + const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'vscode.css')); + const styleMainUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'main.css')); + + // Use a nonce to only allow a specific script to be run. + const nonce = this.getNonce(); + + return ` + + + + + + + + + + + + + + Cat Colors + + +
    +
+ + + + + + `; + } + + private getNonce() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; + } +} diff --git a/client/web/depends-dot/main.css b/client/web/depends-dot/main.css new file mode 100644 index 00000000..f2ef3933 --- /dev/null +++ b/client/web/depends-dot/main.css @@ -0,0 +1,3 @@ +body { + background-color: transparent; +} diff --git a/client/web/depends-dot/main.html b/client/web/depends-dot/main.html new file mode 100644 index 00000000..1512c566 --- /dev/null +++ b/client/web/depends-dot/main.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + Cat Colors + + +
    +
+ + + + + + diff --git a/client/web/depends-dot/main.js b/client/web/depends-dot/main.js new file mode 100644 index 00000000..9bc12cde --- /dev/null +++ b/client/web/depends-dot/main.js @@ -0,0 +1,98 @@ +//@ts-check + +// This script will be run within the webview itself +// It cannot access the main VS Code APIs directly. +(function () { + const vscode = acquireVsCodeApi(); + + const oldState = vscode.getState() || { colors: [] }; + + /** @type {Array<{ value: string }>} */ + let colors = oldState.colors; + + updateColorList(colors); + + document.querySelector('.add-color-button').addEventListener('click', () => { + addColor(); + }); + + // Handle messages sent from the extension to the webview + window.addEventListener('message', event => { + const message = event.data; // The json data that the extension sent + switch (message.type) { + case 'addColor': + { + addColor(); + break; + } + case 'clearColors': + { + colors = []; + updateColorList(colors); + break; + } + + } + }); + + /** + * @param {Array<{ value: string }>} colors + */ + function updateColorList(colors) { + const ul = document.querySelector('.color-list'); + ul.textContent = ''; + for (const color of colors) { + const li = document.createElement('li'); + li.className = 'color-entry'; + + const colorPreview = document.createElement('div'); + colorPreview.className = 'color-preview'; + colorPreview.style.backgroundColor = `#${color.value}`; + colorPreview.addEventListener('click', () => { + onColorClicked(color.value); + }); + li.appendChild(colorPreview); + + const input = document.createElement('input'); + input.className = 'color-input'; + input.type = 'text'; + input.value = color.value; + input.addEventListener('change', (e) => { + const value = e.target.value; + if (!value) { + // Treat empty value as delete + colors.splice(colors.indexOf(color), 1); + } else { + color.value = value; + } + updateColorList(colors); + }); + li.appendChild(input); + + ul.appendChild(li); + } + + // Update the saved state + vscode.setState({ colors: colors }); + } + + /** + * @param {string} color + */ + function onColorClicked(color) { + vscode.postMessage({ type: 'colorSelected', value: color }); + } + + /** + * @returns string + */ + function getNewCalicoColor() { + const colors = ['020202', 'f1eeee', 'a85b20', 'daab70', 'efcb99']; + return colors[Math.floor(Math.random() * colors.length)]; + } + + function addColor() { + colors.push({ value: getNewCalicoColor() }); + updateColorList(colors); + } +}()); diff --git a/client/web/depends-dot/reset.css b/client/web/depends-dot/reset.css new file mode 100644 index 00000000..717b7cbe --- /dev/null +++ b/client/web/depends-dot/reset.css @@ -0,0 +1,31 @@ +/* From https://github.com/microsoft/vscode-extension-samples/blob/5ddd30fc052e03bbec52e5d84627eaa543fb0de8/webview-view-sample/media/vscode.css under MIT license */ +html { + box-sizing: border-box; + font-size: 13px; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +body, +h1, +h2, +h3, +h4, +h5, +h6, +p, +ol, +ul { + margin: 0; + padding: 0; + font-weight: normal; +} + +img { + max-width: 100%; + height: auto; +} diff --git a/client/web/depends-dot/vscode.css b/client/web/depends-dot/vscode.css new file mode 100644 index 00000000..082e24ec --- /dev/null +++ b/client/web/depends-dot/vscode.css @@ -0,0 +1,92 @@ +/* From https://github.com/microsoft/vscode-extension-samples/blob/5ddd30fc052e03bbec52e5d84627eaa543fb0de8/webview-view-sample/media/vscode.css under MIT license */ +:root { + --container-paddding: 20px; + --input-padding-vertical: 6px; + --input-padding-horizontal: 4px; + --input-margin-vertical: 4px; + --input-margin-horizontal: 0; +} + +body { + padding: 0 var(--container-paddding); + color: var(--vscode-foreground); + font-size: var(--vscode-font-size); + font-weight: var(--vscode-font-weight); + font-family: var(--vscode-font-family); + background-color: var(--vscode-editor-background); +} + +ol, +ul { + padding-left: var(--container-paddding); +} + +body > *, +form > * { + margin-block-start: var(--input-margin-vertical); + margin-block-end: var(--input-margin-vertical); +} + +*:focus { + outline-color: var(--vscode-focusBorder) !important; +} + +a { + color: var(--vscode-textLink-foreground); +} + +a:hover, +a:active { + color: var(--vscode-textLink-activeForeground); +} + +code { + font-size: var(--vscode-editor-font-size); + font-family: var(--vscode-editor-font-family); +} + +button { + border: none; + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + width: 100%; + text-align: center; + outline: 1px solid transparent; + outline-offset: 2px !important; + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); +} + +button:hover { + cursor: pointer; + background: var(--vscode-button-hoverBackground); +} + +button:focus { + outline-color: var(--vscode-focusBorder); +} + +button.secondary { + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); +} + +button.secondary:hover { + background: var(--vscode-button-secondaryHoverBackground); +} + +input:not([type='checkbox']), +textarea { + display: block; + width: 100%; + border: none; + font-family: var(--vscode-font-family); + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + color: var(--vscode-input-foreground); + outline-color: var(--vscode-input-border); + background-color: var(--vscode-input-background); +} + +input::placeholder, +textarea::placeholder { + color: var(--vscode-input-placeholderForeground); +} diff --git a/package.json b/package.json index e0beacc7..52f4add3 100644 --- a/package.json +++ b/package.json @@ -559,6 +559,14 @@ "contextualTitle": "Devtool workspaces", "icon": "$(symbol-property)", "when": "bitbake.active" + }, + { + "type": "webview", + "id": "bitbake.oeDependsDot", + "name": "Dependency Graph", + "contextualTitle": "Dependency graph", + "icon": "$(graph)", + "when": "bitbake.active" } ] }, From 67cce1664669dd9515e5e7a4a7d63e48bfc11474 Mon Sep 17 00:00:00 2001 From: Enguerrand de Ribaucourt Date: Wed, 8 Jan 2025 09:29:40 +0100 Subject: [PATCH 2/8] WIP: Feature: Add dependsDot command --- client/src/ui/BitbakeCommands.ts | 26 ++++++++++++++++++++++++++ package.json | 14 ++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/client/src/ui/BitbakeCommands.ts b/client/src/ui/BitbakeCommands.ts index ef06c134..09473da0 100644 --- a/client/src/ui/BitbakeCommands.ts +++ b/client/src/ui/BitbakeCommands.ts @@ -54,6 +54,8 @@ export function registerBitbakeCommands (context: vscode.ExtensionContext, bitba vscode.commands.registerCommand('bitbake.stop-toaster', async () => { await stopToaster(bitBakeProjectScanner.bitbakeDriver) }), vscode.commands.registerCommand('bitbake.clear-workspace-state', async () => { await clearAllWorkspaceState(context) }), vscode.commands.registerCommand('bitbake.examine-dependency-taskexp', async (uri) => { await examineDependenciesTaskexp(bitbakeWorkspace, bitBakeProjectScanner, uri) }), + // Repurpose to be per recipe? (and ask for image if not already supplied) + vscode.commands.registerCommand('bitbake.oe-depends-dot', async (uri) => { await runOeDependsDot(bitbakeWorkspace, bitBakeProjectScanner, uri) }), // Handles enqueued parsing requests (onSave) vscode.tasks.onDidEndTask((e) => { if (e.execution.task.name === 'Bitbake: Parse') { @@ -615,6 +617,30 @@ export async function examineDependenciesTaskexp (bitbakeWorkspace: BitbakeWorks } } +export async function runOeDependsDot (bitbakeWorkspace: BitbakeWorkspace, bitBakeProjectScanner: BitBakeProjectScanner, uri?: unknown): Promise { + if (isTaskexpStarted) { + void vscode.window.showInformationMessage('taskexp is already started') + return + } + const chosenImage = await selectRecipe(bitbakeWorkspace, bitBakeProjectScanner, uri) + const chosenRecipe = await selectRecipe(bitbakeWorkspace, bitBakeProjectScanner, uri) + if (chosenImage !== undefined && chosenRecipe !== undefined) { + logger.debug(`Command: oe-depends-dot: ${chosenRecipe}`) + isTaskexpStarted = true + const process = await runBitbakeTerminal(bitBakeProjectScanner.bitbakeDriver, + { + specialCommand: `bitbake -g ${chosenRecipe} -u taskexp` + } as BitbakeTaskDefinition, + `Bitbake: taskexp: ${chosenRecipe}`) + process.onExit((e) => { + isTaskexpStarted = false + if (e.exitCode !== 0) { + void vscode.window.showErrorMessage(`Failed to start taskexp with exit code ${e.exitCode}. See terminal output.`) + } + }) + } +} + async function openBitbakeDevshell (terminalProvider: BitbakeTerminalProfileProvider, bitbakeWorkspace: BitbakeWorkspace, bitBakeProjectScanner: BitBakeProjectScanner, uri?: unknown): Promise { const chosenRecipe = await selectRecipe(bitbakeWorkspace, bitBakeProjectScanner, uri) if (chosenRecipe === undefined) return diff --git a/package.json b/package.json index 52f4add3..4f056e69 100644 --- a/package.json +++ b/package.json @@ -467,6 +467,11 @@ "title": "BitBake: Examine recipe's dependencies with taskexp", "description": "Examine the recipe's dependencies with taskexp." }, + { + "command": "bitbake.oe-depends-dot", + "title": "BitBake: Generate a dependency graph with oe-depends-dot", + "description": "Generate a dependency graph with oe-depends-dot" + }, { "command": "bitbake.devtool-modify", "title": "BitBake: Devtool: Modify recipe", @@ -634,6 +639,10 @@ "command": "bitbake.pick-configuration", "group": "1@bitbake_dev@6" }, + { + "command": "bitbake.oe-depends-dot", + "group": "1@bitbake_dev@7" + }, { "command": "bitbake.devtool-modify", "group": "2@bitbake_devtool@0" @@ -826,6 +835,11 @@ "group": "1@bitbake_dev@4", "when": "viewItem == bitbakeRecipeCtx" }, + { + "command": "bitbake.oe-depends-dot", + "group": "1@bitbake_dev@5", + "when": "viewItem == bitbakeRecipeCtx" + }, { "command": "bitbake.devtool-modify", "group": "2@bitbake_devtool@0", From 51d756528b6acaad8f8f5049b20f16761d1515b8 Mon Sep 17 00:00:00 2001 From: Enguerrand de Ribaucourt Date: Wed, 8 Jan 2025 10:36:20 +0100 Subject: [PATCH 3/8] Copyright: Update copyright year to 2025 --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 872fb82a..dd613aeb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -31,7 +31,7 @@ export default [ { pattern: " \\* Copyright \\(c\\) .*\\. All rights reserved\\.", template: - " * Copyright (c) 2023 Savoir-faire Linux. All rights reserved.", + " * Copyright (c) 2025 Savoir-faire Linux. All rights reserved.", }, " * Licensed under the MIT License. See License.txt in the project root for license information.", " * ------------------------------------------------------------------------------------------ ", From 3e2e7e157dddba024e8991da75d4866df13c96b9 Mon Sep 17 00:00:00 2001 From: Enguerrand de Ribaucourt Date: Wed, 8 Jan 2025 11:02:22 +0100 Subject: [PATCH 4/8] Feature: Use EJS to render webview view HTML EJS is the traditional templating engine for Node.js. It is used to generate HTML with plain JavaScript. --- client/package-lock.json | 141 +++++++++++++++++++++++++++++++ client/package.json | 2 + client/src/ui/DependsDotView.ts | 49 +++-------- client/web/depends-dot/main.html | 10 +-- 4 files changed, 162 insertions(+), 40 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 6a51bda8..01ff6476 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,12 +12,14 @@ "src/lib" ], "dependencies": { + "ejs": "^3.1.10", "find": "^0.3.0", "node-pty": "^1.0.0", "semver": "^7.6.3", "vscode-languageclient": "^9.0.1" }, "devDependencies": { + "@types/ejs": "^3.1.5", "@types/find": "^0.2.4", "@types/semver": "^7.5.8", "@types/vscode": "^1.96.0" @@ -55,6 +57,12 @@ "node": "*" } }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, "node_modules/@types/find": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@types/find/-/find-0.2.4.tgz", @@ -73,6 +81,25 @@ "integrity": "sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==", "dev": true }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, "node_modules/balanced-match": { "version": "1.0.2", "license": "MIT" @@ -84,6 +111,64 @@ "balanced-match": "^1.0.0" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, "node_modules/find": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/find/-/find-0.3.0.tgz", @@ -92,6 +177,51 @@ "traverse-chain": "~0.1.0" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimatch": { "version": "5.1.6", "license": "ISC", @@ -127,6 +257,17 @@ "node": ">=10" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/traverse-chain": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", diff --git a/client/package.json b/client/package.json index fc2c934e..2dd84b66 100644 --- a/client/package.json +++ b/client/package.json @@ -25,12 +25,14 @@ "vscode": "^1.92.0" }, "dependencies": { + "ejs": "^3.1.10", "find": "^0.3.0", "node-pty": "^1.0.0", "semver": "^7.6.3", "vscode-languageclient": "^9.0.1" }, "devDependencies": { + "@types/ejs": "^3.1.5", "@types/find": "^0.2.4", "@types/semver": "^7.5.8", "@types/vscode": "^1.96.0" diff --git a/client/src/ui/DependsDotView.ts b/client/src/ui/DependsDotView.ts index 570007ef..e4be90ab 100644 --- a/client/src/ui/DependsDotView.ts +++ b/client/src/ui/DependsDotView.ts @@ -4,6 +4,7 @@ * ------------------------------------------------------------------------------------------ */ import * as vscode from 'vscode' +import ejs from 'ejs' import { BitbakeDriver } from '../driver/BitbakeDriver' export class DependsDotView { @@ -31,7 +32,7 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { this.extensionUri = extensionUri } - resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken): Thenable | void { + async resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken): Promise { this.view = webviewView; webviewView.webview.options = { @@ -43,7 +44,7 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { ] }; - webviewView.webview.html = this.getHtmlForWebview(webviewView.webview); + webviewView.webview.html = await this.getHtmlForWebview(webviewView.webview); webviewView.webview.onDidReceiveMessage(data => { switch (data.type) { @@ -56,11 +57,9 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { }); } - private getHtmlForWebview(webview: vscode.Webview) { - // Get the local path to main script run in the webview, then convert it to a uri we can use in the webview. + private getHtmlForWebview(webview: vscode.Webview): Promise { + const htmlUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'main.html')); const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'main.js')); - - // Do the same for the stylesheet. const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'reset.css')); const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'vscode.css')); const styleMainUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'main.css')); @@ -68,35 +67,15 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { // Use a nonce to only allow a specific script to be run. const nonce = this.getNonce(); - return ` - - - - - - - - - - - - - - Cat Colors - - -
    -
- - - - - - `; + const html = ejs.renderFile(htmlUri.fsPath, { + nonce: nonce, + scriptUri: scriptUri, + styleResetUri: styleResetUri, + styleVSCodeUri: styleVSCodeUri, + styleMainUri: styleMainUri, + webview: webview + }); + return html; } private getNonce() { diff --git a/client/web/depends-dot/main.html b/client/web/depends-dot/main.html index 1512c566..a5457561 100644 --- a/client/web/depends-dot/main.html +++ b/client/web/depends-dot/main.html @@ -8,13 +8,13 @@ and only allow scripts that have a specific nonce. (See the 'webview-sample' extension sample for img-src content security policy examples) --> - + - - - + + + Cat Colors @@ -24,6 +24,6 @@ - + From 5e604d542ab3de8ce0dd8a2310b2c922806fb0df Mon Sep 17 00:00:00 2001 From: Enguerrand de Ribaucourt Date: Wed, 8 Jan 2025 14:58:11 +0100 Subject: [PATCH 5/8] Feature: Implement a basic OE Depends Dot view --- client/src/ui/DependsDotView.ts | 116 ++++++++++++++++++++++++++++--- client/web/depends-dot/main.html | 45 ++++++++++-- client/web/depends-dot/main.js | 103 +++++++-------------------- package.json | 4 +- 4 files changed, 175 insertions(+), 93 deletions(-) diff --git a/client/src/ui/DependsDotView.ts b/client/src/ui/DependsDotView.ts index e4be90ab..7ea2d317 100644 --- a/client/src/ui/DependsDotView.ts +++ b/client/src/ui/DependsDotView.ts @@ -6,6 +6,24 @@ import * as vscode from 'vscode' import ejs from 'ejs' import { BitbakeDriver } from '../driver/BitbakeDriver' +import { logger } from '../lib/src/utils/OutputLogger' +import { BitbakeTaskDefinition } from './BitbakeTaskProvider' +import { runBitbakeTerminal } from './BitbakeTerminal' +import { finishProcessExecution } from '../utils/ProcessUtils' + +/* + TODO Beautify the view + - make div elements side by side + - Add some spacing + TODO Display a graph rather than text + TODO Make the graph interactive (click on elements open their .bb file) (bonus: right click brings the commands menu) + TODO Auto-refresh the dotfile when needed when click dependsDot + TODO display a checkbox wether the graph is up-to-date, successful or failed + TODO gray out the results when the graph or package is not up-to-date + TODO Use the select recipe command to get the image recipe list (not for the packageName though?) + TODO Add tests for this feature + TODO Save field values on workspace reload +*/ export class DependsDotView { private readonly provider: DependsDotViewProvider @@ -27,6 +45,10 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { private view?: vscode.WebviewView; private extensionUri: vscode.Uri + private depType: string = "-w"; + private graphRecipe: string = ""; + private packageName: string = ""; + constructor (bitbakeDriver: BitbakeDriver, extensionUri: vscode.Uri) { this.bitbakeDriver = bitbakeDriver this.extensionUri = extensionUri @@ -46,17 +68,30 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { webviewView.webview.html = await this.getHtmlForWebview(webviewView.webview); - webviewView.webview.onDidReceiveMessage(data => { - switch (data.type) { - case 'colorSelected': - { - vscode.window.activeTextEditor?.insertSnippet(new vscode.SnippetString(`#${data.value}`)); - break; - } - } - }); + webviewView.webview.onDidReceiveMessage(this.onWebviewMessage.bind(this)); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private onWebviewMessage(data: any) : any { + switch (data.type) { + case 'depType': + this.depType = data.value === "depends" ? "-d" : "-w"; + break; + case 'graphRecipe': + this.graphRecipe = data.value; + break; + case 'packageName': + this.packageName = data.value; + break; + case 'genDotFile': + this.genDotFile(); + break; + case 'runOeDepends': + this.runOeDepends(); + break; + } + } + private getHtmlForWebview(webview: vscode.Webview): Promise { const htmlUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'main.html')); const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'main.js')); @@ -64,7 +99,6 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'vscode.css')); const styleMainUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'client', 'web', 'depends-dot', 'main.css')); - // Use a nonce to only allow a specific script to be run. const nonce = this.getNonce(); const html = ejs.renderFile(htmlUri.fsPath, { @@ -78,6 +112,7 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { return html; } + /// The Nonce is a random value used to validate the CSP policy private getNonce() { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; @@ -86,4 +121,65 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { } return text; } + + private async genDotFile() : Promise { + if(this.graphRecipe === "") { + logger.error("genDotFile: No image recipe selected"); + void vscode.window.showErrorMessage(`Please select an image recipe first`); + return; + } + logger.info(`genDotFile: ${this.graphRecipe}`) + // isTaskexpStarted = true + // TODO add blocker for runOeDependsDOt to wait for completion. + // TODO do not bring to foreground + const process = await runBitbakeTerminal(this.bitbakeDriver, + { + specialCommand: `bitbake -g ${this.graphRecipe}`, + } as BitbakeTaskDefinition, + `Bitbake: genDotFile: ${this.graphRecipe}`,) + process.onExit((e) => { + // isTaskexpStarted = false + if (e.exitCode !== 0) { + void vscode.window.showErrorMessage(`Failed to generate dependency graph with exit code ${e.exitCode}. See terminal output.`) + } + }) + } + + private async runOeDepends() : Promise { + if(this.packageName === "") { + logger.error("genDotFile: No package selected"); + void vscode.window.showErrorMessage(`Please select a package first`); + return; + } + logger.info(`runOeDepends: ${this.packageName}`); + // TODO do not bring to foreground + const process = runBitbakeTerminal(this.bitbakeDriver, + { + specialCommand: `oe-depends-dot -k ${this.packageName} ${this.depType} ./task-depends.dot`, + } as BitbakeTaskDefinition, + `Bitbake: oeDependsDot: ${this.packageName}`,) + const result = await finishProcessExecution(process) + if (result.status !== 0) { + void vscode.window.showErrorMessage(`Failed to run oe-depends-dot with exit code ${result.status}. See terminal output.`) + } + const filtered_output = this.filterOeDependsOutput(result.stdout.toString()); + this.view?.webview.postMessage({ type: 'results', value: filtered_output }); + } + + /// Remove all lines of output that do not contain the actual results + private filterOeDependsOutput(output: string): string { + let filtered_output = '' + if(this.depType === "-d") { + filtered_output = output + .split('\n') + .filter(line => line.includes('Depends: ')) + .join('\n'); + } else { + filtered_output = output + .split('\n') + .filter(line => line.includes(' -> ')) + .join('\n'); + } + return filtered_output; + } } diff --git a/client/web/depends-dot/main.html b/client/web/depends-dot/main.html index a5457561..c2f062a4 100644 --- a/client/web/depends-dot/main.html +++ b/client/web/depends-dot/main.html @@ -1,5 +1,11 @@ + + + @@ -16,13 +22,44 @@ - Cat Colors + OE Depends Dot + -
    -
+ +
+

Recipe graph:

+ + +
+ +
+

Dependency type:

+
    +
  • Why
  • +
  • Depends
  • +
+
+ +
+

Package:

+ +
+ +
+ +
- +
+

Results:

+ +
diff --git a/client/web/depends-dot/main.js b/client/web/depends-dot/main.js index 9bc12cde..0212088b 100644 --- a/client/web/depends-dot/main.js +++ b/client/web/depends-dot/main.js @@ -1,98 +1,47 @@ +/** -------------------------------------------------------------------------------------------- + * Copyright (c) 2023 Savoir-faire Linux. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ //@ts-check // This script will be run within the webview itself // It cannot access the main VS Code APIs directly. (function () { const vscode = acquireVsCodeApi(); + const oldState = vscode.getState() || { }; - const oldState = vscode.getState() || { colors: [] }; + // HTML Listeners + document.querySelector('#depType').addEventListener('click', () => { + vscode.postMessage({ type: 'depType', value: event.target.value }); + }); - /** @type {Array<{ value: string }>} */ - let colors = oldState.colors; + document.querySelector('#graphRecipe').addEventListener('input', () => { + const value = document.querySelector('#graphRecipe').value; + vscode.postMessage({ type: 'graphRecipe', value }); + }); - updateColorList(colors); + document.querySelector('#packageName').addEventListener('input', () => { + const value = document.querySelector('#packageName').value; + vscode.postMessage({ type: 'packageName', value }); + }); + + document.querySelector('#genDotFile').addEventListener('click', () => { + vscode.postMessage({ type: 'genDotFile' }); + }); - document.querySelector('.add-color-button').addEventListener('click', () => { - addColor(); + document.querySelector('#runOeDepends').addEventListener('click', () => { + vscode.postMessage({ type: 'runOeDepends' }); }); - // Handle messages sent from the extension to the webview + // Extension Listeners window.addEventListener('message', event => { const message = event.data; // The json data that the extension sent switch (message.type) { - case 'addColor': + case 'results': { - addColor(); + document.querySelector('#results').textContent = message.value; break; } - case 'clearColors': - { - colors = []; - updateColorList(colors); - break; - } - } }); - - /** - * @param {Array<{ value: string }>} colors - */ - function updateColorList(colors) { - const ul = document.querySelector('.color-list'); - ul.textContent = ''; - for (const color of colors) { - const li = document.createElement('li'); - li.className = 'color-entry'; - - const colorPreview = document.createElement('div'); - colorPreview.className = 'color-preview'; - colorPreview.style.backgroundColor = `#${color.value}`; - colorPreview.addEventListener('click', () => { - onColorClicked(color.value); - }); - li.appendChild(colorPreview); - - const input = document.createElement('input'); - input.className = 'color-input'; - input.type = 'text'; - input.value = color.value; - input.addEventListener('change', (e) => { - const value = e.target.value; - if (!value) { - // Treat empty value as delete - colors.splice(colors.indexOf(color), 1); - } else { - color.value = value; - } - updateColorList(colors); - }); - li.appendChild(input); - - ul.appendChild(li); - } - - // Update the saved state - vscode.setState({ colors: colors }); - } - - /** - * @param {string} color - */ - function onColorClicked(color) { - vscode.postMessage({ type: 'colorSelected', value: color }); - } - - /** - * @returns string - */ - function getNewCalicoColor() { - const colors = ['020202', 'f1eeee', 'a85b20', 'daab70', 'efcb99']; - return colors[Math.floor(Math.random() * colors.length)]; - } - - function addColor() { - colors.push({ value: getNewCalicoColor() }); - updateColorList(colors); - } }()); diff --git a/package.json b/package.json index 4f056e69..50d664c0 100644 --- a/package.json +++ b/package.json @@ -568,8 +568,8 @@ { "type": "webview", "id": "bitbake.oeDependsDot", - "name": "Dependency Graph", - "contextualTitle": "Dependency graph", + "name": "Dependency Analyzer", + "contextualTitle": "Dependency Analyzer", "icon": "$(graph)", "when": "bitbake.active" } From 10f39466ffb285641a99381061504445b86c8dc7 Mon Sep 17 00:00:00 2001 From: Enguerrand de Ribaucourt Date: Wed, 8 Jan 2025 16:45:27 +0100 Subject: [PATCH 6/8] Feature: Present simple recipe dependency graphs The oe-depends-dot output is now no longer a simple text box. Dynamic HTML content with clickable recipes is now generated. We could in the future do more complex/merged graphs like the git-graph extension does. --- client/src/extension.ts | 2 +- client/src/ui/DependsDotView.ts | 42 ++++++++++++++++++--- client/web/depends-dot/main.css | 8 ++++ client/web/depends-dot/main.html | 15 ++++---- client/web/depends-dot/main.js | 63 +++++++++++++++++++++++++++++++- 5 files changed, 114 insertions(+), 16 deletions(-) diff --git a/client/src/extension.ts b/client/src/extension.ts index e5364ad8..498e8ea3 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -162,7 +162,7 @@ export async function activate (context: vscode.ExtensionContext): Promise bitbakeRecipesView.registerView(context) devtoolWorkspacesView = new DevtoolWorkspacesView(bitBakeProjectScanner) devtoolWorkspacesView.registerView(context) - dependsDotWebview = new DependsDotView(bitbakeDriver, context.extensionUri) + dependsDotWebview = new DependsDotView(bitBakeProjectScanner, context.extensionUri) dependsDotWebview.registerView(context) void vscode.commands.executeCommand('setContext', 'bitbake.active', true) const bitbakeStatusBar = new BitbakeStatusBar(bitBakeProjectScanner) diff --git a/client/src/ui/DependsDotView.ts b/client/src/ui/DependsDotView.ts index 7ea2d317..6f16f8e6 100644 --- a/client/src/ui/DependsDotView.ts +++ b/client/src/ui/DependsDotView.ts @@ -10,6 +10,9 @@ import { logger } from '../lib/src/utils/OutputLogger' import { BitbakeTaskDefinition } from './BitbakeTaskProvider' import { runBitbakeTerminal } from './BitbakeTerminal' import { finishProcessExecution } from '../utils/ProcessUtils' +import { BitBakeProjectScanner } from '../driver/BitBakeProjectScanner' +import { ElementInfo } from '../lib/src/types/BitbakeScanResult' +import path from 'path' /* TODO Beautify the view @@ -22,14 +25,17 @@ import { finishProcessExecution } from '../utils/ProcessUtils' TODO gray out the results when the graph or package is not up-to-date TODO Use the select recipe command to get the image recipe list (not for the packageName though?) TODO Add tests for this feature - TODO Save field values on workspace reload + TODO Save field values on workspace reload (https://code.visualstudio.com/api/extension-guides/webview#getstate-and-setstate and serializer) + TODO test styling in white mode and high-contrast mode + TODO sanitize text input (server side) + TODO add a gif in the README for this feature */ export class DependsDotView { private readonly provider: DependsDotViewProvider - constructor (bitbakeDriver: BitbakeDriver, extensionUri: vscode.Uri) { - this.provider = new DependsDotViewProvider(bitbakeDriver, extensionUri) + constructor (bitbakeProjectScanner: BitBakeProjectScanner, extensionUri: vscode.Uri) { + this.provider = new DependsDotViewProvider(bitbakeProjectScanner, extensionUri) } registerView (context: vscode.ExtensionContext): void { @@ -41,6 +47,7 @@ export class DependsDotView { class DependsDotViewProvider implements vscode.WebviewViewProvider { private readonly bitbakeDriver: BitbakeDriver + private readonly bitbakeProjectScanner: BitBakeProjectScanner public static readonly viewType = "bitbake.oeDependsDot" private view?: vscode.WebviewView; private extensionUri: vscode.Uri @@ -49,8 +56,9 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { private graphRecipe: string = ""; private packageName: string = ""; - constructor (bitbakeDriver: BitbakeDriver, extensionUri: vscode.Uri) { - this.bitbakeDriver = bitbakeDriver + constructor (bitbakeProjectScanner: BitBakeProjectScanner, extensionUri: vscode.Uri) { + this.bitbakeDriver = bitbakeProjectScanner.bitbakeDriver + this.bitbakeProjectScanner = bitbakeProjectScanner this.extensionUri = extensionUri } @@ -89,6 +97,9 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { case 'runOeDepends': this.runOeDepends(); break; + case 'openRecipe': + this.openRecipe(data.value); + break; } } @@ -163,7 +174,7 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { void vscode.window.showErrorMessage(`Failed to run oe-depends-dot with exit code ${result.status}. See terminal output.`) } const filtered_output = this.filterOeDependsOutput(result.stdout.toString()); - this.view?.webview.postMessage({ type: 'results', value: filtered_output }); + this.view?.webview.postMessage({ type: 'results', value: filtered_output, depType: this.depType }); } /// Remove all lines of output that do not contain the actual results @@ -173,6 +184,7 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { filtered_output = output .split('\n') .filter(line => line.includes('Depends: ')) + .map(line => line.replace('Depends: ', '')) .join('\n'); } else { filtered_output = output @@ -182,4 +194,22 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { } return filtered_output; } + + private openRecipe(recipeName: string) { + recipeName = recipeName.replace(/\r/g, ''); + let recipeFound = false; + this.bitbakeProjectScanner.scanResult._recipes.forEach((recipe: ElementInfo) => { + // TODO fix resolving -native recipes (put that logic in a utility function) (could be shared with BitbakeRecipesView.getChildren) + // TODO fix resolving some packages like xz or busybox (only when in the bottom row?) + if (recipe.name === recipeName) { + if (recipe.path !== undefined) { + vscode.window.showTextDocument(vscode.Uri.file(path.format(recipe.path))); + recipeFound = true; + } + } + }) + if (!recipeFound) { + vscode.window.showErrorMessage(`Project scan was not able to resolve ${recipeName}`); + } + } } diff --git a/client/web/depends-dot/main.css b/client/web/depends-dot/main.css index f2ef3933..96b7ba5f 100644 --- a/client/web/depends-dot/main.css +++ b/client/web/depends-dot/main.css @@ -1,3 +1,11 @@ +/* More variables in https://code.visualstudio.com/api/references/theme-color */ + body { background-color: transparent; } + +.packageLine:hover { + color: var(--vscode-list-hoverForeground); + background: var(--vscode-list-hoverBackground); + cursor: pointer; +} diff --git a/client/web/depends-dot/main.html b/client/web/depends-dot/main.html index c2f062a4..587c3d2b 100644 --- a/client/web/depends-dot/main.html +++ b/client/web/depends-dot/main.html @@ -34,21 +34,21 @@ Button to generate the dependency graph -->
-

Recipe graph:

+

Select an image Recipe to analyze:

- +
-

Dependency type:

+

You want to know:

    -
  • Why
  • -
  • Depends
  • +
  • Why a package is included

  • +
  • What it Depends on

-

Package:

+

Select which Package you want to examine:

@@ -58,7 +58,8 @@

Package:

Results:

- +
+
diff --git a/client/web/depends-dot/main.js b/client/web/depends-dot/main.js index 0212088b..20d0c32a 100644 --- a/client/web/depends-dot/main.js +++ b/client/web/depends-dot/main.js @@ -11,7 +11,7 @@ const oldState = vscode.getState() || { }; // HTML Listeners - document.querySelector('#depType').addEventListener('click', () => { + document.querySelector('#depType').addEventListener('click', (event) => { vscode.postMessage({ type: 'depType', value: event.target.value }); }); @@ -36,12 +36,71 @@ // Extension Listeners window.addEventListener('message', event => { const message = event.data; // The json data that the extension sent + // TODO remove empty final line, especially when asking for an inexistent package switch (message.type) { case 'results': { - document.querySelector('#results').textContent = message.value; + if(message.depType === '-w') { + renderWhy(message, vscode); + } else { + renderDependencies(message, vscode); + } break; } } }); }()); + +function renderDependencies(message, vscode) { + const resultsDiv = document.querySelector('#results'); + resultsDiv.innerHTML = ''; + + const packages = message.value.split(' '); + packages.forEach(pkg => { + addPackageLine(resultsDiv, pkg, '•', vscode); + }); +} + +function renderWhy(message, vscode) { + const resultsDiv = document.querySelector('#results'); + resultsDiv.innerHTML = ''; + + const dependencyChains = message.value.split('\n'); + for(let i = 0; i < dependencyChains.length; i++) { + const chain = dependencyChains[i]; + const chainDiv = document.createElement('div'); + chainDiv.className = 'dependencyChain'; + resultsDiv.appendChild(chainDiv); + renderDependencyChain(chain, chainDiv, vscode); + } +} + +function renderDependencyChain(chain, element, vscode) { + // Use the unicode box drawing characters to draw the lines + // https://www.compart.com/en/unicode/block/U+2500 + const packages = chain.split(' -> '); + for(let i = 0; i < packages.length; i++) { + const pkg = packages[i]; + let icon = '┃'; + if(i === 0) { icon = '┳'; } + if(i === packages.length - 1) { icon = '┻'; } + addPackageLine(element, pkg, icon, vscode); + } +} + +function addPackageLine(element, name, graphIcon, vscode) { + const div = document.createElement('div'); + div.className = 'packageLine'; + div.innerHTML = `${graphIcon} ${name}`; + element.appendChild(div); + div.addEventListener('click', () => { + vscode.postMessage({ type: 'openRecipe', value: name }); + }); +} + +function addIconLine(element, icon) { + const div = document.createElement('div'); + div.className = 'iconLine'; + div.innerHTML = `${icon}`; + element.appendChild(div); +} From f7053eb087a1dca53da79fc803dac095ed72064c Mon Sep 17 00:00:00 2001 From: Enguerrand de Ribaucourt Date: Mon, 3 Feb 2025 17:19:24 +0100 Subject: [PATCH 7/8] wip --- client/src/ui/DependsDotView.ts | 2 +- client/web/depends-dot/main.css | 6 ++++++ client/web/depends-dot/main.html | 17 +++++++++++------ client/web/depends-dot/reset.css | 2 ++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/client/src/ui/DependsDotView.ts b/client/src/ui/DependsDotView.ts index 6f16f8e6..e33d9cb5 100644 --- a/client/src/ui/DependsDotView.ts +++ b/client/src/ui/DependsDotView.ts @@ -198,7 +198,7 @@ class DependsDotViewProvider implements vscode.WebviewViewProvider { private openRecipe(recipeName: string) { recipeName = recipeName.replace(/\r/g, ''); let recipeFound = false; - this.bitbakeProjectScanner.scanResult._recipes.forEach((recipe: ElementInfo) => { + this.bitbakeProjectScanner.activeScanResult._recipes.forEach((recipe: ElementInfo) => { // TODO fix resolving -native recipes (put that logic in a utility function) (could be shared with BitbakeRecipesView.getChildren) // TODO fix resolving some packages like xz or busybox (only when in the bottom row?) if (recipe.name === recipeName) { diff --git a/client/web/depends-dot/main.css b/client/web/depends-dot/main.css index 96b7ba5f..d53d2601 100644 --- a/client/web/depends-dot/main.css +++ b/client/web/depends-dot/main.css @@ -9,3 +9,9 @@ body { background: var(--vscode-list-hoverBackground); cursor: pointer; } + +.inputHorizontal { + display: flex; + flex-direction: row; /* Arrange items horizontally */ + padding: 0; +} diff --git a/client/web/depends-dot/main.html b/client/web/depends-dot/main.html index 587c3d2b..73b37045 100644 --- a/client/web/depends-dot/main.html +++ b/client/web/depends-dot/main.html @@ -41,10 +41,14 @@

Select an image Recipe to analyze:

You want to know:

-
    -
  • Why a package is included

  • -
  • What it Depends on

  • -
+
+ + +
+
+ + +
@@ -57,9 +61,10 @@

Select which Package you want to examine:

+

Results:

-
-
+
+
diff --git a/client/web/depends-dot/reset.css b/client/web/depends-dot/reset.css index 717b7cbe..a2934e4d 100644 --- a/client/web/depends-dot/reset.css +++ b/client/web/depends-dot/reset.css @@ -19,6 +19,8 @@ h5, h6, p, ol, +input, +label, ul { margin: 0; padding: 0; From 6c1343c39356ed20a6f0a6b9e4a0d6e1601ee48a Mon Sep 17 00:00:00 2001 From: Enguerrand de Ribaucourt Date: Wed, 30 Jul 2025 11:09:31 +0200 Subject: [PATCH 8/8] wip: debug --- client/src/language/languageClient.ts | 73 +++++++++++++-------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/client/src/language/languageClient.ts b/client/src/language/languageClient.ts index 9a5a76f2..14275db9 100644 --- a/client/src/language/languageClient.ts +++ b/client/src/language/languageClient.ts @@ -56,27 +56,26 @@ export async function activateLanguageServer (context: ExtensionContext, bitBake synchronize: { configurationSection: 'bitbake' }, - middleware: { - provideCompletionItem: middlewareProvideCompletion, - provideDefinition: middlewareProvideDefinition, - provideHover: middlewareProvideHover, - provideReferences: middlewareProvideReferences, - prepareRename: middlewarePrepareRename, - provideRenameEdits: middlewareProvideRenameEdits - } + // middleware: { + // provideDefinition: middlewareProvideDefinition, + // provideHover: middlewareProvideHover, + // provideReferences: middlewareProvideReferences, + // prepareRename: middlewarePrepareRename, + // provideRenameEdits: middlewareProvideRenameEdits + // } } languages.setLanguageConfiguration('bitbake', getLanguageConfiguration()) - languages.onDidChangeDiagnostics(e => { - e.uris.forEach(uri => { - void updateDiagnostics(uri) - }) - }) + // languages.onDidChangeDiagnostics(e => { + // e.uris.forEach(uri => { + // void updateDiagnostics(uri) + // }) + // }) - context.subscriptions.push( - languages.registerCodeActionsProvider('bitbake', new BitbakeCodeActionProvider()) - ) + // context.subscriptions.push( + // languages.registerCodeActionsProvider('bitbake', new BitbakeCodeActionProvider()) + // ) if (context.storageUri?.fsPath === undefined) { logger.error('Failed to get storage path') @@ -113,27 +112,27 @@ export async function activateLanguageServer (context: ExtensionContext, bitBake void embeddedLanguageDocsManager.saveEmbeddedLanguageDocs(embeddedLanguageDocs) }) - window.tabGroups.onDidChangeTabs((event) => { - [...event.opened, ...event.changed].forEach((tab) => { - if (tab.input instanceof TabInputText) { - const uri = tab.input.uri - if (embeddedLanguageDocsManager.embeddedLanguageDocsFolder === undefined) { - return - } - // Close embedded document tabs when they open automatically - if (uri.fsPath.includes(embeddedLanguageDocsManager.embeddedLanguageDocsFolder)) { - if ( - // Prevent prompt to appear on unsaved files - !tab.isDirty && - // Make possible to open embedded documents in a tab - !tab.isPreview && !tab.isActive && !tab.isPinned - ) { - void window.tabGroups.close(tab, false) - } - } - } - }) - }) + // window.tabGroups.onDidChangeTabs((event) => { + // [...event.opened, ...event.changed].forEach((tab) => { + // if (tab.input instanceof TabInputText) { + // const uri = tab.input.uri + // if (embeddedLanguageDocsManager.embeddedLanguageDocsFolder === undefined) { + // return + // } + // // Close embedded document tabs when they open automatically + // if (uri.fsPath.includes(embeddedLanguageDocsManager.embeddedLanguageDocsFolder)) { + // if ( + // // Prevent prompt to appear on unsaved files + // !tab.isDirty && + // // Make possible to open embedded documents in a tab + // !tab.isPreview && !tab.isActive && !tab.isPinned + // ) { + // void window.tabGroups.close(tab, false) + // } + // } + // } + // }) + // }) // Start the client and launch the server await client.start()