Skip to content

Commit ebbda96

Browse files
committed
refactor(lsp): extract platform-specific utilities and improve code consistency
- Extract platform detection logic into shared utility functions (getExecutableName, getNpmCommand) - Remove trailing whitespace throughout LSP installer and manager modules - Improve code consistency by centralizing platform-specific command resolution - Update imports to use new utility functions from pathUtils - Simplify npm command resolution by delegating to shared utility - Enhance maintainability by reducing platform-specific logic duplication across modules
1 parent fe4af74 commit ebbda96

File tree

9 files changed

+327
-158
lines changed

9 files changed

+327
-158
lines changed

src/main/lsp/installer.ts

Lines changed: 144 additions & 102 deletions
Large diffs are not rendered by default.

src/main/lspManager.ts

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import { logger } from '@shared/utils/Logger'
1414
import { toAppError } from '@shared/utils/errorHandler'
15+
import { getExecutableName } from '@shared/utils/pathUtils'
1516
import { spawn, ChildProcess } from 'child_process'
1617
import * as path from 'path'
1718
import * as fs from 'fs'
@@ -66,7 +67,7 @@ async function findNearestRoot(
6667
excludePatterns?: string[]
6768
): Promise<string | undefined> {
6869
let currentDir = startDir
69-
70+
7071
while (currentDir.length >= stopDir.length) {
7172
// 检查排除模式
7273
if (excludePatterns) {
@@ -77,20 +78,20 @@ async function findNearestRoot(
7778
}
7879
}
7980
}
80-
81+
8182
// 检查目标模式
8283
for (const pattern of patterns) {
8384
const targetPath = path.join(currentDir, pattern)
8485
if (fs.existsSync(targetPath)) {
8586
return currentDir
8687
}
8788
}
88-
89+
8990
const parentDir = path.dirname(currentDir)
9091
if (parentDir === currentDir) break
9192
currentDir = parentDir
9293
}
93-
94+
9495
return undefined
9596
}
9697

@@ -156,8 +157,7 @@ async function getGoplsCommand(): Promise<{ command: string; args: string[] } |
156157
}
157158

158159
// 检查 GOPATH/bin
159-
const isWindows = process.platform === 'win32'
160-
const goplsName = isWindows ? 'gopls.exe' : 'gopls'
160+
const goplsName = getExecutableName('gopls')
161161
const goPathBin = process.env.GOPATH ? path.join(process.env.GOPATH, 'bin', goplsName) : null
162162

163163
if (goPathBin && fs.existsSync(goPathBin)) {
@@ -173,8 +173,7 @@ async function getRustAnalyzerCommand(): Promise<{ command: string; args: string
173173
return { command: 'rust-analyzer', args: [] }
174174
}
175175

176-
const isWindows = process.platform === 'win32'
177-
const raName = isWindows ? 'rust-analyzer.exe' : 'rust-analyzer'
176+
const raName = getExecutableName('rust-analyzer')
178177
const cargoHome =
179178
process.env.CARGO_HOME || path.join(process.env.HOME || process.env.USERPROFILE || '', '.cargo')
180179
const raPath = path.join(cargoHome, 'bin', raName)
@@ -204,7 +203,7 @@ async function getVueServerCommand(): Promise<{ command: string; args: string[]
204203
if (commandExists('vue-language-server')) {
205204
return { command: 'vue-language-server', args: ['--stdio'] }
206205
}
207-
206+
208207
return null
209208
}
210209

@@ -241,6 +240,21 @@ async function getDenoCommand(): Promise<{ command: string; args: string[] } | n
241240
return null
242241
}
243242

243+
// PHP LSP (intelephense)
244+
async function getPhpServerCommand(): Promise<{ command: string; args: string[] } | null> {
245+
const serverPath = getInstalledServerPath('php')
246+
if (serverPath) {
247+
return { command: process.execPath, args: [serverPath, '--stdio'] }
248+
}
249+
250+
// 检查全局安装的 intelephense
251+
if (commandExists('intelephense')) {
252+
return { command: 'intelephense', args: ['--stdio'] }
253+
}
254+
255+
return null
256+
}
257+
244258
// ============ 服务器配置 ============
245259

246260
const LSP_SERVERS: LspServerConfig[] = [
@@ -314,7 +328,7 @@ const LSP_SERVERS: LspServerConfig[] = [
314328
const fileDir = path.dirname(filePath)
315329
const crateRoot = await findNearestRoot(fileDir, workspacePath, ['Cargo.toml', 'Cargo.lock'])
316330
if (!crateRoot) return workspacePath
317-
331+
318332
// 向上查找 workspace 根目录
319333
let currentDir = crateRoot
320334
while (currentDir.length >= workspacePath.length) {
@@ -331,7 +345,7 @@ const LSP_SERVERS: LspServerConfig[] = [
331345
if (parentDir === currentDir) break
332346
currentDir = parentDir
333347
}
334-
348+
335349
return crateRoot
336350
},
337351
},
@@ -395,6 +409,21 @@ const LSP_SERVERS: LspServerConfig[] = [
395409
return root || null // 找不到 deno.json 返回 null,表示不应该使用 Deno LSP
396410
},
397411
},
412+
{
413+
name: 'php',
414+
languages: ['php'],
415+
getCommand: getPhpServerCommand,
416+
// 智能根目录检测:查找 PHP 项目配置文件
417+
findRoot: async (filePath, workspacePath) => {
418+
const fileDir = path.dirname(filePath)
419+
const root = await findNearestRoot(
420+
fileDir,
421+
workspacePath,
422+
['composer.json', 'composer.lock', 'phpunit.xml', 'phpstan.neon']
423+
)
424+
return root || workspacePath
425+
},
426+
},
398427
]
399428

400429
// ============ LSP 管理器 ============
@@ -405,10 +434,10 @@ class LspManager {
405434
private documentVersions: Map<string, number> = new Map() // 启用文档版本管理
406435
private diagnosticsCache: CacheService<any[]>
407436
private startingServers: Set<string> = new Set()
408-
437+
409438
// 跟踪每个服务器打开的文档
410439
private serverOpenedDocuments: Map<string, Map<string, { languageId: string; version: number; text: string }>> = new Map()
411-
440+
412441
// 空闲关闭配置
413442
private serverLastActivity: Map<string, number> = new Map()
414443
private idleCheckInterval: NodeJS.Timeout | null = null
@@ -438,7 +467,7 @@ class LspManager {
438467
this.languageToServer.set(lang, config.name)
439468
}
440469
}
441-
470+
442471
// 启动空闲检查定时器
443472
this.startIdleCheck()
444473
}
@@ -452,7 +481,7 @@ class LspManager {
452481

453482
private startIdleCheck() {
454483
if (this.idleCheckInterval) return
455-
484+
456485
this.idleCheckInterval = setInterval(() => {
457486
const now = Date.now()
458487
for (const [key, lastActivity] of this.serverLastActivity) {
@@ -555,7 +584,7 @@ class LspManager {
555584
// 自动重启逻辑(改进版)
556585
if (code !== 0 && code !== null && inst) {
557586
const now = Date.now()
558-
587+
559588
// 如果距离上次崩溃超过冷却时间,重置计数
560589
if (now - inst.lastCrashTime > LspManager.CRASH_COOLDOWN_MS) {
561590
inst.crashCount = 1
@@ -568,7 +597,7 @@ class LspManager {
568597
if (inst.crashCount <= LspManager.MAX_CRASH_COUNT) {
569598
const delay = Math.min(1000 * inst.crashCount, 5000) // 递增延迟,最大 5 秒
570599
logger.lsp.warn(`[LSP ${key}] Server crashed (${inst.crashCount}/${LspManager.MAX_CRASH_COUNT}), restarting in ${delay}ms...`)
571-
600+
572601
setTimeout(() => {
573602
// 再次检查是否应该重启(可能用户已手动停止)
574603
if (!this.servers.has(key)) {
@@ -716,7 +745,7 @@ class LspManager {
716745
sendRequest(key: string, method: string, params: any, timeoutMs = 30000): Promise<any> {
717746
// 更新活动时间
718747
this.updateActivity(key)
719-
748+
720749
return new Promise((resolve, reject) => {
721750
const instance = this.servers.get(key)
722751
if (!instance?.process?.stdin || !instance.process.stdin.writable) {
@@ -747,7 +776,7 @@ class LspManager {
747776
sendNotification(key: string, method: string, params: any): void {
748777
// 更新活动时间
749778
this.updateActivity(key)
750-
779+
751780
const instance = this.servers.get(key)
752781
if (!instance?.process?.stdin || !instance.process.stdin.writable) return
753782
const body = JSON.stringify({ jsonrpc: '2.0', method, params })
@@ -797,9 +826,9 @@ class LspManager {
797826
dynamicRegistration: false,
798827
},
799828
},
800-
workspace: {
801-
workspaceFolders: true,
802-
applyEdit: true,
829+
workspace: {
830+
workspaceFolders: true,
831+
applyEdit: true,
803832
configuration: true,
804833
// 文件监视支持
805834
didChangeWatchedFiles: {
@@ -812,16 +841,16 @@ class LspManager {
812841
async stopServerByKey(key: string): Promise<void> {
813842
const instance = this.servers.get(key)
814843
if (!instance?.process) return
815-
844+
816845
// 清除该服务器相关的诊断缓存(按前缀删除)
817846
const workspaceUri = `file:///${instance.workspacePath.replace(/\\/g, '/')}`
818847
const altUri = workspaceUri.replace('file:///', 'file://')
819-
848+
820849
// 获取要删除的 URI 列表
821850
const urisToDelete = this.diagnosticsCache.keys().filter(
822851
uri => uri.startsWith(workspaceUri) || uri.startsWith(altUri)
823852
)
824-
853+
825854
for (const uri of urisToDelete) {
826855
this.diagnosticsCache.delete(uri)
827856
// 通知前端清除诊断
@@ -833,18 +862,18 @@ class LspManager {
833862
}
834863
})
835864
}
836-
865+
837866
// 清除文档跟踪(服务器关闭后文档状态无效)
838867
this.serverOpenedDocuments.delete(key)
839-
868+
840869
try {
841870
await this.sendRequest(key, 'shutdown', null, 3000)
842871
this.sendNotification(key, 'exit', null)
843872
} catch { }
844873
instance.process.kill()
845874
this.servers.delete(key)
846875
this.serverLastActivity.delete(key)
847-
876+
848877
logger.lsp.info(`[LSP ${key}] Server stopped and diagnostics cleared`)
849878
}
850879

src/renderer/components/layout/LspStatusIndicator.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const LANGUAGE_TO_SERVER: Record<string, string> = {
3434
c: 'clangd',
3535
cpp: 'clangd',
3636
vue: 'vue',
37+
php: 'php',
3738
}
3839

3940
// 服务器显示名称
@@ -47,6 +48,7 @@ const SERVER_NAMES: Record<string, string> = {
4748
rust: 'rust-analyzer',
4849
clangd: 'clangd (C/C++)',
4950
vue: 'Vue Language Server',
51+
php: 'Intelephense (PHP)',
5052
}
5153

5254
// 安装说明
@@ -60,6 +62,7 @@ const INSTALL_HINTS: Record<string, { auto: boolean; hint: string; builtin?: boo
6062
rust: { auto: false, hint: '请运行: rustup component add rust-analyzer' },
6163
clangd: { auto: false, hint: '请安装 LLVM/Clang' },
6264
vue: { auto: true, hint: '可自动安装' },
65+
php: { auto: true, hint: '可自动安装 Intelephense' },
6366
}
6467

6568
export default function LspStatusIndicator() {
@@ -80,7 +83,7 @@ export default function LspStatusIndicator() {
8083

8184
// 获取服务器状态
8285
useEffect(() => {
83-
api.lsp.getServerStatus().then(setServerStatus).catch(() => {})
86+
api.lsp.getServerStatus().then(setServerStatus).catch(() => { })
8487
}, [])
8588

8689
// 安装服务器

src/renderer/components/layout/TitleBar.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default function TitleBar() {
2222
${isMac ? 'pl-[76px] pr-4' : 'pl-4 pr-4'}
2323
`}>
2424
{/* Logo - Clickable to show about */}
25-
<div
25+
<div
2626
onClick={() => setShowAbout(true)}
2727
className="no-drag flex items-center gap-2.5 opacity-80 hover:opacity-100 transition-all cursor-pointer group"
2828
>
@@ -77,22 +77,22 @@ export default function TitleBar() {
7777
{/* Windows Controls */}
7878
{!isMac && (
7979
<div className="no-drag flex items-center h-full pl-2 border-l border-white/5">
80-
<div className="flex items-center">
80+
<div className="flex items-center gap-0.5">
8181
<button
8282
onClick={() => api.window.minimize()}
83-
className="w-10 h-8 flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-white/5 transition-all group"
83+
className="w-9 h-8 rounded-lg flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-white/5 transition-all group"
8484
>
8585
<Minus className="w-4 h-4 opacity-70 group-hover:opacity-100" />
8686
</button>
8787
<button
8888
onClick={() => api.window.maximize()}
89-
className="w-10 h-8 flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-white/5 transition-all group"
89+
className="w-9 h-8 rounded-lg flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-white/5 transition-all group"
9090
>
9191
<Square className="w-3 h-3 opacity-70 group-hover:opacity-100" />
9292
</button>
9393
<button
9494
onClick={() => api.window.close()}
95-
className="w-10 h-8 flex items-center justify-center text-text-muted hover:text-white hover:bg-red-500/90 transition-all group"
95+
className="w-9 h-8 rounded-lg flex items-center justify-center text-text-muted hover:text-white hover:bg-red-500/90 transition-all group"
9696
>
9797
<X className="w-4 h-4 opacity-70 group-hover:opacity-100" />
9898
</button>

src/renderer/components/tree/VirtualFileTree.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,13 @@ export const VirtualFileTree = memo(function VirtualFileTree({
185185

186186
for (const normalizedPath of pathsToExpand) {
187187
// 在当前层级的 items 中查找匹配的目录
188-
const targetDir = currentItems.find(item =>
188+
const targetDir = currentItems.find(item =>
189189
item.isDirectory && pathEquals(item.path, normalizedPath)
190190
)
191191

192192
if (targetDir) {
193193
pathsToExpandActual.push(targetDir.path)
194-
194+
195195
// 展开该目录
196196
const isExpanded = expandedFolders.has(targetDir.path)
197197
if (!isExpanded) {
@@ -323,7 +323,7 @@ export const VirtualFileTree = memo(function VirtualFileTree({
323323
} else {
324324
// 检查文件类型
325325
const fileType = getFileType(node.item.path)
326-
326+
327327
if (fileType === 'image' || fileType === 'binary') {
328328
// 图片和二进制文件不需要读取内容,直接打开
329329
openFile(node.item.path, '')
@@ -507,9 +507,13 @@ export const VirtualFileTree = memo(function VirtualFileTree({
507507
}
508508
}}
509509
onKeyDown={(e) => {
510-
if (keybindingService.matches(e, 'list.select') && e.currentTarget.value.trim()) {
510+
// 输入法组合中不处理回车
511+
if (e.nativeEvent.isComposing) return
512+
513+
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
514+
e.preventDefault()
511515
onCreateSubmit(creatingIn.path, e.currentTarget.value.trim(), creatingIn.type)
512-
} else if (keybindingService.matches(e, 'list.cancel')) {
516+
} else if (e.key === 'Escape') {
513517
onCancelCreate()
514518
}
515519
}}
@@ -594,8 +598,14 @@ export const VirtualFileTree = memo(function VirtualFileTree({
594598
onChange={(e) => setRenameValue(e.target.value)}
595599
onBlur={handleRenameSubmit}
596600
onKeyDown={(e) => {
597-
if (keybindingService.matches(e, 'list.select')) handleRenameSubmit()
598-
if (keybindingService.matches(e, 'list.cancel')) setRenamingPath(null)
601+
// 输入法组合中不处理回车
602+
if (e.nativeEvent.isComposing) return
603+
604+
if (e.key === 'Enter') {
605+
e.preventDefault()
606+
handleRenameSubmit()
607+
}
608+
if (e.key === 'Escape') setRenamingPath(null)
599609
}}
600610
onClick={(e) => e.stopPropagation()}
601611
className="flex-1 h-5 text-[13px] px-1 py-0"

src/renderer/services/keybindingService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,13 @@ class KeybindingService {
132132
const parsed = JSON.parse(localData)
133133
this.overrides = new Map(Object.entries(parsed))
134134
// 异步同步到文件(不阻塞)
135-
api.settings.set('keybindings', parsed).catch(() => {})
135+
api.settings.set('keybindings', parsed).catch(() => { })
136136
return
137137
}
138138
} catch (e) {
139139
// localStorage 读取失败,继续从文件读取
140140
}
141-
141+
142142
// 从文件读取
143143
try {
144144
const saved = await api.settings.get('keybindings') as Record<string, string>

0 commit comments

Comments
 (0)