Repository navigation
Expand file tree
/
Copy pathmain.js
More file actions
80 lines (70 loc) · 20.7 KB
/
Copy pathmain.js
File metadata and controls
80 lines (70 loc) · 20.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
const{app,BrowserWindow,ipcMain,dialog,Menu,shell}=require('electron');const fs=require('fs'),path=require('path'),{spawn}=require('child_process'),net=require('net');let win,terminal=null;
function send(command,arg){if(win&&!win.isDestroyed())win.webContents.send('menu:command',{command,arg})}
function installMenu(){const isMac=process.platform==='darwin';const template=[
{label:'File',submenu:[{label:'New File',accelerator:'CmdOrCtrl+N',click:()=>send('newFile')},{label:'New Folder',click:()=>send('newFolder')},{type:'separator'},{label:'Open Folder...',accelerator:'CmdOrCtrl+O',click:()=>send('openFolder')},{label:'Open Recent',submenu:[{label:'Show Recent Projects',click:()=>send('recent')}]},{type:'separator'},{label:'Save',accelerator:'CmdOrCtrl+S',click:()=>send('save')},{label:'Save All',accelerator:'CmdOrCtrl+Shift+S',click:()=>send('saveAll')},{label:'Close Editor',accelerator:'CmdOrCtrl+W',click:()=>send('closeEditor')},{type:'separator'},{role:isMac?'close':'quit',label:'Exit'}]},
{label:'Edit',submenu:[{role:'undo'},{role:'redo'},{type:'separator'},{role:'cut'},{role:'copy'},{role:'paste'},{role:'selectAll'},{type:'separator'},{label:'Find',accelerator:'CmdOrCtrl+F',click:()=>send('find')},{label:'Replace',accelerator:'CmdOrCtrl+H',click:()=>send('replace')},{label:'Find in Files',accelerator:'CmdOrCtrl+Shift+F',click:()=>send('projectSearch')},{label:'Format Document',accelerator:'Shift+Alt+F',click:()=>send('format')},{label:'Toggle Line Comment',accelerator:'CmdOrCtrl+/',click:()=>send('comment')}]},
{label:'Selection',submenu:[{label:'Select All',accelerator:'CmdOrCtrl+A',click:()=>send('selectAll')},{label:'Add Cursor Above',accelerator:'CmdOrCtrl+Alt+Up',click:()=>send('cursorAbove')},{label:'Add Cursor Below',accelerator:'CmdOrCtrl+Alt+Down',click:()=>send('cursorBelow')},{label:'Duplicate Selection',accelerator:'Shift+Alt+Down',click:()=>send('duplicateLine')}]},
{label:'View',submenu:[{label:'Command Palette...',accelerator:'CmdOrCtrl+Shift+P',click:()=>send('palette')},{type:'separator'},{label:'Explorer',accelerator:'CmdOrCtrl+Shift+E',click:()=>send('view','files')},{label:'Search',click:()=>send('view','search')},{label:'Source Control',accelerator:'CmdOrCtrl+Shift+G',click:()=>send('view','git')},{label:'Problems',accelerator:'CmdOrCtrl+Shift+M',click:()=>send('view','problems')},{label:'Outline',click:()=>send('view','outline')},{label:'Terminal',accelerator:'CmdOrCtrl+`',click:()=>send('terminal')},{type:'separator'},{label:'Toggle Sidebar',accelerator:'CmdOrCtrl+B',click:()=>send('toggleSidebar')},{label:'Toggle Bottom Panel',accelerator:'CmdOrCtrl+J',click:()=>send('togglePanel')},{type:'separator'},{role:'zoomIn'},{role:'zoomOut'},{role:'resetZoom'},{role:'togglefullscreen'}]},
{label:'Go',submenu:[{label:'Go to File...',accelerator:'CmdOrCtrl+P',click:()=>send('quickOpen')},{label:'Go to Symbol...',accelerator:'CmdOrCtrl+Shift+O',click:()=>send('symbols')},{label:'Go to Line...',accelerator:'CmdOrCtrl+G',click:()=>send('goLine')},{type:'separator'},{label:'Go to Definition',accelerator:'F12',click:()=>send('definition')},{label:'Go to References',accelerator:'Shift+F12',click:()=>send('references')},{label:'Rename Symbol',accelerator:'F2',click:()=>send('rename')}]},
{label:'Run',submenu:[{label:'Run Command',accelerator:'F5',click:()=>send('run')},{label:'Stop',accelerator:'Shift+F5',click:()=>send('stop')},{type:'separator'},{label:'Check Project',click:()=>send('checkProject')},{label:'Check Python Project',click:()=>send('checkPython')},{label:'Check TypeScript Project',click:()=>send('checkTS')}]},
{label:'Source Control',submenu:[{label:'Refresh',click:()=>send('gitRefresh')},{label:'Stage All',click:()=>send('gitStageAll')},{label:'Commit...',click:()=>send('gitCommit')},{type:'separator'},{label:'Pull',click:()=>send('gitPull')},{label:'Push',click:()=>send('gitPush')},{label:'View Changes',click:()=>send('gitDiff')}]},
{label:'Terminal',submenu:[{label:'New Terminal',accelerator:'CmdOrCtrl+Shift+`',click:()=>send('newTerminal')},{label:'Restart Terminal',click:()=>send('restartTerminal')},{label:'Clear Terminal',click:()=>send('clearTerminal')},{label:'Close Panel',click:()=>send('closePanel')}]},
{label:'Help',submenu:[{label:'Keyboard Shortcuts',accelerator:'CmdOrCtrl+K CmdOrCtrl+S',click:()=>send('shortcuts')},{label:'Documentation',click:()=>send('docs')},{type:'separator'},{label:'About PeaBox IDE',click:()=>dialog.showMessageBox(win,{type:'info',title:'About PeaBox IDE',message:'PeaBox IDE 0.8.0',detail:'A lightweight, local development environment.\n\nMonaco editor • Pyright language intelligence • Git • Integrated terminal'})}]}
];Menu.setApplicationMenu(Menu.buildFromTemplate(template))}
ipcMain.on('context:editor',()=>{Menu.buildFromTemplate([{label:'Undo',role:'undo'},{label:'Redo',role:'redo'},{type:'separator'},{label:'Cut',role:'cut'},{label:'Copy',role:'copy'},{label:'Paste',role:'paste'},{type:'separator'},{label:'Go to Definition',click:()=>send('definition')},{label:'Find References',click:()=>send('references')},{label:'Rename Symbol',click:()=>send('rename')},{label:'Format Document',click:()=>send('format')}]).popup({window:win})});
ipcMain.on('context:file',(_,p)=>{Menu.buildFromTemplate([{label:'Open',click:()=>send('openPath',p)},{label:'Rename',click:()=>send('renamePath',p)},{label:'Delete',click:()=>send('deletePath',p)},{type:'separator'},{label:'Copy Path',click:()=>require('electron').clipboard.writeText(p)},{label:'Reveal in File Explorer',click:()=>shell.showItemInFolder(p)},{label:'Open Terminal Here',click:()=>send('terminalAt',p)}]).popup({window:win})});
function walk(root){function r(d){return fs.readdirSync(d,{withFileTypes:true}).filter(x=>!['node_modules','.git','dist','.venv','__pycache__'].includes(x.name)).map(x=>{let p=path.join(d,x.name);return x.isDirectory()?{name:x.name,path:p,dir:true,children:r(p)}:{name:x.name,path:p,dir:false}})}return{root,children:r(root)}}
app.whenReady().then(()=>{installMenu();win=new BrowserWindow({width:1450,height:900,webPreferences:{preload:path.join(__dirname,'preload.js'),contextIsolation:true}});win.loadFile('index.html')});app.on('window-all-closed',()=>app.quit());
ipcMain.handle('folder:open',async()=>{let x=await dialog.showOpenDialog({properties:['openDirectory']});return x.canceled?null:walk(x.filePaths[0])});ipcMain.handle('folder:load',(_,p)=>walk(p));ipcMain.handle('folder:refresh',(_,p)=>walk(p));ipcMain.handle('file:read',(_,p)=>({content:fs.readFileSync(p,'utf8')}));ipcMain.handle('file:write',(_,p,c)=>fs.writeFileSync(p,c));
ipcMain.handle('terminal:start',(_,cwd)=>{if(terminal)terminal.kill();terminal=spawn('powershell.exe',[],{cwd});terminal.stdout.on('data',d=>win.webContents.send('terminal:data',d.toString()));terminal.stderr.on('data',d=>win.webContents.send('terminal:data',d.toString()));return true});ipcMain.on('terminal:input',(_,d)=>terminal&&terminal.stdin.write(d));
let lspProc=null,lspBuf=Buffer.alloc(0),lspSeq=1,lspPending=new Map(),lspRoot=null;
function uriOf(p){return 'file:///'+p.replace(/\\/g,'/').replace(/^\//,'')}
function lspSend(msg){if(!lspProc)return;let body=Buffer.from(JSON.stringify(msg),'utf8');lspProc.stdin.write(`Content-Length: ${body.length}\r\n\r\n`);lspProc.stdin.write(body)}
function lspRequest(method,params){return new Promise((resolve,reject)=>{if(!lspProc)return reject(new Error('Language server not running'));let id=lspSeq++;lspPending.set(id,{resolve,reject,t:setTimeout(()=>{lspPending.delete(id);reject(new Error('Language server timeout'))},10000)});lspSend({jsonrpc:'2.0',id,method,params})})}
function parseLsp(data){lspBuf=Buffer.concat([lspBuf,data]);while(true){let i=lspBuf.indexOf('\r\n\r\n');if(i<0)return;let h=lspBuf.slice(0,i).toString(),m=/Content-Length:\s*(\d+)/i.exec(h);if(!m){lspBuf=lspBuf.slice(i+4);continue}let n=+m[1];if(lspBuf.length<i+4+n)return;let raw=lspBuf.slice(i+4,i+4+n).toString();lspBuf=lspBuf.slice(i+4+n);try{let x=JSON.parse(raw);if(x.id!=null&&lspPending.has(x.id)){let q=lspPending.get(x.id);clearTimeout(q.t);lspPending.delete(x.id);x.error?q.reject(new Error(x.error.message)):q.resolve(x.result)}else if(x.method==='textDocument/publishDiagnostics')win.webContents.send('lsp:diagnostics',x.params)}catch{}}}
async function startLsp(root){if(lspProc){lspProc.kill();lspProc=null}lspRoot=root;let server=path.join(__dirname,'node_modules','pyright','langserver.index.js');lspProc=spawn(process.execPath,[server,'--stdio'],{cwd:root,stdio:['pipe','pipe','pipe'],windowsHide:true});lspProc.stdout.on('data',parseLsp);lspProc.stderr.on('data',d=>win.webContents.send('lsp:log',d.toString()));lspProc.on('close',()=>{lspProc=null;win.webContents.send('lsp:status','stopped')});let capabilities={workspace:{workspaceFolders:true,configuration:true},textDocument:{synchronization:{didSave:true},completion:{completionItem:{snippetSupport:true,documentationFormat:['markdown','plaintext']}},hover:{contentFormat:['markdown','plaintext']},definition:{linkSupport:true},references:{},rename:{prepareSupport:true},codeAction:{codeActionLiteralSupport:{codeActionKind:{valueSet:['quickfix','refactor','source']}}},documentSymbol:{hierarchicalDocumentSymbolSupport:true},publishDiagnostics:{relatedInformation:true}}};let init=await lspRequest('initialize',{processId:process.pid,rootUri:uriOf(root),workspaceFolders:[{uri:uriOf(root),name:path.basename(root)}],capabilities,clientInfo:{name:'PeaBox IDE',version:'0.7.0'}});lspSend({jsonrpc:'2.0',method:'initialized',params:{}});lspSend({jsonrpc:'2.0',method:'workspace/didChangeConfiguration',params:{settings:{python:{analysis:{typeCheckingMode:'standard',autoSearchPaths:true,useLibraryCodeForTypes:true}}}}});win.webContents.send('lsp:status','ready');return init}
ipcMain.handle('lsp:start',(_,root)=>startLsp(root).then(()=>true).catch(e=>({error:e.message})));
ipcMain.handle('lsp:open',(_,p,text,version=1)=>{lspSend({jsonrpc:'2.0',method:'textDocument/didOpen',params:{textDocument:{uri:uriOf(p),languageId:'python',version,text}}});return true});
ipcMain.handle('lsp:change',(_,p,text,version)=>{lspSend({jsonrpc:'2.0',method:'textDocument/didChange',params:{textDocument:{uri:uriOf(p),version},contentChanges:[{text}]}});return true});
ipcMain.handle('lsp:save',(_,p,text)=>{lspSend({jsonrpc:'2.0',method:'textDocument/didSave',params:{textDocument:{uri:uriOf(p)},text}});return true});
ipcMain.handle('lsp:request',(_,method,params)=>lspRequest(method,params));
ipcMain.handle('lsp:stop',()=>{if(lspProc)lspProc.kill();lspProc=null;return true});
ipcMain.handle('git:file-content',async(_,cwd,rel,rev)=>{let spec=rev?`${rev}:${rel.replace(/\\/g,'/')}`:`HEAD:${rel.replace(/\\/g,'/')}`;return cmd(cwd,`git show "${spec}"`)});
ipcMain.handle('git:log',(_,cwd)=>cmd(cwd,'git log -20 --pretty=format:"%h%x09%an%x09%ad%x09%s" --date=short'));
ipcMain.handle('git:stage-all',(_,cwd)=>cmd(cwd,'git add -A'));
ipcMain.handle('git:create-branch',(_,cwd,b)=>cmd(cwd,`git switch -c "${b}"`));
ipcMain.handle('project:info',async(_,cwd)=>{let names=fs.readdirSync(cwd);let type=names.includes('pyproject.toml')||names.includes('requirements.txt')?'Python':names.includes('package.json')?'Node / JavaScript':names.includes('go.mod')?'Go':names.includes('Cargo.toml')?'Rust':'General';let count=0;function rec(d){for(let x of fs.readdirSync(d,{withFileTypes:true})){if(['node_modules','.git','dist','.venv','__pycache__'].includes(x.name))continue;let p=path.join(d,x.name);x.isDirectory()?rec(p):count++}}try{rec(cwd)}catch{}let g=await cmd(cwd,'git status --short --branch');return{name:path.basename(cwd),type,fileCount:count,git:g.text}});
ipcMain.handle('profile:load',(_,cwd)=>{let p=path.join(cwd,'.peabox','project.json');try{return JSON.parse(fs.readFileSync(p,'utf8'))}catch{return null}});
ipcMain.handle('profile:create',(_,cwd,data)=>{let d=path.join(cwd,'.peabox');fs.mkdirSync(d,{recursive:true});fs.writeFileSync(path.join(d,'project.json'),JSON.stringify(data,null,2));return true});
ipcMain.handle('file:absolute',(_,cwd,rel)=>path.resolve(cwd,rel));
let dapSocket=null,dapBuf=Buffer.alloc(0),dapSeq=1,dapPending=new Map(),debugProc=null,dapPort=5678;
function dapSend(msg){if(!dapSocket)return;let b=Buffer.from(JSON.stringify(msg),'utf8');dapSocket.write(`Content-Length: ${b.length}\r\n\r\n`);dapSocket.write(b)}
function dapReq(command,args={}){return new Promise((resolve,reject)=>{if(!dapSocket)return reject(new Error('debug adapter not connected'));let seq=dapSeq++;dapPending.set(seq,{resolve,reject,t:setTimeout(()=>{dapPending.delete(seq);reject(new Error(command+' timed out'))},10000)});dapSend({seq,type:'request',command,arguments:args})})}
function dapParse(d){dapBuf=Buffer.concat([dapBuf,d]);while(true){let i=dapBuf.indexOf('\r\n\r\n');if(i<0)return;let m=/Content-Length:\s*(\d+)/i.exec(dapBuf.slice(0,i).toString());if(!m){dapBuf=dapBuf.slice(i+4);continue}let n=+m[1];if(dapBuf.length<i+4+n)return;let x;try{x=JSON.parse(dapBuf.slice(i+4,i+4+n).toString())}catch{dapBuf=dapBuf.slice(i+4+n);continue}dapBuf=dapBuf.slice(i+4+n);if(x.type==='response'&&dapPending.has(x.request_seq)){let q=dapPending.get(x.request_seq);clearTimeout(q.t);dapPending.delete(x.request_seq);x.success?q.resolve(x.body||{}):q.reject(new Error(x.message||x.body?.error?.format||'DAP request failed'))}else if(x.type==='event')win.webContents.send('dap:event',x)}}
function connectDap(port){return new Promise((resolve,reject)=>{let tries=0;function go(){dapSocket=net.createConnection({host:'127.0.0.1',port},()=>resolve(true));dapSocket.on('data',dapParse);dapSocket.on('close',()=>{win.webContents.send('dap:status','disconnected');dapSocket=null});dapSocket.on('error',e=>{if(++tries<25){try{dapSocket.destroy()}catch{};setTimeout(go,120)}else reject(e)})}go()})}
ipcMain.handle('dap:start',async(_,cfg)=>{try{if(dapSocket)dapSocket.destroy();if(debugProc)debugProc.kill();dapPort=cfg.port||5678;let py=cfg.python||'python';let program=cfg.program;debugProc=spawn(py,['-m','debugpy','--listen',String(dapPort),'--wait-for-client',program,...(cfg.args||[])],{cwd:cfg.cwd,env:process.env,windowsHide:true});debugProc.stdout.on('data',d=>win.webContents.send('dap:event',{event:'output',body:{category:'stdout',output:d.toString()},type:'event'}));debugProc.stderr.on('data',d=>win.webContents.send('dap:event',{event:'output',body:{category:'stderr',output:d.toString()},type:'event'}));debugProc.on('close',c=>win.webContents.send('dap:event',{event:'terminated',body:{exitCode:c},type:'event'}));await connectDap(dapPort);let init=await dapReq('initialize',{clientID:'peabox-ide',clientName:'PeaBox IDE',adapterID:'python',pathFormat:'path',linesStartAt1:true,columnsStartAt1:true,supportsVariableType:true,supportsVariablePaging:true,supportsRunInTerminalRequest:false,supportsProgressReporting:false});await dapReq('attach',{connect:{host:'127.0.0.1',port:dapPort},justMyCode:true});win.webContents.send('dap:status','connected');return{ok:true,capabilities:init}}catch(e){return{ok:false,error:e.message}}});
ipcMain.handle('dap:req',(_,c,a)=>dapReq(c,a));ipcMain.handle('dap:stop',async()=>{try{if(dapSocket)await dapReq('disconnect',{terminateDebuggee:true})}catch{}try{dapSocket?.destroy()}catch{}try{debugProc?.kill()}catch{}dapSocket=null;debugProc=null;return true});
// v0.11 Generic LSP Manager. Each language owns an independent stdio JSON-RPC server.
const lspServers=new Map();
const LSP_SPECS={
python:{ids:['python'],extensions:['.py','.pyi'],builtin:true,command:null,args:[]},
go:{ids:['go'],extensions:['.go'],command:'gopls',args:[]},
rust:{ids:['rust'],extensions:['.rs'],command:'rust-analyzer',args:[]},
cpp:{ids:['c','cpp'],extensions:['.c','.h','.cc','.cpp','.cxx','.hpp'],command:'clangd',args:['--background-index']}
};
function langForPath(p){let e=path.extname(p).toLowerCase();for(let[k,v]of Object.entries(LSP_SPECS))if(v.extensions.includes(e))return k;return null}
function mkUri(p){return 'file:///'+p.replace(/\\/g,'/').replace(/^\//,'')}
function lspSpawnSpec(language,root){let spec=LSP_SPECS[language];if(!spec)throw new Error('No LSP configured for '+language);if(spec.builtin)return{exe:process.execPath,args:[path.join(__dirname,'node_modules','pyright','langserver.index.js'),'--stdio']};return{exe:spec.command,args:spec.args}}
function lspStartGeneric(language,root){return new Promise(async(resolve)=>{if(lspServers.has(language))return resolve({ok:true,reused:true});let sp;try{sp=lspSpawnSpec(language,root)}catch(e){return resolve({ok:false,error:e.message})}let proc;try{proc=spawn(sp.exe,sp.args,{cwd:root,stdio:['pipe','pipe','pipe'],windowsHide:true})}catch(e){return resolve({ok:false,error:e.message})}let st={language,root,proc,buf:Buffer.alloc(0),seq:1,pending:new Map(),ready:false};lspServers.set(language,st);proc.on('error',e=>{win.webContents.send('lspm:status',{language,status:'error',detail:e.message});lspServers.delete(language)});proc.stderr.on('data',d=>win.webContents.send('lspm:log',{language,text:d.toString()}));proc.stdout.on('data',d=>lspParseGeneric(st,d));proc.on('close',c=>{win.webContents.send('lspm:status',{language,status:'stopped',detail:String(c)});lspServers.delete(language)});try{let caps={workspace:{workspaceFolders:true,configuration:true},textDocument:{synchronization:{didSave:true},completion:{completionItem:{snippetSupport:true,documentationFormat:['markdown','plaintext']}},hover:{contentFormat:['markdown','plaintext']},definition:{linkSupport:true},references:{},rename:{prepareSupport:true},codeAction:{},documentSymbol:{hierarchicalDocumentSymbolSupport:true},publishDiagnostics:{relatedInformation:true},signatureHelp:{}}};let result=await lspReqGeneric(st,'initialize',{processId:process.pid,rootUri:mkUri(root),workspaceFolders:[{uri:mkUri(root),name:path.basename(root)}],capabilities:caps,clientInfo:{name:'PeaBox IDE',version:'0.11.0'}});lspNotify(st,'initialized',{});st.ready=true;win.webContents.send('lspm:status',{language,status:'ready'});resolve({ok:true,capabilities:result?.capabilities||{}})}catch(e){try{proc.kill()}catch{}lspServers.delete(language);resolve({ok:false,error:e.message})}})}
function lspNotify(st,method,params){let x={jsonrpc:'2.0',method,params},b=Buffer.from(JSON.stringify(x));st.proc.stdin.write(`Content-Length: ${b.length}\r\n\r\n`);st.proc.stdin.write(b)}
function lspReqGeneric(st,method,params){return new Promise((resolve,reject)=>{let id=st.seq++,x={jsonrpc:'2.0',id,method,params},b=Buffer.from(JSON.stringify(x));st.pending.set(id,{resolve,reject,t:setTimeout(()=>{st.pending.delete(id);reject(new Error(method+' timeout'))},10000)});st.proc.stdin.write(`Content-Length: ${b.length}\r\n\r\n`);st.proc.stdin.write(b)})}
function lspParseGeneric(st,d){st.buf=Buffer.concat([st.buf,d]);while(true){let i=st.buf.indexOf('\r\n\r\n');if(i<0)return;let m=/Content-Length:\s*(\d+)/i.exec(st.buf.slice(0,i).toString());if(!m){st.buf=st.buf.slice(i+4);continue}let n=+m[1];if(st.buf.length<i+4+n)return;let x;try{x=JSON.parse(st.buf.slice(i+4,i+4+n))}catch{st.buf=st.buf.slice(i+4+n);continue}st.buf=st.buf.slice(i+4+n);if(x.id!=null&&st.pending.has(x.id)){let q=st.pending.get(x.id);clearTimeout(q.t);st.pending.delete(x.id);x.error?q.reject(new Error(x.error.message)):q.resolve(x.result)}else if(x.method==='textDocument/publishDiagnostics')win.webContents.send('lspm:diagnostics',{language:st.language,params:x.params});else if(x.method==='workspace/applyEdit'&&x.id!=null){win.webContents.send('lspm:applyEdit',{language:st.language,id:x.id,edit:x.params?.edit});}}}
ipcMain.handle('lspm:start',(_,language,root)=>lspStartGeneric(language,root));
ipcMain.handle('lspm:detect',async(_,root)=>{let out=[];for(let[k,v]of Object.entries(LSP_SPECS)){if(v.builtin){out.push({language:k,available:true,builtin:true,command:'Pyright (bundled)'});continue}let r=await cmd(root,process.platform==='win32'?`where ${v.command}`:`command -v ${v.command}`);out.push({language:k,available:r.code===0,builtin:false,command:v.command})}return out});
ipcMain.handle('lspm:open',(_,language,p,text,version=1)=>{let st=lspServers.get(language);if(!st)return false;lspNotify(st,'textDocument/didOpen',{textDocument:{uri:mkUri(p),languageId:LSP_SPECS[language].ids[0],version,text}});return true});
ipcMain.handle('lspm:change',(_,language,p,text,version)=>{let st=lspServers.get(language);if(!st)return false;lspNotify(st,'textDocument/didChange',{textDocument:{uri:mkUri(p),version},contentChanges:[{text}]});return true});
ipcMain.handle('lspm:save',(_,language,p,text)=>{let st=lspServers.get(language);if(!st)return false;lspNotify(st,'textDocument/didSave',{textDocument:{uri:mkUri(p)},text});return true});
ipcMain.handle('lspm:req',(_,language,method,params)=>{let st=lspServers.get(language);if(!st)throw new Error(language+' language server not running');return lspReqGeneric(st,method,params)});
ipcMain.handle('lspm:stop',(_,language)=>{let st=lspServers.get(language);if(st){try{st.proc.kill()}catch{}lspServers.delete(language)}return true});
ipcMain.handle('lspm:stop-all',()=>{for(let st of lspServers.values())try{st.proc.kill()}catch{}lspServers.clear();return true});
ipcMain.handle('state:load',()=>({recent:[],settings:{}}));ipcMain.handle('state:save',()=>true);