///|
priv struct LspDocument {
text : String
version : Int
opened : Bool
}
///|
/// Transport-independent LSP 3.17 engine. Files enter only through client messages.
struct LspServer {
documents : Map[String, LspDocument]
workspace_files : Map[String, String]
mut initialized : Bool
mut shutting_down : Bool
mut exiting : Bool
mut literal : Bool
}
///|
pub fn new_lsp_server() -> LspServer {
{
documents: Map([]),
workspace_files: Map([]),
initialized: false,
shutting_down: false,
exiting: false,
literal: false,
}
}
///|
pub fn LspServer::should_exit(self : LspServer) -> Bool {
self.exiting
}
///|
pub fn LspServer::exit_code(self : LspServer) -> Int {
if self.shutting_down {
0
} else {
1
}
}
///|
fn lsp_string(fields : Map[String, Json], key : String) -> String raise {
guard fields.get(key) is Some(String(text)) else {
raise Failure("Expected string field " + key)
}
text
}
///|
fn lsp_object(
fields : Map[String, Json],
key : String,
) -> Map[String, Json] raise {
guard fields.get(key) is Some(Object(object)) else {
raise Failure("Expected object field " + key)
}
object
}
///|
fn lsp_int(fields : Map[String, Json], key : String) -> Int raise {
guard fields.get(key) is Some(Number(n, ..)) &&
n >= -2147483648.0 &&
n <= 2147483647.0 &&
n == n.to_int().to_double() else {
raise Failure("Expected integer " + key)
}
n.to_int()
}
///|
pub fn lsp_position(text : String, offset : Int) -> Json {
let chars = text.to_array()
let mut line = 0
let mut column = 0
for i in 0.. 65535 { 2 } else { 1 }
}
}
Json::object({
"line": Json::number(line.to_double()),
"character": Json::number(column.to_double()),
})
}
///|
fn lsp_offset(text : String, position : Map[String, Json]) -> Int raise {
let wanted_line = lsp_int(position, "line")
let wanted_column = lsp_int(position, "character")
if wanted_line < 0 || wanted_column < 0 {
raise Failure("Negative LSP position")
}
let chars = text.to_array()
let mut line = 0
let mut column = 0
let mut i = 0
while i <= chars.length() {
if line == wanted_line && column == wanted_column {
return i
}
if i == chars.length() {
break
}
let c = chars[i]
if c == '\r' || c == '\n' {
if line == wanted_line {
raise Failure("Position exceeds line length")
}
if c == '\r' && chars.get(i + 1) == Some('\n') {
i += 1
}
line += 1
column = 0
} else {
column += if c.to_int() > 65535 { 2 } else { 1 }
if line == wanted_line && column > wanted_column {
raise Failure("Position splits a Unicode surrogate pair")
}
}
i += 1
}
raise Failure("Position exceeds document")
}
///|
fn lsp_range(text : String, start : Int, end : Int) -> Json {
Json::object({
"start": lsp_position(text, start),
"end": lsp_position(text, end),
})
}
///|
fn lsp_virtual_uri(uri : String) -> String raise {
if !uri.has_prefix("file://") {
raise Failure("References require file:// URIs")
}
let raw = uri[7:].to_owned()
let chars = raw.to_array()
let output = @buffer.Buffer()
let mut i = 0
while i < chars.length() {
if chars[i] == '%' {
guard i + 2 < chars.length() &&
kv3_hex(chars[i + 1]) is Some(a) &&
kv3_hex(chars[i + 2]) is Some(b) else {
raise Failure("Invalid URI encoding")
}
output.write_byte((a * 16 + b).to_byte())
i += 3
} else {
output.write_bytes(@utf8.encode(chars[i].to_string()[:])[:])
i += 1
}
}
let decoded = @utf8.decode(output.to_bytes()[:])
if decoded.contains("\u0000") {
raise Failure("NUL in URI")
}
normalize_reference(decoded)
}
///|
fn LspServer::put(
self : LspServer,
uri : String,
document : LspDocument,
) -> Unit raise {
if uri.length() > 32768 || uri.contains("\u0000") {
raise Failure("Invalid document URI")
}
if !self.documents.contains(uri) && self.documents.length() >= 64 {
raise Failure("Workspace exceeds 64 documents")
}
let mut total = document.text.char_length()
for name, previous in self.documents {
if name != uri {
total += previous.text.char_length()
}
}
if total > 16777216 || document.text.char_length() > 8388608 {
raise Failure("LSP document character budget exceeded")
}
self.documents[uri] = document
}
///|
fn lsp_is_kv3(uri : String, text : String) -> Bool {
uri.to_lower().has_suffix(".kv3") ||
text.trim_start().to_owned().has_prefix("