// ƿit Language Server Protocol (LSP) main loop. // Communicates via JSON-RPC over stdin/stdout. var fd = use('fd') var json_mod = use('json') var protocol = use('protocol') var analysis_make = use('analysis') var completions = use('completions') var hover_mod = use('hover') var symbols = use('symbols') // Get tokenize_mod and parse_mod from the environment. // These are the same functions the compiler uses internally. var tokenize_mod = use('tokenize') var parse_mod = use('parse') // Create analysis module bound to tokenize/parse var analysis = analysis_make(tokenize_mod, parse_mod) // Document store: URI -> {text, version, ast, tokens, errors} var docs = {} // Log to stderr for debugging (does not interfere with protocol). var log = function(msg) { fd.write(2, `[pit-lsp] ${msg}\n`) } // Publish diagnostics for a document. var publish_diagnostics = function(uri, doc) { var diags = analysis.diagnostics(doc) protocol.notify("textDocument/publishDiagnostics", { uri: uri, diagnostics: diags }) } // Parse a document and publish diagnostics. var parse_and_notify = function(uri, src, version) { var doc = analysis.update(docs, uri, {src: src, version: version}) publish_diagnostics(uri, doc) } // Handle initialize request. var handle_initialize = function(id, params) { protocol.respond(id, { capabilities: { textDocumentSync: { openClose: true, change: 1, save: {includeText: true} }, completionProvider: { triggerCharacters: [".", "$"] }, hoverProvider: true, definitionProvider: true, documentSymbolProvider: true }, serverInfo: { name: "pit-lsp", version: "0.1.0" } }) } // Handle textDocument/didOpen notification. var handle_did_open = function(params) { var td = params.textDocument parse_and_notify(td.uri, td.text, td.version) } // Handle textDocument/didChange notification (full text sync). var handle_did_change = function(params) { var td = params.textDocument var changes = params.contentChanges if (length(changes) > 0) { parse_and_notify(td.uri, changes[0].text, td.version) } } // Handle textDocument/didClose notification. var handle_did_close = function(params) { var uri = params.textDocument.uri analysis.remove(docs, uri) // Clear diagnostics protocol.notify("textDocument/publishDiagnostics", { uri: uri, diagnostics: [] }) } // Handle textDocument/didSave notification. var handle_did_save = function(params) { var td = params.textDocument if (params.text != null) { parse_and_notify(td.uri, params.text, td.version) } } // Handle textDocument/completion request. var handle_completion = function(id, params) { var uri = params.textDocument.uri var pos = params.position var doc = docs[uri] var items = [] if (doc != null) { items = completions.complete(doc, pos.line, pos.character) } protocol.respond(id, items) } // Handle textDocument/hover request. var handle_hover = function(id, params) { var uri = params.textDocument.uri var pos = params.position var doc = docs[uri] var result = null if (doc != null) { result = hover_mod.hover(doc, pos.line, pos.character, analysis.token_at) } protocol.respond(id, result) } // Handle textDocument/definition request. var handle_definition = function(id, params) { var uri = params.textDocument.uri var pos = params.position var doc = docs[uri] var result = null if (doc != null) { result = symbols.definition(doc, pos.line, pos.character, analysis.token_at) } protocol.respond(id, result) } // Handle textDocument/documentSymbol request. var handle_document_symbol = function(id, params) { var uri = params.textDocument.uri var doc = docs[uri] var result = [] if (doc != null) { result = symbols.document_symbols(doc) } protocol.respond(id, result) } // Dispatch a single message. Wrapped in a function for disruption handling. var dispatch_message = function(msg) { var method = msg.method if (method == "initialize") { handle_initialize(msg.id, msg.params) } else if (method == "initialized") { // no-op } else if (method == "textDocument/didOpen") { handle_did_open(msg.params) } else if (method == "textDocument/didChange") { handle_did_change(msg.params) } else if (method == "textDocument/didClose") { handle_did_close(msg.params) } else if (method == "textDocument/didSave") { handle_did_save(msg.params) } else if (method == "textDocument/completion") { handle_completion(msg.id, msg.params) } else if (method == "textDocument/hover") { handle_hover(msg.id, msg.params) } else if (method == "textDocument/definition") { handle_definition(msg.id, msg.params) } else if (method == "textDocument/documentSymbol") { handle_document_symbol(msg.id, msg.params) } else if (method == "shutdown") { protocol.respond(msg.id, null) return "shutdown" } else if (method == "exit") { return "exit" } else { if (msg.id != null) { protocol.respond_error(msg.id, -32601, `Method not found: ${method}`) } } return null } disruption { log(`error handling ${msg.method}`) if (msg.id != null) { protocol.respond_error(msg.id, -32603, `Internal error handling ${msg.method}`) } return null } // Main loop. log("starting") var running = true var msg = null var result = null while (running) { msg = protocol.read_message() if (msg == null) { running = false break } result = dispatch_message(msg) if (result == "exit") { running = false } } log("stopped")