feat: improve editor tooling and fix warnings

Neovim improvements:
- Add tree-sitter text objects for functions, types, blocks
- Add folding support
- Enhanced REPL integration (toggle, send line/selection)
- New commands: LuxCheck, LuxReplToggle, LuxSend
- Better keybindings with localleader

VS Code extension:
- Full syntax highlighting with TextMate grammar
- LSP client integration
- 20+ snippets for common patterns
- Commands: run, format, check, REPL
- Keybindings and context menu

Fixes:
- Fix all cargo warnings with #[allow(dead_code)] annotations
- Clean up unused variables

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 18:23:55 -05:00
parent ebc0bdb109
commit a6eb349d59
14 changed files with 1437 additions and 9 deletions

View File

@@ -6,8 +6,10 @@ Neovim support for the Lux programming language.
- Syntax highlighting (vim regex and tree-sitter) - Syntax highlighting (vim regex and tree-sitter)
- LSP integration (diagnostics, hover, completions, go-to-definition) - LSP integration (diagnostics, hover, completions, go-to-definition)
- Tree-sitter text objects for code navigation
- Code folding
- REPL integration with send-to-REPL
- Commands for running, formatting, and testing - Commands for running, formatting, and testing
- REPL integration
## Installation ## Installation
@@ -23,10 +25,19 @@ Neovim support for the Lux programming language.
lsp = { lsp = {
enabled = true, enabled = true,
autostart = true, autostart = true,
inlay_hints = false, -- requires neovim 0.10+
}, },
format = { format = {
on_save = false, on_save = false,
}, },
repl = {
position = "bottom", -- "bottom", "right", or "float"
size = 30, -- percentage of screen
},
diagnostics = {
virtual_text = true,
update_in_insert = false,
},
}) })
end, end,
ft = "lux", ft = "lux",
@@ -58,6 +69,7 @@ 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/ftplugin ~/.config/nvim/ftplugin
ln -s /path/to/lux/editors/nvim/syntax ~/.config/nvim/syntax ln -s /path/to/lux/editors/nvim/syntax ~/.config/nvim/syntax
ln -s /path/to/lux/editors/nvim/lua/lux ~/.config/nvim/lua/lux ln -s /path/to/lux/editors/nvim/lua/lux ~/.config/nvim/lua/lux
ln -s /path/to/lux/editors/nvim/after ~/.config/nvim/after
``` ```
## Tree-sitter Support ## Tree-sitter Support
@@ -69,6 +81,32 @@ For tree-sitter based highlighting, install the grammar:
require("nvim-treesitter.configs").setup({ require("nvim-treesitter.configs").setup({
ensure_installed = { "lux" }, ensure_installed = { "lux" },
highlight = { enable = true }, highlight = { enable = true },
indent = { enable = true },
-- Enable text objects
textobjects = {
select = {
enable = true,
keymaps = {
["af"] = "@function.outer",
["if"] = "@function.inner",
["ac"] = "@class.outer",
["ic"] = "@class.inner",
["ab"] = "@block.outer",
["ib"] = "@block.inner",
},
},
move = {
enable = true,
goto_next_start = {
["]f"] = "@function.outer",
["]c"] = "@class.outer",
},
goto_previous_start = {
["[f"] = "@function.outer",
["[c"] = "@class.outer",
},
},
},
}) })
-- Register the parser -- Register the parser
@@ -89,7 +127,11 @@ parser_config.lux = {
| `:LuxRun` | Run the current file | | `:LuxRun` | Run the current file |
| `:LuxFormat` | Format the current file | | `:LuxFormat` | Format the current file |
| `:LuxTest` | Run tests | | `:LuxTest` | Run tests |
| `:LuxCheck` | Type check the current file |
| `:LuxRepl` | Start the REPL in a terminal | | `:LuxRepl` | Start the REPL in a terminal |
| `:LuxReplToggle` | Toggle the REPL window |
| `:LuxSendLine` | Send current line to REPL |
| `:'<,'>LuxSend` | Send selection to REPL |
## Key Mappings ## Key Mappings
@@ -100,18 +142,56 @@ Default mappings in Lux files (using `<localleader>`):
| `<localleader>r` | Run current file | | `<localleader>r` | Run current file |
| `<localleader>f` | Format current file | | `<localleader>f` | Format current file |
| `<localleader>t` | Run tests | | `<localleader>t` | Run tests |
| `<localleader>c` | Type check file |
| `<localleader>R` | Toggle REPL |
| `<localleader>l` | Send line to REPL |
| `<localleader>s` (visual) | Send selection to REPL |
LSP mappings (when LSP is active): LSP mappings (when LSP is active):
| Mapping | Action | | Mapping | Action |
|---------|--------| |---------|--------|
| `gd` | Go to definition | | `gd` | Go to definition |
| `gD` | Go to declaration |
| `K` | Hover information | | `K` | Hover information |
| `gr` | Find references | | `gr` | Find references |
| `gi` | Go to implementation |
| `<leader>rn` | Rename symbol | | `<leader>rn` | Rename symbol |
| `<leader>ca` | Code actions | | `<leader>ca` | Code actions |
| `<leader>e` | Show diagnostics float |
| `<leader>q` | Diagnostics to location list |
| `[d` / `]d` | Previous/next diagnostic | | `[d` / `]d` | Previous/next diagnostic |
## REPL Integration
The REPL can be positioned at the bottom, right, or as a floating window:
```lua
require("lux").setup({
repl = {
position = "float", -- floating window
size = 30,
},
})
```
Send code to the running REPL:
- `<localleader>l` - Send current line
- Select code visually, then `<localleader>s` - Send selection
## Text Objects
With `nvim-treesitter-textobjects`, you can use:
| Object | Description |
|--------|-------------|
| `af`/`if` | Function outer/inner |
| `ac`/`ic` | Type/class outer/inner |
| `ab`/`ib` | Block outer/inner |
| `aa`/`ia` | Parameter outer/inner |
Example: `dif` deletes the body of a function, `vaf` selects the entire function.
## Configuration ## Configuration
```lua ```lua
@@ -124,11 +204,27 @@ require("lux").setup({
enabled = true, enabled = true,
-- Auto-start LSP when opening .lux files -- Auto-start LSP when opening .lux files
autostart = true, autostart = true,
-- Show inlay hints (requires neovim 0.10+)
inlay_hints = false,
}, },
format = { format = {
-- Format on save -- Format on save
on_save = false, on_save = false,
}, },
repl = {
-- Position: "bottom", "right", or "float"
position = "bottom",
-- Size as percentage of screen
size = 30,
},
diagnostics = {
-- Show virtual text for diagnostics
virtual_text = true,
-- Update diagnostics in insert mode
update_in_insert = false,
},
}) })
``` ```

View File

@@ -0,0 +1,29 @@
; Folding queries for Lux
; Allows folding function bodies, type definitions, etc.
; Fold function bodies
(function_declaration) @fold
; Fold type definitions
(type_declaration) @fold
; Fold effect declarations
(effect_declaration) @fold
; Fold handler declarations
(handler_declaration) @fold
; Fold trait declarations
(trait_declaration) @fold
; Fold impl blocks
(impl_declaration) @fold
; Fold match expressions
(match_expression) @fold
; Fold block expressions
(block_expression) @fold
; Fold multi-line comments (doc comments)
(doc_comment)+ @fold

View File

@@ -0,0 +1,55 @@
; Tree-sitter text objects for Lux
; Use with nvim-treesitter-textobjects
; Function text objects
(function_declaration) @function.outer
(function_declaration body: (_) @function.inner)
; Class/type text objects (treat type declarations like classes)
(type_declaration) @class.outer
(type_declaration definition: (_) @class.inner)
; Parameter text objects
(parameters) @parameter.outer
(parameter) @parameter.inner
; Block text objects
(block_expression) @block.outer
(block_expression (_) @block.inner)
; Match arm text objects (like case statements)
(match_arm) @statement.outer
(match_arm consequence: (_) @statement.inner)
; Conditional text objects
(if_expression) @conditional.outer
(if_expression consequence: (_) @conditional.inner)
; Comment text objects
(line_comment) @comment.outer
(doc_comment) @comment.outer
; Call text objects
(call_expression) @call.outer
(call_expression arguments: (_) @call.inner)
; Effect/handler text objects (Lux-specific)
(effect_declaration) @effect.outer
(effect_declaration body: (_) @effect.inner)
(handler_declaration) @handler.outer
(handler_declaration body: (_) @handler.inner)
; Loop text objects (using recursion as loops in Lux)
(match_expression) @loop.outer
(match_expression body: (_) @loop.inner)
; Assignment text objects
(let_declaration) @assignment.outer
(let_declaration value: (_) @assignment.inner)
; Attribute/property text objects
(property_clause) @attribute.outer
; Return value (last expression in block)
(block_expression (_) @return.inner . "}")

View File

@@ -1,5 +1,5 @@
-- Lux language support for Neovim -- Lux language support for Neovim
-- Provides LSP configuration and utilities -- Provides LSP configuration, REPL integration, and utilities
local M = {} local M = {}
@@ -12,12 +12,35 @@ M.config = {
enabled = true, enabled = true,
-- Auto-start LSP when opening .lux files -- Auto-start LSP when opening .lux files
autostart = true, autostart = true,
-- Show inline hints (requires neovim 0.10+)
inlay_hints = false,
}, },
-- Formatting options -- Formatting options
format = { format = {
-- Format on save -- Format on save
on_save = false, on_save = false,
}, },
-- REPL options
repl = {
-- Position of REPL window: "bottom", "right", "float"
position = "bottom",
-- Size of REPL window (percentage)
size = 30,
},
-- Diagnostics options
diagnostics = {
-- Show virtual text for diagnostics
virtual_text = true,
-- Update diagnostics in insert mode
update_in_insert = false,
},
}
-- REPL state
M.repl = {
bufnr = nil,
winnr = nil,
jobid = nil,
} }
-- Find lux binary -- Find lux binary
@@ -37,6 +60,8 @@ local function find_lux_binary()
-- Try common locations -- Try common locations
local common_paths = { local common_paths = {
"./result/bin/lux", "./result/bin/lux",
"./target/release/lux",
"./target/debug/lux",
"~/.local/bin/lux", "~/.local/bin/lux",
"/usr/local/bin/lux", "/usr/local/bin/lux",
} }
@@ -70,7 +95,7 @@ function M.setup_lsp()
filetypes = { "lux" }, filetypes = { "lux" },
root_dir = function(fname) root_dir = function(fname)
return lspconfig.util.find_git_ancestor(fname) return lspconfig.util.find_git_ancestor(fname)
or lspconfig.util.root_pattern("lux.toml")(fname) or lspconfig.util.root_pattern("lux.toml", "package.lux")(fname)
or vim.fn.getcwd() or vim.fn.getcwd()
end, end,
settings = {}, settings = {},
@@ -87,6 +112,7 @@ function M.setup_lsp()
-- Key mappings -- Key mappings
local opts = { noremap = true, silent = true, buffer = bufnr } local opts = { noremap = true, silent = true, buffer = bufnr }
vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts) vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
vim.keymap.set("n", "gD", vim.lsp.buf.declaration, opts)
vim.keymap.set("n", "K", vim.lsp.buf.hover, 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", "gi", vim.lsp.buf.implementation, opts)
vim.keymap.set("n", "<C-k>", vim.lsp.buf.signature_help, opts) vim.keymap.set("n", "<C-k>", vim.lsp.buf.signature_help, opts)
@@ -95,6 +121,13 @@ function M.setup_lsp()
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, 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_prev, opts)
vim.keymap.set("n", "]d", vim.diagnostic.goto_next, opts) vim.keymap.set("n", "]d", vim.diagnostic.goto_next, opts)
vim.keymap.set("n", "<leader>e", vim.diagnostic.open_float, opts)
vim.keymap.set("n", "<leader>q", vim.diagnostic.setloclist, opts)
-- Enable inlay hints if supported
if M.config.lsp.inlay_hints and vim.lsp.inlay_hint then
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
end
-- Format on save if enabled -- Format on save if enabled
if M.config.format.on_save then if M.config.format.on_save then
@@ -110,31 +143,213 @@ function M.setup_lsp()
}) })
end end
-- Configure diagnostics display
local function setup_diagnostics()
vim.diagnostic.config({
virtual_text = M.config.diagnostics.virtual_text,
signs = true,
underline = true,
update_in_insert = M.config.diagnostics.update_in_insert,
severity_sort = true,
float = {
border = "rounded",
source = "always",
header = "",
prefix = "",
},
})
-- Custom diagnostic signs
local signs = { Error = " ", Warn = " ", Hint = " ", Info = " " }
for type, icon in pairs(signs) do
local hl = "DiagnosticSign" .. type
vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = "" })
end
end
-- Run current file -- Run current file
function M.run_file() function M.run_file()
local file = vim.fn.expand("%:p") local file = vim.fn.expand("%:p")
local lux_binary = find_lux_binary() local lux_binary = find_lux_binary()
vim.cmd("!" .. lux_binary .. " " .. vim.fn.shellescape(file))
-- Save the file first
vim.cmd("write")
-- Run in a terminal split
vim.cmd("botright split | resize 15")
vim.fn.termopen(lux_binary .. " " .. vim.fn.shellescape(file), {
on_exit = function(_, exit_code, _)
if exit_code == 0 then
vim.notify("Lux: Run completed successfully", vim.log.levels.INFO)
end
end,
})
vim.cmd("startinsert")
end end
-- Format current file -- Format current file
function M.format_file() function M.format_file()
local file = vim.fn.expand("%:p") local file = vim.fn.expand("%:p")
local lux_binary = find_lux_binary() local lux_binary = find_lux_binary()
vim.cmd("!" .. lux_binary .. " fmt " .. vim.fn.shellescape(file))
-- Save first
vim.cmd("write")
-- Format
local output = vim.fn.system(lux_binary .. " fmt " .. vim.fn.shellescape(file))
if vim.v.shell_error ~= 0 then
vim.notify("Format error: " .. output, vim.log.levels.ERROR)
else
vim.cmd("edit!") -- Reload the file vim.cmd("edit!") -- Reload the file
vim.notify("Formatted", vim.log.levels.INFO)
end
end end
-- Run tests -- Run tests
function M.run_tests() function M.run_tests()
local lux_binary = find_lux_binary() local lux_binary = find_lux_binary()
vim.cmd("!" .. lux_binary .. " test") vim.cmd("botright split | resize 15")
vim.fn.termopen(lux_binary .. " test")
vim.cmd("startinsert")
end end
-- Start REPL in terminal -- Start REPL in terminal
function M.start_repl() function M.start_repl()
local lux_binary = find_lux_binary() local lux_binary = find_lux_binary()
vim.cmd("terminal " .. lux_binary)
-- Close existing REPL if open
if M.repl.bufnr and vim.api.nvim_buf_is_valid(M.repl.bufnr) then
vim.api.nvim_buf_delete(M.repl.bufnr, { force = true })
end
-- Create REPL window based on config
local pos = M.config.repl.position
local size = M.config.repl.size
if pos == "float" then
local width = math.floor(vim.o.columns * 0.8)
local height = math.floor(vim.o.lines * 0.8)
local row = math.floor((vim.o.lines - height) / 2)
local col = math.floor((vim.o.columns - width) / 2)
M.repl.bufnr = vim.api.nvim_create_buf(false, true)
M.repl.winnr = vim.api.nvim_open_win(M.repl.bufnr, true, {
relative = "editor",
width = width,
height = height,
row = row,
col = col,
style = "minimal",
border = "rounded",
})
elseif pos == "right" then
vim.cmd("botright vsplit")
vim.cmd("vertical resize " .. math.floor(vim.o.columns * size / 100))
M.repl.winnr = vim.api.nvim_get_current_win()
M.repl.bufnr = vim.api.nvim_get_current_buf()
else -- bottom
vim.cmd("botright split")
vim.cmd("resize " .. math.floor(vim.o.lines * size / 100))
M.repl.winnr = vim.api.nvim_get_current_win()
M.repl.bufnr = vim.api.nvim_get_current_buf()
end
M.repl.jobid = vim.fn.termopen(lux_binary, {
on_exit = function()
M.repl.jobid = nil
M.repl.bufnr = nil
M.repl.winnr = nil
end,
})
vim.cmd("startinsert")
end
-- Send text to REPL
function M.send_to_repl(text)
if not M.repl.jobid then
vim.notify("No REPL running. Start one with :LuxRepl", vim.log.levels.WARN)
return
end
-- Send the text followed by newline
vim.fn.chansend(M.repl.jobid, text .. "\n")
-- Focus the REPL window
if M.repl.winnr and vim.api.nvim_win_is_valid(M.repl.winnr) then
vim.api.nvim_set_current_win(M.repl.winnr)
vim.cmd("startinsert")
end
end
-- Send current line to REPL
function M.send_line_to_repl()
local line = vim.api.nvim_get_current_line()
M.send_to_repl(line)
end
-- Send visual selection to REPL
function M.send_selection_to_repl()
-- Get visual selection
local start_pos = vim.fn.getpos("'<")
local end_pos = vim.fn.getpos("'>")
local lines = vim.api.nvim_buf_get_lines(0, start_pos[2] - 1, end_pos[2], false)
if #lines == 0 then
return
end
-- Trim the selection
if #lines == 1 then
lines[1] = string.sub(lines[1], start_pos[3], end_pos[3])
else
lines[1] = string.sub(lines[1], start_pos[3])
lines[#lines] = string.sub(lines[#lines], 1, end_pos[3])
end
local text = table.concat(lines, "\n")
M.send_to_repl(text)
end
-- Toggle REPL window
function M.toggle_repl()
if M.repl.winnr and vim.api.nvim_win_is_valid(M.repl.winnr) then
vim.api.nvim_win_hide(M.repl.winnr)
M.repl.winnr = nil
elseif M.repl.bufnr and vim.api.nvim_buf_is_valid(M.repl.bufnr) then
-- Reopen existing REPL
local pos = M.config.repl.position
local size = M.config.repl.size
if pos == "right" then
vim.cmd("botright vsplit")
vim.cmd("vertical resize " .. math.floor(vim.o.columns * size / 100))
else
vim.cmd("botright split")
vim.cmd("resize " .. math.floor(vim.o.lines * size / 100))
end
vim.api.nvim_set_current_buf(M.repl.bufnr)
M.repl.winnr = vim.api.nvim_get_current_win()
vim.cmd("startinsert")
else
M.start_repl()
end
end
-- Check current file for errors
function M.check_file()
local file = vim.fn.expand("%:p")
local lux_binary = find_lux_binary()
-- Save first
vim.cmd("write")
local output = vim.fn.system(lux_binary .. " check " .. vim.fn.shellescape(file))
if vim.v.shell_error ~= 0 then
vim.notify("Type check errors:\n" .. output, vim.log.levels.ERROR)
else
vim.notify("No errors found", vim.log.levels.INFO)
end
end end
-- Setup function -- Setup function
@@ -142,6 +357,9 @@ function M.setup(opts)
-- Merge user config -- Merge user config
M.config = vim.tbl_deep_extend("force", M.config, opts or {}) M.config = vim.tbl_deep_extend("force", M.config, opts or {})
-- Setup diagnostics
setup_diagnostics()
-- Setup LSP if enabled -- Setup LSP if enabled
if M.config.lsp.enabled and M.config.lsp.autostart then if M.config.lsp.enabled and M.config.lsp.autostart then
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("FileType", {
@@ -158,6 +376,37 @@ function M.setup(opts)
vim.api.nvim_create_user_command("LuxFormat", M.format_file, { desc = "Format 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("LuxTest", M.run_tests, { desc = "Run Lux tests" })
vim.api.nvim_create_user_command("LuxRepl", M.start_repl, { desc = "Start Lux REPL" }) vim.api.nvim_create_user_command("LuxRepl", M.start_repl, { desc = "Start Lux REPL" })
vim.api.nvim_create_user_command("LuxReplToggle", M.toggle_repl, { desc = "Toggle Lux REPL" })
vim.api.nvim_create_user_command("LuxCheck", M.check_file, { desc = "Type check current Lux file" })
vim.api.nvim_create_user_command("LuxSendLine", M.send_line_to_repl, { desc = "Send line to REPL" })
-- Create command with range for sending selection
vim.api.nvim_create_user_command("LuxSend", function(opts)
if opts.range == 2 then
M.send_selection_to_repl()
else
M.send_line_to_repl()
end
end, { range = true, desc = "Send to REPL" })
-- Setup keymaps for Lux files
vim.api.nvim_create_autocmd("FileType", {
pattern = "lux",
callback = function(ev)
local buf_opts = { noremap = true, silent = true, buffer = ev.buf }
-- Run/format/test commands
vim.keymap.set("n", "<localleader>r", M.run_file, buf_opts)
vim.keymap.set("n", "<localleader>f", M.format_file, buf_opts)
vim.keymap.set("n", "<localleader>t", M.run_tests, buf_opts)
vim.keymap.set("n", "<localleader>c", M.check_file, buf_opts)
-- REPL commands
vim.keymap.set("n", "<localleader>R", M.toggle_repl, buf_opts)
vim.keymap.set("n", "<localleader>l", M.send_line_to_repl, buf_opts)
vim.keymap.set("v", "<localleader>s", M.send_selection_to_repl, buf_opts)
end,
})
end end
return M return M

152
editors/vscode/README.md Normal file
View File

@@ -0,0 +1,152 @@
# Lux Language Extension for VS Code
Visual Studio Code extension for the Lux programming language.
## Features
- **Syntax Highlighting**: Full syntax highlighting for Lux files
- **LSP Integration**: Real-time diagnostics, hover info, completions, go-to-definition
- **Snippets**: Common code patterns (functions, types, effects, handlers)
- **REPL Integration**: Start REPL and send code to it
- **Commands**: Run, format, type-check files
## Installation
### From VS Code Marketplace
Search for "Lux Language" in the VS Code extensions panel.
### From Source
1. Clone the repository
2. Navigate to `editors/vscode`
3. Run `npm install`
4. Run `npm run compile`
5. Press F5 to launch a development VS Code window
### Manual VSIX Install
```bash
cd editors/vscode
npm install
npm run compile
npx vsce package
code --install-extension lux-lang-0.1.0.vsix
```
## Requirements
- **Lux binary**: Install Lux and ensure `lux` is in your PATH, or configure `lux.lspPath`
- **Node.js**: For development
## Configuration
| Setting | Default | Description |
|---------|---------|-------------|
| `lux.lspPath` | `""` | Path to Lux binary. Empty = search PATH |
| `lux.lsp.enabled` | `true` | Enable the language server |
| `lux.format.onSave` | `false` | Format files on save |
| `lux.diagnostics.enabled` | `true` | Show inline diagnostics |
## Commands
| Command | Keybinding | Description |
|---------|------------|-------------|
| `Lux: Run Current File` | `Ctrl+Shift+R` | Run the active file |
| `Lux: Format Document` | - | Format the active file |
| `Lux: Type Check File` | - | Type check without running |
| `Lux: Start REPL` | - | Open Lux REPL in terminal |
| `Lux: Send Selection to REPL` | `Ctrl+Enter` | Send selected code to REPL |
| `Lux: Restart Language Server` | - | Restart the LSP |
## Snippets
| Prefix | Description |
|--------|-------------|
| `fn` | Function definition |
| `fne` | Function with effects |
| `type` | Type definition |
| `effect` | Effect definition |
| `handler` | Effect handler |
| `match` | Match expression |
| `if` | If expression |
| `let` | Let binding |
| `run` | Run with handlers |
| `trait` | Trait definition |
| `impl` | Impl block |
| `matchopt` | Match on Option |
| `matchres` | Match on Result |
| `print` | Console.print |
| `main` | Main function template |
## Language Features
### Syntax Highlighting
- Keywords (`fn`, `let`, `type`, `effect`, `handler`, `match`, etc.)
- Built-in types (`Int`, `Float`, `Bool`, `String`, `Option`, `Result`)
- Operators (`=>`, `|>`, `==`, etc.)
- String interpolation
- Comments (line `//` and doc `///`)
### LSP Features
The Lux language server provides:
- **Diagnostics**: Real-time error and warning display
- **Hover**: Type information on hover
- **Completions**: Context-aware suggestions
- **Go to Definition**: Navigate to definitions
### Code Folding
Fold:
- Function bodies
- Type definitions
- Effect declarations
- Handler blocks
- Match expressions
## Development
```bash
# Install dependencies
npm install
# Compile
npm run compile
# Watch mode
npm run watch
# Lint
npm run lint
# Package
npx vsce package
```
## Troubleshooting
### LSP not starting
1. Check `lux.lspPath` is correct
2. Ensure `lux` binary is executable
3. Run `Lux: Restart Language Server`
4. Check Output panel for errors
### No syntax highlighting
1. Ensure file has `.lux` extension
2. Try reloading VS Code
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make changes
4. Submit a pull request
## License
MIT License - see the main Lux repository for details.

View File

@@ -0,0 +1,53 @@
{
"comments": {
"lineComment": "//",
"blockComment": ["/*", "*/"]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"],
["<", ">"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "<", "close": ">", "notIn": ["string"] },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] }
],
"surroundingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "<", "close": ">" },
{ "open": "\"", "close": "\"" },
{ "open": "'", "close": "'" }
],
"folding": {
"markers": {
"start": "^\\s*//\\s*#?region\\b",
"end": "^\\s*//\\s*#?endregion\\b"
}
},
"wordPattern": "[a-zA-Z_][a-zA-Z0-9_]*",
"indentationRules": {
"increaseIndentPattern": "^.*\\{[^}]*$|^.*\\([^)]*$|^.*\\[[^\\]]*$|^\\s*(fn|type|effect|handler|trait|impl|match|if|else)\\b.*$",
"decreaseIndentPattern": "^\\s*[}\\])]"
},
"onEnterRules": [
{
"beforeText": "^\\s*///.*$",
"action": { "indent": "none", "appendText": "/// " }
},
{
"beforeText": ".*\\{\\s*$",
"action": { "indent": "indent" }
},
{
"beforeText": "^\\s*\\}\\s*$",
"action": { "indent": "outdent" }
}
]
}

150
editors/vscode/package.json Normal file
View File

@@ -0,0 +1,150 @@
{
"name": "lux-lang",
"displayName": "Lux Language",
"description": "Language support for Lux - a functional language with algebraic effects",
"version": "0.1.0",
"publisher": "lux-lang",
"repository": {
"type": "git",
"url": "https://github.com/lux-lang/lux"
},
"engines": {
"vscode": "^1.75.0"
},
"categories": [
"Programming Languages"
],
"keywords": [
"lux",
"functional",
"effects",
"algebraic effects"
],
"activationEvents": [
"onLanguage:lux"
],
"main": "./out/extension.js",
"contributes": {
"languages": [
{
"id": "lux",
"aliases": ["Lux", "lux"],
"extensions": [".lux"],
"configuration": "./language-configuration.json",
"icon": {
"light": "./icons/lux-light.png",
"dark": "./icons/lux-dark.png"
}
}
],
"grammars": [
{
"language": "lux",
"scopeName": "source.lux",
"path": "./syntaxes/lux.tmLanguage.json"
}
],
"configuration": {
"title": "Lux",
"properties": {
"lux.lspPath": {
"type": "string",
"default": "",
"description": "Path to the Lux binary. If empty, searches PATH."
},
"lux.lsp.enabled": {
"type": "boolean",
"default": true,
"description": "Enable the Lux language server"
},
"lux.format.onSave": {
"type": "boolean",
"default": false,
"description": "Format files on save"
},
"lux.diagnostics.enabled": {
"type": "boolean",
"default": true,
"description": "Enable inline diagnostics"
}
}
},
"commands": [
{
"command": "lux.run",
"title": "Lux: Run Current File"
},
{
"command": "lux.format",
"title": "Lux: Format Document"
},
{
"command": "lux.check",
"title": "Lux: Type Check File"
},
{
"command": "lux.startRepl",
"title": "Lux: Start REPL"
},
{
"command": "lux.sendToRepl",
"title": "Lux: Send Selection to REPL"
},
{
"command": "lux.restartLsp",
"title": "Lux: Restart Language Server"
}
],
"keybindings": [
{
"command": "lux.run",
"key": "ctrl+shift+r",
"mac": "cmd+shift+r",
"when": "editorLangId == lux"
},
{
"command": "lux.sendToRepl",
"key": "ctrl+enter",
"mac": "cmd+enter",
"when": "editorLangId == lux && editorHasSelection"
}
],
"menus": {
"editor/context": [
{
"command": "lux.run",
"when": "editorLangId == lux",
"group": "lux"
},
{
"command": "lux.sendToRepl",
"when": "editorLangId == lux && editorHasSelection",
"group": "lux"
}
]
},
"snippets": [
{
"language": "lux",
"path": "./snippets/lux.json"
}
]
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"lint": "eslint src --ext ts"
},
"devDependencies": {
"@types/node": "^18.x",
"@types/vscode": "^1.75.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.0.0",
"typescript": "^5.0.0"
},
"dependencies": {
"vscode-languageclient": "^9.0.0"
}
}

View File

@@ -0,0 +1,182 @@
{
"Function": {
"prefix": "fn",
"body": [
"fn ${1:name}(${2:params}): ${3:ReturnType} =",
" ${0:body}"
],
"description": "Define a function"
},
"Function with Effects": {
"prefix": "fne",
"body": [
"fn ${1:name}(${2:params}): ${3:ReturnType} with {${4:Effects}} = {",
" ${0:body}",
"}"
],
"description": "Define a function with effects"
},
"Type Definition": {
"prefix": "type",
"body": [
"type ${1:Name} =",
" | ${2:Variant}(${3:fields})"
],
"description": "Define an algebraic data type"
},
"Effect Definition": {
"prefix": "effect",
"body": [
"effect ${1:Name} {",
" fn ${2:operation}(${3:params}): ${4:ReturnType}",
"}"
],
"description": "Define an effect"
},
"Handler": {
"prefix": "handler",
"body": [
"handler ${1:name}: ${2:Effect} {",
" fn ${3:operation}(${4:params}) = {",
" ${5:body}",
" resume(${0:value})",
" }",
"}"
],
"description": "Define an effect handler"
},
"Match Expression": {
"prefix": "match",
"body": [
"match ${1:value} {",
" ${2:Pattern} => ${3:result},",
" ${0:_ => default}",
"}"
],
"description": "Pattern matching expression"
},
"If Expression": {
"prefix": "if",
"body": [
"if ${1:condition} then ${2:trueCase}",
"else ${0:falseCase}"
],
"description": "Conditional expression"
},
"Let Binding": {
"prefix": "let",
"body": [
"let ${1:name} = ${0:value}"
],
"description": "Let binding"
},
"Run Expression": {
"prefix": "run",
"body": [
"run ${1:expr} with {",
" ${0:handlers}",
"}"
],
"description": "Run expression with handlers"
},
"Trait Definition": {
"prefix": "trait",
"body": [
"trait ${1:Name}<${2:T}> {",
" fn ${3:method}(${4:self}: ${2:T}): ${5:ReturnType}",
"}"
],
"description": "Define a trait"
},
"Impl Block": {
"prefix": "impl",
"body": [
"impl ${1:Trait} for ${2:Type} {",
" fn ${3:method}(${4:self}): ${5:ReturnType} =",
" ${0:body}",
"}"
],
"description": "Implement a trait"
},
"Option Match": {
"prefix": "matchopt",
"body": [
"match ${1:option} {",
" Some(${2:value}) => ${3:someCase},",
" None => ${0:noneCase}",
"}"
],
"description": "Match on Option"
},
"Result Match": {
"prefix": "matchres",
"body": [
"match ${1:result} {",
" Ok(${2:value}) => ${3:okCase},",
" Err(${4:error}) => ${0:errCase}",
"}"
],
"description": "Match on Result"
},
"Console Print": {
"prefix": "print",
"body": [
"Console.print(${0:\"message\"})"
],
"description": "Print to console"
},
"Main Function": {
"prefix": "main",
"body": [
"fn main(): Unit with {Console} = {",
" ${0:body}",
"}",
"",
"let output = run main() with {}"
],
"description": "Main function template"
},
"Doc Comment": {
"prefix": "///",
"body": [
"/// ${1:Description}",
"///",
"/// # Examples",
"/// ```lux",
"/// ${0:example}",
"/// ```"
],
"description": "Documentation comment"
},
"Record Type": {
"prefix": "record",
"body": [
"type ${1:Name} = {",
" ${2:field}: ${3:Type},",
" ${0:...}",
"}"
],
"description": "Define a record type"
},
"List Map": {
"prefix": "lmap",
"body": [
"List.map(${1:list}, fn(${2:x}: ${3:T}): ${4:U} => ${0:transform})"
],
"description": "Map over a list"
},
"List Filter": {
"prefix": "lfilter",
"body": [
"List.filter(${1:list}, fn(${2:x}: ${3:T}): Bool => ${0:predicate})"
],
"description": "Filter a list"
},
"List Fold": {
"prefix": "lfold",
"body": [
"List.fold(${1:list}, ${2:initial}, fn(${3:acc}: ${4:A}, ${5:x}: ${6:T}): ${4:A} => ${0:combine})"
],
"description": "Fold over a list"
}
}

View File

@@ -0,0 +1,241 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { spawn, ChildProcess } from 'child_process';
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
} from 'vscode-languageclient/node';
let client: LanguageClient | undefined;
let replTerminal: vscode.Terminal | undefined;
export function activate(context: vscode.ExtensionContext) {
console.log('Lux extension activated');
// Start LSP if enabled
const config = vscode.workspace.getConfiguration('lux');
if (config.get('lsp.enabled', true)) {
startLspClient(context);
}
// Register commands
context.subscriptions.push(
vscode.commands.registerCommand('lux.run', runCurrentFile),
vscode.commands.registerCommand('lux.format', formatDocument),
vscode.commands.registerCommand('lux.check', checkFile),
vscode.commands.registerCommand('lux.startRepl', startRepl),
vscode.commands.registerCommand('lux.sendToRepl', sendToRepl),
vscode.commands.registerCommand('lux.restartLsp', () => restartLspClient(context))
);
// Format on save if enabled
if (config.get('format.onSave', false)) {
context.subscriptions.push(
vscode.workspace.onWillSaveTextDocument((event) => {
if (event.document.languageId === 'lux') {
event.waitUntil(formatDocumentAsync(event.document));
}
})
);
}
}
export function deactivate(): Thenable<void> | undefined {
if (replTerminal) {
replTerminal.dispose();
}
if (client) {
return client.stop();
}
return undefined;
}
function getLuxBinary(): string {
const config = vscode.workspace.getConfiguration('lux');
const customPath = config.get<string>('lspPath');
if (customPath && customPath.trim() !== '') {
return customPath;
}
// Try common locations
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const possiblePaths = [
workspaceFolder ? path.join(workspaceFolder, 'result', 'bin', 'lux') : null,
workspaceFolder ? path.join(workspaceFolder, 'target', 'release', 'lux') : null,
workspaceFolder ? path.join(workspaceFolder, 'target', 'debug', 'lux') : null,
].filter(Boolean) as string[];
// Check if any exist
for (const p of possiblePaths) {
try {
require('fs').accessSync(p, require('fs').constants.X_OK);
return p;
} catch {
// Continue to next
}
}
// Fall back to PATH
return 'lux';
}
function startLspClient(context: vscode.ExtensionContext) {
const luxBinary = getLuxBinary();
const serverOptions: ServerOptions = {
command: luxBinary,
args: ['lsp'],
};
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'lux' }],
synchronize: {
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.lux'),
},
};
client = new LanguageClient(
'luxLanguageServer',
'Lux Language Server',
serverOptions,
clientOptions
);
client.start();
}
async function restartLspClient(context: vscode.ExtensionContext) {
if (client) {
await client.stop();
}
startLspClient(context);
vscode.window.showInformationMessage('Lux language server restarted');
}
async function runCurrentFile() {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.languageId !== 'lux') {
vscode.window.showErrorMessage('No Lux file is active');
return;
}
// Save the file first
await editor.document.save();
const filePath = editor.document.uri.fsPath;
const luxBinary = getLuxBinary();
// Create terminal and run
const terminal = vscode.window.createTerminal('Lux Run');
terminal.show();
terminal.sendText(`${luxBinary} "${filePath}"`);
}
async function formatDocument() {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.languageId !== 'lux') {
vscode.window.showErrorMessage('No Lux file is active');
return;
}
await editor.document.save();
const formatted = await formatDocumentAsync(editor.document);
if (formatted.length > 0) {
const edit = new vscode.WorkspaceEdit();
edit.set(editor.document.uri, formatted);
await vscode.workspace.applyEdit(edit);
}
}
async function formatDocumentAsync(document: vscode.TextDocument): Promise<vscode.TextEdit[]> {
const luxBinary = getLuxBinary();
const filePath = document.uri.fsPath;
return new Promise((resolve) => {
const process = spawn(luxBinary, ['fmt', filePath]);
let stdout = '';
let stderr = '';
process.stdout.on('data', (data) => { stdout += data; });
process.stderr.on('data', (data) => { stderr += data; });
process.on('close', (code) => {
if (code === 0) {
// File was formatted, need to reload
resolve([]);
} else {
vscode.window.showErrorMessage(`Format error: ${stderr}`);
resolve([]);
}
});
});
}
async function checkFile() {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.languageId !== 'lux') {
vscode.window.showErrorMessage('No Lux file is active');
return;
}
await editor.document.save();
const filePath = editor.document.uri.fsPath;
const luxBinary = getLuxBinary();
const process = spawn(luxBinary, ['check', filePath]);
let stderr = '';
process.stderr.on('data', (data) => { stderr += data; });
process.on('close', (code) => {
if (code === 0) {
vscode.window.showInformationMessage('No type errors found');
} else {
vscode.window.showErrorMessage(`Type check failed:\n${stderr}`);
}
});
}
function startRepl() {
if (replTerminal) {
replTerminal.dispose();
}
const luxBinary = getLuxBinary();
replTerminal = vscode.window.createTerminal({
name: 'Lux REPL',
shellPath: luxBinary,
});
replTerminal.show();
}
async function sendToRepl() {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active editor');
return;
}
// Get selection or current line
let text: string;
if (editor.selection.isEmpty) {
text = editor.document.lineAt(editor.selection.active.line).text;
} else {
text = editor.document.getText(editor.selection);
}
// Start REPL if not running
if (!replTerminal) {
startRepl();
// Wait a bit for REPL to start
await new Promise(resolve => setTimeout(resolve, 500));
}
if (replTerminal) {
replTerminal.sendText(text);
replTerminal.show();
}
}

View File

@@ -0,0 +1,203 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "Lux",
"scopeName": "source.lux",
"patterns": [
{ "include": "#comments" },
{ "include": "#strings" },
{ "include": "#numbers" },
{ "include": "#keywords" },
{ "include": "#operators" },
{ "include": "#types" },
{ "include": "#functions" },
{ "include": "#variables" }
],
"repository": {
"comments": {
"patterns": [
{
"name": "comment.line.documentation.lux",
"match": "///.*$"
},
{
"name": "comment.line.double-slash.lux",
"match": "//.*$"
},
{
"name": "comment.block.lux",
"begin": "/\\*",
"end": "\\*/"
}
]
},
"strings": {
"patterns": [
{
"name": "string.quoted.double.lux",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.lux",
"match": "\\\\[nrt\\\\\"'0{}]"
},
{
"name": "meta.embedded.interpolation.lux",
"begin": "\\{",
"end": "\\}",
"beginCaptures": {
"0": { "name": "punctuation.section.interpolation.begin.lux" }
},
"endCaptures": {
"0": { "name": "punctuation.section.interpolation.end.lux" }
},
"patterns": [
{ "include": "$self" }
]
}
]
},
{
"name": "string.quoted.single.lux",
"match": "'[^'\\\\]'"
},
{
"name": "string.quoted.single.lux",
"match": "'\\\\[nrt\\\\\"'0]'"
}
]
},
"numbers": {
"patterns": [
{
"name": "constant.numeric.float.lux",
"match": "\\b\\d+\\.\\d+\\b"
},
{
"name": "constant.numeric.hex.lux",
"match": "\\b0x[0-9a-fA-F]+\\b"
},
{
"name": "constant.numeric.binary.lux",
"match": "\\b0b[01]+\\b"
},
{
"name": "constant.numeric.integer.lux",
"match": "\\b\\d+\\b"
}
]
},
"keywords": {
"patterns": [
{
"name": "keyword.control.lux",
"match": "\\b(if|then|else|match|with|run|resume)\\b"
},
{
"name": "keyword.declaration.lux",
"match": "\\b(fn|let|type|effect|handler|trait|impl|for)\\b"
},
{
"name": "keyword.other.lux",
"match": "\\b(import|export|is)\\b"
},
{
"name": "constant.language.boolean.lux",
"match": "\\b(true|false)\\b"
},
{
"name": "constant.language.unit.lux",
"match": "\\(\\)"
},
{
"name": "constant.language.option.lux",
"match": "\\b(None|Some)\\b"
},
{
"name": "constant.language.result.lux",
"match": "\\b(Ok|Err)\\b"
}
]
},
"operators": {
"patterns": [
{
"name": "keyword.operator.arrow.lux",
"match": "=>|->|<-"
},
{
"name": "keyword.operator.pipe.lux",
"match": "\\|>"
},
{
"name": "keyword.operator.comparison.lux",
"match": "==|!=|<=|>=|<|>"
},
{
"name": "keyword.operator.logical.lux",
"match": "&&|\\|\\||!"
},
{
"name": "keyword.operator.arithmetic.lux",
"match": "[+\\-*/%]"
},
{
"name": "keyword.operator.assignment.lux",
"match": "="
},
{
"name": "keyword.operator.type.lux",
"match": ":"
},
{
"name": "punctuation.separator.variant.lux",
"match": "\\|"
}
]
},
"types": {
"patterns": [
{
"name": "support.type.primitive.lux",
"match": "\\b(Int|Float|Bool|String|Char|Unit)\\b"
},
{
"name": "support.type.builtin.lux",
"match": "\\b(Option|Result|List)\\b"
},
{
"name": "entity.name.type.lux",
"match": "\\b[A-Z][a-zA-Z0-9_]*\\b"
}
]
},
"functions": {
"patterns": [
{
"name": "entity.name.function.definition.lux",
"match": "(?<=fn\\s+)[a-z_][a-zA-Z0-9_]*"
},
{
"name": "entity.name.function.call.lux",
"match": "\\b[a-z_][a-zA-Z0-9_]*(?=\\s*\\()"
},
{
"name": "entity.name.function.method.lux",
"match": "(?<=\\.)[a-z_][a-zA-Z0-9_]*(?=\\s*\\()"
}
]
},
"variables": {
"patterns": [
{
"name": "variable.parameter.lux",
"match": "(?<=[(:,]\\s*)[a-z_][a-zA-Z0-9_]*(?=\\s*:)"
},
{
"name": "variable.other.lux",
"match": "\\b[a-z_][a-zA-Z0-9_]*\\b"
}
]
}
}
}

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"outDir": "out",
"lib": ["ES2020"],
"sourceMap": true,
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"exclude": ["node_modules", ".vscode-test"]
}

View File

@@ -3,6 +3,8 @@
//! This module compiles Lux programs to native machine code using Cranelift. //! This module compiles Lux programs to native machine code using Cranelift.
//! Currently supports a subset of the language for performance-critical code. //! Currently supports a subset of the language for performance-critical code.
#![allow(dead_code)]
use crate::ast::{Expr, Program, Declaration, FunctionDecl, BinaryOp, UnaryOp, LiteralKind, Statement}; use crate::ast::{Expr, Program, Declaration, FunctionDecl, BinaryOp, UnaryOp, LiteralKind, Statement};
use cranelift_codegen::ir::{AbiParam, InstBuilder, Value, types}; use cranelift_codegen::ir::{AbiParam, InstBuilder, Value, types};
use cranelift_codegen::ir::condcodes::IntCC; use cranelift_codegen::ir::condcodes::IntCC;

View File

@@ -12,6 +12,7 @@ use crate::parser::Parser;
/// Formatter configuration /// Formatter configuration
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct FormatConfig { pub struct FormatConfig {
/// Number of spaces for indentation /// Number of spaces for indentation
pub indent_size: usize, pub indent_size: usize,

View File

@@ -161,7 +161,7 @@ impl PackageManager {
println!("Installing {} dependencies...", manifest.dependencies.len()); println!("Installing {} dependencies...", manifest.dependencies.len());
println!(); println!();
for (name, dep) in &manifest.dependencies { for (_name, dep) in &manifest.dependencies {
self.install_dependency(dep)?; self.install_dependency(dep)?;
} }
@@ -351,6 +351,7 @@ impl PackageManager {
} }
/// Get the search paths for installed packages /// Get the search paths for installed packages
#[allow(dead_code)]
pub fn get_package_paths(&self) -> Vec<PathBuf> { pub fn get_package_paths(&self) -> Vec<PathBuf> {
let mut paths = vec![]; let mut paths = vec![];