From 6d72bba82d8e29bbbf0751ff0ce773e6c4d5f6c2 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 3 Jul 2026 15:39:12 +0800 Subject: [PATCH 01/23] =?UTF-8?q?feat(lsp):=20=E9=97=AE=E9=A2=98=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E8=B7=A8=E6=96=87=E4=BB=B6=E8=81=9A=E5=90=88=E8=AF=8A?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 allDiagnostics store:拦截 lsp:messages 的 publishDiagnostics,按文件(URI→路径)聚合; 问题面板改为按文件分组展示(当前文件优先),点击诊断打开对应文件并定位。 --- src/App.vue | 14 +++++- src/components/DiagnosticsPanel.vue | 51 +++++++++++++++------ src/editor/allDiagnostics.ts | 71 +++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 src/editor/allDiagnostics.ts diff --git a/src/App.vue b/src/App.vue index 7b821eef..7ffd0eac 100644 --- a/src/App.vue +++ b/src/App.vue @@ -420,7 +420,8 @@ @@ -551,6 +552,7 @@ import {foldAll, unfoldAll, matchBrackets} from '@codemirror/language' import {selectParentSyntax} from '@codemirror/commands' import {format as formatSql} from 'sql-formatter' import {diagnostics} from './editor/lspDiagnostics' +import {initDiagnosticsAggregator} from './editor/allDiagnostics' import {useGitPermalink} from './composables/useGitPermalink' import {useRevealInTree} from './composables/useRevealInTree' import {useWorkspaceRoots} from './composables/useWorkspaceRoots' @@ -1581,6 +1583,15 @@ const openSearchResult = async (path: string, line: number) => { gotoLine(line) } +// 问题面板:打开诊断所在文件并定位 +const openDiagnostic = async (path: string, line: number, col: number) => { + if (path && path !== currentFilePath.value) { + await smartOpen(path) + await nextTick() + } + gotoLine(line, col) +} + // LSP 跨文件跳转定义:编辑器扩展派发 lsp:open-location,这里打开目标文件并定位 const onLspOpenLocation = async (e: Event) => { const detail = (e as CustomEvent).detail as {path: string; line: number; character?: number} @@ -2385,6 +2396,7 @@ useGlobalShortcuts(matchShortcut, shortcutDispatch, isOverlayOpen) const {init: initTheme, setTheme: setAppTheme} = useTheme() onMounted(async () => { + initDiagnosticsAggregator() await initTheme() await initialize() await buildLanguageRegistry() diff --git a/src/components/DiagnosticsPanel.vue b/src/components/DiagnosticsPanel.vue index b09448fe..b511dd1a 100644 --- a/src/components/DiagnosticsPanel.vue +++ b/src/components/DiagnosticsPanel.vue @@ -16,18 +16,25 @@ - +
-
+
{{ t('diag.empty') }}
- +
@@ -35,18 +42,34 @@ diff --git a/src/composables/useLaunchPresets.ts b/src/composables/useLaunchPresets.ts new file mode 100644 index 00000000..3b121ee4 --- /dev/null +++ b/src/composables/useLaunchPresets.ts @@ -0,0 +1,40 @@ +// 运行预设(launch presets):把一组运行输入(参数/stdin/环境变量)存为命名条目,便于切换。 +import {ref} from 'vue' +import {kvGetJSON, kvSetJSON} from './useKvStore' + +export interface LaunchPreset { + name: string + args: string + stdin: string + env: string +} + +const KEY = 'launch-presets' + +export function useLaunchPresets() { + const presets = ref(kvGetJSON(KEY, [])) + const persist = () => kvSetJSON(KEY, presets.value) + + const save = (name: string, args: string, stdin: string, env: string) => { + const n = name.trim() + if (!n) { + return + } + const entry: LaunchPreset = {name: n, args, stdin, env} + const idx = presets.value.findIndex(p => p.name === n) + if (idx >= 0) { + presets.value = presets.value.map((p, i) => (i === idx ? entry : p)) + } + else { + presets.value = [...presets.value, entry] + } + persist() + } + + const remove = (name: string) => { + presets.value = presets.value.filter(p => p.name !== name) + persist() + } + + return {presets, save, remove} +} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 55edadd4..fa7b1685 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -825,6 +825,14 @@ "noOutput": "No output", "noOutputHint": "Try running some code" }, + "launch": { + "presets": "Run presets", + "namePlaceholder": "Preset name…", + "save": "Save", + "empty": "No run presets yet", + "emptyInputs": "(empty inputs)", + "delete": "Delete" + }, "workspaces": { "title": "Named workspaces", "namePlaceholder": "Name this workspace…", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 9e6050f4..aff88d4d 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -825,6 +825,14 @@ "noOutput": "没有输出", "noOutputHint": "可以尝试运行一些代码" }, + "launch": { + "presets": "运行预设", + "namePlaceholder": "预设名称…", + "save": "保存", + "empty": "还没有运行预设", + "emptyInputs": "(空输入)", + "delete": "删除" + }, "workspaces": { "title": "命名工作区", "namePlaceholder": "为当前工作区命名…", From e4ebf1c3a2ba2711df871780aa88df40e9708364 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sat, 4 Jul 2026 15:21:14 +0800 Subject: [PATCH 04/23] =?UTF-8?q?feat(run):=20=E8=BF=90=E8=A1=8C=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E6=94=AF=E6=8C=81=E8=BF=87=E6=BB=A4=EF=BC=88=E4=BB=85?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=90=AB=E5=85=B3=E9=94=AE=E5=AD=97=E7=9A=84?= =?UTF-8?q?=E8=A1=8C=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 控制台头部新增过滤开关,展开后输入关键字仅渲染匹配行并显示行数; 不影响原始输出与复制。 --- src/components/ConsoleOutput.vue | 43 +++++++++++++++++++++++++++++--- src/i18n/locales/en.json | 3 +++ src/i18n/locales/zh-CN.json | 3 +++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/components/ConsoleOutput.vue b/src/components/ConsoleOutput.vue index be76d1e5..a6491e37 100644 --- a/src/components/ConsoleOutput.vue +++ b/src/components/ConsoleOutput.vue @@ -12,6 +12,15 @@
{{ t('console.copied') }} + + +
+ +
+ + + {{ t('console.filterCount', { n: matchCount }) }} +
+
@@ -63,7 +81,7 @@ diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 3e5326c5..b7c1e664 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -690,6 +690,8 @@ "formatWithAi": "AI format code", "aiFixDiagnostics": "AI fix diagnostics", "aiGenDoc": "AI generate doc comment", + "aiTranslate": "AI translate comments/docs", + "aiMultiEdit": "AI multi-file edit", "history": "Run history", "diff": "Diff (current vs saved)", "preview": "Live preview (Markdown / HTML)", @@ -1174,6 +1176,19 @@ "failed": "AI action failed: ", "noCode": "No code to process" }, + "aiMulti": { + "title": "AI multi-file edit", + "instructionPlaceholder": "Describe the change to make across files, e.g. switch all fetch calls to a shared request wrapper", + "contextFiles": "Files in context ({n} selected)", + "cancel": "Cancel", + "generate": "Generate edits", + "thinking": "AI is thinking…", + "back": "Back", + "apply": "Apply {n} file(s)", + "noChange": "AI proposed no changes", + "tooLarge": "(file too large, diff skipped)", + "failed": "Generation failed: " + }, "task": { "title": "Run tasks", "labelPlaceholder": "Task name", @@ -1376,6 +1391,9 @@ "noDiagnostics": "No diagnostics in the current file", "replUnsupported": "No built-in REPL for the current language", "replStarted": "REPL started: {cmd}", + "aiMultiNeedFiles": "Open the files you want to edit first", + "aiMultiApplied": "AI edits applied ({n} file(s))", + "aiMultiWriteFailed": "Failed to write {name}: ", "sqlFormatted": "SQL formatted", "sqlFormatFailed": "SQL format failed: ", "sqlFormatOnlySql": "Only available when editing SQL", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 951e452c..436e3af5 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -691,6 +691,7 @@ "aiFixDiagnostics": "AI 修复诊断", "aiGenDoc": "AI 生成文档注释", "aiTranslate": "AI 翻译注释/文档", + "aiMultiEdit": "AI 多文件编辑", "history": "执行历史", "diff": "差异对比(当前 vs 已保存)", "preview": "实时预览(Markdown / HTML)", @@ -1175,6 +1176,19 @@ "failed": "AI 操作失败:", "noCode": "没有可处理的代码" }, + "aiMulti": { + "title": "AI 多文件编辑", + "instructionPlaceholder": "描述要跨文件完成的修改,例如:把所有 fetch 调用改为使用统一的 request 封装", + "contextFiles": "参与的文件(已选 {n} 个)", + "cancel": "取消", + "generate": "生成修改", + "thinking": "AI 思考中…", + "back": "返回", + "apply": "应用 {n} 个文件", + "noChange": "AI 未提出任何修改", + "tooLarge": "(文件过大,已跳过差异计算)", + "failed": "生成失败:" + }, "task": { "title": "运行任务", "labelPlaceholder": "任务名", @@ -1377,6 +1391,9 @@ "noDiagnostics": "当前文件没有诊断问题", "replUnsupported": "当前语言暂无内置 REPL", "replStarted": "已启动 REPL:{cmd}", + "aiMultiNeedFiles": "请先打开需要参与编辑的文件", + "aiMultiApplied": "已应用 AI 修改({n} 个文件)", + "aiMultiWriteFailed": "写入 {name} 失败:", "sqlFormatted": "SQL 已格式化", "sqlFormatFailed": "SQL 格式化失败: ", "sqlFormatOnlySql": "仅在 SQL 编辑时可用", From 7f3ca990a0878205858110eb4f98a7052f146323 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 5 Jul 2026 08:28:06 +0800 Subject: [PATCH 08/23] =?UTF-8?q?feat(sql):=20=E8=A1=A8=E6=A0=BC=E8=A1=8C?= =?UTF-8?q?=E5=86=85=E7=BC=96=E8=BE=91=E5=9B=9E=E5=86=99=EF=BC=88=E5=8F=8C?= =?UTF-8?q?=E5=87=BB=E6=94=B9=E5=80=BC=EF=BC=8C=E7=94=9F=E6=88=90=20UPDATE?= =?UTF-8?q?=20=E7=A1=AE=E8=AE=A4=E5=90=8E=E5=86=99=E5=BA=93=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 86 +++++++++++++++++++++++++++++++ src/components/SqlResultTable.vue | 7 ++- src/components/SqlTableView.vue | 11 +++- src/components/VirtualTable.vue | 65 +++++++++++++++++++++-- src/i18n/locales/en.json | 8 +++ src/i18n/locales/zh-CN.json | 8 +++ 6 files changed, 177 insertions(+), 8 deletions(-) diff --git a/src/App.vue b/src/App.vue index 4fb0e58f..28e6e276 100644 --- a/src/App.vue +++ b/src/App.vue @@ -201,8 +201,10 @@ :is-running="isRunning" :execution-time="lastExecutionTime" :paging="sqlPaging" + :editable-table="sqlEditableTable" @prev="sqlPrevPage" @next="sqlNextPage" + @edit-cell="onSqlEditCell" @clear="clearOutput"/> @@ -328,6 +330,18 @@
+ + +
+

{{ t('sqlEdit.confirmHint') }}

+
{{ pendingSqlUpdate.sql }}
+
+ + +
+
+
+ @@ -2108,6 +2122,78 @@ const loadSqlPage = async (offset: number, record: boolean) => { const sqlPrevPage = () => sqlPage.offset > 0 && loadSqlPage(Math.max(0, sqlPage.offset - SQL_PAGE_SIZE), false) const sqlNextPage = () => sqlPage.hasMore && loadSqlPage(sqlPage.offset + SQL_PAGE_SIZE, false) +// ===== SQL 表格行内编辑回写 ===== +// 从当前分页 SQL 解析唯一目标表(单表 SELECT 才允许编辑;含 JOIN/子查询/多表则返回 null) +const sqlEditableTable = computed(() => { + if (!sqlPage.active || !sqlPage.sql) { + return null + } + const s = sqlPage.sql.replace(/\s+/g, ' ').trim() + const m = /\bfrom\s+([`"[]?[A-Za-z_][\w.]*[`"\]]?)/i.exec(s) + if (!m) { + return null + } + if (/\bjoin\b/i.test(s) || /\bfrom\s*\(/i.test(s)) { + return null + } + // FROM 与后续子句之间若出现逗号则为多表,禁止编辑 + const rest = s.slice(m.index + m[0].length).split(/\bwhere\b|\bgroup\b|\border\b|\blimit\b|\bhaving\b/i)[0] + if (rest.includes(',')) { + return null + } + return m[1].replace(/[`"[\]]/g, '') +}) + +// 生成 SQL 字面量(跨 SQLite/MySQL/Postgres 的基本类型) +const sqlLiteral = (v: any): string => { + if (v === null || v === undefined) { + return 'NULL' + } + if (typeof v === 'number') { + return String(v) + } + if (typeof v === 'boolean') { + return v ? '1' : '0' + } + return `'${String(v).replace(/'/g, "''")}'` +} + +const pendingSqlUpdate = ref<{ sql: string } | null>(null) +// 单元格编辑 → 生成 UPDATE(WHERE 用该行全部原值定位)→ 确认后回写 +const onSqlEditCell = (p: { table: string; column: string; row: any[]; columns: string[]; oldValue: any; newValue: any }) => { + const where = p.columns + .map((col, i) => { + const val = p.row[i] + return val === null || val === undefined ? `${col} IS NULL` : `${col} = ${sqlLiteral(val)}` + }) + .join(' AND ') + const sql = `UPDATE ${p.table} SET ${p.column} = ${sqlLiteral(p.newValue)} WHERE ${where}` + pendingSqlUpdate.value = {sql} +} +const runPendingSqlUpdate = async () => { + const pending = pendingSqlUpdate.value + if (!pending) { + return + } + pendingSqlUpdate.value = null + try { + const source = sqlPage.source ?? resolveActiveSource() + const res = sqlTxn.active.value + ? await sqlTxn.exec(pending.sql) + : await invoke('run_sql', {sql: pending.sql, source}) + if (res.error) { + toast.error(t('sqlEdit.failed') + res.error) + return + } + toast.success(t('sqlEdit.done')) + // 回写成功后刷新当前页,保证表格与数据库一致 + await loadSqlPage(sqlPage.offset, false) + } + catch (error) { + toast.error(t('sqlEdit.failed') + error) + } +} + const runSql = async (sqlOverride?: string) => { const sql = sqlOverride ?? code.value if (!sql.trim()) { diff --git a/src/components/SqlResultTable.vue b/src/components/SqlResultTable.vue index 762ea57e..2e6709a5 100644 --- a/src/components/SqlResultTable.vue +++ b/src/components/SqlResultTable.vue @@ -15,7 +15,9 @@
{{ t('sql.resultSet', { n: ri + 1, rows: rs.rows.length }) }}
- +
@@ -37,7 +39,8 @@ const {t} = useI18n() interface ResultSet { columns: string[]; rows: any[][] } interface SqlResult { result_sets: ResultSet[]; messages: string[]; error: string | null; elapsed_ms: number } -const props = defineProps<{ output: string; emptyText?: string; fillHeight?: boolean }>() +const props = defineProps<{ output: string; emptyText?: string; fillHeight?: boolean; editable?: boolean }>() +const emit = defineEmits<{ editCell: [payload: { column: string; row: any[]; columns: string[]; oldValue: any; newValue: any }] }>() const result = computed(() => { if (!props.output.trim()) { diff --git a/src/components/SqlTableView.vue b/src/components/SqlTableView.vue index e06d6259..69646c9a 100644 --- a/src/components/SqlTableView.vue +++ b/src/components/SqlTableView.vue @@ -62,7 +62,8 @@
- +
@@ -84,8 +85,14 @@ const props = defineProps<{ isRunning: boolean executionTime?: number paging?: { active: boolean; offset: number; pageSize: number; hasMore: boolean } + editableTable?: string | null +}>() +const emit = defineEmits<{ + clear: [] + prev: [] + next: [] + editCell: [payload: { table: string; column: string; row: any[]; columns: string[]; oldValue: any; newValue: any }] }>() -const emit = defineEmits<{ clear: []; prev: []; next: [] }>() const {t} = useI18n() const viewMode = ref<'table' | 'chart' | 'pivot'>('table') diff --git a/src/components/VirtualTable.vue b/src/components/VirtualTable.vue index b874c842..fa85116b 100644 --- a/src/components/VirtualTable.vue +++ b/src/components/VirtualTable.vue @@ -16,8 +16,23 @@ {{ start + i + 1 }} - {{ fmt(row[ci]) }} + + + + @@ -29,11 +44,50 @@ diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a7563b38..ab3988b9 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1033,6 +1033,26 @@ "failed": "Write-back failed: ", "done": "Updated" }, + "qb": { + "title": "Query builder", + "loading": "Loading schema…", + "noTables": "No tables in the current source (pick a database first for MySQL)", + "table": "Table", + "columns": "Columns", + "allColumns": "All columns (*)", + "where": "Where", + "pickColumn": "Pick column", + "value": "Value", + "addCondition": "Add condition", + "orderBy": "Order by", + "noOrder": "No order", + "limit": "Limit", + "preview": "SQL preview", + "copy": "Copy", + "copied": "SQL copied", + "insert": "Insert to editor", + "run": "Run" + }, "about": { "title": "About CodeForge", "tagline": "CodeForge is a lightweight, high-performance desktop code runner designed for developers, students and coding enthusiasts.", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 19ab76b1..9549b022 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1033,6 +1033,26 @@ "failed": "写回失败:", "done": "已更新" }, + "qb": { + "title": "查询构建器", + "loading": "加载表结构中…", + "noTables": "当前数据源没有可用的表(MySQL 请先选择数据库)", + "table": "表", + "columns": "列", + "allColumns": "全部列 (*)", + "where": "条件", + "pickColumn": "选择列", + "value": "值", + "addCondition": "添加条件", + "orderBy": "排序", + "noOrder": "不排序", + "limit": "限制", + "preview": "SQL 预览", + "copy": "复制", + "copied": "已复制 SQL", + "insert": "插入编辑器", + "run": "运行" + }, "about": { "title": "关于 CodeForge", "tagline": "CodeForge 是一款轻量级、高性能的桌面代码执行器,专为开发者、学生和编程爱好者设计。", diff --git a/src/utils/dbSchema.ts b/src/utils/dbSchema.ts index 53ab712e..ecdfc076 100644 --- a/src/utils/dbSchema.ts +++ b/src/utils/dbSchema.ts @@ -1,6 +1,26 @@ -// 按数据库类型构造「列」与「外键」查询。用于 schema 浏览 / ER 图。 +// 按数据库类型构造「列」与「外键」查询。用于 schema 浏览 / ER 图 / 查询构建器。 const esc = (s: string) => s.replace(/'/g, "''") +export interface Col { name: string; type: string } +export interface Tbl { name: string; columns: Col[] } + +// 标识符引用(MySQL/ClickHouse 用反引号,其余用双引号) +export const quoteIdent = (kind: string, name: string) => + kind === 'mysql' || kind === 'clickhouse' ? `\`${name}\`` : `"${name}"` + +// 把 columnsSql 返回的 (tbl, col, typ) 行分组为按表聚合的结构 +export const groupTables = (rows: any[][]): Tbl[] => { + const map = new Map() + for (const row of rows) { + const tbl = String(row[0]) + if (!map.has(tbl)) { + map.set(tbl, {name: tbl, columns: []}) + } + map.get(tbl)!.columns.push({name: String(row[1]), type: String(row[2] ?? '')}) + } + return [...map.values()] +} + // 返回 (tbl, col, typ) 行 export function columnsSql(kind: string, db?: string): string { if (kind === 'mysql') { From 412e3f6ddd719092dd457cbf956bba5af91b1cf9 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Mon, 6 Jul 2026 10:43:35 +0800 Subject: [PATCH 10/23] =?UTF-8?q?refactor(sql):=20=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=99=A8=E6=94=B9=E4=B8=BA=E6=8B=96=E6=8B=BD?= =?UTF-8?q?=E5=BC=8F=EF=BC=88=E5=88=97=E6=8B=96=E5=85=A5=20SELECT/WHERE/OR?= =?UTF-8?q?DER=20BY=20=E8=90=BD=E5=8C=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/QueryBuilder.vue | 179 ++++++++++++++++++++------------ src/i18n/locales/en.json | 12 +-- src/i18n/locales/zh-CN.json | 12 +-- 3 files changed, 124 insertions(+), 79 deletions(-) diff --git a/src/components/QueryBuilder.vue b/src/components/QueryBuilder.vue index 28c3f7b9..f467170c 100644 --- a/src/components/QueryBuilder.vue +++ b/src/components/QueryBuilder.vue @@ -8,97 +8,115 @@
-
+
{{ t('qb.title') }} · {{ activeLabel() }} + {{ t('qb.dragHint') }}
-
-
{{ t('qb.loading') }}
-
{{ error }}
-
{{ t('qb.noTables') }}
+
{{ t('qb.loading') }}
+
{{ error }}
+
{{ t('qb.noTables') }}
- +
- - + +
@@ -110,7 +128,7 @@ diff --git a/src/components/QueryBuilder.vue b/src/components/QueryBuilder.vue index 4af41a54..0c644176 100644 --- a/src/components/QueryBuilder.vue +++ b/src/components/QueryBuilder.vue @@ -132,31 +132,10 @@ {{ t('qb.dropHere', { col: draggedRef?.c }) }}
- -
- - -
+ {{ refLabel(it) }} + AS @@ -176,7 +155,10 @@ {{ t('qb.dropHere', { col: draggedRef?.c }) }}
+ {{ refLabel(w) }} + @@ -293,13 +275,15 @@