From f2e3d2830043f99176b1d6589b1bbc57fc0ecae0 Mon Sep 17 00:00:00 2001 From: hverlin Date: Thu, 9 Jan 2025 09:31:56 +0100 Subject: [PATCH 1/2] feat: Add task dependency viewer --- package.json | 12 ++++ pnpm-lock.yaml | 93 +++++++++++++++++++++++++ src/commands.ts | 1 + src/miseExtension.ts | 7 ++ src/miseService.ts | 20 ++++++ src/webviewPanel.ts | 35 +++++++++- src/webviews/App.tsx | 3 + src/webviews/TasksDependencies.tsx | 79 +++++++++++++++++++++ src/webviews/components/DotRenderer.tsx | 33 +++++++++ src/webviews/index.tsx | 6 ++ 10 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 src/webviews/TasksDependencies.tsx create mode 100644 src/webviews/components/DotRenderer.tsx diff --git a/package.json b/package.json index d18a4d7..fa59f9e 100644 --- a/package.json +++ b/package.json @@ -436,6 +436,11 @@ "title": "Mise: Show mise tracked configurations", "icon": "$(list-ordered)" }, + { + "command": "mise.visualizeTasksDeps", + "title": "Mise: Visualize Tasks Dependencies", + "icon": "$(git-merge)" + }, { "command": "mise.setEnvVariable", "title": "Mise: Set Environment Variable", @@ -462,6 +467,11 @@ "when": "view == miseTasksView", "group": "navigation@2" }, + { + "command": "mise.visualizeTasksDeps", + "when": "view == miseTasksView", + "group": "navigation@2" + }, { "command": "mise.listAllTools", "when": "view == miseToolsView", @@ -653,6 +663,8 @@ "@vscode/codicons": "^0.0.36", "async-cache-dedupe": "^2.2.0", "cheerio": "^1.0.0", + "d3": "^7.9.0", + "d3-graphviz": "^5.6.0", "react": "18.3.1", "react-dom": "18.3.1", "react-error-boundary": "^4.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aed277c..fee5de5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,12 @@ importers: cheerio: specifier: ^1.0.0 version: 1.0.0 + d3: + specifier: ^7.9.0 + version: 7.9.0 + d3-graphviz: + specifier: ^5.6.0 + version: 5.6.0(d3-selection@3.0.0) react: specifier: 18.3.1 version: 18.3.1 @@ -431,6 +437,10 @@ packages: '@expressive-code/plugin-text-markers@0.38.3': resolution: {integrity: sha512-dPK3+BVGTbTmGQGU3Fkj3jZ3OltWUAlxetMHI6limUGCWBCucZiwoZeFM/WmqQa71GyKRzhBT+iEov6kkz2xVA==} + '@hpcc-js/wasm@2.22.4': + resolution: {integrity: sha512-58JkRkxZffiBAbZhc7z+9iaaAOmn1cyxLL3rRwsUvco/I0Wwb7uVAlHM9HiU6XASe2k11jrIjCFff1t9QKjlqg==} + hasBin: true + '@img/sharp-darwin-arm64@0.33.5': resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1114,6 +1124,10 @@ packages: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -1221,6 +1235,7 @@ packages: bun@1.1.42: resolution: {integrity: sha512-PckeNolMEBaBEzixTMvp0jJD9r/9lly8AfctILi1ve14zwwChFjsxI4TJLQO2yezzOjVeG0u7xf8WQFbS7GjAA==} + cpu: [arm64, x64, aarch64] os: [darwin, linux, win32] hasBin: true @@ -1276,6 +1291,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1396,6 +1415,12 @@ packages: resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} engines: {node: '>=12'} + d3-graphviz@5.6.0: + resolution: {integrity: sha512-46OOyRv5Ioo9kZBc919FVIYPD/ObtdSZxOK1hv+qwmD7TunpPvvmsI1dSdxhVgH4GragJxFZ31+TQC5aOuXzzw==} + engines: {node: '>=14'} + peerDependencies: + d3-selection: ^3.0.0 + d3-hierarchy@3.1.2: resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} engines: {node: '>=12'} @@ -1665,6 +1690,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.3.0: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} @@ -2397,6 +2426,10 @@ packages: remove-accents@0.5.0: resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -2778,6 +2811,10 @@ packages: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + wrap-ansi@9.0.0: resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} engines: {node: '>=18'} @@ -2788,6 +2825,10 @@ packages: xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2795,6 +2836,10 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@1.1.1: resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} engines: {node: '>=12.20'} @@ -3200,6 +3245,10 @@ snapshots: dependencies: '@expressive-code/core': 0.38.3 + '@hpcc-js/wasm@2.22.4': + dependencies: + yargs: 17.7.2 + '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.0.4 @@ -3834,6 +3883,10 @@ snapshots: ansi-regex@6.1.0: {} + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansi-styles@6.2.1: {} arg@5.0.2: {} @@ -4090,6 +4143,12 @@ snapshots: cli-spinners@2.9.2: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} collapse-white-space@2.1.0: {} @@ -4197,6 +4256,18 @@ snapshots: dependencies: d3-array: 3.2.4 + d3-graphviz@5.6.0(d3-selection@3.0.0): + dependencies: + '@hpcc-js/wasm': 2.22.4 + d3-dispatch: 3.0.1 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + d3-hierarchy@3.1.2: {} d3-interpolate@3.0.1: @@ -4510,6 +4581,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.3.0: {} github-from-package@0.0.0: {} @@ -5735,6 +5808,8 @@ snapshots: remove-accents@0.5.0: {} + require-directory@2.1.1: {} + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -6176,6 +6251,12 @@ snapshots: dependencies: string-width: 7.2.0 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@9.0.0: dependencies: ansi-styles: 6.2.1 @@ -6186,10 +6267,22 @@ snapshots: xxhash-wasm@1.1.0: {} + y18n@5.0.8: {} + yallist@3.1.1: {} yargs-parser@21.1.1: {} + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@1.1.1: {} zod-to-json-schema@3.23.5(zod@3.24.0): diff --git a/src/commands.ts b/src/commands.ts index 3273135..eeb069e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -11,6 +11,7 @@ export const MISE_INSTALL_TOOL = "mise.installTool"; export const MISE_LIST_ALL_TOOLS = "mise.listAllTools"; export const MISE_SHOW_SETTINGS = "mise.showSettings"; export const MISE_SHOW_TRACKED_CONFIG = "mise.showTrackedConfig"; +export const MISE_VISUALIZE_TASKS_DEPS = "mise.visualizeTasksDeps"; export const MISE_OPEN_ENV_VAR_DEFINITION = "mise.openEnvVariableDefinition"; export const MISE_OPEN_FILE = "mise.openFile"; export const MISE_OPEN_LOGS = "mise.openLogs"; diff --git a/src/miseExtension.ts b/src/miseExtension.ts index f9340d4..2a5be1b 100644 --- a/src/miseExtension.ts +++ b/src/miseExtension.ts @@ -15,6 +15,7 @@ import { MISE_SELECT_WORKSPACE_FOLDER, MISE_SHOW_SETTINGS, MISE_SHOW_TRACKED_CONFIG, + MISE_VISUALIZE_TASKS_DEPS, } from "./commands"; import { CONFIGURATION_FLAGS, @@ -346,6 +347,12 @@ export class MiseExtension { }), ); + context.subscriptions.push( + vscode.commands.registerCommand(MISE_VISUALIZE_TASKS_DEPS, async () => { + WebViewPanel.createOrShow(context, this.miseService, "TASKS_DEPS"); + }), + ); + const allTomlFilesSelector = { scheme: "file", language: "toml" }; context.subscriptions.push( diff --git a/src/miseService.ts b/src/miseService.ts index 605d205..d54e744 100644 --- a/src/miseService.ts +++ b/src/miseService.ts @@ -95,8 +95,14 @@ function ensureMiseCommand( export class MiseService { private readonly context: vscode.ExtensionContext; + private readonly eventEmitter: vscode.EventEmitter; constructor(context: vscode.ExtensionContext) { this.context = context; + this.eventEmitter = new vscode.EventEmitter(); + } + + subscribeToReloadEvent(listener: () => void): vscode.Disposable { + return this.eventEmitter.event(listener); } private hasVerifiedMiseVersion = false; @@ -143,6 +149,7 @@ export class MiseService { async invalidateCache() { await Promise.all([this.dedupeCache.clear(), this.cache.clear()]); + this.eventEmitter.fire(); } async initializeMisePath() { @@ -1042,4 +1049,17 @@ export class MiseService { logger.info(`New symlink created ${linkPath} -> ${binPath}`); return configuredPath; } + + async getTaskDependencies(tasks: string[] | undefined) { + if (!this.getMiseBinaryPath()) { + return ""; + } + + const taskString = tasks ? tasks.map((task) => `"${task}"`).join(" ") : ""; + + const { stdout } = await this.cache.execCmd({ + command: `tasks deps ${taskString} --dot`, + }); + return stdout; + } } diff --git a/src/webviewPanel.ts b/src/webviewPanel.ts index 6e2ba6b..bb1b5a4 100644 --- a/src/webviewPanel.ts +++ b/src/webviewPanel.ts @@ -7,7 +7,20 @@ import { MISE_EDIT_SETTING } from "./commands"; import type { MiseService } from "./miseService"; import { logger } from "./utils/logger"; -type PanelView = "TOOLS" | "SETTINGS" | "TRACKED_CONFIGS"; +type PanelView = "TOOLS" | "SETTINGS" | "TRACKED_CONFIGS" | "TASKS_DEPS"; + +function panelTitleForView(view: PanelView) { + switch (view) { + case "TOOLS": + return "Tools"; + case "SETTINGS": + return "Settings"; + case "TRACKED_CONFIGS": + return "Tracked Configs"; + case "TASKS_DEPS": + return "Tasks Dependencies"; + } +} export default class WebViewPanel { public static currentPanels: Record = {}; @@ -54,7 +67,7 @@ export default class WebViewPanel { this._panel = vscode.window.createWebviewPanel( WebViewPanel.viewType, - `Mise: ${this.view === "TOOLS" ? "Tools" : this.view === "SETTINGS" ? "Settings" : "Tracked Configs"}`, + `Mise: ${panelTitleForView(this.view)}`, column, { retainContextWhenHidden: false, @@ -117,6 +130,16 @@ export default class WebViewPanel { this.miseService.getTrackedConfigFiles(), ); } + case "taskDeps": { + return executeAction(message, () => + this.miseService.getTaskDependencies(message.variables.tasks), + ); + } + case "tasks": { + return executeAction(message, () => + this.miseService.getTasks(), + ); + } } break; case "mutation": @@ -180,6 +203,14 @@ export default class WebViewPanel { "workbench.action.webview.reloadWebviewAction", ); }); + + miseService.subscribeToReloadEvent(() => { + this._panel.webview.postMessage({ + type: "invalidateQueries", + requestId: "invalidateQueries", + data: null, + }); + }); } public dispose() { diff --git a/src/webviews/App.tsx b/src/webviews/App.tsx index 5a6b5ab..254c2fd 100644 --- a/src/webviews/App.tsx +++ b/src/webviews/App.tsx @@ -1,4 +1,5 @@ import { Settings } from "./Settings"; +import { TasksDependencies } from "./TasksDependencies"; import { Tools } from "./Tools"; import { TrackedConfigs } from "./TrackedConfigs"; @@ -13,6 +14,8 @@ export function App() { ) : view === "SETTINGS" ? ( + ) : view === "TASKS_DEPS" ? ( + ) : ( )} diff --git a/src/webviews/TasksDependencies.tsx b/src/webviews/TasksDependencies.tsx new file mode 100644 index 0000000..4740d47 --- /dev/null +++ b/src/webviews/TasksDependencies.tsx @@ -0,0 +1,79 @@ +import { useQuery } from "@tanstack/react-query"; +import { + VscodeMultiSelect, + VscodeOption, +} from "@vscode-elements/react-elements"; +import { useState } from "react"; +import { Graphviz } from "./components/DotRenderer"; +import { vscodeClient } from "./webviewVsCodeApi"; + +export const TasksDependencies = () => { + const [selectedOptions, setSelectedOptions] = useState([]); + + const tasksQuery = useQuery({ + queryKey: ["tasks"], + queryFn: ({ queryKey }) => + vscodeClient.request({ queryKey }) as Promise, + }); + + const taskDepsQuery = useQuery({ + queryKey: ["taskDeps", selectedOptions], + queryFn: () => + vscodeClient.request({ + queryKey: ["taskDeps"], + variables: { + tasks: selectedOptions.filter((t) => + tasksQuery.data?.some((task) => task.name === t), + ), + }, + }) as Promise, + }); + + const taskDepsDot = taskDepsQuery.data?.split?.("digraph {")[1]; + + const dotString = ` +digraph { + rankdir="TB"; + splines=true; + overlap=false; + nodesep="0.3"; + ranksep="0.8"; + fontname="Lato"; + graph [ bgcolor="transparent" ] + node [ shape="plaintext" style="filled, rounded" fontname="Lato" margin=0.2 ] + edge [ fontname="Lato" color="#aaa" ] + +${taskDepsDot} +`; + + return ( +
+ { + // @ts-ignore + setSelectedOptions(e.target?.value ?? []); + }} + > + {tasksQuery.data?.map((task) => ( + + {task.name} + + ))} + + {taskDepsQuery.isPending ? ( + "..." + ) : taskDepsQuery.error ? ( +
Error: {taskDepsQuery.error.message}
+ ) : ( + + )} +
+ ); +}; diff --git a/src/webviews/components/DotRenderer.tsx b/src/webviews/components/DotRenderer.tsx new file mode 100644 index 0000000..9c60d5d --- /dev/null +++ b/src/webviews/components/DotRenderer.tsx @@ -0,0 +1,33 @@ +// @ts-ignore +import { type GraphvizOptions, graphviz } from "d3-graphviz"; +import React, { useEffect, useMemo } from "react"; + +interface IGraphvizProps { + dot: string; + options?: GraphvizOptions; + className?: string; +} + +const defaultOptions: GraphvizOptions = { + fit: true, + height: "calc(100dvh - 30px)", + width: "100%", + zoom: true, +}; + +let counter = 0; +const getId = () => `graphviz${counter++}`; + +export const Graphviz = ({ dot, className, options = {} }: IGraphvizProps) => { + const id = useMemo(getId, []); + + // biome-ignore lint/correctness/useExhaustiveDependencies: todo + useEffect(() => { + graphviz(`#${id}`, { + ...defaultOptions, + ...options, + }).renderDot(dot); + }, [dot, options]); + + return
; +}; diff --git a/src/webviews/index.tsx b/src/webviews/index.tsx index 8bd0005..687dc2b 100644 --- a/src/webviews/index.tsx +++ b/src/webviews/index.tsx @@ -11,6 +11,12 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1 } }, }); +window.addEventListener("message", (event) => { + if (event.data.type === "invalidateQueries") { + void queryClient.invalidateQueries(); + } +}); + // @ts-ignore function Fallback({ error, resetErrorBoundary }) { return ( From e291f38d3a8a11c2a9761dba8234ca5af6686a9c Mon Sep 17 00:00:00 2001 From: Hugues Verlin Date: Thu, 9 Jan 2025 09:33:22 +0100 Subject: [PATCH 2/2] chore(main): release 0.42.0 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9d3b05..6ad613c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.42.0](https://github.com/hverlin/mise-vscode/compare/v0.41.0...v0.42.0) (2025-01-09) + + +### Features + +* Add task dependency viewer ([f2e3d28](https://github.com/hverlin/mise-vscode/commit/f2e3d2830043f99176b1d6589b1bbc57fc0ecae0)) + ## [0.41.0](https://github.com/hverlin/mise-vscode/compare/v0.40.1...v0.41.0) (2025-01-06) diff --git a/package.json b/package.json index fa59f9e..bbc6b0d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Mise VSCode", "publisher": "hverlin", "description": "mise support for Visual Studio code (dev-tools, tasks and environment variables)", - "version": "0.41.0", + "version": "0.42.0", "repository": { "type": "git", "url": "https://github.com/hverlin/mise-vscode"