From 792ad5b3e8996fa7e382ed0042d41cf6c3826210 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 14 Jun 2026 22:19:45 +0800 Subject: [PATCH 01/15] =?UTF-8?q?chore(release):=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7=E5=8D=87=E8=87=B3=2026.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 97d9af9..b3c0f68 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codeforge", "private": true, - "version": "26.1.0", + "version": "26.2.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 90f98bf..f4f47d7 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "CodeForge" -version = "26.1.0" +version = "26.2.0" dependencies = [ "async-trait", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 36b63e5..2206837 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "CodeForge" -version = "26.1.0" +version = "26.2.0" description = "CodeForge 是一款轻量级、高性能的桌面代码执行器,专为开发者、学生和编程爱好者设计。" authors = ["devlive-community"] edition = "2024" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 405ce35..11ab8f6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CodeForge", - "version": "26.1.0", + "version": "26.2.0", "identifier": "org.devlive.codeforge", "build": { "beforeDevCommand": "pnpm dev", From 574f7161537c0490b764581696d52bc846520adc Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 14 Jun 2026 22:19:57 +0800 Subject: [PATCH 02/15] =?UTF-8?q?perf(lsp):=20=E5=86=99=E5=85=A5=E7=A7=BB?= =?UTF-8?q?=E8=87=B3=E7=8B=AC=E7=AB=8B=E7=BA=BF=E7=A8=8B=E5=B9=B6=E5=90=88?= =?UTF-8?q?=E6=89=B9=E8=BD=AC=E5=8F=91=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stdin 写入改为经 channel 交独立线程处理,避免语言服务器索引时 管道写阻塞主线程命令循环 - 读取消息由逐条 emit 改为合批 lsp:messages,避免重服务器索引时 IPC 洪流压垮 webview 主线程 --- src-tauri/src/lsp.rs | 81 +++++++++++++++++++++++++++++--------- src/editor/lspTransport.ts | 6 ++- 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/lsp.rs b/src-tauri/src/lsp.rs index 233c87d..0b399f7 100644 --- a/src-tauri/src/lsp.rs +++ b/src-tauri/src/lsp.rs @@ -5,13 +5,14 @@ use serde::Serialize; use std::collections::HashMap; use std::io::{BufReader, Read, Write}; use std::path::PathBuf; -use std::process::{Child, ChildStdin, Command, Stdio}; +use std::process::{Child, Command, Stdio}; use std::sync::Mutex as StdMutex; use tauri::{AppHandle, Emitter, State}; struct Server { child: Child, - stdin: ChildStdin, + // 写入通过独立线程,避免 stdin 写阻塞主线程(语言服务器索引时可能来不及读) + tx: std::sync::mpsc::Sender>, } pub struct LspState { @@ -27,9 +28,9 @@ impl LspState { } #[derive(Clone, Serialize)] -struct LspEvent { +struct LspBatch { language: String, - message: String, + messages: Vec, } /// 语言 -> (可执行名, 参数)。新增语言在此加一行。 @@ -308,21 +309,49 @@ pub fn lsp_start( let stdout = child.stdout.take().ok_or("无法获取 stdout")?; let stderr = child.stderr.take(); - // 读取线程:解析 Content-Length 帧,原样转发 JSON 给前端 - let app_reader = app.clone(); + // 读取线程:解析 Content-Length 帧,把消息体丢进 channel + let (msg_tx, msg_rx) = std::sync::mpsc::channel::(); let lang_reader = language.clone(); std::thread::spawn(move || { let mut reader = BufReader::new(stdout); while let Some(body) = read_message(&mut reader) { + if msg_tx.send(body).is_err() { + break; + } + } + // 发送端 drop → 下面的 emitter 收到 Err 后发 lsp:exit + let _ = &lang_reader; + }); + + // 发射线程:合批转发。索引时语言服务器会突发成千上万条通知, + // 逐条 emit 会让 webview 主线程被 IPC 反序列化压垮(编辑器/环境检查随之卡死)。 + // 这里把"此刻已排队"的消息一次性打包成一个事件,空闲时则单条即时下发。 + let app_reader = app.clone(); + let lang_emit = language.clone(); + std::thread::spawn(move || { + loop { + // 阻塞等待第一条;channel 关闭即语言服务器退出 + let first = match msg_rx.recv() { + Ok(m) => m, + Err(_) => break, + }; + let mut batch = vec![first]; + // 排空当前已到达的消息,凑成一批(上限防止单批过大) + while let Ok(m) = msg_rx.try_recv() { + batch.push(m); + if batch.len() >= 512 { + break; + } + } let _ = app_reader.emit( - "lsp:message", - LspEvent { - language: lang_reader.clone(), - message: body, + "lsp:messages", + LspBatch { + language: lang_emit.clone(), + messages: batch, }, ); } - let _ = app_reader.emit("lsp:exit", lang_reader.clone()); + let _ = app_reader.emit("lsp:exit", lang_emit.clone()); }); // 排空 stderr,避免阻塞 @@ -337,11 +366,27 @@ pub fn lsp_start( }); } + // 写入线程:独占 stdin,从 channel 取帧写入。 + // 这样 lsp_send 只需把帧塞进 channel(瞬时返回),即便语言服务器索引时 + // 不读 stdin 导致管道写阻塞,也只阻塞这个后台线程,不会卡住主线程的命令循环。 + let (tx, rx) = std::sync::mpsc::channel::>(); + std::thread::spawn(move || { + let mut stdin = stdin; + while let Ok(frame) = rx.recv() { + if stdin.write_all(&frame).is_err() { + break; + } + if stdin.flush().is_err() { + break; + } + } + }); + state .servers .lock() .map_err(|e| e.to_string())? - .insert(language, Server { child, stdin }); + .insert(language, Server { child, tx }); Ok(true) } @@ -352,16 +397,16 @@ pub fn lsp_send( language: String, message: String, ) -> Result<(), String> { - let mut servers = state.servers.lock().map_err(|e| e.to_string())?; + let servers = state.servers.lock().map_err(|e| e.to_string())?; let server = servers - .get_mut(&language) + .get(&language) .ok_or_else(|| "语言服务器未启动".to_string())?; let frame = format!("Content-Length: {}\r\n\r\n{}", message.len(), message); + // 仅入队,不在持锁/主线程上做阻塞写 server - .stdin - .write_all(frame.as_bytes()) - .map_err(|e| e.to_string())?; - server.stdin.flush().map_err(|e| e.to_string())?; + .tx + .send(frame.into_bytes()) + .map_err(|_| "语言服务器写入通道已关闭".to_string())?; Ok(()) } diff --git a/src/editor/lspTransport.ts b/src/editor/lspTransport.ts index 491793e..5dc3697 100644 --- a/src/editor/lspTransport.ts +++ b/src/editor/lspTransport.ts @@ -17,9 +17,11 @@ export class TauriLspTransport implements Transport { } private async init() { - const un1 = await listen<{ language: string; message: string }>('lsp:message', (e) => { + const un1 = await listen<{ language: string; messages: string[] }>('lsp:messages', (e) => { if (e.payload.language === this.language) { - this.msgCb?.(e.payload.message) + for (const m of e.payload.messages) { + this.msgCb?.(m) + } } }) const un2 = await listen('lsp:exit', (e) => { From d39859eabaabaf4933c3f5fe574adf415f31ce99 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 14 Jun 2026 22:20:08 +0800 Subject: [PATCH 03/15] =?UTF-8?q?fix(lsp):=20=E7=A7=BB=E9=99=A4=E4=B8=8E?= =?UTF-8?q?=E5=86=85=E7=BD=AE=E8=A1=A5=E5=85=A8=E5=86=B2=E7=AA=81=E7=9A=84?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=20autocompletion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit languageServerWithTransport 已内置带 override 的 autocompletion (含默认补全键位),再叠加自定义 lspCompletion 会触发 "Config merge conflict for field override" 导致建 state 失败。 改为仅依赖库内置补全,删除冗余的 lspCompletion。 --- src/editor/lspCompletion.ts | 44 ------------------------------------- src/editor/lspExtension.ts | 7 +++--- 2 files changed, 4 insertions(+), 47 deletions(-) delete mode 100644 src/editor/lspCompletion.ts diff --git a/src/editor/lspCompletion.ts b/src/editor/lspCompletion.ts deleted file mode 100644 index 185efb5..0000000 --- a/src/editor/lspCompletion.ts +++ /dev/null @@ -1,44 +0,0 @@ -// 显式接入 LSP 自动补全:补全源 + 输入即触发 + 键位(Ctrl+Space 触发、Enter/Tab 接受) -import {autocompletion, completionKeymap, type CompletionContext, type CompletionResult} from '@codemirror/autocomplete' -import {keymap} from '@codemirror/view' -import {Prec} from '@codemirror/state' -import {languageServerPlugin} from 'codemirror-languageserver' - -const offsetToPos = (doc: any, offset: number) => { - const line = doc.lineAt(offset) - return {line: line.number - 1, character: offset - line.from} -} - -// 向 LSP 请求补全(复用库内 plugin 的 requestCompletion) -const lspSource = async (ctx: CompletionContext): Promise => { - const {state, pos, explicit, view} = ctx - const plugin: any = view?.plugin(languageServerPlugin as any) - if (!plugin?.requestCompletion) { - return null - } - const line = state.doc.lineAt(pos) - const before = line.text[pos - line.from - 1] - const triggers: string[] | undefined = plugin.client?.capabilities?.completionProvider?.triggerCharacters - let triggerKind = 1 // Invoked - let triggerCharacter: string | undefined - if (!explicit && triggers && before && triggers.includes(before)) { - triggerKind = 2 // TriggerCharacter - triggerCharacter = before - } - // 非显式触发且不在单词中:不打扰 - if (!explicit && triggerKind === 1 && !ctx.matchBefore(/[\w.]$/)) { - return null - } - try { - return await plugin.requestCompletion(ctx, offsetToPos(state.doc, pos), {triggerKind, triggerCharacter}) - } - catch { - return null - } -} - -// 高优先级,确保 LSP 补全源生效;并补上补全键位 -export const lspCompletion = [ - Prec.highest(autocompletion({override: [lspSource], activateOnTyping: true, defaultKeymap: false})), - Prec.high(keymap.of(completionKeymap)) -] diff --git a/src/editor/lspExtension.ts b/src/editor/lspExtension.ts index 4d92efa..1011beb 100644 --- a/src/editor/lspExtension.ts +++ b/src/editor/lspExtension.ts @@ -4,7 +4,6 @@ import {LanguageServerClient, languageServerWithTransport} from 'codemirror-lang import {TauriLspTransport} from './lspTransport' import {setLspState} from './lspStatus' import {lspCustomHover} from './lspHover' -import {lspCompletion} from './lspCompletion' // 代次:每次构建 LSP 扩展自增,过期 client 的回调据此忽略 let stateGen = 0 @@ -120,8 +119,10 @@ export async function createLspExtensions( allowHTMLContent: true, autoClose: true }) - // 自绘悬浮 + 显式 LSP 补全(本编辑器未启用 basicSetup, 需自带补全键位与源) - return [base, lspCustomHover, lspCompletion] + // base 已内置 LSP 补全(源 + 默认补全键位 Ctrl+Space/Enter/方向键); + // 不要再叠加自定义 autocompletion,否则与其 override 字段冲突("Config merge conflict for field override")。 + // 仅追加自绘悬浮样式。 + return [base, lspCustomHover] } catch { return null From a60ee854ea2d2142c23ff319b7a9b78e60630435 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 14 Jun 2026 22:20:20 +0800 Subject: [PATCH 04/15] =?UTF-8?q?fix(lsp):=20EditorView=20=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20shallowRef=20=E4=BF=AE=E5=A4=8D=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E5=90=8E=E6=95=B4=E9=A1=B5=E5=8D=A1=E6=AD=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit editorView/extensions 用普通 ref 会被深度响应式代理,连同内部 LSP client 一起被 Proxy。watch(editorView) 因此在 view 每次内部 mutation 时触发,与 applyDiffMarkers 的 dispatch 形成 mutation→watch→dispatch 无限循环;LSP 接入后 client 持续 mutate 会立即点燃该循环导致整页卡死。改用 shallowRef,watch 仅在 view 重建时触发。 --- src/App.vue | 8 ++++++-- src/composables/useCodeMirrorEditor.ts | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/App.vue b/src/App.vue index 589c822..fbe9299 100644 --- a/src/App.vue +++ b/src/App.vue @@ -366,7 +366,7 @@ diff --git a/src/editor/lspExtension.ts b/src/editor/lspExtension.ts index 34ae870..41b6bad 100644 --- a/src/editor/lspExtension.ts +++ b/src/editor/lspExtension.ts @@ -3,16 +3,37 @@ import {invoke} from '@tauri-apps/api/core' import { LanguageServerClient, languageServerWithTransport, + languageServerPlugin, formatDocument, formatSelection, formattingOptions, renameSymbol } from 'codemirror-languageserver' -import {keymap} from '@codemirror/view' +import {keymap, type EditorView} from '@codemirror/view' +import {Prec} from '@codemirror/state' import {TauriLspTransport} from './lspTransport' import {setLspState} from './lspStatus' import {lspCustomHover} from './lspHover' +// CodeMirror 偏移量 → LSP 0 基行列 +const offsetToLspPos = (doc: any, offset: number) => { + const line = doc.lineAt(offset) + return {line: line.number - 1, character: offset - line.from} +} + +// file:// URI 还原为本地路径(toUri 的逆操作) +const uriToPath = (uri: string): string | null => { + if (!uri.startsWith('file://')) { + return null + } + let p = decodeURIComponent(uri.slice('file://'.length)) + // Windows: file:///C:/... → C:/... + if (/^\/[A-Za-z]:/.test(p)) { + p = p.slice(1) + } + return p +} + // 代次:每次构建 LSP 扩展自增,过期 client 的回调据此忽略 let stateGen = 0 @@ -145,7 +166,42 @@ export async function createLspExtensions( tabSize: fmtOptions?.tabSize ?? 4, insertSpaces: fmtOptions?.insertSpaces ?? true }) - return [base, lspCustomHover, lspKeymap, fmt] + + // 跨文件跳转定义:库自带的 F12 只处理同文件,跨文件时丢弃结果。 + // 这里覆盖 F12——同文件交给库内部移动光标,跨文件则派发事件由 App 打开目标文件并定位。 + const gotoDefinition = (view: EditorView): boolean => { + const plugin: any = view.plugin(languageServerPlugin as any) + if (!plugin?.requestDefinition) { + return false + } + const pos = view.state.selection.main.head + Promise.resolve(plugin.requestDefinition(view, offsetToLspPos(view.state.doc, pos))) + .then((loc: any) => { + // 同文件:requestDefinition 内部已移动光标,无需处理 + if (!loc?.uri || loc.uri === documentUri) { + return + } + const targetPath = uriToPath(loc.uri) + if (!targetPath) { + return + } + window.dispatchEvent(new CustomEvent('lsp:open-location', { + detail: { + path: targetPath, + line: (loc.range?.start?.line ?? 0) + 1, + character: loc.range?.start?.character ?? 0 + } + })) + }) + .catch(() => {}) + return true + } + // Prec.highest 确保覆盖 base 内置的 F12 绑定 + const gotoKeymap = Prec.highest(keymap.of([ + {key: 'F12', run: gotoDefinition, preventDefault: true} + ])) + + return [base, lspCustomHover, lspKeymap, fmt, gotoKeymap] } catch { return null From 8f7ad000d8ed4c1641dc32a6a738e1c574093592 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 14 Jun 2026 22:34:28 +0800 Subject: [PATCH 07/15] =?UTF-8?q?feat(lsp):=20Cmd+Click=20=E8=B7=A8?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=B7=B3=E8=BD=AC=E5=AE=9A=E4=B9=89=E5=B9=B6?= =?UTF-8?q?=E7=B2=BE=E7=A1=AE=E5=AE=9A=E4=BD=8D=E5=88=B0=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽出 gotoDefinitionAt(view, pos),F12 与 Cmd/Ctrl+Click 共用 - 以 Prec.highest 覆盖 base 的 mousedown 处理器,抢先处理后返回 true 阻止其同文件-only 的旧逻辑 - gotoLine 增加可选 character 参数,跨文件跳转定位到定义的精确列 --- src/App.vue | 8 +++++--- src/editor/lspExtension.ts | 31 +++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/App.vue b/src/App.vue index 97030fa..688900f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -985,14 +985,16 @@ const openSearch = () => { showSearch.value = true } -const gotoLine = (line: number) => { +const gotoLine = (line: number, character?: number) => { const view = editorView.value if (!view) { return } const target = Math.max(1, Math.min(line, view.state.doc.lines)) const l = view.state.doc.line(target) - view.dispatch({selection: {anchor: l.from}, scrollIntoView: true}) + // 带列号时精确定位到列(夹在行内),否则定位到行首 + const anchor = character != null ? Math.min(l.from + character, l.to) : l.from + view.dispatch({selection: {anchor}, scrollIntoView: true}) view.focus() } @@ -1052,7 +1054,7 @@ const onLspOpenLocation = async (e: Event) => { } await smartOpen(detail.path) await nextTick() - gotoLine(detail.line) + gotoLine(detail.line, detail.character) } // 全局替换后:刷新涉及到的已打开标签(保留有未保存修改的标签) diff --git a/src/editor/lspExtension.ts b/src/editor/lspExtension.ts index 41b6bad..4aef437 100644 --- a/src/editor/lspExtension.ts +++ b/src/editor/lspExtension.ts @@ -9,7 +9,7 @@ import { formattingOptions, renameSymbol } from 'codemirror-languageserver' -import {keymap, type EditorView} from '@codemirror/view' +import {keymap, EditorView} from '@codemirror/view' import {Prec} from '@codemirror/state' import {TauriLspTransport} from './lspTransport' import {setLspState} from './lspStatus' @@ -167,14 +167,13 @@ export async function createLspExtensions( insertSpaces: fmtOptions?.insertSpaces ?? true }) - // 跨文件跳转定义:库自带的 F12 只处理同文件,跨文件时丢弃结果。 - // 这里覆盖 F12——同文件交给库内部移动光标,跨文件则派发事件由 App 打开目标文件并定位。 - const gotoDefinition = (view: EditorView): boolean => { + // 跨文件跳转定义:库自带的 F12 / Cmd+Click 只处理同文件,跨文件时丢弃结果。 + // 这里在指定位置请求定义——同文件交给库内部移动光标,跨文件则派发事件由 App 打开目标文件并定位。 + const gotoDefinitionAt = (view: EditorView, pos: number): boolean => { const plugin: any = view.plugin(languageServerPlugin as any) if (!plugin?.requestDefinition) { return false } - const pos = view.state.selection.main.head Promise.resolve(plugin.requestDefinition(view, offsetToLspPos(view.state.doc, pos))) .then((loc: any) => { // 同文件:requestDefinition 内部已移动光标,无需处理 @@ -196,12 +195,28 @@ export async function createLspExtensions( .catch(() => {}) return true } - // Prec.highest 确保覆盖 base 内置的 F12 绑定 + // Prec.highest 确保覆盖 base 内置的 F12 绑定与 Cmd+Click 处理器 const gotoKeymap = Prec.highest(keymap.of([ - {key: 'F12', run: gotoDefinition, preventDefault: true} + {key: 'F12', run: (v) => gotoDefinitionAt(v, v.state.selection.main.head), preventDefault: true} ])) + const gotoMouse = Prec.highest(EditorView.domEventHandlers({ + mousedown: (event, view) => { + if (!event.ctrlKey && !event.metaKey) { + return false + } + const pos = view.posAtCoords({x: event.clientX, y: event.clientY}) + if (pos == null) { + return false + } + const ok = gotoDefinitionAt(view, pos) + if (ok) { + event.preventDefault() + } + return ok + } + })) - return [base, lspCustomHover, lspKeymap, fmt, gotoKeymap] + return [base, lspCustomHover, lspKeymap, fmt, gotoKeymap, gotoMouse] } catch { return null From 75c7275b5eec5f39ca127c2c48653df9e257ac91 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 14 Jun 2026 22:41:27 +0800 Subject: [PATCH 08/15] =?UTF-8?q?feat(lsp):=20=E6=96=B0=E5=A2=9E=20Java/Ko?= =?UTF-8?q?tlin/Swift/Scala/YAML/Shell/Haskell=20=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server_cmd 增加对应语言到语言服务器的映射 - server_defs 增加检测与一键安装项 - 前端 LANGUAGE_ID / LANGUAGE_EXT 同步登记,使其可启动 LSP --- src-tauri/src/lsp.rs | 44 ++++++++++++++++++++++++++++++++++++++ src/editor/lspExtension.ts | 13 +++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/lsp.rs b/src-tauri/src/lsp.rs index 0b399f7..2459774 100644 --- a/src-tauri/src/lsp.rs +++ b/src-tauri/src/lsp.rs @@ -50,6 +50,13 @@ fn server_cmd(language: &str) -> Option<(&'static str, Vec<&'static str>)> { "html" => Some(("vscode-html-language-server", vec!["--stdio"])), "css" => Some(("vscode-css-language-server", vec!["--stdio"])), "json" => Some(("vscode-json-language-server", vec!["--stdio"])), + "java" => Some(("jdtls", vec![])), + "kotlin" => Some(("kotlin-language-server", vec![])), + "swift" => Some(("sourcekit-lsp", vec![])), + "scala" => Some(("metals", vec![])), + "yaml" => Some(("yaml-language-server", vec!["--stdio"])), + "shell" => Some(("bash-language-server", vec!["start"])), + "haskell" => Some(("haskell-language-server-wrapper", vec!["--lsp"])), _ => None, } } @@ -107,6 +114,43 @@ fn server_defs() -> Vec<(&'static str, &'static str, &'static str, &'static str) "vscode-html-language-server", "npm i -g vscode-langservers-extracted", ), + ("java", "Java (jdtls)", "jdtls", "brew install jdtls"), + ( + "kotlin", + "Kotlin", + "kotlin-language-server", + "brew install kotlin-language-server", + ), + ( + "swift", + "Swift (sourcekit-lsp)", + "sourcekit-lsp", + "xcode-select --install", + ), + ( + "scala", + "Scala (metals)", + "metals", + "coursier install metals", + ), + ( + "yaml", + "YAML", + "yaml-language-server", + "npm i -g yaml-language-server", + ), + ( + "shell", + "Shell / Bash", + "bash-language-server", + "npm i -g bash-language-server", + ), + ( + "haskell", + "Haskell (HLS)", + "haskell-language-server-wrapper", + "ghcup install hls", + ), ] } diff --git a/src/editor/lspExtension.ts b/src/editor/lspExtension.ts index 4aef437..494bb53 100644 --- a/src/editor/lspExtension.ts +++ b/src/editor/lspExtension.ts @@ -60,14 +60,23 @@ const LANGUAGE_ID: Record = { ruby: 'ruby', html: 'html', css: 'css', - json: 'json' + json: 'json', + java: 'java', + kotlin: 'kotlin', + swift: 'swift', + scala: 'scala', + yaml: 'yaml', + shell: 'shellscript', + haskell: 'haskell' } // 草稿(未保存)时用的文件扩展名,构造 untitled 文档 URI const LANGUAGE_EXT: Record = { python: 'py', typescript: 'ts', javascript: 'js', rust: 'rs', go: 'go', c: 'c', cpp: 'cpp', 'objective-c': 'm', 'objective-cpp': 'mm', - lua: 'lua', php: 'php', ruby: 'rb', html: 'html', css: 'css', json: 'json' + lua: 'lua', php: 'php', ruby: 'rb', html: 'html', css: 'css', json: 'json', + java: 'java', kotlin: 'kt', swift: 'swift', scala: 'scala', yaml: 'yaml', + shellscript: 'sh', haskell: 'hs' } export const lspSupportsLanguage = (language?: string): boolean => From 9c2790d111fed15c7a58410734e295c4803cbc7f Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 14 Jun 2026 22:46:29 +0800 Subject: [PATCH 09/15] =?UTF-8?q?feat(lsp):=20=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95(=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E5=AE=9A=E4=B9=89/=E9=87=8D=E5=91=BD=E5=90=8D/=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽出可复用的 runGotoDefinition(从 plugin 读 documentUri),供 F12/Cmd+Click/右键菜单共用 - 在编辑器内容区右键弹出菜单:跳转定义、重命名、格式化文档/选区 - 仅当前语言支持 LSP 时弹出;菜单位置夹取到视口内 --- src/App.vue | 58 +++++++++++++++++++++++++++++++++- src/editor/lspExtension.ts | 65 +++++++++++++++++++++----------------- 2 files changed, 93 insertions(+), 30 deletions(-) diff --git a/src/App.vue b/src/App.vue index 688900f..6baad2e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -360,14 +360,37 @@ @refresh="refreshGitStatus" @close="showGit = false"/> + +
+
+ + +
+ + +
+
+ diff --git a/src/editor/lspExtension.ts b/src/editor/lspExtension.ts index 494bb53..f8a7d06 100644 --- a/src/editor/lspExtension.ts +++ b/src/editor/lspExtension.ts @@ -34,6 +34,39 @@ const uriToPath = (uri: string): string | null => { return p } +/** + * 在指定位置(默认光标处)请求跳转定义。 + * 同文件交给库内部移动光标;跨文件则派发 lsp:open-location 由 App 打开目标文件并定位。 + * 供 F12 键位、Cmd+Click 与右键菜单共用。 + */ +export const runGotoDefinition = (view: EditorView, pos?: number): boolean => { + const plugin: any = view.plugin(languageServerPlugin as any) + if (!plugin?.requestDefinition) { + return false + } + const at = pos ?? view.state.selection.main.head + const currentUri = plugin.documentUri + Promise.resolve(plugin.requestDefinition(view, offsetToLspPos(view.state.doc, at))) + .then((loc: any) => { + if (!loc?.uri || loc.uri === currentUri) { + return + } + const targetPath = uriToPath(loc.uri) + if (!targetPath) { + return + } + window.dispatchEvent(new CustomEvent('lsp:open-location', { + detail: { + path: targetPath, + line: (loc.range?.start?.line ?? 0) + 1, + character: loc.range?.start?.character ?? 0 + } + })) + }) + .catch(() => {}) + return true +} + // 代次:每次构建 LSP 扩展自增,过期 client 的回调据此忽略 let stateGen = 0 @@ -177,36 +210,10 @@ export async function createLspExtensions( }) // 跨文件跳转定义:库自带的 F12 / Cmd+Click 只处理同文件,跨文件时丢弃结果。 - // 这里在指定位置请求定义——同文件交给库内部移动光标,跨文件则派发事件由 App 打开目标文件并定位。 - const gotoDefinitionAt = (view: EditorView, pos: number): boolean => { - const plugin: any = view.plugin(languageServerPlugin as any) - if (!plugin?.requestDefinition) { - return false - } - Promise.resolve(plugin.requestDefinition(view, offsetToLspPos(view.state.doc, pos))) - .then((loc: any) => { - // 同文件:requestDefinition 内部已移动光标,无需处理 - if (!loc?.uri || loc.uri === documentUri) { - return - } - const targetPath = uriToPath(loc.uri) - if (!targetPath) { - return - } - window.dispatchEvent(new CustomEvent('lsp:open-location', { - detail: { - path: targetPath, - line: (loc.range?.start?.line ?? 0) + 1, - character: loc.range?.start?.character ?? 0 - } - })) - }) - .catch(() => {}) - return true - } + // 这里用 runGotoDefinition 接管——同文件交给库内部移动光标,跨文件派发事件由 App 打开。 // Prec.highest 确保覆盖 base 内置的 F12 绑定与 Cmd+Click 处理器 const gotoKeymap = Prec.highest(keymap.of([ - {key: 'F12', run: (v) => gotoDefinitionAt(v, v.state.selection.main.head), preventDefault: true} + {key: 'F12', run: (v) => runGotoDefinition(v), preventDefault: true} ])) const gotoMouse = Prec.highest(EditorView.domEventHandlers({ mousedown: (event, view) => { @@ -217,7 +224,7 @@ export async function createLspExtensions( if (pos == null) { return false } - const ok = gotoDefinitionAt(view, pos) + const ok = runGotoDefinition(view, pos) if (ok) { event.preventDefault() } From 061bef95886b5000830cac30d8e1b70a3b7b7647 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 14 Jun 2026 22:51:39 +0800 Subject: [PATCH 10/15] =?UTF-8?q?feat(lsp):=20=E6=96=B0=E5=A2=9E=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E9=9D=A2=E6=9D=BF=EF=BC=8C=E8=81=9A=E5=90=88=E8=AF=8A?= =?UTF-8?q?=E6=96=AD=E5=B9=B6=E6=94=AF=E6=8C=81=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lspDiagnostics 去抖收集编辑器诊断到响应式 store(顺带降低索引期 高频刷新的渲染压力) - DiagnosticsPanel 底部面板列出错误/警告,点击跳转到对应行列 - 状态栏 LSP 区显示错误/警告计数,点击开关问题面板 --- src/App.vue | 11 ++++++- src/components/DiagnosticsPanel.vue | 51 +++++++++++++++++++++++++++++ src/components/StatusBar.vue | 27 ++++++++++----- src/editor/lspDiagnostics.ts | 41 +++++++++++++++++++++++ src/editor/lspExtension.ts | 3 +- 5 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 src/components/DiagnosticsPanel.vue create mode 100644 src/editor/lspDiagnostics.ts diff --git a/src/App.vue b/src/App.vue index 6baad2e..b4b79b2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -263,7 +263,7 @@ @close="showTerminal = false; terminalMounted = false"/> - + @@ -360,6 +360,11 @@ @refresh="refreshGitStatus" @close="showGit = false"/> + + +
{ gotoLine(detail.line, detail.character) } +// LSP 问题面板显隐 +const showDiagnostics = ref(false) + // ===== 编辑器 LSP 右键菜单(跳转定义 / 重命名 / 格式化)===== const editorCtx = reactive({visible: false, x: 0, y: 0}) const closeEditorCtx = () => { diff --git a/src/components/DiagnosticsPanel.vue b/src/components/DiagnosticsPanel.vue new file mode 100644 index 0000000..138b667 --- /dev/null +++ b/src/components/DiagnosticsPanel.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/StatusBar.vue b/src/components/StatusBar.vue index ae03b36..6d41bce 100644 --- a/src/components/StatusBar.vue +++ b/src/components/StatusBar.vue @@ -22,13 +22,19 @@
- -
- - - {{ lspState.status === 'connecting' ? 'LSP 索引中' : 'LSP' }} -
+ +
@@ -46,10 +52,11 @@