373 lines
10 KiB
Plaintext
373 lines
10 KiB
Plaintext
// ƿ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")
|