feat(editor): add cut and paste to query editor context menu

This commit is contained in:
高桥凉介 authored and GitHub committed 2026-08-10 18:51:44 +08:00
1 parent 1a3645a04d
commit 150d0108b7
9 files changed
+91 -1

No files matched your search

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, shallowRef, computed, nextTick } from "vue";
import { CaseLower, CaseUpper, Code2, Download, FileCode, Pencil, PencilRuler, Play, Copy, List, Search, Sparkles, Table2, TextSelect, Trash2 } from "@lucide/vue";
import { CaseLower, CaseUpper, ClipboardPaste, Code2, Download, FileCode, Pencil, PencilRuler, Play, Copy, List, Scissors, Search, Sparkles, Table2, TextSelect, Trash2 } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import type { CompletionContext } from "@codemirror/autocomplete";
import { Transaction, StateEffect } from "@codemirror/state";
@@ -1324,6 +1324,50 @@ async function copySelectedSqlFromContextMenu() {
}
}
async function cutSelectedSqlFromContextMenu() {
if (!canCopySelectedSql.value) return;
const currentView = view.value;
if (!currentView) return;
try {
await copyToClipboard(selectedSql.value);
// 剪切:复制后删除选中内容
const selection = currentView.state.selection.main;
if (!selection.empty) {
currentView.dispatch({
changes: { from: selection.from, to: selection.to },
selection: { anchor: selection.from, head: selection.from },
scrollIntoView: true,
userEvent: "input.cut",
});
}
toast(t("grid.cut"));
focusEditor();
} catch (e: any) {
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
}
}
async function pasteClipboardSqlFromContextMenu() {
if (props.readOnly) return;
const currentView = view.value;
if (!currentView) return;
try {
const text = await readTextFromClipboard();
if (!text) return;
const selection = currentView.state.selection.main;
// 粘贴:替换选中内容或在光标处插入
currentView.dispatch({
changes: { from: selection.from, to: selection.to, insert: text },
selection: { anchor: selection.from + text.length, head: selection.from + text.length },
scrollIntoView: true,
userEvent: "input.paste",
});
focusEditor();
} catch (e: any) {
toast(t("editor.contextMenu.pasteClipboardReadFailed", { message: e?.message || String(e) }), 5000);
}
}
function selectAllSqlFromContextMenu() {
const currentView = view.value;
if (!currentView) return;
@@ -1617,6 +1661,20 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
icon: Copy,
shortcut: "Mod+C",
},
{
label: t("editor.contextMenu.cutSelection"),
action: cutSelectedSqlFromContextMenu,
disabled: !canCopySelectedSql.value || props.readOnly,
icon: Scissors,
shortcut: "Mod+X",
},
{
label: t("editor.contextMenu.pasteFromClipboard"),
action: pasteClipboardSqlFromContextMenu,
disabled: props.readOnly,
icon: ClipboardPaste,
shortcut: "Mod+V",
},
{
label: t("editor.contextMenu.sendToAi"),
action: () => {
+4
View File
@@ -1186,6 +1186,9 @@ export default {
export: "Export",
exportQueryResultTo: "Export current query result to {format}",
copySelection: "Copy selection",
cutSelection: "Cut selection",
pasteFromClipboard: "Paste",
pasteClipboardReadFailed: "Failed to read clipboard: {message}",
sendToAi: "Send to AI",
uppercaseSelection: "Convert to uppercase",
lowercaseSelection: "Convert to lowercase",
@@ -1848,6 +1851,7 @@ export default {
sortUnsupported: "This SQL does not support full-result sorting. Try again with a single SELECT query.",
truncatedHint: "Results were truncated after loading {count} rows. Use the footer pagination to browse loaded data; exporting the full result reruns the database query.",
victoriaMetricsTruncatedHint: "Results exceeded the loading limit, so only the first {count} rows were loaded. Rows per page only paginates loaded data; shorten the time range or add filters to retrieve a complete result.",
cut: "Cut",
},
exportProgress: {
streamingUnsupported: "Streaming export is unsupported for this query. Simplify it or use a supported driver.",
+4
View File
@@ -1172,6 +1172,9 @@ export default withEnglishFallback({
expandSelectStarUnavailable: "No se pudieron cargar las columnas de esta tabla",
findReplace: "Buscar/Reemplazar",
deleteEmptyLines: "Eliminar líneas vacías",
cutSelection: "Cortar selección",
pasteFromClipboard: "Pegar",
pasteClipboardReadFailed: "Error al leer el portapapeles: {message}",
},
search: {
find: "Buscar",
@@ -1789,6 +1792,7 @@ export default withEnglishFallback({
binaryImportApplied: "Se importaron {count} bytes, que se escribirán en la base de datos al guardar los cambios.",
binaryImportFailed: "Error al importar el archivo: {message}",
binaryImportTooLarge: "El archivo es demasiado grande ({size}, límite {limit}). No se admite importar archivos grandes en una sola celda.",
cut: "Cortado",
},
exportProgress: {
streamingUnsupported: "La exportación en streaming no es compatible con esta consulta. Simplifíquela o use un controlador compatible.",
+4
View File
@@ -1170,6 +1170,9 @@ export default withEnglishFallback({
expandSelectStarUnavailable: "Impossibile caricare le colonne di questa tabella",
findReplace: "Trova/Sostituisci",
deleteEmptyLines: "Elimina righe vuote",
cutSelection: "Taglia selezione",
pasteFromClipboard: "Incolla",
pasteClipboardReadFailed: "Impossibile leggere gli appunti: {message}",
},
search: {
find: "Trova",
@@ -1787,6 +1790,7 @@ export default withEnglishFallback({
binaryImportApplied: "Importati {count} byte, scritti nel database dopo il salvataggio delle modifiche.",
binaryImportFailed: "Importazione del file non riuscita: {message}",
binaryImportTooLarge: "Il file è troppo grande ({size}, limite {limit}). L'importazione di file di grandi dimensioni in una singola cella non è supportata.",
cut: "Tagliato",
},
exportProgress: {
streamingUnsupported: "L'esportazione in streaming non è supportata per questa query. Semplificala o usa un driver supportato.",
+4
View File
@@ -1190,6 +1190,9 @@ export default withEnglishFallback({
expandSelectStarUnavailable: "このテーブルの列を読み込めません",
findReplace: "検索/置換",
deleteEmptyLines: "空行を削除",
cutSelection: "選択内容を切り取る",
pasteFromClipboard: "貼り付け",
pasteClipboardReadFailed: "クリップボードの読み取りに失敗しました:{message}",
},
search: {
find: "検索",
@@ -1814,6 +1817,7 @@ export default withEnglishFallback({
binaryImportApplied: "{count} バイトをインポートしました。変更を保存するとデータベースに書き込まれます。",
binaryImportFailed: "ファイルのインポートに失敗しました:{message}",
binaryImportTooLarge: "ファイルが大きすぎます({size}、上限 {limit})。大きなファイルの単一セルへのインポートはサポートされていません。",
cut: "切り取りました",
},
exportProgress: {
streamingUnsupported: "このクエリはストリーミングエクスポートに対応していません。クエリを簡略化するか、対応しているドライバーを使用してください。",
+4
View File
@@ -1080,6 +1080,9 @@ export default withEnglishFallback({
export: "내보내기",
exportQueryResultTo: "현재 쿼리 결과를 {format}(으)로 내보내기",
copySelection: "선택 영역 복사",
cutSelection: "선택 영역 잘라내기",
pasteFromClipboard: "붙여넣기",
pasteClipboardReadFailed: "클립보드 읽기 실패: {message}",
sendToAi: "AI로 보내기",
uppercaseSelection: "대문자로 변환",
lowercaseSelection: "소문자로 변환",
@@ -1373,6 +1376,7 @@ export default withEnglishFallback({
exported: "내보냈습니다",
exportFailed: "내보내기 실패: {message}",
copied: "복사했습니다",
cut: "잘라냈습니다",
copyFailed: "복사 실패: {message}",
previewSqlEmpty: "미리볼 보류 중인 SQL 변경이 없습니다",
renderMode: "렌더링 모드",
+4
View File
@@ -1171,6 +1171,9 @@ export default withEnglishFallback({
expandSelectStarUnavailable: "Não foi possível carregar as colunas desta tabela",
findReplace: "Localizar/substituir",
deleteEmptyLines: "Excluir linhas vazias",
cutSelection: "Recortar seleção",
pasteFromClipboard: "Colar",
pasteClipboardReadFailed: "Falha ao ler a área de transferência: {message}",
},
search: {
find: "Localizar",
@@ -1789,6 +1792,7 @@ export default withEnglishFallback({
binaryImportApplied: "{count} bytes importados, gravados no banco de dados após salvar as alterações.",
binaryImportFailed: "Falha ao importar o arquivo: {message}",
binaryImportTooLarge: "O arquivo é muito grande ({size}, limite {limit}). Não há suporte para importar arquivos grandes em uma única célula.",
cut: "Recortado",
},
exportProgress: {
streamingUnsupported: "A exportação em streaming não é compatível com esta consulta. Simplifique-a ou use um driver compatível.",
+4
View File
@@ -1111,6 +1111,9 @@ export default withEnglishFallback({
export: "导出",
exportQueryResultTo: "导出当前查询结果集到 {format}",
copySelection: "复制选中内容",
cutSelection: "剪切选中内容",
pasteFromClipboard: "粘贴",
pasteClipboardReadFailed: "读取剪贴板失败:{message}",
sendToAi: "发送到 AI",
uppercaseSelection: "转为大写",
lowercaseSelection: "转为小写",
@@ -1414,6 +1417,7 @@ export default withEnglishFallback({
xlsxHeaderOriginal: "表头使用字段名称",
xlsxHeaderComment: "表头使用注释",
copied: "已复制",
cut: "已剪切",
copyFailed: "复制失败:{message}",
previewSqlEmpty: "暂无待预览的 SQL 更改",
renderMode: "渲染模式",
+4
View File
@@ -1170,6 +1170,9 @@ export default withEnglishFallback({
expandSelectStarUnavailable: "無法讀取此資料表的欄位資訊",
findReplace: "查找/取代",
deleteEmptyLines: "刪除空行",
cutSelection: "剪下選取內容",
pasteFromClipboard: "貼上",
pasteClipboardReadFailed: "讀取剪貼簿失敗:{message}",
},
search: {
find: "尋找",
@@ -1788,6 +1791,7 @@ export default withEnglishFallback({
xlsxHeaderPrompt: "請選擇匯出 Excel 時使用的表頭格式:",
xlsxHeaderOriginal: "表頭使用欄位名稱",
xlsxHeaderComment: "表頭使用註解",
cut: "已剪下",
},
exportProgress: {
streamingUnsupported: "目前查詢暫不支援串流匯出,請簡化查詢或使用支援的驅動程式。",