import Vapor import Foundation actor LSPProxy { static let shared = LSPProxy() private var process: Process? private var stdinPipe: Pipe? private var clients: [WebSocket] = [] func start(projectPath: String) async throws { // If already running for a DIFFERENT project, restart for the new one. // Keeping the old process would give wrong completions/diagnostics. if process != nil { stop() } // Resolve sourcekit-lsp via xcrun so we respect the active xcode-select toolchain. // Run on a background thread because waitUntilExit() blocks. let lspPath = await withCheckedContinuation { cont in DispatchQueue.global(qos: .userInitiated).async { cont.resume(returning: Self.resolveLSPPathSync()) } } guard FileManager.default.fileExists(atPath: lspPath) else { throw NSError(domain: "LSPProxy", code: 1, userInfo: [NSLocalizedDescriptionKey: "sourcekit-lsp not found at \(lspPath)"]) } let p = Process() p.executableURL = URL(fileURLWithPath: lspPath) p.currentDirectoryURL = URL(fileURLWithPath: projectPath) let inPipe = Pipe(); let outPipe = Pipe(); let errPipe = Pipe() p.standardInput = inPipe; p.standardOutput = outPipe; p.standardError = errPipe stdinPipe = inPipe outPipe.fileHandleForReading.readabilityHandler = { [weak self] h in let d = h.availableData; guard !d.isEmpty else { return } Task { await self?.broadcastToClients(d) } } p.terminationHandler = { [weak self] _ in Task { await self?.handleTermination() } } try p.run(); process = p } func stop() { process?.terminate(); process = nil; stdinPipe = nil; clients.removeAll() } private func handleTermination() { process = nil; broadcastStatus("lsp_terminated"); clients.removeAll() } func addClient(_ ws: WebSocket) { clients.append(ws) } func removeClient(_ ws: WebSocket) { clients.removeAll { $0 === ws } } func forwardToLSP(_ message: Data) { guard let pipe = stdinPipe else { return } let header = "Content-Length: \(message.count)\r\n\r\n" if let hd = header.data(using: .utf8) { pipe.fileHandleForWriting.write(hd) pipe.fileHandleForWriting.write(message) } } private func broadcastToClients(_ data: Data) { let messages = extractJSONBodies(from: data) for msg in messages { guard let text = String(data: msg, encoding: .utf8) else { continue } for ws in clients where !ws.isClosed { let w = ws; Task { try? await w.send(text) } } } } private func broadcastStatus(_ s: String) { for ws in clients where !ws.isClosed { let w = ws; let msg = #"{"padxcode_status":"\#(s)"}"#; Task { try? await w.send(msg) } } } private static func resolveLSPPathSync() -> String { // Try xcrun first — respects xcode-select. let xcrun = Process() xcrun.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") xcrun.arguments = ["--find", "sourcekit-lsp"] let pipe = Pipe() xcrun.standardOutput = pipe; xcrun.standardError = Pipe() if let _ = try? xcrun.run() { xcrun.waitUntilExit() if xcrun.terminationStatus == 0, let path = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines), !path.isEmpty { return path } } // Fallback to known default location. return "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp" } private func extractJSONBodies(from data: Data) -> [Data] { var results: [Data] = []; var rem = data let sep = Data([0x0D,0x0A,0x0D,0x0A]) while !rem.isEmpty { guard let sr = rem.range(of: sep) else { break } let hd = rem[rem.startIndex..