1212
1313import { logger } from '@shared/utils/Logger'
1414import { toAppError } from '@shared/utils/errorHandler'
15+ import { getExecutableName } from '@shared/utils/pathUtils'
1516import { spawn , ChildProcess } from 'child_process'
1617import * as path from 'path'
1718import * 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
246260const 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
0 commit comments