Merge branch 'feat/scl_formatter' into develop
This commit is contained in:
@@ -1,59 +1,63 @@
|
|||||||
# AGENTS.md - SCL Language Server (Unified)
|
# AGENTS.md - SCL Language Server
|
||||||
|
|
||||||
A unified Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion.
|
A unified Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion.
|
||||||
|
|
||||||
## Reference Project for Testing
|
|
||||||
|
|
||||||
For manual testing and SCL/UDT/DB file examples, use:
|
|
||||||
```
|
|
||||||
~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build/Lint/Test Commands
|
## Build/Lint/Test Commands
|
||||||
|
|
||||||
### LSP Server
|
|
||||||
```bash
|
```bash
|
||||||
|
# LSP Server
|
||||||
make start # Start LSP server
|
make start # Start LSP server
|
||||||
make test # Run LSP in test mode
|
make test # Run LSP in test mode
|
||||||
lua src/main.lua --test # Direct test mode execution
|
lua src/main.lua --test # Direct test execution
|
||||||
```
|
|
||||||
|
|
||||||
### Tree-sitter Parser
|
# Tree-sitter Parser
|
||||||
```bash
|
|
||||||
tree-sitter generate # Generate parser from grammar.js
|
tree-sitter generate # Generate parser from grammar.js
|
||||||
tree-sitter test # Run tree-sitter tests
|
tree-sitter test # Run parser tests
|
||||||
tree-sitter parse <file.scl> # Parse a single SCL file
|
tree-sitter parse <file.scl> # Parse single SCL file
|
||||||
|
|
||||||
|
# Lua Linting
|
||||||
|
luacheck src/ # Lint LSP server code
|
||||||
|
luacheck lua/scl/ # Lint plugin modules
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
scl_lsp/
|
scl_lsp/
|
||||||
├── src/ # Standalone LSP server (uses dofile)
|
├── src/ # LSP server (uses dofile)
|
||||||
│ ├── main.lua # Entry point, JSON-RPC protocol
|
│ ├── main.lua # Entry point, JSON-RPC protocol
|
||||||
│ ├── parser.lua # Regex-based SCL parser
|
│ ├── parser.lua # SCL parser
|
||||||
│ ├── diagnostics.lua # Linter diagnostics
|
│ ├── diagnostics.lua # Linter
|
||||||
│ └── formatter.lua # Document formatter
|
│ ├── formatter.lua # Document formatter
|
||||||
├── lua/scl/ # Neovim plugin modules (uses require)
|
│ ├── treesitter.lua # Tree-sitter integration
|
||||||
│ ├── blink_cmp_source.lua # blink.cmp completion source
|
│ ├── plc_json.lua # External UDT loading
|
||||||
│ ├── udt_parser.lua # UDT (.udt) parser
|
│ └── json.lua # JSON encoder/decoder
|
||||||
│ ├── db_parser.lua # Global DB (.db) parser
|
├── lua/scl/ # Neovim plugin (uses require)
|
||||||
│ ├── fb_parser.lua # Function Block parser
|
│ ├── init.lua # Main plugin setup
|
||||||
│ ├── variables.lua # Local variable extraction
|
│ ├── blink_cmp_source.lua
|
||||||
│ ├── workspace_types.lua # Workspace scanning
|
│ ├── udt_parser.lua # UDT parser
|
||||||
│ └── multiline_params.lua # FB parameter filling
|
│ ├── db_parser.lua # Global DB parser
|
||||||
└── queries/ # Tree-sitter queries
|
│ ├── fb_parser.lua # Function Block parser
|
||||||
|
│ ├── variables.lua # Variable extraction
|
||||||
|
│ ├── workspace_types.lua # Workspace scanning
|
||||||
|
│ ├── multiline_params.lua
|
||||||
|
│ ├── auto_prefix.lua
|
||||||
|
│ └── attr_toggle.lua # Interactive attribute block toggle
|
||||||
|
├── queries/ # Tree-sitter queries
|
||||||
|
│ ├── highlights.scm
|
||||||
|
│ ├── indents.scm
|
||||||
|
│ └── folds.scm
|
||||||
|
└── grammar.js # Tree-sitter grammar
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
|
|
||||||
### Module Pattern
|
### Module Pattern
|
||||||
All modules follow the standard Lua module pattern:
|
|
||||||
```lua
|
```lua
|
||||||
-- Module description comment at top
|
-- File header comment
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
-- Private module-level cache
|
-- Private cache
|
||||||
local cache = {}
|
local cache = {}
|
||||||
|
|
||||||
function M.public_function()
|
function M.public_function()
|
||||||
@@ -66,37 +70,37 @@ return M
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Naming Conventions
|
### Naming Conventions
|
||||||
- Functions/variables: `snake_case` (e.g., `get_udt_members`, `var_types`)
|
- Functions/variables: `snake_case` (e.g., `get_udt_members`)
|
||||||
- Constants: `UPPER_SNAKE_CASE` (e.g., `PROJECT_MARKERS`)
|
- Constants: `UPPER_SNAKE_CASE` (e.g., `PROJECT_MARKERS`)
|
||||||
- Private functions: declare as `local function` before public functions
|
- Private functions: declare as `local function` before public functions
|
||||||
- Boolean variables: prefix with `is_`, `has_` (e.g., `is_udt`, `has_members`)
|
- Booleans: prefix with `is_`, `has_` (e.g., `is_udt`, `has_members`)
|
||||||
|
|
||||||
### Imports
|
### Imports
|
||||||
- `lua/scl/` modules: use `require("scl.module_name")`
|
- `lua/scl/` modules: use `require("scl.module_name")`
|
||||||
- `src/` modules: use `dofile(script_path .. "/module.lua")`
|
- `src/` modules: use `dofile(script_path .. "/module.lua")`
|
||||||
- External dependencies: wrap in `pcall()` for safety
|
- External dependencies: wrap in `pcall()` for safety
|
||||||
|
|
||||||
### Comments
|
### Code Formatting
|
||||||
- Every file must have a header comment explaining its purpose
|
- `lua/scl/`: 2 spaces indentation
|
||||||
- All functions should have a brief comment describing what they do
|
- `src/`: tab-based indentation
|
||||||
- Complex logic requires inline comments explaining the "why"
|
- Max line length: 120 characters
|
||||||
- Non-obvious regex patterns must have explanatory comments
|
- No trailing whitespace
|
||||||
|
|
||||||
### Parser Module API Convention
|
### Parser Module API
|
||||||
All parser modules (udt_parser, db_parser, fb_parser) must implement:
|
All parser modules must implement:
|
||||||
- `parse_*_file(filepath)` - Parse a file and cache result
|
- `parse_*_file(filepath)` - Parse and cache
|
||||||
- `parse_*_content(content, filename)` - Parse string content
|
- `parse_*_content(content, filename)` - Parse string
|
||||||
- `get_*(name)` - Get single cached item by name
|
- `get_*(name)` - Get cached item
|
||||||
- `get_all_*_names()` - Return list of all cached names
|
- `get_all_*_names()` - List all cached names
|
||||||
- `get_*_members(name)` - Get members/fields for a type
|
- `get_*_members(name)` - Get members/fields
|
||||||
- `is_*_type(name)` - Check if name is a known type
|
- `is_*_type(name)` - Check if known type
|
||||||
- `clear_cache()` - Clear all cached data
|
- `clear_cache()` - Clear cached data
|
||||||
- `get_cache_count()` - Return number of cached items
|
- `get_cache_count()` - Return cache size
|
||||||
|
|
||||||
### Cache Management
|
### Cache Management
|
||||||
- Use module-level local tables for caching: `local cache = {}`
|
|
||||||
- Clear caches by iterating and setting to nil, not by reassigning:
|
|
||||||
```lua
|
```lua
|
||||||
|
local cache = {}
|
||||||
|
|
||||||
function M.clear_cache()
|
function M.clear_cache()
|
||||||
for k in pairs(cache) do
|
for k in pairs(cache) do
|
||||||
cache[k] = nil
|
cache[k] = nil
|
||||||
@@ -104,10 +108,22 @@ function M.clear_cache()
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Lua Reserved Keywords
|
||||||
|
|
||||||
|
Cannot use reserved keywords as table keys directly:
|
||||||
|
```lua
|
||||||
|
-- WRONG: syntax error
|
||||||
|
local range = { start = pos1, end = pos2 }
|
||||||
|
|
||||||
|
-- CORRECT: bracket notation
|
||||||
|
local range = { start = pos1, ["end"] = pos2 }
|
||||||
|
```
|
||||||
|
|
||||||
|
Critical for LSP range objects with `end` field.
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
### Return Pattern
|
### Return Pattern
|
||||||
Functions that can fail return `nil, "error message"`:
|
|
||||||
```lua
|
```lua
|
||||||
function M.parse_file(filepath)
|
function M.parse_file(filepath)
|
||||||
local file = io.open(filepath, "r")
|
local file = io.open(filepath, "r")
|
||||||
@@ -120,7 +136,6 @@ end
|
|||||||
```
|
```
|
||||||
|
|
||||||
### External Dependencies
|
### External Dependencies
|
||||||
Always wrap external requires in `pcall()`:
|
|
||||||
```lua
|
```lua
|
||||||
local ok, module = pcall(require, "some_module")
|
local ok, module = pcall(require, "some_module")
|
||||||
if ok and module then
|
if ok and module then
|
||||||
@@ -128,41 +143,187 @@ if ok and module then
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
### Validation
|
## Completion Triggers
|
||||||
Check required parameters early and return sensible defaults:
|
|
||||||
```lua
|
|
||||||
function M.get_members(type_name)
|
|
||||||
if not type_name then
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
-- ... continue
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Completion System
|
|
||||||
|
|
||||||
### Trigger Characters
|
|
||||||
- `#` - Local variables (after BEGIN)
|
- `#` - Local variables (after BEGIN)
|
||||||
- `.` - Member access (UDT/DB members)
|
- `.` - Member access (UDT/DB)
|
||||||
- `"` - Global DB names
|
- `"` - Global DB names
|
||||||
- `(` - FB/Function parameters
|
- `(` - FB/Function parameters
|
||||||
- ` ` (space) - General completion
|
- ` ` (space) - General completion
|
||||||
|
|
||||||
### Active Portion Detection
|
## Formatter Options
|
||||||
When matching patterns in completion, use the "active portion" of the line (after the last operator) to correctly handle expressions like:
|
|
||||||
```scl
|
The formatter supports collapsing verbose attribute blocks:
|
||||||
"DB1".member := "DB2".member
|
|
||||||
|
```lua
|
||||||
|
-- Before formatting:
|
||||||
|
statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int;
|
||||||
|
|
||||||
|
-- After formatting:
|
||||||
|
statCntrPartsIn{...} : Int;
|
||||||
```
|
```
|
||||||
Find the last `:=`, `=>`, `=`, `<>`, `>=`, `<=`, `>`, `<`, `AND`, `OR`, `NOT` and only match patterns after it.
|
|
||||||
|
### Formatter Configuration
|
||||||
|
```lua
|
||||||
|
local options = {
|
||||||
|
insertSpaces = false,
|
||||||
|
tabSize = 1,
|
||||||
|
collapseAttributes = true, -- Enable attribute collapsing (default: true)
|
||||||
|
collapsePatterns = { -- Override default patterns
|
||||||
|
"^%s*{%s*EXTERNAL",
|
||||||
|
"^%s*{ S7_",
|
||||||
|
},
|
||||||
|
extendCollapsePatterns = { -- Add custom patterns
|
||||||
|
"^%s*{%s*CUSTOM",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default Collapse Patterns
|
||||||
|
- `^%s*{%s*EXTERNAL` - Variable attributes: `{EXTERNALACCESSIBLE := 'false'; ...}`
|
||||||
|
- `^%s*{ S7_` - Block attributes: `{ S7_Optimized_Access := 'TRUE' }`
|
||||||
|
- `^%s*{%s*%w+%s*:=` - Generic attributes with assignments
|
||||||
|
|
||||||
|
### Per-Variable Collapse Rules
|
||||||
|
Control collapsing per variable using rules (evaluated before global patterns):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local options = {
|
||||||
|
collapseVariableRules = {
|
||||||
|
-- Collapse attributes for variables starting with "stat"
|
||||||
|
{
|
||||||
|
variablePattern = "^stat",
|
||||||
|
collapse = true,
|
||||||
|
},
|
||||||
|
-- Never collapse for "temp" prefix variables
|
||||||
|
{
|
||||||
|
variablePattern = "^temp",
|
||||||
|
collapse = false,
|
||||||
|
},
|
||||||
|
-- Collapse only if both variable AND attribute match
|
||||||
|
{
|
||||||
|
variablePattern = "^config",
|
||||||
|
attributePattern = "EXTERNAL",
|
||||||
|
collapse = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rule properties:**
|
||||||
|
- `variablePattern` - Lua pattern to match variable name (optional)
|
||||||
|
- `attributePattern` - Lua pattern to match attribute content (optional)
|
||||||
|
- `collapse` - `true` to collapse to `{...}`, `false` to keep expanded
|
||||||
|
|
||||||
|
Rules are checked in order; first match wins.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `:SCLShowVariables` | Show local variables in current file |
|
| `:SCLShowVariables` | Show local variables |
|
||||||
| `:SCLShowWorkspaceTypes` | Show workspace UDTs, FBs, and Global DBs count |
|
| `:SCLShowWorkspaceTypes` | Show workspace types count |
|
||||||
| `:SCLRescanWorkspaceTypes` | Rescan workspace for types and DBs |
|
| `:SCLRescanWorkspaceTypes` | Rescan workspace |
|
||||||
| `:SCLPrefixWord` | Manually prefix current word with `#` |
|
| `:SCLPrefixWord` | Prefix word with `#` |
|
||||||
| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call |
|
| `:SCLMultilineParams` | Fill FB parameters |
|
||||||
| `:LspSCLFormat` | Format current SCL file |
|
| `:LspSCLFormat` | Format SCL file |
|
||||||
| `:SCLGeneratePlcJson` | Generate plc.data.json from data_types/ |
|
| `:SCLToggleAttrBlock` | Toggle attribute block under cursor |
|
||||||
|
| `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer |
|
||||||
|
| `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks |
|
||||||
|
|
||||||
|
## Keybindings (SCL/UDT files)
|
||||||
|
|
||||||
|
### Attribute Block Toggle
|
||||||
|
Interactive expand/collapse of individual `{...}` blocks (uses `<Leader>x` prefix):
|
||||||
|
|
||||||
|
| Key | Mode | Description |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| `<Leader>xa` | Normal/Insert | Toggle block under cursor |
|
||||||
|
| `<Leader>xae` | Normal | Expand all collapsed blocks |
|
||||||
|
| `<Leader>xac` | Normal | Collapse all attribute blocks |
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
1. Place cursor inside any `{...}` block
|
||||||
|
2. Press `<Leader>xa` to toggle between collapsed `{...}` and expanded content
|
||||||
|
3. Original content is stored per-buffer and persists until buffer is closed
|
||||||
|
4. Works on both formatter-collapsed blocks and manually collapsed ones
|
||||||
|
|
||||||
|
### Other Keybindings
|
||||||
|
|
||||||
|
| Key | Mode | Description |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| `<Leader>xa` | Normal/Insert | Toggle attribute block under cursor |
|
||||||
|
| `<Leader>xae` | Normal | Expand all attribute blocks |
|
||||||
|
| `<Leader>xac` | Normal | Collapse all attribute blocks |
|
||||||
|
| `<Space>mp` | Insert | Fill multiline FB parameters |
|
||||||
|
| `<Tab>` | Insert | Jump to next parameter or regular Tab |
|
||||||
|
|
||||||
|
Note: `<Leader>` is typically `\` (backslash) or `<Space>` depending on your configuration.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
**If commands or keybindings don't work:**
|
||||||
|
|
||||||
|
1. Check if setup() was called:
|
||||||
|
```lua
|
||||||
|
:lua print(vim.inspect(require("scl_lsp")))
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Verify commands exist:
|
||||||
|
```vim
|
||||||
|
:command SCLToggleAttrBlock
|
||||||
|
```
|
||||||
|
Should show the command definition.
|
||||||
|
|
||||||
|
3. Check for errors during setup:
|
||||||
|
```lua
|
||||||
|
:lua require("scl_lsp").setup({})
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Verify leader key:
|
||||||
|
```vim
|
||||||
|
:echo mapleader
|
||||||
|
```
|
||||||
|
If empty, your leader is `\` (backslash).
|
||||||
|
|
||||||
|
5. Manual test keybinding:
|
||||||
|
```vim
|
||||||
|
:nmap <Leader>xa
|
||||||
|
```
|
||||||
|
Should show the mapping.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Manual testing using: `~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project`
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Open `.scl` file in Neovim
|
||||||
|
2. Test LSP features (hover, completion, go-to-definition)
|
||||||
|
3. Verify diagnostics
|
||||||
|
4. Test formatting
|
||||||
|
5. Check workspace scanning
|
||||||
|
|
||||||
|
## LSP Implementation Notes
|
||||||
|
|
||||||
|
### Method Name Conversion
|
||||||
|
```lua
|
||||||
|
local method_name = message.method:gsub("/", "_")
|
||||||
|
local handler = handlers[method_name]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request vs Notification
|
||||||
|
```lua
|
||||||
|
if message.id then
|
||||||
|
-- Request: send response
|
||||||
|
return { id = message.id, result = result }
|
||||||
|
end
|
||||||
|
-- Notification: no response needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Script Path Pattern
|
||||||
|
```lua
|
||||||
|
local script_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or ""
|
||||||
|
if script_path == "" then
|
||||||
|
script_path = "."
|
||||||
|
end
|
||||||
|
package.path = package.path .. ";" .. script_path .. "/?.lua"
|
||||||
|
```
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ A comprehensive Neovim/LazyVim plugin providing **Language Server Protocol (LSP)
|
|||||||
| **Workspace UDT Scanner** | Scans project for user-defined types (.udt files) |
|
| **Workspace UDT Scanner** | Scans project for user-defined types (.udt files) |
|
||||||
| **Workspace Global DB Scanner** | Scans project for global data blocks (.db files) |
|
| **Workspace Global DB Scanner** | Scans project for global data blocks (.db files) |
|
||||||
| **blink.cmp Integration** | Smart completion source for blink.cmp |
|
| **blink.cmp Integration** | Smart completion source for blink.cmp |
|
||||||
|
| **Attribute Block Toggle** | Interactive expand/collapse of `{...}` blocks with `<Leader>xa` |
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -116,6 +117,9 @@ require("scl_lsp").setup({
|
|||||||
| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call |
|
| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call |
|
||||||
| `:LspSCLFormat` | Format current SCL file |
|
| `:LspSCLFormat` | Format current SCL file |
|
||||||
| `:SCLGeneratePlcJson` | Generate plc.data.json from data_types/ |
|
| `:SCLGeneratePlcJson` | Generate plc.data.json from data_types/ |
|
||||||
|
| `:SCLToggleAttrBlock` | Toggle attribute block under cursor |
|
||||||
|
| `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer |
|
||||||
|
| `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks |
|
||||||
|
|
||||||
## Keybindings
|
## Keybindings
|
||||||
|
|
||||||
@@ -127,9 +131,32 @@ require("scl_lsp").setup({
|
|||||||
| `<leader>w` | Workspace Symbols |
|
| `<leader>w` | Workspace Symbols |
|
||||||
| `<Space>mp` | Fill multiline FB/Function parameters (insert mode) |
|
| `<Space>mp` | Fill multiline FB/Function parameters (insert mode) |
|
||||||
| `<Tab>` | Jump to next parameter (insert mode) |
|
| `<Tab>` | Jump to next parameter (insert mode) |
|
||||||
|
| `<Leader>xa` | Toggle attribute block under cursor |
|
||||||
|
| `<Leader>xae` | Expand all attribute blocks |
|
||||||
|
| `<Leader>xac` | Collapse all attribute blocks |
|
||||||
| `<leader>fw` | Workspace Symbols (Telescope) |
|
| `<leader>fw` | Workspace Symbols (Telescope) |
|
||||||
| `<leader>ca` | Code Actions |
|
| `<leader>ca` | Code Actions |
|
||||||
|
|
||||||
|
## Attribute Block Toggle
|
||||||
|
|
||||||
|
Interactive expand/collapse of SCL variable attribute blocks:
|
||||||
|
|
||||||
|
```scl
|
||||||
|
-- Before: Verbose attributes
|
||||||
|
statCounter{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int;
|
||||||
|
|
||||||
|
-- After: Collapsed
|
||||||
|
statCounter{...} : Int;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
- Place cursor inside any `{...}` block
|
||||||
|
- Press `<Leader>xa` to toggle expand/collapse
|
||||||
|
- Use `<Leader>xae` to expand all blocks in buffer
|
||||||
|
- Use `<Leader>xac` to collapse all blocks
|
||||||
|
|
||||||
|
The formatter also supports automatic collapsing via `collapseAttributes` option.
|
||||||
|
|
||||||
## External UDT Support
|
## External UDT Support
|
||||||
|
|
||||||
### Option 1: Create `.plc.json`
|
### Option 1: Create `.plc.json`
|
||||||
@@ -253,10 +280,24 @@ myVar.temperature := 25.5;
|
|||||||
END_FUNCTION_BLOCK
|
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
|
## Requirements
|
||||||
|
|
||||||
- **Neovim 0.9+**
|
- **Neovim 0.11+** (uses `vim.lsp.start()`)
|
||||||
- **nvim-lspconfig** - LSP client
|
|
||||||
- **nvim-treesitter** - Syntax highlighting
|
- **nvim-treesitter** - Syntax highlighting
|
||||||
- **blink.cmp** - Optional, for completion
|
- **blink.cmp** - Optional, for completion
|
||||||
- **Lua 5.1+** - LSP server runtime
|
- **Lua 5.1+** - LSP server runtime
|
||||||
|
|||||||
+43
-12
@@ -1,6 +1,21 @@
|
|||||||
-- SCL Diagnostics - Linter functionality for the LSP server
|
-- SCL Diagnostics - Linter functionality for the LSP server
|
||||||
local M = {}
|
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 = {
|
local DiagnosticSeverity = {
|
||||||
Error = 1,
|
Error = 1,
|
||||||
@@ -19,7 +34,7 @@ end
|
|||||||
local function range_to_lsp(start_line, start_char, end_line, end_char)
|
local function range_to_lsp(start_line, start_char, end_line, end_char)
|
||||||
return {
|
return {
|
||||||
start = position_to_lsp(start_line, start_char),
|
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
|
end
|
||||||
|
|
||||||
@@ -83,8 +98,8 @@ function M.validate_diagnostics(content, workspace_types, root_dir)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if in_var_section and trimmed:match("^[%w_]+%s*:") then
|
if in_var_section and (trimmed:match("^[%w_]+%s*%b{}%s*:") or trimmed:match("^[%w_]+%s*:")) then
|
||||||
local var_name = trimmed:match("^([%w_]+)%s*:")
|
local var_name = trimmed:match("^([%w_]+)%s*%b{}%s*:") or trimmed:match("^([%w_]+)%s*:")
|
||||||
local var_start = line:find(var_name, 1, true)
|
local var_start = line:find(var_name, 1, true)
|
||||||
|
|
||||||
if var_name and not var_name:match("^%d") then
|
if var_name and not var_name:match("^%d") then
|
||||||
@@ -99,21 +114,37 @@ function M.validate_diagnostics(content, workspace_types, root_dir)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local type_part = line:match(":%s*([^;]+)")
|
local type_part = nil
|
||||||
|
local brace_depth = 0
|
||||||
|
local last_colon = nil
|
||||||
|
for i = 1, #line do
|
||||||
|
local c = line:sub(i, i)
|
||||||
|
if c == "{" then
|
||||||
|
brace_depth = brace_depth + 1
|
||||||
|
elseif c == "}" then
|
||||||
|
brace_depth = brace_depth - 1
|
||||||
|
elseif c == ":" and brace_depth == 0 then
|
||||||
|
last_colon = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if last_colon then
|
||||||
|
type_part = line:sub(last_colon + 1)
|
||||||
|
end
|
||||||
if type_part then
|
if type_part then
|
||||||
type_part = type_part:gsub("%s*:=.*$", "")
|
type_part = type_part:gsub("%s*:=.*$", "")
|
||||||
type_part = type_part:gsub("%s*$", "")
|
type_part = type_part:gsub("%s*$", "")
|
||||||
type_part = type_part:gsub("^%s+", "")
|
type_part = type_part:gsub("^%s+", "")
|
||||||
|
|
||||||
local base_type = type_part:match("^(%w+)")
|
local base_type = type_part:match("^(%w+)")
|
||||||
|
local upper_type = base_type and base_type:upper() or nil
|
||||||
if base_type and not all_types[base_type] and
|
if base_type and not all_types[base_type] and
|
||||||
base_type ~= "BOOL" and base_type ~= "INT" and base_type ~= "DINT" and
|
upper_type ~= "BOOL" and upper_type ~= "INT" and upper_type ~= "DINT" and
|
||||||
base_type ~= "REAL" and base_type ~= "LREAL" and base_type ~= "BYTE" and
|
upper_type ~= "REAL" and upper_type ~= "LREAL" and upper_type ~= "BYTE" and
|
||||||
base_type ~= "WORD" and base_type ~= "DWORD" and base_type ~= "LWORD" and
|
upper_type ~= "WORD" and upper_type ~= "DWORD" and upper_type ~= "LWORD" and
|
||||||
base_type ~= "CHAR" and base_type ~= "STRING" and base_type ~= "WCHAR" and
|
upper_type ~= "CHAR" and upper_type ~= "STRING" and upper_type ~= "WCHAR" and
|
||||||
base_type ~= "WSTRING" and base_type ~= "TIME" and base_type ~= "DATE" and
|
upper_type ~= "WSTRING" and upper_type ~= "TIME" and upper_type ~= "DATE" and
|
||||||
base_type ~= "TIME_OF_DAY" and base_type ~= "DATE_AND_TIME" and
|
upper_type ~= "TIME_OF_DAY" and upper_type ~= "DATE_AND_TIME" and
|
||||||
base_type ~= "S5TIME" and base_type ~= "LTIME" then
|
upper_type ~= "S5TIME" and upper_type ~= "LTIME" then
|
||||||
table.insert(diagnostics, {
|
table.insert(diagnostics, {
|
||||||
range = range_to_lsp(line_num, 0, line_num, #line),
|
range = range_to_lsp(line_num, 0, line_num, #line),
|
||||||
severity = DiagnosticSeverity.Warning,
|
severity = DiagnosticSeverity.Warning,
|
||||||
|
|||||||
+497
-61
@@ -1,6 +1,18 @@
|
|||||||
-- SCL Formatter - Document formatting for the LSP server
|
-- SCL Formatter - Document formatting for the LSP server
|
||||||
|
-- Stack-based implementation for proper nested structure handling
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
-- Default patterns for attribute blocks to collapse
|
||||||
|
-- Users can extend these via options.collapse_patterns
|
||||||
|
local DEFAULT_COLLAPSE_PATTERNS = {
|
||||||
|
-- Match S7 variable attributes: {EXTERNALACCESSIBLE := 'false'; ...}
|
||||||
|
"^%s*{%s*EXTERNAL",
|
||||||
|
-- Match S7 block attributes: { S7_Optimized_Access := 'TRUE' } or {S7_...}
|
||||||
|
"^%s*{%s*S7_",
|
||||||
|
-- Match generic attribute blocks with assignments
|
||||||
|
"^%s*{%s*%w+%s*:=",
|
||||||
|
}
|
||||||
|
|
||||||
local function position(line, character)
|
local function position(line, character)
|
||||||
return { line = line, character = character }
|
return { line = line, character = character }
|
||||||
end
|
end
|
||||||
@@ -8,86 +20,496 @@ end
|
|||||||
local function range(start_line, start_char, end_line, end_char)
|
local function range(start_line, start_char, end_line, end_char)
|
||||||
return {
|
return {
|
||||||
start = position(start_line, start_char),
|
start = position(start_line, start_char),
|
||||||
end = position(end_line, end_char),
|
["end"] = position(end_line, end_char),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Block types with their corresponding enders
|
||||||
|
local BLOCKS = {
|
||||||
|
-- Control structures
|
||||||
|
["IF"] = "END_IF",
|
||||||
|
["FOR"] = "END_FOR",
|
||||||
|
["WHILE"] = "END_WHILE",
|
||||||
|
["REPEAT"] = "END_REPEAT",
|
||||||
|
["CASE"] = "END_CASE",
|
||||||
|
["REGION"] = "END_REGION",
|
||||||
|
-- Declarations
|
||||||
|
["FUNCTION"] = "END_FUNCTION",
|
||||||
|
["FUNCTION_BLOCK"] = "END_FUNCTION_BLOCK",
|
||||||
|
["ORGANIZATION_BLOCK"] = "END_ORGANIZATION_BLOCK",
|
||||||
|
["TYPE"] = "END_TYPE",
|
||||||
|
["STRUCT"] = "END_STRUCT",
|
||||||
|
-- VAR sections (special handling)
|
||||||
|
["VAR"] = "END_VAR",
|
||||||
|
["VAR_INPUT"] = "END_VAR",
|
||||||
|
["VAR_OUTPUT"] = "END_VAR",
|
||||||
|
["VAR_IN_OUT"] = "END_VAR",
|
||||||
|
["VAR_TEMP"] = "END_VAR",
|
||||||
|
["VAR_CONSTANT"] = "END_VAR",
|
||||||
|
["VAR_RETAIN"] = "END_VAR",
|
||||||
|
["VAR_NON_RETAIN"] = "END_VAR",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Keywords that don't change indent level themselves (handled specially)
|
||||||
|
local SAME_LEVEL = {
|
||||||
|
["THEN"] = true,
|
||||||
|
["ELSE"] = true,
|
||||||
|
["ELSIF"] = true,
|
||||||
|
["DO"] = true,
|
||||||
|
["OF"] = true,
|
||||||
|
["BEGIN"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Control structure enders that need semicolons
|
||||||
|
local NEEDS_SEMICOLON = {
|
||||||
|
["END_IF"] = true,
|
||||||
|
["END_FOR"] = true,
|
||||||
|
["END_WHILE"] = true,
|
||||||
|
["END_REPEAT"] = true,
|
||||||
|
["END_CASE"] = true,
|
||||||
|
["END_REGION"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- VAR keywords for special handling
|
||||||
|
local VAR_KEYWORDS = {
|
||||||
|
["VAR"] = true,
|
||||||
|
["VAR_INPUT"] = true,
|
||||||
|
["VAR_OUTPUT"] = true,
|
||||||
|
["VAR_IN_OUT"] = true,
|
||||||
|
["VAR_TEMP"] = true,
|
||||||
|
["VAR_CONSTANT"] = true,
|
||||||
|
["VAR_RETAIN"] = true,
|
||||||
|
["VAR_NON_RETAIN"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Declaration keywords (reset indent)
|
||||||
|
local DECLARATIONS = {
|
||||||
|
["FUNCTION"] = true,
|
||||||
|
["FUNCTION_BLOCK"] = true,
|
||||||
|
["ORGANIZATION_BLOCK"] = true,
|
||||||
|
["TYPE"] = true,
|
||||||
|
["STRUCT"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
function M.format_document(content, options)
|
function M.format_document(content, options)
|
||||||
options = options or {}
|
options = options or {}
|
||||||
local indent_size = options.indentSize or 4
|
local use_spaces = options.insertSpaces or false
|
||||||
local indent_char = options.insertSpaces and " " or "\t"
|
local indent_size = options.tabSize or 1
|
||||||
|
local indent_char = use_spaces and string.rep(" ", indent_size) or "\t"
|
||||||
|
|
||||||
|
-- Attribute block collapsing options
|
||||||
|
local collapse_attributes = options.collapseAttributes
|
||||||
|
if collapse_attributes == nil then
|
||||||
|
collapse_attributes = true -- Default: enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
-- User can provide custom patterns or extend defaults
|
||||||
|
local collapse_patterns = options.collapsePatterns or DEFAULT_COLLAPSE_PATTERNS
|
||||||
|
if options.extendCollapsePatterns then
|
||||||
|
-- Extend defaults with user patterns
|
||||||
|
for _, pattern in ipairs(options.extendCollapsePatterns) do
|
||||||
|
table.insert(collapse_patterns, pattern)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse content into lines
|
||||||
local lines = {}
|
local lines = {}
|
||||||
for line in content:gmatch("([^\n]*)\n") do
|
for line in content:gmatch("([^\n]*)\n") do
|
||||||
table.insert(lines, line)
|
table.insert(lines, line)
|
||||||
end
|
end
|
||||||
if #lines == 0 or content:sub(-1) ~= "\n" then
|
local last_line = content:match("([^\n]+)$")
|
||||||
table.insert(lines, "")
|
if last_line and last_line ~= "" then
|
||||||
|
table.insert(lines, last_line)
|
||||||
|
end
|
||||||
|
if #lines == 0 then
|
||||||
|
return { { range = range(0, 0, 0, 0), newText = "" } }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Stack of active blocks
|
||||||
|
-- Each entry: { type = "IF", indent = 1, is_case = false }
|
||||||
|
local stack = {}
|
||||||
local formatted_lines = {}
|
local formatted_lines = {}
|
||||||
local indent_level = 0
|
|
||||||
|
|
||||||
|
-- FB call state
|
||||||
|
local in_fb_call = false
|
||||||
|
local fb_name_length = 0
|
||||||
|
local fb_call_first_line = false
|
||||||
|
local fb_paren_offset = 0 -- Offset to align parameters with opening paren
|
||||||
|
|
||||||
|
-- Helper: get current indent string
|
||||||
local function get_indent()
|
local function get_indent()
|
||||||
return string.rep(indent_char, indent_level * indent_size)
|
local level = 0
|
||||||
end
|
for _, block in ipairs(stack) do
|
||||||
|
if not block.no_indent then
|
||||||
local function is_block_keyword(line)
|
level = level + 1
|
||||||
local kw = line:match('^%s*(%w+)')
|
|
||||||
if not kw then return false end
|
|
||||||
local block_keywords = {
|
|
||||||
IF = true, ELSIF = true, ELSE = true,
|
|
||||||
FOR = true, WHILE = true, REPEAT = true,
|
|
||||||
CASE = true,
|
|
||||||
FUNCTION = true, FUNCTION_BLOCK = true, ORGANIZATION_BLOCK = true,
|
|
||||||
TYPE = true, STRUCT = true,
|
|
||||||
}
|
|
||||||
return block_keywords[kw]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_end_block(line)
|
|
||||||
local kw = line:match('^%s*(%w+)')
|
|
||||||
if not kw then return false end
|
|
||||||
local end_keywords = {
|
|
||||||
END_IF = true, END_FOR = true, END_WHILE = true, END_REPEAT = true,
|
|
||||||
END_CASE = true, END_FUNCTION = true, END_FUNCTION_BLOCK = true,
|
|
||||||
END_ORGANIZATION_BLOCK = true, END_TYPE = true, END_STRUCT = true,
|
|
||||||
}
|
|
||||||
return end_keywords[kw]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function process_line(line)
|
|
||||||
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
|
||||||
|
|
||||||
if trimmed == "" or trimmed:match("^//") or trimmed:match("^%(%*") then
|
|
||||||
table.insert(formatted_lines, line)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if is_end_block(trimmed) then
|
|
||||||
indent_level = math.max(0, indent_level - 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
local formatted_line = get_indent() .. trimmed
|
|
||||||
table.insert(formatted_lines, formatted_line)
|
|
||||||
|
|
||||||
if is_block_keyword(trimmed) and not is_end_block(trimmed) then
|
|
||||||
if not trimmed:match("CASE%s+.+OF%s*$") and
|
|
||||||
not trimmed:match("IF%s+.+THEN%s*$") and
|
|
||||||
not trimmed:match("ELSIF%s+.+THEN%s*$") and
|
|
||||||
not trimmed:match("ELSE%s*$") and
|
|
||||||
not trimmed:match("FOR%s+.+DO%s*$") and
|
|
||||||
not trimmed:match("WHILE%s+.+DO%s*$") and
|
|
||||||
not trimmed:match("REPEAT%s*$") then
|
|
||||||
indent_level = indent_level + 1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
return string.rep(indent_char, level)
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, line in ipairs(lines) do
|
-- Helper: get continuation indent for FB calls
|
||||||
process_line(line)
|
local function get_continuation_indent()
|
||||||
|
local base = get_indent()
|
||||||
|
return base .. string.rep(" ", fb_paren_offset)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Helper: check if line is a case label
|
||||||
|
local function is_case_label(trimmed)
|
||||||
|
return trimmed:match("^[%w_#]+%s*:%s*$") ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: extract variable name from a line (before any attribute block)
|
||||||
|
local function extract_variable_name(line)
|
||||||
|
-- Match pattern: variableName{...} or variableName : type
|
||||||
|
-- Variable names can contain letters, numbers, underscores
|
||||||
|
local var_name = line:match("^%s*([%w_]+)%s*[%({:]")
|
||||||
|
return var_name
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: collapse attribute blocks like {EXTERNALACCESSIBLE := 'false'; ...} to {...}
|
||||||
|
local function collapse_attribute_block(line)
|
||||||
|
if not collapse_attributes then
|
||||||
|
return line
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if line contains an attribute block
|
||||||
|
local attr_start, attr_end = line:find("{.-}")
|
||||||
|
if not attr_start then
|
||||||
|
return line
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Extract the content before the attribute block
|
||||||
|
local before_attr = line:sub(1, attr_start - 1)
|
||||||
|
local attr_content = line:sub(attr_start, attr_end)
|
||||||
|
local after_attr = line:sub(attr_end + 1)
|
||||||
|
|
||||||
|
-- Extract variable name for per-variable rules
|
||||||
|
local var_name = extract_variable_name(line)
|
||||||
|
|
||||||
|
-- Check per-variable collapse rules first (highest priority)
|
||||||
|
if options.collapseVariableRules and var_name then
|
||||||
|
for _, rule in ipairs(options.collapseVariableRules) do
|
||||||
|
local var_pattern = rule.variablePattern
|
||||||
|
local attr_pattern = rule.attributePattern
|
||||||
|
local should_collapse = rule.collapse
|
||||||
|
|
||||||
|
-- Check if variable name matches
|
||||||
|
local var_matches = not var_pattern or var_name:match(var_pattern)
|
||||||
|
|
||||||
|
-- Check if attribute content matches (if specified)
|
||||||
|
local attr_matches = not attr_pattern or attr_content:match(attr_pattern)
|
||||||
|
|
||||||
|
if var_matches and attr_matches then
|
||||||
|
if should_collapse then
|
||||||
|
return before_attr .. "{...}" .. after_attr
|
||||||
|
else
|
||||||
|
-- Explicitly don't collapse this match
|
||||||
|
return line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if this attribute block matches any global collapse pattern
|
||||||
|
for _, pattern in ipairs(collapse_patterns) do
|
||||||
|
if attr_content:match(pattern) then
|
||||||
|
-- Collapse to {...}
|
||||||
|
return before_attr .. "{...}" .. after_attr
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return line
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: check if line starts FB call
|
||||||
|
local function is_fb_call_start(line)
|
||||||
|
local trimmed = line:gsub("^%s+", "")
|
||||||
|
if not (trimmed:match("^#%w+") or trimmed:match('^"')) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local paren_pos = trimmed:find("%(")
|
||||||
|
if not paren_pos then return false end
|
||||||
|
local assign_pos = trimmed:find(":=")
|
||||||
|
if assign_pos and assign_pos < paren_pos then return false end
|
||||||
|
local open_count, close_count = 0, 0
|
||||||
|
for i = 1, #trimmed do
|
||||||
|
local c = trimmed:sub(i, i)
|
||||||
|
if c == "(" then open_count = open_count + 1
|
||||||
|
elseif c == ")" then close_count = close_count + 1 end
|
||||||
|
end
|
||||||
|
return open_count > 0 and close_count < open_count
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: check if FB call ends on this line
|
||||||
|
local function fb_call_has_closing(line)
|
||||||
|
local open_count, close_count = 0, 0
|
||||||
|
for i = 1, #line do
|
||||||
|
local c = line:sub(i, i)
|
||||||
|
if c == "(" then open_count = open_count + 1
|
||||||
|
elseif c == ")" then close_count = close_count + 1 end
|
||||||
|
end
|
||||||
|
-- FB call ends when we have closes > opens, not when they're equal
|
||||||
|
-- (equal means no parens on this line, which shouldn't end the call)
|
||||||
|
return close_count > 0 and close_count >= open_count
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: check if line ends with comma (FB call continuation)
|
||||||
|
local function is_continuation(line)
|
||||||
|
return line:gsub("^%s+", ""):gsub("%s+$", ""):match(".*,$") ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: extract keyword from line
|
||||||
|
local function get_keyword(trimmed)
|
||||||
|
return trimmed:match("^(%w+_%w+)") or trimmed:match("^(%w+)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: find case block in stack
|
||||||
|
local function find_case_block()
|
||||||
|
for i = #stack, 1, -1 do
|
||||||
|
if stack[i].type == "CASE" then
|
||||||
|
return stack[i], i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: pop blocks until matching ender found
|
||||||
|
local function pop_to_ender(ender_type)
|
||||||
|
while #stack > 0 do
|
||||||
|
local top = stack[#stack]
|
||||||
|
table.remove(stack)
|
||||||
|
local expected_ender = BLOCKS[top.type]
|
||||||
|
if expected_ender == ender_type then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Process each line
|
||||||
|
for line_num, line in ipairs(lines) do
|
||||||
|
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
local kw = get_keyword(trimmed)
|
||||||
|
|
||||||
|
-- Handle empty lines (preserve as-is)
|
||||||
|
if trimmed == "" then
|
||||||
|
table.insert(formatted_lines, "")
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle comments (re-indent with current level)
|
||||||
|
if trimmed:match("^//") or trimmed:match("^%(") then
|
||||||
|
-- Apply attribute block collapsing to comments too (for consistency)
|
||||||
|
local collapsed = collapse_attribute_block(trimmed)
|
||||||
|
table.insert(formatted_lines, get_indent() .. collapsed)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Apply attribute block collapsing
|
||||||
|
trimmed = collapse_attribute_block(trimmed)
|
||||||
|
|
||||||
|
-- Check for FB call start
|
||||||
|
if not in_fb_call and is_fb_call_start(trimmed) then
|
||||||
|
local fb_name = trimmed:match("^#?([%w_]+)") or trimmed:match('^"?([%w_]+)"?')
|
||||||
|
if fb_name then
|
||||||
|
fb_name_length = #fb_name
|
||||||
|
in_fb_call = true
|
||||||
|
fb_call_first_line = true
|
||||||
|
-- Calculate paren offset: find position of opening paren + 1
|
||||||
|
-- This aligns continuation lines with the content inside the parens
|
||||||
|
local paren_pos = trimmed:find("%(")
|
||||||
|
if paren_pos then
|
||||||
|
fb_paren_offset = paren_pos + 1 -- +1 to align with content after paren
|
||||||
|
else
|
||||||
|
fb_paren_offset = fb_name_length + 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle FB call lines
|
||||||
|
if in_fb_call then
|
||||||
|
local is_first = fb_call_first_line
|
||||||
|
local has_closing = fb_call_has_closing(trimmed)
|
||||||
|
local is_cont = is_continuation(trimmed)
|
||||||
|
|
||||||
|
local formatted
|
||||||
|
if has_closing then
|
||||||
|
formatted = (is_first and get_indent() or get_continuation_indent()) .. trimmed
|
||||||
|
in_fb_call = false
|
||||||
|
fb_call_first_line = false
|
||||||
|
elseif is_cont then
|
||||||
|
formatted = is_first and (get_indent() .. trimmed) or (get_continuation_indent() .. trimmed)
|
||||||
|
fb_call_first_line = false
|
||||||
|
else
|
||||||
|
formatted = get_continuation_indent() .. trimmed
|
||||||
|
end
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle block declarations (FUNCTION_BLOCK, etc.)
|
||||||
|
if DECLARATIONS[kw] then
|
||||||
|
-- Clear stack for new block
|
||||||
|
stack = {}
|
||||||
|
table.insert(formatted_lines, trimmed)
|
||||||
|
table.insert(stack, { type = kw, indent = 0, is_declaration = true })
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle declaration enders
|
||||||
|
if kw and not BLOCKS[kw] then
|
||||||
|
for starter, ender in pairs(BLOCKS) do
|
||||||
|
if DECLARATIONS[starter] and kw == ender then
|
||||||
|
stack = {}
|
||||||
|
table.insert(formatted_lines, trimmed)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle BEGIN (transition to code section)
|
||||||
|
if kw == "BEGIN" then
|
||||||
|
-- Pop any VAR blocks
|
||||||
|
while #stack > 0 and VAR_KEYWORDS[stack[#stack].type] do
|
||||||
|
table.remove(stack)
|
||||||
|
end
|
||||||
|
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||||
|
-- BEGIN itself doesn't add indent, but code inside does
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle VAR section start
|
||||||
|
if VAR_KEYWORDS[kw] then
|
||||||
|
-- Pop to declaration level
|
||||||
|
while #stack > 0 and not stack[#stack].is_declaration do
|
||||||
|
table.remove(stack)
|
||||||
|
end
|
||||||
|
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||||
|
table.insert(stack, { type = kw, indent = #stack, is_var = true })
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle END_VAR
|
||||||
|
if kw == "END_VAR" then
|
||||||
|
-- Pop VAR blocks
|
||||||
|
while #stack > 0 and VAR_KEYWORDS[stack[#stack].type] do
|
||||||
|
table.remove(stack)
|
||||||
|
end
|
||||||
|
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle block enders (END_IF, END_FOR, etc.)
|
||||||
|
if kw and BLOCKS[kw] == nil then
|
||||||
|
-- Check if it's an ender
|
||||||
|
for starter, ender in pairs(BLOCKS) do
|
||||||
|
if kw == ender and not DECLARATIONS[starter] then
|
||||||
|
-- Pop matching block
|
||||||
|
pop_to_ender(kw)
|
||||||
|
-- Add semicolon if needed
|
||||||
|
local formatted = get_indent() .. trimmed
|
||||||
|
if NEEDS_SEMICOLON[kw] and trimmed:sub(-1) ~= ";" then
|
||||||
|
formatted = formatted .. ";"
|
||||||
|
end
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle same-level keywords (THEN, ELSE, ELSIF)
|
||||||
|
if SAME_LEVEL[kw] then
|
||||||
|
local formatted
|
||||||
|
if kw == "THEN" then
|
||||||
|
-- Pop IF block temporarily to get correct indent
|
||||||
|
local if_block = nil
|
||||||
|
for i = #stack, 1, -1 do
|
||||||
|
if stack[i].type == "IF" then
|
||||||
|
if_block = stack[i]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if if_block then
|
||||||
|
formatted = string.rep(indent_char, if_block.indent) .. trimmed
|
||||||
|
else
|
||||||
|
formatted = get_indent() .. trimmed
|
||||||
|
end
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
-- Push content block after THEN
|
||||||
|
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
||||||
|
elseif kw == "ELSE" or kw == "ELSIF" then
|
||||||
|
-- Pop THEN_CONTENT if present
|
||||||
|
if #stack > 0 and stack[#stack].type == "THEN_CONTENT" then
|
||||||
|
table.remove(stack)
|
||||||
|
end
|
||||||
|
-- Find IF block for correct indent
|
||||||
|
local if_block = nil
|
||||||
|
for i = #stack, 1, -1 do
|
||||||
|
if stack[i].type == "IF" then
|
||||||
|
if_block = stack[i]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if if_block then
|
||||||
|
formatted = string.rep(indent_char, if_block.indent) .. trimmed
|
||||||
|
else
|
||||||
|
formatted = get_indent() .. trimmed
|
||||||
|
end
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
-- Push new content block
|
||||||
|
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
||||||
|
else
|
||||||
|
formatted = get_indent() .. trimmed
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
end
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle case labels
|
||||||
|
if is_case_label(trimmed) then
|
||||||
|
local case_block, case_idx = find_case_block()
|
||||||
|
if case_block then
|
||||||
|
-- Pop to just after CASE
|
||||||
|
while #stack > case_idx do
|
||||||
|
table.remove(stack)
|
||||||
|
end
|
||||||
|
-- Case label at CASE level + 1
|
||||||
|
local label_indent = string.rep(indent_char, case_block.indent + 1)
|
||||||
|
table.insert(formatted_lines, label_indent .. trimmed)
|
||||||
|
-- Push case content level
|
||||||
|
table.insert(stack, { type = "CASE_LABEL", indent = case_block.indent + 1, no_indent = true })
|
||||||
|
else
|
||||||
|
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||||
|
end
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle block starters (IF, FOR, CASE, REGION, etc.)
|
||||||
|
if kw and BLOCKS[kw] and not VAR_KEYWORDS[kw] and not DECLARATIONS[kw] then
|
||||||
|
local current_level = 0
|
||||||
|
for _, block in ipairs(stack) do
|
||||||
|
if not block.no_indent then
|
||||||
|
current_level = current_level + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local formatted = string.rep(indent_char, current_level) .. trimmed
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
table.insert(stack, { type = kw, indent = current_level })
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Regular line
|
||||||
|
local formatted = get_indent() .. trimmed
|
||||||
|
-- Add semicolon if it looks like it needs one
|
||||||
|
if NEEDS_SEMICOLON[kw] and trimmed:sub(-1) ~= ";" then
|
||||||
|
formatted = formatted .. ";"
|
||||||
|
end
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
|
||||||
|
::continue::
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Join lines
|
||||||
local result = table.concat(formatted_lines, "\n")
|
local result = table.concat(formatted_lines, "\n")
|
||||||
if content:sub(-1) == "\n" then
|
if content:sub(-1) == "\n" then
|
||||||
result = result .. "\n"
|
result = result .. "\n"
|
||||||
@@ -103,11 +525,25 @@ end
|
|||||||
|
|
||||||
function M.get_formatting_options()
|
function M.get_formatting_options()
|
||||||
return {
|
return {
|
||||||
insertSpaces = true,
|
insertSpaces = false,
|
||||||
tabSize = 4,
|
tabSize = 1,
|
||||||
trimTrailingWhitespace = true,
|
trimTrailingWhitespace = true,
|
||||||
insertFinalNewline = true,
|
insertFinalNewline = true,
|
||||||
trimFinalNewlines = true,
|
trimFinalNewlines = true,
|
||||||
|
collapseAttributes = true, -- Collapse variable attribute blocks to {...}
|
||||||
|
-- collapsePatterns = { ... }, -- Override default collapse patterns
|
||||||
|
-- extendCollapsePatterns = { ... }, -- Extend default patterns with custom ones
|
||||||
|
-- collapseVariableRules = { -- Per-variable collapse rules (highest priority)
|
||||||
|
-- {
|
||||||
|
-- variablePattern = "^stat", -- Match variable names starting with "stat"
|
||||||
|
-- attributePattern = "EXTERNAL", -- Optional: also match attribute content
|
||||||
|
-- collapse = true, -- true to collapse, false to expand
|
||||||
|
-- },
|
||||||
|
-- {
|
||||||
|
-- variablePattern = "^temp", -- Match variables starting with "temp"
|
||||||
|
-- collapse = false, -- Never collapse these
|
||||||
|
-- },
|
||||||
|
-- },
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+91
-70
@@ -13,7 +13,7 @@ end
|
|||||||
|
|
||||||
local function escape(s)
|
local function escape(s)
|
||||||
s = s:gsub("\\", "\\\\")
|
s = s:gsub("\\", "\\\\")
|
||||||
s = s:gsub("\"", "\\\"")
|
s = s:gsub('"', '\\"')
|
||||||
s = s:gsub("\n", "\\n")
|
s = s:gsub("\n", "\\n")
|
||||||
s = s:gsub("\r", "\\r")
|
s = s:gsub("\r", "\\r")
|
||||||
s = s:gsub("\t", "\\t")
|
s = s:gsub("\t", "\\t")
|
||||||
@@ -29,7 +29,7 @@ function json.encode(data)
|
|||||||
elseif t == "number" then
|
elseif t == "number" then
|
||||||
return tostring(data)
|
return tostring(data)
|
||||||
elseif t == "string" then
|
elseif t == "string" then
|
||||||
return "\"" .. escape(data) .. "\""
|
return '"' .. escape(data) .. '"'
|
||||||
elseif t == "table" then
|
elseif t == "table" then
|
||||||
local parts = {}
|
local parts = {}
|
||||||
if is_array(data) then
|
if is_array(data) then
|
||||||
@@ -39,7 +39,7 @@ function json.encode(data)
|
|||||||
return "[" .. table.concat(parts, ",") .. "]"
|
return "[" .. table.concat(parts, ",") .. "]"
|
||||||
else
|
else
|
||||||
for k, v in pairs(data) do
|
for k, v in pairs(data) do
|
||||||
table.insert(parts, "\"" .. escape(k) .. "\":" .. json.encode(v))
|
table.insert(parts, '"' .. escape(k) .. '":' .. json.encode(v))
|
||||||
end
|
end
|
||||||
return "{" .. table.concat(parts, ",") .. "}"
|
return "{" .. table.concat(parts, ",") .. "}"
|
||||||
end
|
end
|
||||||
@@ -49,7 +49,7 @@ function json.encode(data)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function skip_whitespace(s, i)
|
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
|
i = i + 1
|
||||||
end
|
end
|
||||||
return i
|
return i
|
||||||
@@ -60,7 +60,7 @@ local function parse_string(s, i)
|
|||||||
i = i + 1
|
i = i + 1
|
||||||
while i <= #s do
|
while i <= #s do
|
||||||
local c = s:sub(i, i)
|
local c = s:sub(i, i)
|
||||||
if c == "\"" then
|
if c == '"' then
|
||||||
return table.concat(result), i + 1
|
return table.concat(result), i + 1
|
||||||
elseif c == "\\" and i < #s then
|
elseif c == "\\" and i < #s then
|
||||||
local next_c = s:sub(i + 1, i + 1)
|
local next_c = s:sub(i + 1, i + 1)
|
||||||
@@ -70,9 +70,9 @@ local function parse_string(s, i)
|
|||||||
table.insert(result, "\r")
|
table.insert(result, "\r")
|
||||||
elseif next_c == "t" then
|
elseif next_c == "t" then
|
||||||
table.insert(result, "\t")
|
table.insert(result, "\t")
|
||||||
elseif next_c == "\\\"" then
|
elseif next_c == '"' then
|
||||||
table.insert(result, "\"")
|
table.insert(result, '"')
|
||||||
elseif next_c == "\\\\" then
|
elseif next_c == "\\" then
|
||||||
table.insert(result, "\\")
|
table.insert(result, "\\")
|
||||||
else
|
else
|
||||||
table.insert(result, next_c)
|
table.insert(result, next_c)
|
||||||
@@ -91,28 +91,85 @@ local function parse_number(s, i)
|
|||||||
if s:sub(i, i) == "-" then
|
if s:sub(i, i) == "-" then
|
||||||
i = i + 1
|
i = i + 1
|
||||||
end
|
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
|
i = i + 1
|
||||||
end
|
end
|
||||||
if s:sub(i, i) == "." then
|
if s:sub(i, i) == "." then
|
||||||
i = i + 1
|
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
|
i = i + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if s:sub(i, i):find("^[eE]") then
|
if s:sub(i, i):match("[eE]") then
|
||||||
i = i + 1
|
i = i + 1
|
||||||
if s:sub(i, i):find("^[+-]") then
|
if s:sub(i, i):match("[+-]") then
|
||||||
i = i + 1
|
i = i + 1
|
||||||
end
|
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
|
i = i + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return tonumber(s:sub(start, i - 1)), i
|
return tonumber(s:sub(start, i - 1)), i
|
||||||
end
|
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)
|
i = skip_whitespace(s, i)
|
||||||
if i > #s then
|
if i > #s then
|
||||||
return nil, i
|
return nil, i
|
||||||
@@ -121,66 +178,30 @@ local function parse_value(s, i)
|
|||||||
local c = s:sub(i, i)
|
local c = s:sub(i, i)
|
||||||
|
|
||||||
if c == "{" then
|
if c == "{" then
|
||||||
local obj = {}
|
return parse_object(s, i)
|
||||||
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
|
|
||||||
elseif c == "[" then
|
elseif c == "[" then
|
||||||
local arr = {}
|
return parse_array(s, i)
|
||||||
i = i + 1
|
elseif c == '"' then
|
||||||
while i <= #s do
|
return parse_string(s, i)
|
||||||
i = skip_whitespace(s, i)
|
elseif c == "t" then
|
||||||
if s:sub(i, i) == "]" then
|
if s:sub(i, i + 3) == "true" then
|
||||||
return arr, i + 1
|
return true, i + 4
|
||||||
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
|
end
|
||||||
return nil, i
|
return nil, i
|
||||||
elseif c == "\"" then
|
elseif c == "f" then
|
||||||
return parse_string(s, i)
|
if s:sub(i, i + 4) == "false" then
|
||||||
elseif s:sub(i, i + 3) == "null" then
|
return false, i + 5
|
||||||
return nil, i + 4
|
end
|
||||||
elseif s:sub(i, i + 3) == "true" then
|
return nil, i
|
||||||
return true, i + 4
|
elseif c == "n" then
|
||||||
elseif s:sub(i, i + 4) == "false" then
|
if s:sub(i, i + 3) == "null" then
|
||||||
return false, i + 5
|
return nil, i + 4
|
||||||
else
|
end
|
||||||
|
return nil, i
|
||||||
|
elseif c == "-" or c:match("%d") then
|
||||||
return parse_number(s, i)
|
return parse_number(s, i)
|
||||||
|
else
|
||||||
|
return nil, i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+32
-16
@@ -296,7 +296,7 @@ function handlers.textDocument_hover(params)
|
|||||||
contents = { kind = "markdown", value = detail },
|
contents = { kind = "markdown", value = detail },
|
||||||
range = {
|
range = {
|
||||||
start = { line = params.position.line, character = word_start - 1 },
|
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
|
end
|
||||||
@@ -315,7 +315,7 @@ function handlers.textDocument_hover(params)
|
|||||||
contents = { kind = "markdown", value = detail },
|
contents = { kind = "markdown", value = detail },
|
||||||
range = {
|
range = {
|
||||||
start = { line = params.position.line, character = word_start - 1 },
|
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
|
end
|
||||||
@@ -449,7 +449,7 @@ function handlers.textDocument_definition(params)
|
|||||||
uri = uri,
|
uri = uri,
|
||||||
range = {
|
range = {
|
||||||
start = { line = pos.line, character = pos.character },
|
start = { line = pos.line, character = pos.character },
|
||||||
endPos = { line = pos.line, character = pos.character + #word },
|
["end"] = { line = pos.line, character = pos.character + #word },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -478,10 +478,10 @@ function handlers.textDocument_documentSymbol(params)
|
|||||||
table.insert(symbols, {
|
table.insert(symbols, {
|
||||||
name = func.kind:upper() .. " " .. func.name,
|
name = func.kind:upper() .. " " .. func.name,
|
||||||
kind = kind_map[func.kind] or 14,
|
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 = {
|
selectionRange = {
|
||||||
start = { line = func.start, character = 0 },
|
start = { line = func.start, character = 0 },
|
||||||
endPos = { line = func.start, character = #func.name },
|
["end"] = { line = func.start, character = #func.name },
|
||||||
},
|
},
|
||||||
containerName = nil,
|
containerName = nil,
|
||||||
})
|
})
|
||||||
@@ -500,11 +500,11 @@ function handlers.textDocument_documentSymbol(params)
|
|||||||
detail = var_info.type or "VAR",
|
detail = var_info.type or "VAR",
|
||||||
range = {
|
range = {
|
||||||
start = { line = var_info.line, character = 0 },
|
start = { line = var_info.line, character = 0 },
|
||||||
endPos = { line = var_info.line, character = #var_name },
|
["end"] = { line = var_info.line, character = #var_name },
|
||||||
},
|
},
|
||||||
selectionRange = {
|
selectionRange = {
|
||||||
start = { line = var_info.line, character = var_info.character },
|
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,
|
containerName = nil,
|
||||||
})
|
})
|
||||||
@@ -526,11 +526,11 @@ function handlers.textDocument_documentSymbol(params)
|
|||||||
detail = detail,
|
detail = detail,
|
||||||
range = {
|
range = {
|
||||||
start = { line = type_info.start_line or 0, character = 0 },
|
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 = {
|
selectionRange = {
|
||||||
start = { line = type_info.start_line or 0, character = 0 },
|
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,
|
containerName = nil,
|
||||||
})
|
})
|
||||||
@@ -656,6 +656,10 @@ function handlers.textDocument_semanticTokensRange(params)
|
|||||||
return handlers.textDocument_semanticTokensFull(params)
|
return handlers.textDocument_semanticTokensFull(params)
|
||||||
end
|
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)
|
function handlers.textDocument_inlayHint(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -708,7 +712,7 @@ function handlers.workspace_symbol(params)
|
|||||||
uri = uri,
|
uri = uri,
|
||||||
range = {
|
range = {
|
||||||
start = { line = line, character = col },
|
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
|
if message.method == "$/cancelRequest" then
|
||||||
return nil
|
return nil
|
||||||
end
|
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
|
if handler then
|
||||||
local ok, result = pcall(handler, message.params)
|
local ok, result = pcall(handler, message.params)
|
||||||
if ok then
|
-- Only return response for requests (have id), not notifications
|
||||||
return { id = message.id, result = result }
|
if message.id then
|
||||||
else
|
if ok then
|
||||||
return { id = message.id, error = { code = -32603, message = tostring(result) } }
|
return { id = message.id, result = result }
|
||||||
|
else
|
||||||
|
return { id = message.id, error = { code = -32603, message = tostring(result) } }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
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
|
end
|
||||||
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function main()
|
local function main()
|
||||||
@@ -871,6 +884,9 @@ local function main()
|
|||||||
if not line then
|
if not line then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
-- Strip trailing \r (Windows line endings)
|
||||||
|
line = line:gsub("\r$", "")
|
||||||
|
|
||||||
if line:match("^Content%-Length:%s*(%d+)") then
|
if line:match("^Content%-Length:%s*(%d+)") then
|
||||||
content_length = tonumber(line:match("^Content%-Length:%s*(%d+)"))
|
content_length = tonumber(line:match("^Content%-Length:%s*(%d+)"))
|
||||||
elseif line == "" then
|
elseif line == "" then
|
||||||
|
|||||||
+14
-3
@@ -78,15 +78,26 @@ function M.extract_variables(content)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local var_name = trimmed:match("^([%w_]+)%s*:")
|
local var_name = trimmed:match("^([%w_]+)%s*%b{}%s*:") or trimmed:match("^([%w_]+)%s*:")
|
||||||
if not var_name then
|
if not var_name then
|
||||||
var_name = trimmed:match("^([%w_]+)%s*,")
|
var_name = trimmed:match("^([%w_]+)%s*%b{}%s*,") or trimmed:match("^([%w_]+)%s*,")
|
||||||
end
|
end
|
||||||
|
|
||||||
if var_name and not variables[var_name] then
|
if var_name and not variables[var_name] then
|
||||||
local col = line:find(var_name, 1, true)
|
local col = line:find(var_name, 1, true)
|
||||||
if col then
|
if col then
|
||||||
local type_start = line:find(":", 1, true)
|
local type_start = nil
|
||||||
|
local brace_depth = 0
|
||||||
|
for i = 1, #line do
|
||||||
|
local c = line:sub(i, i)
|
||||||
|
if c == "{" then
|
||||||
|
brace_depth = brace_depth + 1
|
||||||
|
elseif c == "}" then
|
||||||
|
brace_depth = brace_depth - 1
|
||||||
|
elseif c == ":" and brace_depth == 0 then
|
||||||
|
type_start = i
|
||||||
|
end
|
||||||
|
end
|
||||||
local var_data_type = "unknown"
|
local var_data_type = "unknown"
|
||||||
if type_start then
|
if type_start then
|
||||||
local type_part = line:sub(type_start + 1)
|
local type_part = line:sub(type_start + 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user