From 968da8dcb174c9ff5536a38b433c4c16324f5531 Mon Sep 17 00:00:00 2001 From: hverlin Date: Fri, 27 Dec 2024 12:35:29 +0100 Subject: [PATCH] feat: Improve settings editor --- package.json | 4 + src/commands.ts | 1 + src/miseExtension.ts | 85 +++++++++++++ src/miseService.ts | 42 ++++++- src/providers/envProvider.ts | 4 +- src/providers/miseTomlCodeLensProvider.ts | 4 +- src/utils/miseUtilts.ts | 73 +++++++++++ src/webviewPanel.ts | 16 ++- src/webviews/Settings.tsx | 143 ++++++++++++++++------ src/webviews/TrackedConfigs.tsx | 11 +- src/webviews/settingsSchema.ts | 76 ------------ src/webviews/webviewVsCodeApi.ts | 19 ++- 12 files changed, 347 insertions(+), 131 deletions(-) delete mode 100644 src/webviews/settingsSchema.ts diff --git a/package.json b/package.json index 56e8791..db08c33 100644 --- a/package.json +++ b/package.json @@ -436,6 +436,10 @@ { "command": "mise.fmt", "title": "Mise: Run mise fmt" + }, + { + "command": "mise.editSetting", + "title": "Mise: Edit Setting" } ], "menus": { diff --git a/src/commands.ts b/src/commands.ts index eb0efef..2e9842f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -25,3 +25,4 @@ export const MISE_SET_ENV_VARIABLE = "mise.setEnvVariable"; export const MISE_USE_TOOL = "mise.useTool"; export const MISE_WATCH_TASK = "mise.watchTask"; export const MISE_FMT = "mise.fmt"; +export const MISE_EDIT_SETTING = "mise.editSetting"; diff --git a/src/miseExtension.ts b/src/miseExtension.ts index ac2a411..6c7fa49 100644 --- a/src/miseExtension.ts +++ b/src/miseExtension.ts @@ -2,6 +2,7 @@ import { createCache } from "async-cache-dedupe"; import vscode, { MarkdownString } from "vscode"; import { MISE_CONFIGURE_ALL_SKD_PATHS, + MISE_EDIT_SETTING, MISE_FMT, MISE_INSTALL_ALL, MISE_LIST_ALL_TOOLS, @@ -332,6 +333,90 @@ export class MiseExtension { }), ); + context.subscriptions.push( + vscode.commands.registerCommand( + MISE_EDIT_SETTING, + async (settingName: string) => { + const [schema, allSettings] = await Promise.all([ + this.miseService.getSettingsSchema(), + this.miseService.getSettings(), + ]); + let setting = settingName; + if (!setting) { + const selectedSetting = await vscode.window.showQuickPick( + schema.map((s) => ({ + label: s.key, + description: [ + s.type ? `${s.type}` : "", + s.deprecated ? "Deprecated" : "", + ] + .filter(Boolean) + .join(" | "), + detail: s.description, + })), + { placeHolder: "Select a setting" }, + ); + if (!selectedSetting) { + return; + } + setting = selectedSetting.label; + } + + const settingSchema = schema.find((s) => s.key === setting); + if (!settingSchema) { + logger.warn(`Setting ${setting} not found in schema`); + return; + } + + const currentValue = allSettings[setting]?.value; + + const value = + settingSchema.type === "boolean" + ? await vscode.window.showQuickPick(["true", "false"], { + placeHolder: `Select new value for ${setting}. Current value: ${currentValue}`, + }) + : settingSchema.enum?.length + ? await vscode.window.showQuickPick(settingSchema.enum, { + placeHolder: `Select new value for ${setting}. Current value: ${currentValue}`, + }) + : settingSchema.type === "array" + ? await vscode.window.showInputBox({ + prompt: `Enter new value for ${setting} (comma separated). Current value: ${currentValue}`, + value: + typeof currentValue === "string" + ? JSON.parse(currentValue)?.join(",") + : "", + }) + : await vscode.window.showInputBox({ + prompt: `Enter new value for ${setting}`, + value: + settingSchema.type === "string" + ? (currentValue?.toString() ?? "") + : JSON.stringify(currentValue), + }); + + if (value === undefined) { + return; + } + + const file = + await this.miseService.getMiseTomlConfigFilePathsEvenIfMissing(); + const selectedFilePath = await vscode.window.showQuickPick(file, { + placeHolder: "Select a configuration file", + }); + + if (!selectedFilePath) { + return; + } + + await this.miseService.editSetting(setting, { + filePath: selectedFilePath, + value, + }); + }, + ), + ); + await vscode.commands.executeCommand(MISE_RELOAD); setTimeout(async () => { diff --git a/src/miseService.ts b/src/miseService.ts index 22ce75a..9c2a602 100644 --- a/src/miseService.ts +++ b/src/miseService.ts @@ -19,7 +19,11 @@ import { uniqBy } from "./utils/fn"; import { logger } from "./utils/logger"; import { resolveMisePath } from "./utils/miseBinLocator"; import { type MiseConfig, parseMiseConfig } from "./utils/miseDoctorParser"; -import { idiomaticFileToTool, idiomaticFiles } from "./utils/miseUtilts"; +import { + flattenJsonSchema, + idiomaticFileToTool, + idiomaticFiles, +} from "./utils/miseUtilts"; import { showSettingsNotification } from "./utils/notify"; import { execAsync, @@ -110,9 +114,20 @@ export class MiseService { private longCache = createCache({ ttl: 60, storage: { type: "memory" }, - }).define("execCmd", ({ command, setMiseEnv } = {}) => - this.execMiseCommand(command, { setMiseEnv }), - ); + }) + .define("execCmd", ({ command, setMiseEnv } = {}) => + this.execMiseCommand(command, { setMiseEnv }), + ) + .define("fetchSchema", async () => { + const res = await fetch( + "https://raw.githubusercontent.com/jdx/mise/refs/heads/main/schema/mise.json", + ); + if (!res.ok) { + return []; + } + const json = await res.json(); + return flattenJsonSchema(json.$defs.settings); + }); async invalidateCache() { await Promise.all([this.dedupeCache.clear(), this.cache.clear()]); @@ -833,7 +848,7 @@ export class MiseService { async getSettings() { if (!this.getMiseBinaryPath()) { - return []; + return {}; } const { stdout } = await this.execMiseCommand( @@ -842,6 +857,10 @@ export class MiseService { return flattenSettings(JSON.parse(stdout)); } + async getSettingsSchema() { + return this.longCache.fetchSchema(); + } + async getTrackedConfigFiles() { const trackedConfigFiles = await vscode.workspace.fs.readDirectory( vscode.Uri.file(TRACKED_CONFIG_DIR), @@ -947,4 +966,17 @@ export class MiseService { await this.runMiseToolActionInConsole(`install ${toolName}@${version}`); } + + async editSetting( + setting: string, + { value, filePath }: { value: string; filePath: string }, + ) { + if (!this.getMiseBinaryPath()) { + return; + } + + await this.runMiseToolActionInConsole( + `config set settings.${setting} "${value}" --file "${filePath}"`, + ); + } } diff --git a/src/providers/envProvider.ts b/src/providers/envProvider.ts index c4ccb01..bad3bfe 100644 --- a/src/providers/envProvider.ts +++ b/src/providers/envProvider.ts @@ -242,7 +242,9 @@ function updateTerminalsEnvs(variablesToRemove: [string, string][]) { const commands = variablesToRemove.map(([name]) => `;unset ${name}`).join(""); const isTerminalFocused = vscode.window.activeTerminal !== undefined; - if (isTerminalFocused) { + const isMiseTask = + vscode.window.activeTerminal?.creationOptions?.name?.includes("mise"); + if (isTerminalFocused && !isMiseTask) { return vscode.commands.executeCommand("workbench.action.terminal.relaunch"); } diff --git a/src/providers/miseTomlCodeLensProvider.ts b/src/providers/miseTomlCodeLensProvider.ts index c5c6e6b..37e5fec 100644 --- a/src/providers/miseTomlCodeLensProvider.ts +++ b/src/providers/miseTomlCodeLensProvider.ts @@ -68,8 +68,8 @@ function addListToolsCodeLens(range: vscode.Range): vscode.CodeLens { function addSettingsListCodeLens(range: vscode.Range): vscode.CodeLens { return new vscode.CodeLens(range, { - title: "$(gear) View all settings", - tooltip: "Show all settings", + title: "$(gear) Manage settings", + tooltip: "Manage settings", command: MISE_SHOW_SETTINGS, arguments: [], }); diff --git a/src/utils/miseUtilts.ts b/src/utils/miseUtilts.ts index 3d68c4b..2d65f4a 100644 --- a/src/utils/miseUtilts.ts +++ b/src/utils/miseUtilts.ts @@ -56,3 +56,76 @@ export const getCleanedToolName = (toolName: string) => { .replace("nodejs", "node") .replace("golang", "go"); }; + +type JSONType = "string" | "boolean" | "number" | "object" | "array"; + +export type FlattenedProperty = { + key: string; + type: JSONType; + itemsType: JSONType | undefined; + enum: string[] | undefined; + description: string | undefined; + defaultValue: unknown; + deprecated?: string; +}; + +type PropertyValue = { + type?: JSONType; + description?: string; + default?: unknown; + deprecated?: string; + items?: { type: JSONType }; + enum?: string[]; + properties?: Record; +}; + +type SchemaType = { + properties: Record; +}; + +export function flattenJsonSchema( + schema: SchemaType, + parentKey = "", + result: FlattenedProperty[] = [], +): FlattenedProperty[] { + if (!schema.properties) { + return result; + } + + for (const [key, value] of Object.entries(schema.properties)) { + const currentKey = parentKey ? `${parentKey}.${key}` : key; + + if (value.properties) { + flattenJsonSchema({ properties: value.properties }, currentKey, result); + } else { + result.push({ + key: currentKey, + type: value.type ?? "string", + itemsType: value.items?.type, + description: value.description, + defaultValue: value.default, + enum: value.enum, + ...(value.deprecated && { deprecated: value.deprecated }), + }); + } + } + + return result; +} + +export function getDefaultForType(type?: string): unknown { + switch (type) { + case "string": + return ""; + case "boolean": + return false; + case "number": + return 0; + case "object": + return {}; + case "array": + return []; + default: + return ""; + } +} diff --git a/src/webviewPanel.ts b/src/webviewPanel.ts index de4788a..958ab39 100644 --- a/src/webviewPanel.ts +++ b/src/webviewPanel.ts @@ -4,6 +4,7 @@ import * as os from "node:os"; import path from "node:path"; import * as cheerio from "cheerio"; import * as vscode from "vscode"; +import { MISE_EDIT_SETTING } from "./commands"; import type { MiseService } from "./miseService"; import { logger } from "./utils/logger"; @@ -57,7 +58,7 @@ export default class WebViewPanel { `Mise: ${this.view === "TOOLS" ? "Tools" : this.view === "SETTINGS" ? "Settings" : "Tracked Configs"}`, column, { - retainContextWhenHidden: true, + retainContextWhenHidden: false, enableScripts: true, localResourceRoots: [this._extensionUri], }, @@ -107,6 +108,11 @@ export default class WebViewPanel { this.miseService.getSettings(), ); } + case "settingsSchema": { + return executeAction(message, () => + this.miseService.getSettingsSchema(), + ); + } case "trackedConfigs": { return executeAction(message, () => this.miseService.getTrackedConfigFiles(), @@ -150,6 +156,14 @@ export default class WebViewPanel { ), ); } + case "editSetting": { + return executeAction(message, async () => + vscode.commands.executeCommand( + MISE_EDIT_SETTING, + message.variables?.key, + ), + ); + } } break; } diff --git a/src/webviews/Settings.tsx b/src/webviews/Settings.tsx index a070095..3f17749 100644 --- a/src/webviews/Settings.tsx +++ b/src/webviews/Settings.tsx @@ -1,12 +1,20 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { VscodeCheckbox } from "@vscode-elements/react-elements"; import React, { useState } from "react"; +import { type FlattenedProperty, getDefaultForType } from "../utils/miseUtilts"; import CustomTable from "./components/CustomTable"; -import { flattenJsonSchema, getDefaultForType } from "./settingsSchema"; -import { toDisplayPath, vscodeClient } from "./webviewVsCodeApi"; +import { IconButton } from "./components/IconButton"; +import { + toDisplayPath, + useEditSettingMutation, + useOpenFileMutation, + vscodeClient, +} from "./webviewVsCodeApi"; export const Settings = () => { - const [showModifiedOnly, setShowModifiedOnly] = useState(false); + const [showModifiedOnly, setShowModifiedOnly] = useState(true); + const openFileMutation = useOpenFileMutation(); + const queryClient = useQueryClient(); const settingsQuery = useQuery({ queryKey: ["settings"], @@ -16,20 +24,18 @@ export const Settings = () => { >, }); + const settingMutation = useEditSettingMutation(); + const schemaQuery = useQuery({ queryKey: ["settingsSchema"], - queryFn: async () => { - const res = await fetch( - "https://raw.githubusercontent.com/jdx/mise/refs/heads/main/schema/mise.json", - ); - if (!res.ok) { - return []; - } - const json = await res.json(); - return flattenJsonSchema(json.$defs.settings); - }, + queryFn: ({ queryKey }) => + vscodeClient.request({ queryKey }) as Promise, }); + if (schemaQuery.isPending || settingsQuery.isPending) { + return
; + } + const schema = schemaQuery.data ?? []; if (settingsQuery.isError) { @@ -55,6 +61,7 @@ export const Settings = () => { source, description: schemaDef?.description ?? "", type: schemaDef?.type ?? "", + enum: schemaDef?.enum ?? [], defaultValue: schemaDef?.defaultValue ? schemaDef.defaultValue : getDefaultForType(schemaDef?.type), @@ -75,9 +82,7 @@ export const Settings = () => { } isLoading={settingsQuery.isLoading} data={settingValues.filter( - (value) => - !showModifiedOnly || - JSON.stringify(value.value) !== JSON.stringify(value.defaultValue), + (value) => !showModifiedOnly || value.source, )} columns={[ { @@ -85,14 +90,36 @@ export const Settings = () => { header: "Key", accessorKey: "key", cell: ({ row }) => { + const key = row.original.key; return ( -
-

- {row.original.key} -

-

- {row.original.description} ({row.original.type}) -

+
+ +
+ {row.original.description}{" "} + {row.original.enum?.length > 0 ? ( + +
+ Enum: {row.original.enum.join(", ")} +
+ ) : row.original.type ? ( + `(${row.original.type})` + ) : ( + "" + )} +
); }, @@ -104,22 +131,64 @@ export const Settings = () => { cell: ({ row }) => { const actual = JSON.stringify(row.original.value); const defaultValue = JSON.stringify(row.original.defaultValue); - if (actual === defaultValue) { - return
{actual}
; - } + const source = row.original.source; return ( -
-									value: {actual}
-									
- default: {defaultValue} - {row.original.source && ( - <> -
- source: {toDisplayPath(row.original.source || "")} - +
+
+
{actual}
{" "} + { + void settingMutation.mutate( + { key: row.original.key }, + { onSettled: () => queryClient.invalidateQueries() }, + ); + }} + /> +
+ {actual !== defaultValue && ( +
+ default:{" "} +
+												{defaultValue}
+											
+
+ )} + {source && ( + )} -
+
); }, }, diff --git a/src/webviews/TrackedConfigs.tsx b/src/webviews/TrackedConfigs.tsx index 03c3f46..a36f0b4 100644 --- a/src/webviews/TrackedConfigs.tsx +++ b/src/webviews/TrackedConfigs.tsx @@ -1,19 +1,14 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import CustomTable from "./components/CustomTable"; import { toDisplayPath, trackedConfigsQueryOptions, - vscodeClient, + useOpenFileMutation, } from "./webviewVsCodeApi"; export const TrackedConfigs = () => { const trackedConfigQuery = useQuery(trackedConfigsQueryOptions); - - const openFileMutation = useMutation({ - mutationKey: ["openFile"], - mutationFn: (path: string) => - vscodeClient.request({ mutationKey: ["openFile"], variables: { path } }), - }); + const openFileMutation = useOpenFileMutation(); if (trackedConfigQuery.isError) { return
Error: {trackedConfigQuery.error.message}
; diff --git a/src/webviews/settingsSchema.ts b/src/webviews/settingsSchema.ts deleted file mode 100644 index 402c03e..0000000 --- a/src/webviews/settingsSchema.ts +++ /dev/null @@ -1,76 +0,0 @@ -type FlattenedProperty = { - key: string; - type: string; - description: string | undefined; - defaultValue: unknown; - deprecated?: string; -}; - -type PropertyValue = { - type?: string; - description?: string; - default?: unknown; - deprecated?: string; - items?: { type: string }; - enum?: string[]; - properties?: Record; -}; - -type SchemaType = { - properties: Record; -}; - -export function flattenJsonSchema( - schema: SchemaType, - parentKey = "", - result: FlattenedProperty[] = [], -): FlattenedProperty[] { - if (!schema.properties) { - return result; - } - - for (const [key, value] of Object.entries(schema.properties)) { - const currentKey = parentKey ? `${parentKey}.${key}` : key; - - if (value.properties) { - flattenJsonSchema({ properties: value.properties }, currentKey, result); - } else { - let propertyType = value.type; - - if (value.type === "array" && value.items) { - propertyType = `${value.items.type}[]`; - } - - if (value.enum) { - propertyType = value.enum.map((v) => `"${v}"`).join(" | "); - } - - result.push({ - key: currentKey, - type: propertyType ?? "string", - description: value.description, - defaultValue: value.default, - ...(value.deprecated && { deprecated: value.deprecated }), - }); - } - } - - return result; -} - -export function getDefaultForType(type?: string): unknown { - switch (type) { - case "string": - return ""; - case "boolean": - return false; - case "number": - return 0; - case "object": - return {}; - case "array": - return []; - default: - return ""; - } -} diff --git a/src/webviews/webviewVsCodeApi.ts b/src/webviews/webviewVsCodeApi.ts index fa769cb..868850c 100644 --- a/src/webviews/webviewVsCodeApi.ts +++ b/src/webviews/webviewVsCodeApi.ts @@ -1,4 +1,4 @@ -import { queryOptions } from "@tanstack/react-query"; +import { queryOptions, useMutation } from "@tanstack/react-query"; declare global { interface Window { @@ -69,3 +69,20 @@ export const trackedConfigsQueryOptions = queryOptions({ Array<{ path: string; tools: object }> >, }); + +export const useOpenFileMutation = () => + useMutation({ + mutationKey: ["openFile"], + mutationFn: (path: string) => + vscodeClient.request({ mutationKey: ["openFile"], variables: { path } }), + }); + +export const useEditSettingMutation = () => + useMutation({ + mutationKey: ["editSetting"], + mutationFn: ({ key }: { key: string }) => + vscodeClient.request({ + mutationKey: ["editSetting"], + variables: { key }, + }), + });