feat: add devtools (tree-sitter, neovim, formatter, CLI commands)

- Add tree-sitter grammar for syntax highlighting
- Add Neovim plugin with syntax, LSP integration, and commands
- Add code formatter (lux fmt) with check mode
- Add CLI commands: fmt, check, test, watch, init
- Add project initialization with lux.toml template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 10:30:13 -05:00
parent 961a861822
commit f786d18182
15 changed files with 2578 additions and 7 deletions

134
editors/nvim/README.md Normal file
View File

@@ -0,0 +1,134 @@
# Lux Neovim Plugin
Neovim support for the Lux programming language.
## Features
- Syntax highlighting (vim regex and tree-sitter)
- LSP integration (diagnostics, hover, completions, go-to-definition)
- Commands for running, formatting, and testing
- REPL integration
## Installation
### Using lazy.nvim
```lua
{
"your-org/lux",
config = function()
require("lux").setup({
-- Optional: specify path to lux binary
-- lux_binary = "/path/to/lux",
lsp = {
enabled = true,
autostart = true,
},
format = {
on_save = false,
},
})
end,
ft = "lux",
}
```
### Using packer.nvim
```lua
use {
"your-org/lux",
config = function()
require("lux").setup()
end,
ft = "lux",
}
```
### Manual Installation
Copy the contents of this directory to your Neovim config:
```bash
# Copy to nvim config
cp -r editors/nvim/* ~/.config/nvim/
# Or symlink
ln -s /path/to/lux/editors/nvim/ftdetect ~/.config/nvim/ftdetect
ln -s /path/to/lux/editors/nvim/ftplugin ~/.config/nvim/ftplugin
ln -s /path/to/lux/editors/nvim/syntax ~/.config/nvim/syntax
ln -s /path/to/lux/editors/nvim/lua/lux ~/.config/nvim/lua/lux
```
## Tree-sitter Support
For tree-sitter based highlighting, install the grammar:
```lua
-- In your tree-sitter config
require("nvim-treesitter.configs").setup({
ensure_installed = { "lux" },
highlight = { enable = true },
})
-- Register the parser
local parser_config = require("nvim-treesitter.parsers").get_parser_configs()
parser_config.lux = {
install_info = {
url = "/path/to/lux/editors/tree-sitter-lux",
files = { "src/parser.c" },
},
filetype = "lux",
}
```
## Commands
| Command | Description |
|---------|-------------|
| `:LuxRun` | Run the current file |
| `:LuxFormat` | Format the current file |
| `:LuxTest` | Run tests |
| `:LuxRepl` | Start the REPL in a terminal |
## Key Mappings
Default mappings in Lux files (using `<localleader>`):
| Mapping | Action |
|---------|--------|
| `<localleader>r` | Run current file |
| `<localleader>f` | Format current file |
| `<localleader>t` | Run tests |
LSP mappings (when LSP is active):
| Mapping | Action |
|---------|--------|
| `gd` | Go to definition |
| `K` | Hover information |
| `gr` | Find references |
| `<leader>rn` | Rename symbol |
| `<leader>ca` | Code actions |
| `[d` / `]d` | Previous/next diagnostic |
## Configuration
```lua
require("lux").setup({
-- Path to lux binary (searches PATH if not set)
lux_binary = nil,
lsp = {
-- Enable LSP support
enabled = true,
-- Auto-start LSP when opening .lux files
autostart = true,
},
format = {
-- Format on save
on_save = false,
},
})
```

View File

@@ -0,0 +1,141 @@
; Syntax highlighting queries for Lux
; Comments
(line_comment) @comment
(doc_comment) @comment.documentation
; Keywords
[
"fn"
"let"
"type"
"effect"
"handler"
"trait"
"impl"
"for"
"import"
"export"
"match"
"if"
"then"
"else"
"with"
"run"
"resume"
"is"
] @keyword
; Operators
[
"+"
"-"
"*"
"/"
"%"
"=="
"!="
"<"
">"
"<="
">="
"&&"
"||"
"!"
"="
"=>"
"|>"
":"
"|"
"."
","
] @operator
; Punctuation
[
"("
")"
"{"
"}"
"["
"]"
"<"
">"
] @punctuation.bracket
; Literals
(integer) @number
(float) @number.float
(string) @string
(char) @character
(boolean) @boolean
(unit) @constant.builtin
; String interpolation
(interpolation
"{" @punctuation.special
"}" @punctuation.special)
(escape_sequence) @string.escape
; Types
(type_expression (identifier) @type)
(generic_type (identifier) @type)
(type_declaration name: (identifier) @type.definition)
(type_parameters (identifier) @type.parameter)
; Built-in types
((identifier) @type.builtin
(#match? @type.builtin "^(Int|Float|Bool|String|Char|Unit|Option|Result|List)$"))
; Functions
(function_declaration name: (identifier) @function.definition)
(call_expression function: (identifier) @function.call)
(call_expression function: (member_expression member: (identifier) @function.method))
; Effect operations
(effect_declaration name: (identifier) @type.definition)
(effect_operation name: (identifier) @function.definition)
(handler_declaration name: (identifier) @function.definition)
(handler_declaration effect: (identifier) @type)
(handler_operation name: (identifier) @function.definition)
; Traits
(trait_declaration name: (identifier) @type.definition)
(trait_method name: (identifier) @function.definition)
(impl_declaration trait: (identifier) @type)
; Parameters
(parameter name: (identifier) @variable.parameter)
; Variables
(let_declaration name: (identifier) @variable.definition)
(identifier) @variable
; Patterns
(pattern (identifier) @variable)
(constructor_pattern name: (identifier) @constructor)
(field_pattern name: (identifier) @property)
; Record fields
(field_assignment name: (identifier) @property)
(record_field name: (identifier) @property)
; Member access
(member_expression member: (identifier) @property)
; Modules (capitalized identifiers used as module access)
(member_expression
object: (identifier) @module
(#match? @module "^[A-Z]"))
; Variants/Constructors (capitalized identifiers)
((identifier) @constructor
(#match? @constructor "^[A-Z][a-zA-Z0-9]*$"))
; Special identifiers
((identifier) @constant.builtin
(#match? @constant.builtin "^(None|Some|Ok|Err|true|false)$"))
; Property annotations
(property_clause (identifier) @attribute)

View File

@@ -0,0 +1,22 @@
; Indent rules for Lux
[
(block_expression)
(match_expression)
(if_expression)
(record_expression)
(list_expression)
(effect_declaration)
(handler_declaration)
(trait_declaration)
(impl_declaration)
(enum_definition)
] @indent
[
"}"
"]"
")"
] @outdent
(match_arm "=>" @indent)

View File

@@ -0,0 +1,21 @@
; Scopes
(function_declaration) @scope
(block_expression) @scope
(lambda_expression) @scope
(match_arm) @scope
(handler_declaration) @scope
; Definitions
(function_declaration name: (identifier) @definition.function)
(let_declaration name: (identifier) @definition.var)
(parameter name: (identifier) @definition.parameter)
(type_declaration name: (identifier) @definition.type)
(effect_declaration name: (identifier) @definition.type)
(trait_declaration name: (identifier) @definition.type)
(handler_declaration name: (identifier) @definition.function)
; Pattern bindings
(pattern (identifier) @definition.var)
; References
(identifier) @reference

View File

@@ -0,0 +1,2 @@
" File type detection for Lux
autocmd BufRead,BufNewFile *.lux setfiletype lux

View File

@@ -0,0 +1,29 @@
" Lux filetype settings
" Indentation
setlocal expandtab
setlocal shiftwidth=4
setlocal softtabstop=4
setlocal tabstop=4
" Comments
setlocal commentstring=//\ %s
setlocal comments=://
" Folding
setlocal foldmethod=syntax
" Match pairs
setlocal matchpairs+=<:>
" Format options
setlocal formatoptions-=t
setlocal formatoptions+=croql
" Completion
setlocal omnifunc=v:lua.vim.lsp.omnifunc
" Key mappings for Lux development
nnoremap <buffer> <localleader>r :!lux %<CR>
nnoremap <buffer> <localleader>f :!lux fmt %<CR>
nnoremap <buffer> <localleader>t :!lux test<CR>

View File

@@ -0,0 +1,163 @@
-- Lux language support for Neovim
-- Provides LSP configuration and utilities
local M = {}
-- Default configuration
M.config = {
-- Path to lux binary (will search PATH if not set)
lux_binary = nil,
-- Enable LSP
lsp = {
enabled = true,
-- Auto-start LSP when opening .lux files
autostart = true,
},
-- Formatting options
format = {
-- Format on save
on_save = false,
},
}
-- Find lux binary
local function find_lux_binary()
if M.config.lux_binary then
return M.config.lux_binary
end
-- Try to find in PATH
local handle = io.popen("which lux 2>/dev/null")
if handle then
local result = handle:read("*a"):gsub("%s+", "")
handle:close()
if result ~= "" then
return result
end
end
-- Try common locations
local common_paths = {
"./result/bin/lux",
"~/.local/bin/lux",
"/usr/local/bin/lux",
}
for _, path in ipairs(common_paths) do
local expanded = vim.fn.expand(path)
if vim.fn.executable(expanded) == 1 then
return expanded
end
end
return "lux" -- Fallback, hope it's in PATH
end
-- Setup LSP
function M.setup_lsp()
local lux_binary = find_lux_binary()
-- Check if lspconfig is available
local ok, lspconfig = pcall(require, "lspconfig")
if not ok then
vim.notify("lspconfig not found. Install nvim-lspconfig for LSP support.", vim.log.levels.WARN)
return
end
local configs = require("lspconfig.configs")
-- Register lux LSP if not already registered
if not configs.lux_lsp then
configs.lux_lsp = {
default_config = {
cmd = { lux_binary, "lsp" },
filetypes = { "lux" },
root_dir = function(fname)
return lspconfig.util.find_git_ancestor(fname)
or lspconfig.util.root_pattern("lux.toml")(fname)
or vim.fn.getcwd()
end,
settings = {},
},
}
end
-- Setup the LSP
lspconfig.lux_lsp.setup({
on_attach = function(client, bufnr)
-- Enable completion
vim.bo[bufnr].omnifunc = "v:lua.vim.lsp.omnifunc"
-- Key mappings
local opts = { noremap = true, silent = true, buffer = bufnr }
vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
vim.keymap.set("n", "gi", vim.lsp.buf.implementation, opts)
vim.keymap.set("n", "<C-k>", vim.lsp.buf.signature_help, opts)
vim.keymap.set("n", "gr", vim.lsp.buf.references, opts)
vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, opts)
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, opts)
vim.keymap.set("n", "[d", vim.diagnostic.goto_prev, opts)
vim.keymap.set("n", "]d", vim.diagnostic.goto_next, opts)
-- Format on save if enabled
if M.config.format.on_save then
vim.api.nvim_create_autocmd("BufWritePre", {
buffer = bufnr,
callback = function()
vim.lsp.buf.format({ async = false })
end,
})
end
end,
capabilities = vim.lsp.protocol.make_client_capabilities(),
})
end
-- Run current file
function M.run_file()
local file = vim.fn.expand("%:p")
local lux_binary = find_lux_binary()
vim.cmd("!" .. lux_binary .. " " .. vim.fn.shellescape(file))
end
-- Format current file
function M.format_file()
local file = vim.fn.expand("%:p")
local lux_binary = find_lux_binary()
vim.cmd("!" .. lux_binary .. " fmt " .. vim.fn.shellescape(file))
vim.cmd("edit!") -- Reload the file
end
-- Run tests
function M.run_tests()
local lux_binary = find_lux_binary()
vim.cmd("!" .. lux_binary .. " test")
end
-- Start REPL in terminal
function M.start_repl()
local lux_binary = find_lux_binary()
vim.cmd("terminal " .. lux_binary)
end
-- Setup function
function M.setup(opts)
-- Merge user config
M.config = vim.tbl_deep_extend("force", M.config, opts or {})
-- Setup LSP if enabled
if M.config.lsp.enabled and M.config.lsp.autostart then
vim.api.nvim_create_autocmd("FileType", {
pattern = "lux",
callback = function()
M.setup_lsp()
end,
once = true,
})
end
-- Create user commands
vim.api.nvim_create_user_command("LuxRun", M.run_file, { desc = "Run current Lux file" })
vim.api.nvim_create_user_command("LuxFormat", M.format_file, { desc = "Format current Lux file" })
vim.api.nvim_create_user_command("LuxTest", M.run_tests, { desc = "Run Lux tests" })
vim.api.nvim_create_user_command("LuxRepl", M.start_repl, { desc = "Start Lux REPL" })
end
return M

View File

@@ -0,0 +1,89 @@
" Vim syntax file for Lux
" Language: Lux
" Maintainer: Lux Team
if exists("b:current_syntax")
finish
endif
" Keywords
syn keyword luxKeyword fn let type effect handler trait impl for import export
syn keyword luxKeyword match if then else with run resume is
syn keyword luxKeyword nextgroup=luxFunction skipwhite
" Boolean literals
syn keyword luxBoolean true false
" Built-in types
syn keyword luxType Int Float Bool String Char Unit Option Result List
syn keyword luxType Some None Ok Err
" Operators
syn match luxOperator /[+\-*/%=<>!&|]/
syn match luxOperator /==/
syn match luxOperator /!=/
syn match luxOperator /<=/
syn match luxOperator />=/
syn match luxOperator /&&/
syn match luxOperator /||/
syn match luxOperator /|>/
syn match luxOperator /=>/
" Delimiters
syn match luxDelimiter /[(){}\[\],;:]/
syn match luxDelimiter /|/ contained
" Numbers
syn match luxNumber /\<\d\+\>/
syn match luxFloat /\<\d\+\.\d\+\>/
" Strings
syn region luxString start=/"/ skip=/\\./ end=/"/ contains=luxEscape,luxInterpolation
syn match luxEscape /\\[nrt\\'"0{}]/ contained
syn region luxInterpolation start=/{/ end=/}/ contained contains=TOP
" Characters
syn region luxChar start=/'/ skip=/\\./ end=/'/
" Comments
syn match luxComment /\/\/.*$/
syn match luxDocComment /\/\/\/.*$/
" Function definitions
syn match luxFunction /\<[a-z_][a-zA-Z0-9_]*\>/ contained
syn match luxFunctionCall /\<[a-z_][a-zA-Z0-9_]*\>\s*(/me=e-1
" Type definitions and constructors
syn match luxTypedef /\<[A-Z][a-zA-Z0-9_]*\>/
" Module access
syn match luxModule /\<[A-Z][a-zA-Z0-9_]*\>\./me=e-1
" Effect signatures
syn region luxEffectSig start=/with\s*{/ end=/}/ contains=luxType,luxDelimiter
" Properties
syn match luxProperty /\<is\s\+\(pure\|total\|idempotent\|commutative\|associative\)\>/
" Highlight groups
hi def link luxKeyword Keyword
hi def link luxBoolean Boolean
hi def link luxType Type
hi def link luxOperator Operator
hi def link luxDelimiter Delimiter
hi def link luxNumber Number
hi def link luxFloat Float
hi def link luxString String
hi def link luxEscape SpecialChar
hi def link luxInterpolation Special
hi def link luxChar Character
hi def link luxComment Comment
hi def link luxDocComment SpecialComment
hi def link luxFunction Function
hi def link luxFunctionCall Function
hi def link luxTypedef Type
hi def link luxModule Include
hi def link luxEffectSig Special
hi def link luxProperty PreProc
let b:current_syntax = "lux"