diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 40486accea..83071973da 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -2193,7 +2193,17 @@ onUnmounted(() => { />
- +
+
+
+ + + {{ t("settings.routineSourceOpenModeQueryTabDescription") }} + +
+
+ + +
+
diff --git a/apps/desktop/src/components/layout/AppSidebar.vue b/apps/desktop/src/components/layout/AppSidebar.vue index 775104ace4..f0207256df 100644 --- a/apps/desktop/src/components/layout/AppSidebar.vue +++ b/apps/desktop/src/components/layout/AppSidebar.vue @@ -22,6 +22,7 @@ const emit = defineEmits<{ export: []; startResize: [event: MouseEvent]; collapse: []; + "open-settings": [initialTab: string]; }>(); type ImportSource = "dbx" | "navicat" | "dbeaver" | "datagrip"; @@ -243,7 +244,7 @@ defineExpose({ focusSearch });
- +
diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 5630c1e6ff..9d01ca89c8 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -62,6 +62,10 @@ const sidebarScrollbarTrackRef = ref(null); const sidebarHorizontalScrollbarTrackRef = ref(null); const sidebarContextMenuRef = ref<{ close: () => void } | null>(null); const sidebarContextMenuItems = ref([]); +const emit = defineEmits<{ + "open-settings": [initialTab: string]; +}>(); + const sidebarContextMenuTarget = ref(null); const sidebarDangerDialogRequest = ref(null); const sidebarDangerDialogOpen = ref(false); @@ -1219,6 +1223,10 @@ function openSidebarObjectSource(node: TreeNode, initialEditing: boolean) { }); } +function openSidebarSettings(initialTab: string) { + emit("open-settings", initialTab); +} + function openSidebarProcedure(node: TreeNode) { if (node.type !== "procedure" || !node.connectionId || !node.database) return; beginSidebarAction(); @@ -1605,6 +1613,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes }); @open-ddl="openSidebarDdl" @open-object-source="openSidebarObjectSource" @open-procedure="openSidebarProcedure" + @open-settings="openSidebarSettings" @open-data="openSidebarData" @open-visible-databases="openSidebarVisibleDatabases" @open-visible-schemas="openSidebarVisibleSchemas" diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index ad8b9355d5..11161cc0ba 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -52,6 +52,7 @@ import { ListX, Info, X, + Settings2, } from "@lucide/vue"; import type { ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore"; @@ -316,6 +317,7 @@ const emit = defineEmits<{ "open-ddl": [node: TreeNode]; "open-object-source": [node: TreeNode, initialEditing: boolean]; "open-procedure": [node: TreeNode]; + "open-settings": [initialTab: string]; "open-data": [node: TreeNode, requireSelection: boolean, openMode: DataTabOpenMode, runner: (node: TreeNode, request: SidebarDataOpenRequest) => Promise]; "open-visible-databases": [node: TreeNode]; "open-visible-schemas": [node: TreeNode]; @@ -1508,12 +1510,38 @@ async function refreshDropTableChildObjectPreviewSql() { function openObjectSourceDialog(initialEditing: boolean) { const node = activeNode.value; if (!node.connectionId || !node.database) return; + const connectionId = node.connectionId; + const database = node.database; const objectType = objectSourceKindForTreeNode(node.type); if (!objectType) return; + const openMode = settingsStore.editorSettings.routineSourceOpenMode; + if (openMode === "query-tab") { + void connectionStore + .ensureConnected(connectionId) + .then(async () => { + connectionStore.activeConnectionId = connectionId; + const schema = node.schema || database; + const result = await api.getObjectSource(connectionId, database, schema, node.objectName || node.label, objectType as any, node.signature); + const tabId = queryStore.createTab(connectionId, database, `Source - ${node.label}`, "query", schema, result.source, node.catalog, { forceNew: true }); + const sourceIsEditable = result.editable !== false && !["SEQUENCE", "TRIGGER", "TYPE", "TYPE_BODY"].includes(objectType); + if (sourceIsEditable) { + queryStore.setObjectSource(tabId, { + schema, + name: node.objectName || node.label, + objectType, + signature: node.signature, + }); + } + }) + .catch((e: any) => { + toast(e?.message || String(e), 5000); + }); + return; + } void connectionStore - .ensureConnected(node.connectionId) + .ensureConnected(connectionId) .then(() => { - connectionStore.activeConnectionId = node.connectionId!; + connectionStore.activeConnectionId = connectionId; emit("open-object-source", node, initialEditing); }) .catch((e: any) => { @@ -4150,6 +4178,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean { action: () => emit("open-ddl", node), icon: FileCode, }); + items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 }); } if (canOpenStructureEditor.value) { items.push({ label: t("contextMenu.editStructure"), action: openStructureEditor, icon: PencilRuler }); @@ -4314,6 +4343,8 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean { }); } items.push({ label: "", separator: true }); + items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 }); + items.push({ label: "", separator: true }); items.push({ label: deleteMenuLabel(node.type === "procedure" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction")), action: deleteMenuAction(requestDropObject), @@ -4326,15 +4357,15 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean { if (node.type === "sequence") { items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 }); - items.push({ label: "", separator: true }); items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value }); + items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 }); return true; } if (node.type === "trigger" || node.type === "package" || node.type === "package-body" || node.type === "type" || node.type === "type-body") { items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 }); - items.push({ label: "", separator: true }); items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value }); + items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 }); return true; } return false; diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 12c049798c..bd30e31d9f 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -2036,6 +2036,7 @@ export default { dropTableSuccess: 'Table "{name}" dropped', editView: "Edit View", viewSource: "View Source", + changeOpenMode: "Change open mode", viewDdl: "View DDL", viewDdlLoading: "Loading DDL...", ddlCopied: "DDL copied", @@ -4430,6 +4431,11 @@ export default { sidebarObjectDisplayGroupedDescription: "Group objects by tables, views, procedures, and functions for databases with more object types.", sidebarObjectDisplaySimple: "Simple view", sidebarObjectDisplaySimpleDescription: "List tables and views directly under each database or schema to reduce intermediate levels.", + routineSourceOpenMode: "Function / procedure / view open mode", + routineSourceOpenModeQueryTab: "Data tab", + routineSourceOpenModeQueryTabDescription: "Open source in a new data tab, the same way as running SQL queries.", + routineSourceOpenModeDialog: "Dialog window", + routineSourceOpenModeDialogDescription: "Open source in a popup dialog for quick viewing and editing.", autoSelectActiveSidebarNode: "Always select opened item", autoSelectActiveSidebarNodeDescription: "When switching tabs, select the matching visible table, collection, or SQL file in the sidebar.", openTabsRestoreMode: "Restore tabs on launch", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index a9a299e576..361181d0b2 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -2136,6 +2136,7 @@ export default withEnglishFallback({ createProcedure: "Nuevo procedimiento almacenado", createFunction: "Nueva función", createTrigger: "Nuevo disparador", + changeOpenMode: "Modificar modo de apertura", }, visibleDatabases: { title: "Bases de datos visibles", @@ -4610,6 +4611,11 @@ export default withEnglishFallback({ dateTimeFormatAuto: "Reconocimiento automático", dateTimeFormatSearchPlaceholder: "Seleccionar o ingresar formato", dateTimeFormatEmpty: "Ingresar formato de fecha y hora personalizado", + routineSourceOpenMode: "Modo de apertura de funciones/procedimientos/vistas", + routineSourceOpenModeQueryTab: "Pestaña de datos", + routineSourceOpenModeQueryTabDescription: "Abrir el código fuente en una nueva pestaña de datos, de la misma manera que al ejecutar consultas SQL.", + routineSourceOpenModeDialog: "Ventana emergente", + routineSourceOpenModeDialogDescription: "Abrir el código fuente en una ventana emergente para una visualización y edición rápidas.", }, driverStore: { jreDirRemoveFailed: "No se pudo eliminar el directorio JRE antiguo: {path} (error original: {error})", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 4efdb9489b..8a31369a66 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2134,6 +2134,7 @@ export default withEnglishFallback({ createProcedure: "Nuovo stored procedure", createFunction: "Nuova funzione", createTrigger: "Nuovo trigger", + changeOpenMode: "Modifica modalità apertura", }, visibleDatabases: { title: "Database Visibili", @@ -4608,6 +4609,11 @@ export default withEnglishFallback({ dateTimeFormatAuto: "Riconoscimento automatico", dateTimeFormatSearchPlaceholder: "Seleziona o inserisci il formato", dateTimeFormatEmpty: "Inserisci un formato data/ora personalizzato", + routineSourceOpenMode: "Modalità apertura funzioni/procedure/viste", + routineSourceOpenModeQueryTab: "Scheda dati", + routineSourceOpenModeQueryTabDescription: "Apre il codice sorgente in una nuova scheda dati, come per l'esecuzione di query SQL.", + routineSourceOpenModeDialog: "Finestra a comparsa", + routineSourceOpenModeDialogDescription: "Apre il codice sorgente in una finestra di dialogo a comparsa per una rapida visualizzazione e modifica.", }, driverStore: { jreDirRemoveFailed: "Impossibile rimuovere la vecchia directory JRE: {path} (errore originale: {error})", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 6946738403..58a0e3daf9 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -2135,6 +2135,7 @@ export default withEnglishFallback({ createProcedure: "新規ストアドプロシージャ", createFunction: "新規関数", createTrigger: "新規トリガー", + changeOpenMode: "開き方を変更", }, visibleDatabases: { title: "表示するデータベース", @@ -4608,6 +4609,11 @@ export default withEnglishFallback({ dateTimeFormatAuto: "自動認識", dateTimeFormatSearchPlaceholder: "形式を選択または入力", dateTimeFormatEmpty: "カスタム日時形式を入力", + routineSourceOpenMode: "関数/プロシージャ/ビューの開き方", + routineSourceOpenModeQueryTab: "データタブ", + routineSourceOpenModeQueryTabDescription: "新しいデータタブでソースコードを開きます。SQLクエリの実行と同じ方法です。", + routineSourceOpenModeDialog: "ポップアップウィンドウ", + routineSourceOpenModeDialogDescription: "ポップアップダイアログでソースコードを開き、素早く表示・編集できます。", }, driverStore: { jreDirRemoveFailed: "古い JRE ディレクトリを削除できませんでした: {path}(元のエラー: {error})", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index eed55ca12a..6150951663 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2136,6 +2136,7 @@ export default withEnglishFallback({ createProcedure: "Novo procedimento armazenado", createFunction: "Nova função", createTrigger: "Novo gatilho", + changeOpenMode: "Alterar modo de abertura", }, visibleDatabases: { title: "Bancos de dados visíveis", @@ -4610,6 +4611,11 @@ export default withEnglishFallback({ dateTimeFormatAuto: "Detecção automática", dateTimeFormatSearchPlaceholder: "Selecione ou digite um formato", dateTimeFormatEmpty: "Insira um formato de data e hora personalizado", + routineSourceOpenMode: "Modo de abertura de funções/procedimentos/views", + routineSourceOpenModeQueryTab: "Aba de dados", + routineSourceOpenModeQueryTabDescription: "Abrir código-fonte em uma nova aba de dados, da mesma forma que executar uma consulta SQL.", + routineSourceOpenModeDialog: "Janela pop-up", + routineSourceOpenModeDialogDescription: "Abrir código-fonte em uma caixa de diálogo pop-up, facilitando a visualização e edição rápida.", }, driverStore: { jreDirRemoveFailed: "Não foi possível remover o diretório JRE antigo: {path} (erro original: {error})", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 1a437b46b8..69de738291 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -2037,6 +2037,7 @@ export default withEnglishFallback({ dropTableSuccess: "表「{name}」已删除", editView: "编辑视图", viewSource: "查看源码", + changeOpenMode: "修改打开方式", viewDdl: "查看 DDL", viewDdlLoading: "正在读取 DDL...", ddlCopied: "DDL 已复制", @@ -4430,6 +4431,11 @@ export default withEnglishFallback({ sidebarObjectDisplayGroupedDescription: "按表、视图、过程、函数分组展示,适合对象类型较多的库。", sidebarObjectDisplaySimple: "简单视图", sidebarObjectDisplaySimpleDescription: "数据库或 schema 下直接平铺表和视图,减少中间层级。", + routineSourceOpenMode: "函数/过程/视图打开方式", + routineSourceOpenModeQueryTab: "数据标签页", + routineSourceOpenModeQueryTabDescription: "在新的数据标签页中打开源码,与执行 SQL 查询方式相同。", + routineSourceOpenModeDialog: "弹出窗口", + routineSourceOpenModeDialogDescription: "在弹出对话框中打开源码,方便快速查看和编辑。", autoSelectActiveSidebarNode: "始终选中已打开项目", autoSelectActiveSidebarNodeDescription: "切换标签页时,在侧边栏选中匹配的可见表、集合或 SQL 文件。", openTabsRestoreMode: "启动时恢复标签页", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index a4102fc85f..8045c79788 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2135,6 +2135,7 @@ export default withEnglishFallback({ createProcedure: "新增儲存程序", createFunction: "新增函數", createTrigger: "新增觸發器", + changeOpenMode: "修改開啟方式", }, visibleDatabases: { title: "顯示資料庫", @@ -4279,6 +4280,11 @@ export default withEnglishFallback({ shortcutUppercaseSelection: "選取內容轉為大寫", shortcutLowercaseSelection: "選取內容轉為小寫", shortcutExPasteSqlInCondition: "ExPaste:貼上為 IN 條件", + routineSourceOpenMode: "函式/程序/檢視開啟方式", + routineSourceOpenModeQueryTab: "資料標籤頁", + routineSourceOpenModeQueryTabDescription: "在新的資料標籤頁中開啟原始碼,與執行 SQL 查詢方式相同。", + routineSourceOpenModeDialog: "彈出視窗", + routineSourceOpenModeDialogDescription: "在彈出對話方塊中開啟原始碼,方便快速檢視和編輯。", }, driverStore: { jreDirRemoveFailed: "無法刪除舊的 JRE 目錄:{path}(原始錯誤:{error})", diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index 441823b45c..a9a412e2c5 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -39,6 +39,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "sqlFormatter", "sidebarActivation", "sidebarObjectDisplay", + "routineSourceOpenMode", "sidebarTableSearchEnabled", "autoSelectActiveSidebarNode", "openTabsRestoreMode", diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index e47b75c3e3..7d5f395fb6 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -475,6 +475,7 @@ export interface EditorSettings { sidebarActivation: SidebarActivation; sidebarConnectionSortMode: ConnectionListSortMode; sidebarObjectDisplay: "grouped" | "simple"; + routineSourceOpenMode: "query-tab" | "dialog"; sidebarTableSearchEnabled: boolean; autoSelectActiveSidebarNode: boolean; openTabsRestoreMode: OpenTabsRestoreMode; @@ -647,6 +648,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { sidebarActivation: "single", sidebarConnectionSortMode: "manual", sidebarObjectDisplay: "grouped", + routineSourceOpenMode: "query-tab", sidebarTableSearchEnabled: false, autoSelectActiveSidebarNode: false, openTabsRestoreMode: "all", @@ -946,6 +948,7 @@ export function normalizeEditorSettings(settings: Partial, exist sidebarActivation: settings.sidebarActivation === "single" || settings.sidebarActivation === "double" ? settings.sidebarActivation : DEFAULT_EDITOR_SETTINGS.sidebarActivation, sidebarConnectionSortMode: normalizeConnectionListSortMode(settings.sidebarConnectionSortMode), sidebarObjectDisplay: settings.sidebarObjectDisplay === "simple" || settings.sidebarObjectDisplay === "grouped" ? settings.sidebarObjectDisplay : DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay, + routineSourceOpenMode: settings.routineSourceOpenMode === "query-tab" || settings.routineSourceOpenMode === "dialog" ? settings.routineSourceOpenMode : DEFAULT_EDITOR_SETTINGS.routineSourceOpenMode, sidebarTableSearchEnabled: typeof settings.sidebarTableSearchEnabled === "boolean" ? settings.sidebarTableSearchEnabled : DEFAULT_EDITOR_SETTINGS.sidebarTableSearchEnabled, autoSelectActiveSidebarNode: settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode, openTabsRestoreMode: normalizeOpenTabsRestoreMode( @@ -1424,6 +1427,7 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.sidebarActivation !== undefined) editorSettings.value.sidebarActivation = partial.sidebarActivation; if (partial.sidebarConnectionSortMode !== undefined) editorSettings.value.sidebarConnectionSortMode = normalizeConnectionListSortMode(partial.sidebarConnectionSortMode); if (partial.sidebarObjectDisplay !== undefined) editorSettings.value.sidebarObjectDisplay = partial.sidebarObjectDisplay; + if (partial.routineSourceOpenMode !== undefined) editorSettings.value.routineSourceOpenMode = partial.routineSourceOpenMode; if (partial.sidebarTableSearchEnabled !== undefined) editorSettings.value.sidebarTableSearchEnabled = partial.sidebarTableSearchEnabled; if (partial.autoSelectActiveSidebarNode !== undefined) editorSettings.value.autoSelectActiveSidebarNode = partial.autoSelectActiveSidebarNode; if (partial.openTabsRestoreMode !== undefined) editorSettings.value.openTabsRestoreMode = normalizeOpenTabsRestoreMode(partial.openTabsRestoreMode); diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 202d713dde..717fc683e4 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -351,6 +351,17 @@ test("marked-clean object source tabs close without unsaved confirmation", () => ); }); +test("object source tabs can force distinct clean identities for overloaded routines", () => { + setActivePinia(createPinia()); + const store = useQueryStore(); + const firstId = store.createTab("conn-1", "db", "Source - calculate", "query", "public", "CREATE FUNCTION calculate(integer)", undefined, { forceNew: true }); + const secondId = store.createTab("conn-1", "db", "Source - calculate", "query", "public", "CREATE FUNCTION calculate(text)", undefined, { forceNew: true }); + + assert.notEqual(firstId, secondId); + assert.equal(store.isTabDirty(store.tabs.find((tab) => tab.id === firstId)!), false); + assert.equal(store.isTabDirty(store.tabs.find((tab) => tab.id === secondId)!), false); +}); + test("close all tabs pauses on unsaved query tabs", () => { setActivePinia(createPinia()); const store = useQueryStore(); diff --git a/packages/app-tests/sidebarContextMenuHost.test.ts b/packages/app-tests/sidebarContextMenuHost.test.ts index 5093e2f7ec..61e7ab4aee 100644 --- a/packages/app-tests/sidebarContextMenuHost.test.ts +++ b/packages/app-tests/sidebarContextMenuHost.test.ts @@ -60,6 +60,17 @@ test("tree host owns sidebar data-open generations", () => { assert.match(connectionTree, /createSidebarActionTarget\(node\)/); }); +test("query-tab object source opens clean isolated tabs and honors backend editability", () => { + const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8"); + const openObjectSourceBody = functionBody(runtimeHost, "openObjectSourceDialog"); + + assert.match(openObjectSourceBody, /createTab\(connectionId, database, `Source - \$\{node\.label\}`, "query", schema, result\.source, node\.catalog, \{ forceNew: true \}\)/); + assert.match(openObjectSourceBody, /result\.editable !== false/); + assert.match(openObjectSourceBody, /!\["SEQUENCE", "TRIGGER", "TYPE", "TYPE_BODY"\]\.includes\(objectType\)/); + assert.match(openObjectSourceBody, /signature: node\.signature/); + assert.doesNotMatch(openObjectSourceBody, /queryStore\.updateSql/); +}); + test("table copy menu uses the shared single and multi-selection clipboard path", () => { const runtimeHost = readFileSync("apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue", "utf8"); const copyNameBody = functionBody(runtimeHost, "copyName");