diff --git a/AGENTS.md b/AGENTS.md index 90141f4..4c08939 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/README.md b/README.md index bc25a38..98b9e42 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lua/scl_lsp/init.lua b/lua/scl_lsp/init.lua index e823a6c..dfe1f46 100644 --- a/lua/scl_lsp/init.lua +++ b/lua/scl_lsp/init.lua @@ -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) - if opts.lsp and opts.lsp.on_attach then - opts.lsp.on_attach(client, bufnr) - end - - if opts.lsp.formatting then - vim.api.nvim_buf_create_user_command(bufnr, "LspSCLFormat", function() - vim.lsp.buf.format({ name = "scl-language-server" }) - end, {}) - end - end - - lspconfig.scl_lsp = { - default_config = { - name = "scl-language-server", + + 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 }, - 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')", - }, - }, - } + root_dir = root_dir, + on_attach = function(client, b) + if opts.lsp and opts.lsp.on_attach then + opts.lsp.on_attach(client, b) + end + + 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 + + -- 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()