fix(lsp): Fix LSP server attachment and JSON parsing issues

- Replace lspconfig.scl_lsp registration with vim.lsp.start() + FileType autocmd
  for Neovim 0.11+ compatibility
- Fix Lua reserved keyword 'end' in tables (use bracket notation ['end'])
- Fix JSON decoder pattern matching bugs (s:find -> s:sub:match)
- Add LSP method name conversion (textDocument/didOpen -> textDocument_didOpen)
- Handle request vs notification correctly (only respond to requests with id)
- Strip CRLF line endings for Windows compatibility
- Add semantic token handler aliases
- Update documentation with implementation notes
This commit is contained in:
2026-02-18 13:03:14 +01:00
parent 4429280524
commit 96c0c4584f
6 changed files with 201 additions and 91 deletions
+17 -2
View File
@@ -1,6 +1,21 @@
-- SCL Diagnostics - Linter functionality for the LSP server
local M = {}
local parser = require("scl_lsp.parser")
-- Get script path for loading parser (same pattern as main.lua)
local script_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or ""
if script_path == "" then
script_path = "."
end
if script_path:sub(1, 1) ~= "/" then
local cwd = io.popen("pwd")
if cwd then
script_path = cwd:read("*l") .. "/" .. script_path
cwd:close()
end
end
package.path = package.path .. ";" .. script_path .. "/?.lua"
local parser = dofile(script_path .. "/parser.lua")
local DiagnosticSeverity = {
Error = 1,
@@ -19,7 +34,7 @@ end
local function range_to_lsp(start_line, start_char, end_line, end_char)
return {
start = position_to_lsp(start_line, start_char),
end = position_to_lsp(end_line, end_char),
["end"] = position_to_lsp(end_line, end_char),
}
end
+1 -1
View File
@@ -8,7 +8,7 @@ end
local function range(start_line, start_char, end_line, end_char)
return {
start = position(start_line, start_char),
end = position(end_line, end_char),
["end"] = position(end_line, end_char),
}
end
+91 -70
View File
@@ -13,7 +13,7 @@ end
local function escape(s)
s = s:gsub("\\", "\\\\")
s = s:gsub("\"", "\\\"")
s = s:gsub('"', '\\"')
s = s:gsub("\n", "\\n")
s = s:gsub("\r", "\\r")
s = s:gsub("\t", "\\t")
@@ -29,7 +29,7 @@ function json.encode(data)
elseif t == "number" then
return tostring(data)
elseif t == "string" then
return "\"" .. escape(data) .. "\""
return '"' .. escape(data) .. '"'
elseif t == "table" then
local parts = {}
if is_array(data) then
@@ -39,7 +39,7 @@ function json.encode(data)
return "[" .. table.concat(parts, ",") .. "]"
else
for k, v in pairs(data) do
table.insert(parts, "\"" .. escape(k) .. "\":" .. json.encode(v))
table.insert(parts, '"' .. escape(k) .. '":' .. json.encode(v))
end
return "{" .. table.concat(parts, ",") .. "}"
end
@@ -49,7 +49,7 @@ function json.encode(data)
end
local function skip_whitespace(s, i)
while i <= #s and s:find("^%s", i) do
while i <= #s and s:sub(i, i):match("%s") do
i = i + 1
end
return i
@@ -60,7 +60,7 @@ local function parse_string(s, i)
i = i + 1
while i <= #s do
local c = s:sub(i, i)
if c == "\"" then
if c == '"' then
return table.concat(result), i + 1
elseif c == "\\" and i < #s then
local next_c = s:sub(i + 1, i + 1)
@@ -70,9 +70,9 @@ local function parse_string(s, i)
table.insert(result, "\r")
elseif next_c == "t" then
table.insert(result, "\t")
elseif next_c == "\\\"" then
table.insert(result, "\"")
elseif next_c == "\\\\" then
elseif next_c == '"' then
table.insert(result, '"')
elseif next_c == "\\" then
table.insert(result, "\\")
else
table.insert(result, next_c)
@@ -91,28 +91,85 @@ local function parse_number(s, i)
if s:sub(i, i) == "-" then
i = i + 1
end
while i <= #s and s:find("^%d", s:sub(i, i)) do
while i <= #s and s:sub(i, i):match("%d") do
i = i + 1
end
if s:sub(i, i) == "." then
i = i + 1
while i <= #s and s:find("^%d", s:sub(i, i)) do
while i <= #s and s:sub(i, i):match("%d") do
i = i + 1
end
end
if s:sub(i, i):find("^[eE]") then
if s:sub(i, i):match("[eE]") then
i = i + 1
if s:sub(i, i):find("^[+-]") then
if s:sub(i, i):match("[+-]") then
i = i + 1
end
while i <= #s and s:find("^%d", s:sub(i, i)) do
while i <= #s and s:sub(i, i):match("%d") do
i = i + 1
end
end
return tonumber(s:sub(start, i - 1)), i
end
local function parse_value(s, i)
local parse_value
local function parse_array(s, i)
local arr = {}
i = i + 1
while i <= #s do
i = skip_whitespace(s, i)
if s:sub(i, i) == "]" then
return arr, i + 1
end
local val
val, i = parse_value(s, i)
table.insert(arr, val)
i = skip_whitespace(s, i)
if s:sub(i, i) == "]" then
return arr, i + 1
end
if s:sub(i, i) == "," then
i = i + 1
end
end
return nil, i
end
local function parse_object(s, i)
local obj = {}
i = i + 1
while i <= #s do
i = skip_whitespace(s, i)
if s:sub(i, i) == "}" then
return obj, i + 1
end
if s:sub(i, i) == '"' then
local key
key, i = parse_string(s, i)
i = skip_whitespace(s, i)
if s:sub(i, i) ~= ":" then
return nil, i
end
i = i + 1
local val
val, i = parse_value(s, i)
obj[key] = val
i = skip_whitespace(s, i)
if s:sub(i, i) == "}" then
return obj, i + 1
end
if s:sub(i, i) == "," then
i = i + 1
end
else
return nil, i
end
end
return nil, i
end
parse_value = function(s, i)
i = skip_whitespace(s, i)
if i > #s then
return nil, i
@@ -121,66 +178,30 @@ local function parse_value(s, i)
local c = s:sub(i, i)
if c == "{" then
local obj = {}
i = i + 1
while i <= #s do
i = skip_whitespace(s, i)
if s:sub(i, i) == "}" then
return obj, i + 1
end
if s:sub(i, i) == "\"" then
local key
key, i = parse_string(s, i)
i = skip_whitespace(s, i)
if s:sub(i, i) ~= ":" then
return nil, i
end
i = i + 1
local val
val, i = parse_value(s, i)
obj[key] = val
i = skip_whitespace(s, i)
if s:sub(i, i) == "}" then
return obj, i + 1
end
if s:sub(i, i) == "," then
i = i + 1
end
else
return nil, i
end
end
return nil, i
return parse_object(s, i)
elseif c == "[" then
local arr = {}
i = i + 1
while i <= #s do
i = skip_whitespace(s, i)
if s:sub(i, i) == "]" then
return arr, i + 1
end
local val
val, i = parse_value(s, i)
table.insert(arr, val)
i = skip_whitespace(s, i)
if s:sub(i, i) == "]" then
return arr, i + 1
end
if s:sub(i, i) == "," then
i = i + 1
end
return parse_array(s, i)
elseif c == '"' then
return parse_string(s, i)
elseif c == "t" then
if s:sub(i, i + 3) == "true" then
return true, i + 4
end
return nil, i
elseif c == "\"" then
return parse_string(s, i)
elseif s:sub(i, i + 3) == "null" then
return nil, i + 4
elseif s:sub(i, i + 3) == "true" then
return true, i + 4
elseif s:sub(i, i + 4) == "false" then
return false, i + 5
else
elseif c == "f" then
if s:sub(i, i + 4) == "false" then
return false, i + 5
end
return nil, i
elseif c == "n" then
if s:sub(i, i + 3) == "null" then
return nil, i + 4
end
return nil, i
elseif c == "-" or c:match("%d") then
return parse_number(s, i)
else
return nil, i
end
end
+32 -16
View File
@@ -296,7 +296,7 @@ function handlers.textDocument_hover(params)
contents = { kind = "markdown", value = detail },
range = {
start = { line = params.position.line, character = word_start - 1 },
endPos = { line = params.position.line, character = word_end - 1 },
["end"] = { line = params.position.line, character = word_end - 1 },
},
}
end
@@ -315,7 +315,7 @@ function handlers.textDocument_hover(params)
contents = { kind = "markdown", value = detail },
range = {
start = { line = params.position.line, character = word_start - 1 },
endPos = { line = params.position.line, character = word_end - 1 },
["end"] = { line = params.position.line, character = word_end - 1 },
},
}
end
@@ -449,7 +449,7 @@ function handlers.textDocument_definition(params)
uri = uri,
range = {
start = { line = pos.line, character = pos.character },
endPos = { line = pos.line, character = pos.character + #word },
["end"] = { line = pos.line, character = pos.character + #word },
},
}
end
@@ -478,10 +478,10 @@ function handlers.textDocument_documentSymbol(params)
table.insert(symbols, {
name = func.kind:upper() .. " " .. func.name,
kind = kind_map[func.kind] or 14,
range = { start = { line = func.start, character = 0 }, endPos = { line = func.final, character = 0 } },
range = { start = { line = func.start, character = 0 }, ["end"] = { line = func.final, character = 0 } },
selectionRange = {
start = { line = func.start, character = 0 },
endPos = { line = func.start, character = #func.name },
["end"] = { line = func.start, character = #func.name },
},
containerName = nil,
})
@@ -500,11 +500,11 @@ function handlers.textDocument_documentSymbol(params)
detail = var_info.type or "VAR",
range = {
start = { line = var_info.line, character = 0 },
endPos = { line = var_info.line, character = #var_name },
["end"] = { line = var_info.line, character = #var_name },
},
selectionRange = {
start = { line = var_info.line, character = var_info.character },
endPos = { line = var_info.line, character = var_info.character + #var_name },
["end"] = { line = var_info.line, character = var_info.character + #var_name },
},
containerName = nil,
})
@@ -526,11 +526,11 @@ function handlers.textDocument_documentSymbol(params)
detail = detail,
range = {
start = { line = type_info.start_line or 0, character = 0 },
endPos = { line = type_info.start_line or 0, character = #type_name },
["end"] = { line = type_info.start_line or 0, character = #type_name },
},
selectionRange = {
start = { line = type_info.start_line or 0, character = 0 },
endPos = { line = type_info.start_line or 0, character = #type_name },
["end"] = { line = type_info.start_line or 0, character = #type_name },
},
containerName = nil,
})
@@ -656,6 +656,10 @@ function handlers.textDocument_semanticTokensRange(params)
return handlers.textDocument_semanticTokensFull(params)
end
-- Aliases for method names with slashes converted to underscores
handlers["textDocument_semanticTokens_full"] = handlers.textDocument_semanticTokensFull
handlers["textDocument_semanticTokens_range"] = handlers.textDocument_semanticTokensRange
function handlers.textDocument_inlayHint(params)
local uri = params.textDocument.uri
local doc = documents[uri]
@@ -708,7 +712,7 @@ function handlers.workspace_symbol(params)
uri = uri,
range = {
start = { line = line, character = col },
endPos = { line = line, character = col + length },
["end"] = { line = line, character = col + length },
},
},
})
@@ -848,17 +852,26 @@ local function handle_message(message)
if message.method == "$/cancelRequest" then
return nil
end
local handler = handlers[message.method]
-- Convert method name from "textDocument/didOpen" to "textDocument_didOpen"
local method_name = message.method:gsub("/", "_")
local handler = handlers[method_name]
if handler then
local ok, result = pcall(handler, message.params)
if ok then
return { id = message.id, result = result }
else
return { id = message.id, error = { code = -32603, message = tostring(result) } }
-- Only return response for requests (have id), not notifications
if message.id then
if ok then
return { id = message.id, result = result }
else
return { id = message.id, error = { code = -32603, message = tostring(result) } }
end
end
else
return { id = message.id, error = { code = -32601, message = "Method not found: " .. message.method } }
-- Only return error for requests (have id)
if message.id then
return { id = message.id, error = { code = -32601, message = "Method not found: " .. message.method } }
end
end
return nil
end
local function main()
@@ -871,6 +884,9 @@ local function main()
if not line then
break
end
-- Strip trailing \r (Windows line endings)
line = line:gsub("\r$", "")
if line:match("^Content%-Length:%s*(%d+)") then
content_length = tonumber(line:match("^Content%-Length:%s*(%d+)"))
elseif line == "" then