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 b5d50384fb
commit 246fa85442
3 changed files with 105 additions and 44 deletions
+44
View File
@@ -104,6 +104,19 @@ function M.clear_cache()
end
```
## Lua Reserved Keywords
Lua reserved keywords (like `end`, `for`, `in`, etc.) cannot be used as table keys directly. Use bracket notation:
```lua
-- WRONG: causes syntax error
local range = { start = pos1, end = pos2 }
-- CORRECT: use bracket notation
local range = { start = pos1, ["end"] = pos2 }
```
This is especially important for LSP range objects which require an `end` field per the LSP specification.
## Error Handling
### Return Pattern
@@ -166,3 +179,34 @@ Find the last `:=`, `=>`, `=`, `<>`, `>=`, `<=`, `>`, `<`, `AND`, `OR`, `NOT` an
| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call |
| `:LspSCLFormat` | Format current SCL file |
| `:SCLGeneratePlcJson` | Generate plc.data.json from data_types/ |
## LSP Server Implementation Notes
### Method Name Conversion
LSP methods like `textDocument/didOpen` are converted to handler names like `textDocument_didOpen`:
```lua
local method_name = message.method:gsub("/", "_")
local handler = handlers[method_name]
```
### Request vs Notification
- Requests have `id` field and require a response
- Notifications have no `id` and should not receive a response
```lua
if message.id then
-- Only send response for requests
return { id = message.id, result = result }
end
```
### Line Ending Handling
The LSP server strips CRLF (`\r\n`) line endings for Windows compatibility:
```lua
line = line:gsub("\r$", "")
```
### JSON Parser
The standalone JSON parser in `src/json.lua` handles:
- Objects, arrays, strings, numbers, booleans, null
- Escape sequences in strings
- Whitespace skipping
+16 -2
View File
@@ -253,10 +253,24 @@ myVar.temperature := 25.5;
END_FUNCTION_BLOCK
```
## Architecture
The plugin uses a standalone LSP server (`src/main.lua`) that communicates via JSON-RPC over stdio. The server handles:
- Text document synchronization
- Diagnostics (linting)
- Formatting
- Completion, hover, go-to-definition
- Semantic tokens
The Neovim plugin (`lua/scl_lsp/init.lua`) manages:
- LSP client lifecycle via `vim.lsp.start()`
- FileType autocmd for lazy loading
- blink.cmp integration
- Workspace scanning for UDTs/DBs
## Requirements
- **Neovim 0.9+**
- **nvim-lspconfig** - LSP client
- **Neovim 0.11+** (uses `vim.lsp.start()`)
- **nvim-treesitter** - Syntax highlighting
- **blink.cmp** - Optional, for completion
- **Lua 5.1+** - LSP server runtime
+37 -34
View File
@@ -13,51 +13,54 @@ function M.setup(opts)
setup_package_path()
opts = opts or {}
local lspconfig_avail, lspconfig = pcall(require, "lspconfig")
if not lspconfig_avail then
vim.notify("lspconfig not found. Please install nvim-lspconfig.", vim.log.levels.ERROR)
return
end
local server_path = opts.server_path or vim.fn.expand("~/dev/scl_lsp/src/main.lua")
local root_patterns = { ".git", "data_types", "plc.data.json" }
local function get_root_dir(fname)
local root = vim.fs.root(fname, root_patterns)
return root or vim.fn.fnamemodify(fname, ":h")
end
local function on_attach(client, bufnr)
local function start_lsp(bufnr)
local fname = vim.api.nvim_buf_get_name(bufnr)
if fname == "" then return end
local root_dir = vim.fs.root(fname, root_patterns)
or vim.fn.fnamemodify(fname, ":p:h")
vim.lsp.start({
name = "scl_lsp",
cmd = { "lua", server_path },
root_dir = root_dir,
on_attach = function(client, b)
if opts.lsp and opts.lsp.on_attach then
opts.lsp.on_attach(client, bufnr)
opts.lsp.on_attach(client, b)
end
if opts.lsp.formatting then
vim.api.nvim_buf_create_user_command(bufnr, "LspSCLFormat", function()
vim.lsp.buf.format({ name = "scl-language-server" })
if opts.lsp and opts.lsp.formatting then
vim.api.nvim_buf_create_user_command(b, "LspSCLFormat", function()
vim.lsp.buf.format({ name = "scl_lsp" })
end, {})
end
end,
})
end
lspconfig.scl_lsp = {
default_config = {
name = "scl-language-server",
cmd = { "lua", server_path },
filetypes = { "scl" },
root_dir = get_root_dir,
settings = {},
on_attach = on_attach,
},
docs = {
description = "Language server for Siemens SCL (LSP, Linter, Formatter)",
default_config = {
name = "scl-language-server",
cmd = { "lua", server_path },
filetypes = { "scl" },
root_dir = "root_pattern('.git', 'data_types', 'plc.data.json')",
},
},
}
-- Create FileType autocmd for future buffers
vim.api.nvim_create_autocmd("FileType", {
pattern = "scl",
callback = function(args)
start_lsp(args.buf)
end,
})
-- Create global format command (works on current buffer)
vim.api.nvim_create_user_command("LspSCLFormat", function()
vim.lsp.buf.format({ name = "scl_lsp" })
end, {})
-- Start LSP for any existing scl buffers (handles lazy loading case)
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
if vim.bo[bufnr].filetype == "scl" and vim.api.nvim_buf_is_loaded(bufnr) then
start_lsp(bufnr)
end
end
M.setup_syntax(opts)
M.setup_multiline_params()