// ƿ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') var index_mod = use('index') var explain_mod = use('explain') // Create analysis module bound to tokenize/parse/index var analysis = analysis_make(tokenize_mod, parse_mod, index_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, referencesProvider: true, renameProvider: {prepareProvider: 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) } // Handle textDocument/references request. var handle_references = function(id, params) { var uri = params.textDocument.uri var pos = params.position var doc = docs[uri] var result = [] var tok = null var name = null var refs = null var _i = 0 var ref = null var expl = null var sym_result = null if (doc != null && doc.index != null) { tok = analysis.token_at(doc, pos.line, pos.character) if (tok != null && tok.kind == "name" && tok.value != null) { name = tok.value refs = doc.index.reverse_refs[name] if (refs != null) { _i = 0 while (_i < length(refs)) { ref = refs[_i] if (ref.span != null) { result[] = { uri: uri, range: { start: {line: ref.span.from_row, character: ref.span.from_col}, end: {line: ref.span.to_row, character: ref.span.to_col} } } } _i = _i + 1 } } // Also include the declaration itself if found expl = explain_mod.make(doc.index) sym_result = expl.by_symbol(name) if (sym_result != null && length(sym_result.symbols) > 0) { _i = 0 while (_i < length(sym_result.symbols)) { if (sym_result.symbols[_i].decl_span != null) { result[] = { uri: uri, range: { start: {line: sym_result.symbols[_i].decl_span.from_row, character: sym_result.symbols[_i].decl_span.from_col}, end: {line: sym_result.symbols[_i].decl_span.to_row, character: sym_result.symbols[_i].decl_span.to_col} } } } _i = _i + 1 } } } } protocol.respond(id, result) } // Handle textDocument/prepareRename request. var handle_prepare_rename = function(id, params) { var uri = params.textDocument.uri var pos = params.position var doc = docs[uri] var tok = null var name = null var result = null var expl = null var sym_result = null if (doc != null) { tok = analysis.token_at(doc, pos.line, pos.character) if (tok != null && tok.kind == "name" && tok.value != null) { name = tok.value // Don't allow renaming intrinsics if (doc.index != null) { expl = explain_mod.make(doc.index) sym_result = expl.by_symbol(name) if (sym_result != null && length(sym_result.symbols) > 0) { result = { range: { start: {line: tok.from_row, character: tok.from_column}, end: {line: tok.to_row, character: tok.to_column} }, placeholder: name } } } } } protocol.respond(id, result) } // Handle textDocument/rename request. var handle_rename = function(id, params) { var uri = params.textDocument.uri var pos = params.position var new_name = params.newName var doc = docs[uri] var tok = null var name = null var edits = [] var refs = null var _i = 0 var ref = null var expl = null var sym_result = null if (doc != null && doc.index != null) { tok = analysis.token_at(doc, pos.line, pos.character) if (tok != null && tok.kind == "name" && tok.value != null) { name = tok.value expl = explain_mod.make(doc.index) sym_result = expl.by_symbol(name) // Add edit for declaration if (sym_result != null && length(sym_result.symbols) > 0) { _i = 0 while (_i < length(sym_result.symbols)) { if (sym_result.symbols[_i].decl_span != null) { edits[] = { range: { start: {line: sym_result.symbols[_i].decl_span.from_row, character: sym_result.symbols[_i].decl_span.from_col}, end: {line: sym_result.symbols[_i].decl_span.to_row, character: sym_result.symbols[_i].decl_span.to_col} }, newText: new_name } } _i = _i + 1 } } // Add edits for all references refs = doc.index.reverse_refs[name] if (refs != null) { _i = 0 while (_i < length(refs)) { ref = refs[_i] if (ref.span != null) { edits[] = { range: { start: {line: ref.span.from_row, character: ref.span.from_col}, end: {line: ref.span.to_row, character: ref.span.to_col} }, newText: new_name } } _i = _i + 1 } } } } var changes = {} if (length(edits) > 0) { changes[uri] = edits } protocol.respond(id, {changes: changes}) } // 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 == "textDocument/references") { handle_references(msg.id, msg.params) } else if (method == "textDocument/prepareRename") { handle_prepare_rename(msg.id, msg.params) } else if (method == "textDocument/rename") { handle_rename(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")