2026-04-12 00:42:51 -07:00
|
|
|
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) throws {
|
|
|
|
|
guard process == nil else { return }
|
|
|
|
|
let p = Process()
|
|
|
|
|
p.executableURL = URL(fileURLWithPath:
|
|
|
|
|
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp")
|
|
|
|
|
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 }
|
|
|
|
|
private func handleTermination() { process = nil; broadcastStatus("lsp_terminated") }
|
|
|
|
|
|
|
|
|
|
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 }
|
2026-04-12 10:16:21 -07:00
|
|
|
for ws in clients where !ws.isClosed { let w = ws; Task { try? await w.send(text) } }
|
2026-04-12 00:42:51 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func broadcastStatus(_ s: String) {
|
2026-04-12 10:16:21 -07:00
|
|
|
for ws in clients where !ws.isClosed { let w = ws; let msg = #"{"padxcode_status":"\#(s)"}"#; Task { try? await w.send(msg) } }
|
2026-04-12 00:42:51 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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..<sr.lowerBound]
|
|
|
|
|
guard let hs = String(data: hd, encoding: .utf8),
|
|
|
|
|
let ls = hs.components(separatedBy: "\r\n").first(where: { $0.hasPrefix("Content-Length:") })?
|
|
|
|
|
.components(separatedBy: ":").last?.trimmingCharacters(in: .whitespaces),
|
|
|
|
|
let len = Int(ls) else { break }
|
|
|
|
|
let bs = sr.upperBound
|
|
|
|
|
let be = rem.index(bs, offsetBy: len, limitedBy: rem.endIndex) ?? rem.endIndex
|
|
|
|
|
results.append(Data(rem[bs..<be])); rem = rem[be...]
|
|
|
|
|
}
|
|
|
|
|
return results
|
|
|
|
|
}
|
|
|
|
|
}
|