mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(editor): support Vim config file
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
// (delivery is same-tick), never a pending undelivered one.
|
||||
let lastHandledFormatRequestId = 0;
|
||||
let lastHandledCompressRequestId = 0;
|
||||
let vimMappingsApplied = false;
|
||||
let vimClipboardConfigured = false;
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -59,7 +61,7 @@ import { parkEditorNativeSelection, type EditorNativeSelectionPark } from "@/lib
|
||||
import CodeSnapshotDialog from "@/components/codeSnapshot/CodeSnapshotDialog.vue";
|
||||
import QueryEditorContextMenu, { type QueryEditorContextMenuState, type QueryEditorContextMenuActions } from "./QueryEditorContextMenu.vue";
|
||||
|
||||
import { clipboardLineEndings, readTextFromClipboard } from "@/lib/common/clipboard";
|
||||
import { clipboardLineEndings, copyToClipboard, readTextFromClipboard } from "@/lib/common/clipboard";
|
||||
|
||||
import { resolveExecutableSql, type SqlExecutionOverride } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { supportsExecutionTargetPicker, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
@@ -107,6 +109,8 @@ import { resolveSqlShortcutTableToken } from "@/lib/sql/sqlShortcutTableTarget";
|
||||
import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
|
||||
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
|
||||
import { editorClipboardLineEndingsExtension } from "@/lib/editor/editorClipboardLineEndings";
|
||||
import { applyVimConfig, isVimMappingCommand, loadVimConfig } from "@/lib/editor/vimConfig";
|
||||
import { configureVimSystemClipboard } from "@/lib/editor/vimSystemClipboard";
|
||||
|
||||
import { selectionMatchOccurrences } from "@/lib/editor/codemirrorSelectionMatches";
|
||||
|
||||
@@ -1485,11 +1489,28 @@ function vimModeExtension(enabled = settingsStore.editorSettings.vimModeEnabled)
|
||||
function configureDbxVimCommands(vimApi: typeof import("@replit/codemirror-vim").Vim) {
|
||||
if (codeMirrorRuntime.dbxVimCommandsConfigured) return;
|
||||
codeMirrorRuntime.dbxVimCommandsConfigured = true;
|
||||
if (!vimClipboardConfigured) {
|
||||
configureVimSystemClipboard(vimApi, { readText: readTextFromClipboard, writeText: copyToClipboard });
|
||||
vimClipboardConfigured = true;
|
||||
}
|
||||
vimApi.defineEx("write", "w", (cm) => {
|
||||
cm.cm6?.contentDOM.dispatchEvent(new CustomEvent(DBX_VIM_SAVE_EVENT, { bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
let vimConfigApplied = false;
|
||||
|
||||
function applyVimConfigToCurrentEditor(commands: readonly string[]) {
|
||||
const vimApi = codeMirrorRuntime.codeMirrorVimApi;
|
||||
if (vimConfigApplied || !view.value || !codeMirrorRuntime.codeMirrorGetVimCm || !vimApi) return;
|
||||
const cm = codeMirrorRuntime.codeMirrorGetVimCm(view.value);
|
||||
if (!cm) return;
|
||||
const commandsForEditor = commands.filter((command) => !vimMappingsApplied || !isVimMappingCommand(command));
|
||||
applyVimConfig(commandsForEditor, (command) => vimApi.handleEx(cm as Parameters<typeof vimApi.handleEx>[0], command));
|
||||
vimMappingsApplied = true;
|
||||
vimConfigApplied = true;
|
||||
}
|
||||
|
||||
async function ensureCodeMirrorVim() {
|
||||
if (codeMirrorRuntime.codeMirrorVim && codeMirrorRuntime.codeMirrorVimApi && codeMirrorRuntime.codeMirrorGetVimCm) return true;
|
||||
codeMirrorRuntime.codeMirrorVimImportPromise ??= import("@replit/codemirror-vim");
|
||||
@@ -1880,6 +1901,7 @@ const codeMirrorLifecycle = useQueryEditorCodeMirror({
|
||||
if (initialSettings.vimModeEnabled) {
|
||||
await ensureCodeMirrorVim();
|
||||
}
|
||||
const vimCommands = initialSettings.vimModeEnabled ? await loadVimConfig() : [];
|
||||
const { currentStatementFrameExtension, activeLineHighlighter } = sqlExtensions.createViewDecorations();
|
||||
function updateLargeDocumentMode(currentView: EditorViewType) {
|
||||
largeDocumentMode.value = shouldUseQueryEditorLargeDocumentModeForSize(currentView.state.doc.length, currentView.state.doc.lines);
|
||||
@@ -2152,6 +2174,7 @@ const codeMirrorLifecycle = useQueryEditorCodeMirror({
|
||||
parent: editorElement,
|
||||
onReady() {
|
||||
if (!view.value) return;
|
||||
if (initialSettings.vimModeEnabled) applyVimConfigToCurrentEditor(vimCommands);
|
||||
syncQueryEditorInsertContext(view.value);
|
||||
batchSelection.attach(view.value, tooltipParent);
|
||||
postCompositionKeyGuardCleanup = postCompositionKeyGuard.attach(view.value.contentDOM);
|
||||
@@ -2468,7 +2491,7 @@ async function applyEditorAppearance() {
|
||||
syncEditorFontCssVars(liveFontSize.value, ss.fontFamily);
|
||||
syncEditorDiagnosticCssVars();
|
||||
const themeColors = getCurrentCustomThemeColors();
|
||||
const [themeExt] = await Promise.all([loadEditorTheme(ss.theme, editorThemeAppearance(), themeColors, themePalette.value), ss.vimModeEnabled ? ensureCodeMirrorVim() : Promise.resolve(false)]);
|
||||
const [themeExt, , vimCommands] = await Promise.all([loadEditorTheme(ss.theme, editorThemeAppearance(), themeColors, themePalette.value), ss.vimModeEnabled ? ensureCodeMirrorVim() : Promise.resolve(false), ss.vimModeEnabled ? loadVimConfig() : Promise.resolve([])]);
|
||||
if (
|
||||
!view.value ||
|
||||
!codeMirrorRuntime.codeMirrorTheme ||
|
||||
@@ -2494,6 +2517,8 @@ async function applyEditorAppearance() {
|
||||
codeMirrorRuntime.runKeymapComp.reconfigure(runKeymapExtension(codeMirrorRuntime.editorViewModule.keymap)),
|
||||
],
|
||||
});
|
||||
if (ss.vimModeEnabled) applyVimConfigToCurrentEditor(vimCommands);
|
||||
else vimConfigApplied = false;
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { applyVimConfig, isVimMappingCommand, parseVimConfig } from "../vimConfig";
|
||||
|
||||
const fs = { exists: vi.fn(), readTextFile: vi.fn() };
|
||||
|
||||
vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => true }));
|
||||
vi.mock("@tauri-apps/api/path", () => ({
|
||||
homeDir: () => Promise.resolve("/home/tester"),
|
||||
join: (...parts: string[]) => Promise.resolve(parts.join("/")),
|
||||
}));
|
||||
vi.mock("@tauri-apps/plugin-fs", () => ({
|
||||
exists: (path: string) => fs.exists(path),
|
||||
readTextFile: (path: string) => fs.readTextFile(path),
|
||||
}));
|
||||
|
||||
describe("Vim config", () => {
|
||||
beforeEach(() => {
|
||||
fs.exists.mockReset();
|
||||
fs.readTextFile.mockReset();
|
||||
});
|
||||
|
||||
it.each(["\n", "\r\n"])("parses commands, comments and blank lines with %j endings", (ending) => {
|
||||
expect(parseVimConfig(['" DBX Vim config', "", " imap jj <Esc> ", " nmap Y y$", ' " another comment'].join(ending))).toEqual(["imap jj <Esc>", "nmap Y y$"]);
|
||||
});
|
||||
|
||||
it("loads the file once and reuses its commands for later editors", async () => {
|
||||
fs.exists.mockResolvedValue(true);
|
||||
fs.readTextFile.mockResolvedValue("imap jj <Esc>\r\nnmap Y y$\r\n");
|
||||
vi.resetModules();
|
||||
const { loadVimConfig } = await import("../vimConfig");
|
||||
const [first, second] = await Promise.all([loadVimConfig(), loadVimConfig()]);
|
||||
expect(first).toEqual(["imap jj <Esc>", "nmap Y y$"]);
|
||||
expect(second).toEqual(first);
|
||||
expect(fs.exists).toHaveBeenCalledExactlyOnceWith("/home/tester/.dbx/vimrc");
|
||||
expect(fs.readTextFile).toHaveBeenCalledExactlyOnceWith("/home/tester/.dbx/vimrc");
|
||||
});
|
||||
|
||||
it("starts without a config file", async () => {
|
||||
fs.exists.mockResolvedValue(false);
|
||||
vi.resetModules();
|
||||
const { loadVimConfig } = await import("../vimConfig");
|
||||
expect(await loadVimConfig()).toEqual([]);
|
||||
expect(fs.readTextFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continues when the file cannot be read", async () => {
|
||||
fs.exists.mockResolvedValue(true);
|
||||
fs.readTextFile.mockRejectedValue(new Error("denied"));
|
||||
const warning = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
try {
|
||||
vi.resetModules();
|
||||
const { loadVimConfig } = await import("../vimConfig");
|
||||
expect(await loadVimConfig()).toEqual([]);
|
||||
expect(warning).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
warning.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("continues after an invalid command", () => {
|
||||
const execute = vi.fn((command: string) => {
|
||||
if (command === "bogus") throw new Error("unknown command");
|
||||
});
|
||||
const warning = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
try {
|
||||
applyVimConfig(["imap jj <Esc>", "bogus", "nmap Y y$"], execute);
|
||||
expect(execute.mock.calls.map(([command]) => command)).toEqual(["imap jj <Esc>", "bogus", "nmap Y y$"]);
|
||||
expect(warning).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
warning.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("applies an insert mapping to a CodeMirror Vim editor", async () => {
|
||||
const [{ EditorState }, { EditorView }, { Vim, vim, getCM }] = await Promise.all([import("@codemirror/state"), import("@codemirror/view"), import("@replit/codemirror-vim")]);
|
||||
const parent = document.createElement("div");
|
||||
document.body.append(parent);
|
||||
const view = new EditorView({ parent, state: EditorState.create({ doc: "select 1", extensions: [vim()] }) });
|
||||
try {
|
||||
const cm = getCM(view);
|
||||
expect(cm).not.toBeNull();
|
||||
if (!cm) return;
|
||||
applyVimConfig(["imap jj <Esc>"], (command) => Vim.handleEx(cm, command));
|
||||
Vim.handleKey(cm, "i");
|
||||
expect(cm.state.vim?.insertMode).toBe(true);
|
||||
Vim.handleKey(cm, "j");
|
||||
Vim.handleKey(cm, "j");
|
||||
expect(cm.state.vim?.insertMode).toBe(false);
|
||||
|
||||
const secondParent = document.createElement("div");
|
||||
document.body.append(secondParent);
|
||||
const secondView = new EditorView({ parent: secondParent, state: EditorState.create({ doc: "select 2", extensions: [vim()] }) });
|
||||
try {
|
||||
const secondCm = getCM(secondView);
|
||||
expect(secondCm).not.toBeNull();
|
||||
if (!secondCm) return;
|
||||
Vim.handleKey(secondCm, "i");
|
||||
Vim.handleKey(secondCm, "j");
|
||||
Vim.handleKey(secondCm, "j");
|
||||
expect(secondCm.state.vim?.insertMode).toBe(false);
|
||||
} finally {
|
||||
secondView.destroy();
|
||||
secondParent.remove();
|
||||
}
|
||||
} finally {
|
||||
view.destroy();
|
||||
parent.remove();
|
||||
}
|
||||
});
|
||||
|
||||
it("identifies shared mappings so later editor tabs do not add duplicates", () => {
|
||||
expect(isVimMappingCommand("imap jj <Esc>")).toBe(true);
|
||||
expect(isVimMappingCommand("nmap Y y$")).toBe(true);
|
||||
expect(isVimMappingCommand("inoremap jk <Esc>")).toBe(true);
|
||||
expect(isVimMappingCommand("set ignorecase")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { configureVimSystemClipboard } from "../vimSystemClipboard";
|
||||
|
||||
describe("Vim system clipboard", () => {
|
||||
it("connects unnamed yanks and pastes to the clipboard when configured", async () => {
|
||||
const [{ EditorState }, { EditorView }, { Vim, vim, getCM }] = await Promise.all([import("@codemirror/state"), import("@codemirror/view"), import("@replit/codemirror-vim")]);
|
||||
const clipboard = {
|
||||
readText: vi.fn().mockResolvedValue("external"),
|
||||
writeText: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
configureVimSystemClipboard(Vim, clipboard);
|
||||
const parent = document.createElement("div");
|
||||
document.body.append(parent);
|
||||
const view = new EditorView({ parent, state: EditorState.create({ doc: "A", extensions: [vim()] }) });
|
||||
try {
|
||||
const cm = getCM(view);
|
||||
expect(cm).not.toBeNull();
|
||||
if (!cm) return;
|
||||
const registers = Vim.getRegisterController();
|
||||
registers.pushText(undefined, "yank", "internal");
|
||||
expect(clipboard.writeText).not.toHaveBeenCalled();
|
||||
|
||||
Vim.handleEx(cm, "set clipboard=unnamedplus");
|
||||
expect(Vim.getOption("clipboard", cm as Parameters<typeof Vim.getOption>[1])).toBe("unnamedplus");
|
||||
registers.pushText(undefined, "yank", "selected SQL");
|
||||
expect(clipboard.writeText).toHaveBeenCalledWith("selected SQL");
|
||||
Vim.handleKey(cm, "y");
|
||||
Vim.handleKey(cm, "y");
|
||||
expect(clipboard.writeText).toHaveBeenCalledWith("A\n");
|
||||
|
||||
Vim.handleKey(cm, "p");
|
||||
await vi.waitFor(() => expect(view.state.doc.toString()).toContain("external"));
|
||||
expect(clipboard.readText).toHaveBeenCalledOnce();
|
||||
|
||||
Vim.handleEx(cm, "set clipboard=");
|
||||
clipboard.writeText.mockClear();
|
||||
registers.pushText(undefined, "yank", "local only");
|
||||
expect(clipboard.writeText).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
view.destroy();
|
||||
parent.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
|
||||
/** Commands supported by CodeMirror Vim are read from the desktop user's file. */
|
||||
export function parseVimConfig(text: string): string[] {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && !line.startsWith('"'));
|
||||
}
|
||||
|
||||
// CodeMirror Vim stores mappings globally. Applying one on every tab would
|
||||
// keep prepending duplicate mappings to its shared keymap.
|
||||
export function isVimMappingCommand(command: string): boolean {
|
||||
return /^(?:[niv]?noremap|[niv]?map|[niv]?unmap|[niv]?mapclear)(?:\s|$|!)/.test(command);
|
||||
}
|
||||
|
||||
let loading: Promise<string[]> | null = null;
|
||||
|
||||
/** Read once per app session; a restart picks up edits to ~/.dbx/vimrc. */
|
||||
export function loadVimConfig(): Promise<string[]> {
|
||||
loading ??= (async () => {
|
||||
if (!isTauriRuntime()) return [];
|
||||
try {
|
||||
const { homeDir, join } = await import("@tauri-apps/api/path");
|
||||
const { exists, readTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const file = await join(await homeDir(), ".dbx", "vimrc");
|
||||
if (!(await exists(file))) return [];
|
||||
return parseVimConfig(await readTextFile(file));
|
||||
} catch (error) {
|
||||
console.warn("[vim-config] could not read ~/.dbx/vimrc", error);
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
return loading;
|
||||
}
|
||||
|
||||
/** A bad command must not prevent the remaining commands or editor startup. */
|
||||
export function applyVimConfig(commands: readonly string[], execute: (command: string) => void): void {
|
||||
for (const command of commands) {
|
||||
try {
|
||||
execute(command);
|
||||
} catch (error) {
|
||||
console.warn(`[vim-config] ignored command: ${command}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
type VimApi = typeof import("@replit/codemirror-vim").Vim;
|
||||
|
||||
export interface VimClipboardAccess {
|
||||
readText(): Promise<string>;
|
||||
writeText(text: string): Promise<void>;
|
||||
}
|
||||
|
||||
/** Add `set clipboard=unnamedplus` without replacing CodeMirror Vim's editing commands. */
|
||||
export function configureVimSystemClipboard(vimApi: VimApi, clipboard: VimClipboardAccess): void {
|
||||
let clipboardOption = "";
|
||||
let unnamedPlus = false;
|
||||
|
||||
vimApi.defineOption("clipboard", "", "string", ["cb"], (value) => {
|
||||
if (value !== undefined) {
|
||||
clipboardOption = value;
|
||||
unnamedPlus = value.split(",").includes("unnamedplus");
|
||||
}
|
||||
return clipboardOption;
|
||||
});
|
||||
|
||||
const registers = vimApi.getRegisterController();
|
||||
const pushText = registers.pushText.bind(registers);
|
||||
registers.pushText = (registerName, operator, text, linewise, blockwise) => {
|
||||
if (unnamedPlus && registerName === "+") {
|
||||
// The library writes explicit + yanks through navigator.clipboard. Use
|
||||
// DBX's native clipboard path in the desktop webview instead.
|
||||
const value = linewise && !text.endsWith("\n") ? `${text}\n` : text;
|
||||
registers.getRegister("+").setText(value, linewise, blockwise);
|
||||
registers.unnamedRegister.setText(value, linewise, blockwise);
|
||||
} else {
|
||||
pushText(registerName, operator, text, linewise, blockwise);
|
||||
}
|
||||
if (unnamedPlus && registerName !== "_") {
|
||||
void clipboard.writeText(registers.unnamedRegister.toString()).catch((error) => {
|
||||
console.warn("[vim-config] could not write to system clipboard", error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const findKey = vimApi.findKey.bind(vimApi);
|
||||
const pendingPastes = new WeakSet<object>();
|
||||
vimApi.findKey = (cm, key, origin) => {
|
||||
if (pendingPastes.has(cm)) return () => true;
|
||||
const registerName = cm.state.vim?.inputState.registerName;
|
||||
const command = findKey(cm, key, origin);
|
||||
if (!unnamedPlus || (key !== "p" && key !== "P") || (registerName && registerName !== "+") || !command || cm.state.vim?.insertMode) return command;
|
||||
|
||||
return () => {
|
||||
const vimState = cm.state.vim;
|
||||
if (!vimState) return command();
|
||||
const previousText = registers.unnamedRegister.toString();
|
||||
const previousLinewise = registers.unnamedRegister.linewise;
|
||||
pendingPastes.add(cm);
|
||||
void clipboard
|
||||
.readText()
|
||||
.then((text) => {
|
||||
if (cm.state.vim !== vimState) return;
|
||||
const linewise = text === previousText ? previousLinewise : text.endsWith("\n");
|
||||
registers.unnamedRegister.setText(text, linewise, false);
|
||||
// Explicit + pastes must use the freshly read native clipboard, not
|
||||
// the library's navigator.clipboard branch.
|
||||
vimState.inputState.registerName = undefined;
|
||||
command();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("[vim-config] could not read system clipboard", error);
|
||||
if (cm.state.vim === vimState) {
|
||||
vimState.inputState.registerName = undefined;
|
||||
command();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
pendingPastes.delete(cm);
|
||||
});
|
||||
return true;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,19 @@ description: 使用 DBX 编写、理解、参数化、执行、分析和复用 S
|
||||
|
||||
查询编辑器是 DBX 的 SQL 工作区。它把方言感知的高亮与补全、语义诊断、参数输入、执行进度、多结果、执行计划、查询历史、SQL 库和 AI 助手放在同一个标签页中。
|
||||
|
||||
## Vim 配置
|
||||
|
||||
在桌面版的**编辑器设置**中启用 Vim 模式后,DBX 会读取 `~/.dbx/vimrc`。如文件不存在,请先创建;修改后重启 DBX 才会生效。空行及以 `"` 开头的注释行会被忽略。CodeMirror Vim 支持的命令会应用到每个 SQL 编辑器标签页,例如:
|
||||
|
||||
```vim
|
||||
" DBX Vim 映射
|
||||
imap jj <Esc>
|
||||
nmap Y y$
|
||||
set clipboard=unnamedplus
|
||||
```
|
||||
|
||||
配置 `set clipboard=unnamedplus` 后,Vim 的复制和删除会写入系统剪贴板,普通模式下的 `p` 和 `P` 会从系统剪贴板读取。不支持的命令不会阻止编辑器启动。
|
||||
|
||||
## 基本工作流
|
||||
|
||||
<Steps>
|
||||
|
||||
@@ -5,6 +5,19 @@ description: Write, understand, parameterize, execute, analyze, and reuse SQL in
|
||||
|
||||
The query editor is the SQL workspace in DBX. It combines dialect-aware highlighting and completion, semantic diagnostics, parameter input, execution progress, multiple results, execution plans, query history, the SQL library, and AI assistance in one tab.
|
||||
|
||||
## Vim configuration
|
||||
|
||||
In the desktop app, enable Vim mode in **Editor Settings** to load commands from `~/.dbx/vimrc`. Create the file if needed, then restart DBX after editing it. Blank lines and lines beginning with `"` are ignored. Commands supported by CodeMirror Vim, such as these mappings, apply to every SQL editor tab:
|
||||
|
||||
```vim
|
||||
" DBX Vim mappings
|
||||
imap jj <Esc>
|
||||
nmap Y y$
|
||||
set clipboard=unnamedplus
|
||||
```
|
||||
|
||||
With `set clipboard=unnamedplus`, Vim yanks and deletes write to the system clipboard, and normal-mode `p` and `P` read from it. Unsupported commands do not prevent the editor from starting.
|
||||
|
||||
## Basic Workflow
|
||||
|
||||
<Steps>
|
||||
|
||||
+3
-3
@@ -6,11 +6,11 @@ const capability = JSON.parse(readFileSync(new URL("../../src-tauri/capabilities
|
||||
permissions: (string | ScopedPermission)[];
|
||||
};
|
||||
|
||||
it("grants only exists and readTextFile command scopes for the exact UI tuning file", () => {
|
||||
it("grants only exists and readTextFile command scopes for the exact UI tuning and Vim config files", () => {
|
||||
const scopes = capability.permissions.filter((permission): permission is ScopedPermission => typeof permission !== "string" && permission.identifier.startsWith("fs:"));
|
||||
expect(scopes).toEqual([
|
||||
{ identifier: "fs:allow-exists", allow: [{ path: "$HOME/.dbx/ui-tuning.json" }] },
|
||||
{ identifier: "fs:allow-read-text-file", allow: [{ path: "$HOME/.dbx/ui-tuning.json" }] },
|
||||
{ identifier: "fs:allow-exists", allow: [{ path: "$HOME/.dbx/ui-tuning.json" }, { path: "$HOME/.dbx/vimrc" }] },
|
||||
{ identifier: "fs:allow-read-text-file", allow: [{ path: "$HOME/.dbx/ui-tuning.json" }, { path: "$HOME/.dbx/vimrc" }] },
|
||||
]);
|
||||
expect(capability.permissions).not.toContain("fs:scope");
|
||||
expect(capability.permissions.filter((permission) => typeof permission === "string" && permission.startsWith("fs:"))).toEqual(["fs:default", "fs:allow-write-file", "fs:allow-write-text-file", "fs:allow-stat", "fs:allow-read-file", "fs:allow-open", "fs:allow-read"]);
|
||||
|
||||
@@ -36,13 +36,13 @@
|
||||
"fs:default",
|
||||
{
|
||||
"identifier": "fs:allow-exists",
|
||||
"allow": [{ "path": "$HOME/.dbx/ui-tuning.json" }]
|
||||
"allow": [{ "path": "$HOME/.dbx/ui-tuning.json" }, { "path": "$HOME/.dbx/vimrc" }]
|
||||
},
|
||||
"fs:allow-write-file",
|
||||
"fs:allow-write-text-file",
|
||||
{
|
||||
"identifier": "fs:allow-read-text-file",
|
||||
"allow": [{ "path": "$HOME/.dbx/ui-tuning.json" }]
|
||||
"allow": [{ "path": "$HOME/.dbx/ui-tuning.json" }, { "path": "$HOME/.dbx/vimrc" }]
|
||||
},
|
||||
"fs:allow-stat",
|
||||
"fs:allow-read-file",
|
||||
|
||||
Reference in New Issue
Block a user