mcp-neovim-server

by bigcodegen
Verified
import { attach, Neovim } from 'neovim'; interface NeovimStatus { cursorPosition: [number, number]; mode: string; visualSelection: string; fileName: string; windowLayout: string; currentTab: number; marks: { [key: string]: [number, number] }; registers: { [key: string]: string }; cwd: string; } interface BufferInfo { number: number; name: string; isListed: boolean; isLoaded: boolean; modified: boolean; syntax: string; windowIds: number[]; } interface WindowInfo { id: number; bufferId: number; width: number; height: number; row: number; col: number; } export class NeovimManager { private static instance: NeovimManager; private constructor() { } public static getInstance(): NeovimManager { if (!NeovimManager.instance) { NeovimManager.instance = new NeovimManager(); } return NeovimManager.instance; } private async connect(): Promise<Neovim> { try { const socketPath = process.env.NVIM_SOCKET_PATH || '/tmp/nvim'; return attach({ socket: socketPath }); } catch (error) { console.error('Error connecting to Neovim:', error); throw error; } } public async getBufferContents(): Promise<Map<number, string>> { try { const nvim = await this.connect(); const buffer = await nvim.buffer; const lines = await buffer.lines; const lineMap = new Map<number, string>(); lines.forEach((line: string, index: number) => { lineMap.set(index + 1, line); }); return lineMap; } catch (error) { console.error('Error getting buffer contents:', error); return new Map(); } } public async sendCommand(command: string): Promise<string> { try { const nvim = await this.connect(); // Remove leading colon if present const normalizedCommand = command.startsWith(':') ? command.substring(1) : command; // Handle shell commands (starting with !) if (normalizedCommand.startsWith('!')) { if (process.env.ALLOW_SHELL_COMMANDS !== 'true') { return 'Shell command execution is disabled. Set ALLOW_SHELL_COMMANDS=true environment variable to enable shell commands.'; } try { const shellCommand = normalizedCommand.substring(1).trim(); // Execute the command and capture output directly const output = await nvim.eval(`system('${shellCommand.replace(/'/g, "''")}')`); if (output) { return String(output).trim(); } return 'No output from command'; } catch (error) { console.error('Shell command error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return `Error executing shell command: ${errorMessage}`; } } // For regular Vim commands await nvim.setVvar('errmsg', ''); // Execute the command and capture its output using the execute() function const output = await nvim.call('execute', [normalizedCommand]); // Check for errors const vimerr = await nvim.getVvar('errmsg'); if (vimerr) { console.error('Vim error:', vimerr); return `Error executing command: ${vimerr}`; } // Return the actual command output if any return output ? String(output).trim() : 'Command executed (no output)'; } catch (error) { console.error('Error sending command:', error); return 'Error executing command'; } } public async getNeovimStatus(): Promise<NeovimStatus | string> { try { const nvim = await this.connect(); const window = await nvim.window; const cursor = await window.cursor; const mode = await nvim.mode; const buffer = await nvim.buffer; // Get window layout const layout = await nvim.eval('winlayout()'); const tabpage = await nvim.tabpage; const currentTab = await tabpage.number; // Get marks (a-z) const marks: { [key: string]: [number, number] } = {}; for (const mark of 'abcdefghijklmnopqrstuvwxyz') { try { const pos = await nvim.eval(`getpos("'${mark}")`) as [number, number, number, number]; marks[mark] = [pos[1], pos[2]]; } catch (e) { // Mark not set } } // Get registers (a-z, ", 0-9) const registers: { [key: string]: string } = {}; const registerNames = [...'abcdefghijklmnopqrstuvwxyz', '"', ...Array(10).keys()]; for (const reg of registerNames) { try { registers[reg] = String(await nvim.eval(`getreg('${reg}')`)); } catch (e) { // Register empty } } // Get current working directory const cwd = await nvim.call('getcwd'); const neovimStatus: NeovimStatus = { cursorPosition: cursor, mode: mode.mode, visualSelection: '', fileName: await buffer.name, windowLayout: JSON.stringify(layout), currentTab, marks, registers, cwd }; if (mode.mode.startsWith('v')) { const start = await nvim.eval(`getpos("'<")`) as [number, number, number, number]; const end = await nvim.eval(`getpos("'>")`) as [number, number, number, number]; const lines = await buffer.getLines({ start: start[1] - 1, end: end[1], strictIndexing: true }); neovimStatus.visualSelection = lines.join('\n'); } return neovimStatus; } catch (error) { console.error('Error getting Neovim status:', error); return 'Error getting Neovim status'; } } public async editLines(startLine: number, mode: 'replace' | 'insert' | 'replaceAll', newText: string): Promise<string> { try { const nvim = await this.connect(); const splitByLines = newText.split('\n'); const buffer = await nvim.buffer; if (mode === 'replaceAll') { // Handle full buffer replacement const lineCount = await buffer.length; // Delete all lines and then append new content await buffer.remove(0, lineCount, true); await buffer.insert(splitByLines, 0); return 'Buffer completely replaced'; } else if (mode === 'replace') { await buffer.replace(splitByLines, startLine - 1); return 'Lines replaced successfully'; } else if (mode === 'insert') { await buffer.insert(splitByLines, startLine - 1); return 'Lines inserted successfully'; } return 'Invalid mode specified'; } catch (error) { console.error('Error editing lines:', error); return 'Error editing lines'; } } public async getWindows(): Promise<WindowInfo[]> { try { const nvim = await this.connect(); const windows = await nvim.windows; const windowInfos: WindowInfo[] = []; for (const win of windows) { const buffer = await win.buffer; const [width, height] = await Promise.all([ win.width, win.height ]); const position = await win.position; windowInfos.push({ id: win.id, bufferId: buffer.id, width, height, row: position[0], col: position[1] }); } return windowInfos; } catch (error) { console.error('Error getting windows:', error); return []; } } public async manipulateWindow(command: string): Promise<string> { const validCommands = ['split', 'vsplit', 'only', 'close', 'wincmd h', 'wincmd j', 'wincmd k', 'wincmd l']; if (!validCommands.some(cmd => command.startsWith(cmd))) { return 'Invalid window command'; } try { const nvim = await this.connect(); await nvim.command(command); return 'Window command executed'; } catch (error) { console.error('Error manipulating window:', error); return 'Error executing window command'; } } public async setMark(mark: string, line: number, col: number): Promise<string> { if (!/^[a-z]$/.test(mark)) { return 'Invalid mark name (must be a-z)'; } try { const nvim = await this.connect(); await nvim.command(`mark ${mark}`); const window = await nvim.window; await (window.cursor = [line, col]); return `Mark ${mark} set at line ${line}, column ${col}`; } catch (error) { console.error('Error setting mark:', error); return 'Error setting mark'; } } public async setRegister(register: string, content: string): Promise<string> { const validRegisters = [...'abcdefghijklmnopqrstuvwxyz"']; if (!validRegisters.includes(register)) { return 'Invalid register name'; } try { const nvim = await this.connect(); await nvim.eval(`setreg('${register}', '${content.replace(/'/g, "''")}')`); return `Register ${register} set`; } catch (error) { console.error('Error setting register:', error); return 'Error setting register'; } } public async visualSelect(startLine: number, startCol: number, endLine: number, endCol: number): Promise<string> { try { const nvim = await this.connect(); const window = await nvim.window; // Enter visual mode await nvim.command('normal! v'); // Move cursor to start position await (window.cursor = [startLine, startCol]); // Move cursor to end position (selection will be made) await (window.cursor = [endLine, endCol]); return 'Visual selection made'; } catch (error) { console.error('Error making visual selection:', error); return 'Error making visual selection'; } } public async getOpenBuffers(): Promise<BufferInfo[]> { try { const nvim = await this.connect(); const buffers = await nvim.buffers; const windows = await nvim.windows; const bufferInfos: BufferInfo[] = []; for (const buffer of buffers) { const [ isLoaded, isListedOption, modified, syntax ] = await Promise.all([ buffer.loaded, buffer.getOption('buflisted'), buffer.getOption('modified'), buffer.getOption('syntax') ]); const isListed = Boolean(isListedOption); // Find windows containing this buffer const windowIds = []; for (const win of windows) { const winBuffer = await win.buffer; if (winBuffer.id === buffer.id) { windowIds.push(win.id); } } bufferInfos.push({ number: buffer.id, name: await buffer.name, isListed, isLoaded, modified: Boolean(modified), syntax: String(syntax), windowIds }); } return bufferInfos; } catch (error) { console.error('Error getting open buffers:', error); return []; } } }