From a6eb349d59ea765967742c538a66b4683f11fc13 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 13 Feb 2026 18:23:55 -0500 Subject: [PATCH] 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 --- editors/nvim/README.md | 98 ++++++- editors/nvim/after/queries/lux/folds.scm | 29 ++ .../nvim/after/queries/lux/textobjects.scm | 55 ++++ editors/nvim/lua/lux/init.lua | 263 +++++++++++++++++- editors/vscode/README.md | 152 ++++++++++ editors/vscode/language-configuration.json | 53 ++++ editors/vscode/package.json | 150 ++++++++++ editors/vscode/snippets/lux.json | 182 ++++++++++++ editors/vscode/src/extension.ts | 241 ++++++++++++++++ editors/vscode/syntaxes/lux.tmLanguage.json | 203 ++++++++++++++ editors/vscode/tsconfig.json | 14 + src/compiler.rs | 2 + src/formatter.rs | 1 + src/package.rs | 3 +- 14 files changed, 1437 insertions(+), 9 deletions(-) create mode 100644 editors/nvim/after/queries/lux/folds.scm create mode 100644 editors/nvim/after/queries/lux/textobjects.scm create mode 100644 editors/vscode/README.md create mode 100644 editors/vscode/language-configuration.json create mode 100644 editors/vscode/package.json create mode 100644 editors/vscode/snippets/lux.json create mode 100644 editors/vscode/src/extension.ts create mode 100644 editors/vscode/syntaxes/lux.tmLanguage.json create mode 100644 editors/vscode/tsconfig.json diff --git a/editors/nvim/README.md b/editors/nvim/README.md index 6b293fe..8860a7e 100644 --- a/editors/nvim/README.md +++ b/editors/nvim/README.md @@ -6,8 +6,10 @@ Neovim support for the Lux programming language. - Syntax highlighting (vim regex and tree-sitter) - 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 -- REPL integration ## Installation @@ -23,10 +25,19 @@ Neovim support for the Lux programming language. lsp = { enabled = true, autostart = true, + inlay_hints = false, -- requires neovim 0.10+ }, format = { on_save = false, }, + repl = { + position = "bottom", -- "bottom", "right", or "float" + size = 30, -- percentage of screen + }, + diagnostics = { + virtual_text = true, + update_in_insert = false, + }, }) end, 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/syntax ~/.config/nvim/syntax 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 @@ -69,6 +81,32 @@ For tree-sitter based highlighting, install the grammar: require("nvim-treesitter.configs").setup({ ensure_installed = { "lux" }, 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 @@ -89,7 +127,11 @@ parser_config.lux = { | `:LuxRun` | Run the current file | | `:LuxFormat` | Format the current file | | `:LuxTest` | Run tests | +| `:LuxCheck` | Type check the current file | | `: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 @@ -100,18 +142,56 @@ Default mappings in Lux files (using ``): | `r` | Run current file | | `f` | Format current file | | `t` | Run tests | +| `c` | Type check file | +| `R` | Toggle REPL | +| `l` | Send line to REPL | +| `s` (visual) | Send selection to REPL | LSP mappings (when LSP is active): | Mapping | Action | |---------|--------| | `gd` | Go to definition | +| `gD` | Go to declaration | | `K` | Hover information | | `gr` | Find references | +| `gi` | Go to implementation | | `rn` | Rename symbol | | `ca` | Code actions | +| `e` | Show diagnostics float | +| `q` | Diagnostics to location list | | `[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: +- `l` - Send current line +- Select code visually, then `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 ```lua @@ -124,11 +204,27 @@ require("lux").setup({ enabled = true, -- Auto-start LSP when opening .lux files autostart = true, + -- Show inlay hints (requires neovim 0.10+) + inlay_hints = false, }, format = { -- Format on save 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, + }, }) ``` diff --git a/editors/nvim/after/queries/lux/folds.scm b/editors/nvim/after/queries/lux/folds.scm new file mode 100644 index 0000000..d7c4acc --- /dev/null +++ b/editors/nvim/after/queries/lux/folds.scm @@ -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 diff --git a/editors/nvim/after/queries/lux/textobjects.scm b/editors/nvim/after/queries/lux/textobjects.scm new file mode 100644 index 0000000..ae0b27b --- /dev/null +++ b/editors/nvim/after/queries/lux/textobjects.scm @@ -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 . "}") diff --git a/editors/nvim/lua/lux/init.lua b/editors/nvim/lua/lux/init.lua index 0e9d116..e5c0e2c 100644 --- a/editors/nvim/lua/lux/init.lua +++ b/editors/nvim/lua/lux/init.lua @@ -1,5 +1,5 @@ -- Lux language support for Neovim --- Provides LSP configuration and utilities +-- Provides LSP configuration, REPL integration, and utilities local M = {} @@ -12,12 +12,35 @@ M.config = { enabled = true, -- Auto-start LSP when opening .lux files autostart = true, + -- Show inline hints (requires neovim 0.10+) + inlay_hints = false, }, -- Formatting options format = { -- Format on save 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 @@ -37,6 +60,8 @@ local function find_lux_binary() -- Try common locations local common_paths = { "./result/bin/lux", + "./target/release/lux", + "./target/debug/lux", "~/.local/bin/lux", "/usr/local/bin/lux", } @@ -70,7 +95,7 @@ function M.setup_lsp() filetypes = { "lux" }, root_dir = function(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() end, settings = {}, @@ -87,6 +112,7 @@ function M.setup_lsp() -- Key mappings 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.declaration, 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", "", vim.lsp.buf.signature_help, opts) @@ -95,6 +121,13 @@ function M.setup_lsp() vim.keymap.set("n", "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) + vim.keymap.set("n", "e", vim.diagnostic.open_float, opts) + vim.keymap.set("n", "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 if M.config.format.on_save then @@ -110,31 +143,213 @@ function M.setup_lsp() }) 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 function M.run_file() local file = vim.fn.expand("%:p") 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 -- 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 + + -- 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.notify("Formatted", vim.log.levels.INFO) + end end -- Run tests function M.run_tests() 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 -- Start REPL in terminal function M.start_repl() 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 -- Setup function @@ -142,6 +357,9 @@ function M.setup(opts) -- Merge user config M.config = vim.tbl_deep_extend("force", M.config, opts or {}) + -- Setup diagnostics + setup_diagnostics() + -- Setup LSP if enabled if M.config.lsp.enabled and M.config.lsp.autostart then 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("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("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", "r", M.run_file, buf_opts) + vim.keymap.set("n", "f", M.format_file, buf_opts) + vim.keymap.set("n", "t", M.run_tests, buf_opts) + vim.keymap.set("n", "c", M.check_file, buf_opts) + + -- REPL commands + vim.keymap.set("n", "R", M.toggle_repl, buf_opts) + vim.keymap.set("n", "l", M.send_line_to_repl, buf_opts) + vim.keymap.set("v", "s", M.send_selection_to_repl, buf_opts) + end, + }) end return M diff --git a/editors/vscode/README.md b/editors/vscode/README.md new file mode 100644 index 0000000..10900a5 --- /dev/null +++ b/editors/vscode/README.md @@ -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. diff --git a/editors/vscode/language-configuration.json b/editors/vscode/language-configuration.json new file mode 100644 index 0000000..1284f9e --- /dev/null +++ b/editors/vscode/language-configuration.json @@ -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" } + } + ] +} diff --git a/editors/vscode/package.json b/editors/vscode/package.json new file mode 100644 index 0000000..9fc694c --- /dev/null +++ b/editors/vscode/package.json @@ -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" + } +} diff --git a/editors/vscode/snippets/lux.json b/editors/vscode/snippets/lux.json new file mode 100644 index 0000000..a754cac --- /dev/null +++ b/editors/vscode/snippets/lux.json @@ -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" + } +} diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts new file mode 100644 index 0000000..9fe4cd8 --- /dev/null +++ b/editors/vscode/src/extension.ts @@ -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 | 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('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 { + 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(); + } +} diff --git a/editors/vscode/syntaxes/lux.tmLanguage.json b/editors/vscode/syntaxes/lux.tmLanguage.json new file mode 100644 index 0000000..9952243 --- /dev/null +++ b/editors/vscode/syntaxes/lux.tmLanguage.json @@ -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" + } + ] + } + } +} diff --git a/editors/vscode/tsconfig.json b/editors/vscode/tsconfig.json new file mode 100644 index 0000000..ab7159e --- /dev/null +++ b/editors/vscode/tsconfig.json @@ -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"] +} diff --git a/src/compiler.rs b/src/compiler.rs index b8e0687..82a2c3c 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -3,6 +3,8 @@ //! This module compiles Lux programs to native machine code using Cranelift. //! 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 cranelift_codegen::ir::{AbiParam, InstBuilder, Value, types}; use cranelift_codegen::ir::condcodes::IntCC; diff --git a/src/formatter.rs b/src/formatter.rs index 67ff5e3..3dbde07 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -12,6 +12,7 @@ use crate::parser::Parser; /// Formatter configuration #[derive(Debug, Clone)] +#[allow(dead_code)] pub struct FormatConfig { /// Number of spaces for indentation pub indent_size: usize, diff --git a/src/package.rs b/src/package.rs index d8f320c..1a141e5 100644 --- a/src/package.rs +++ b/src/package.rs @@ -161,7 +161,7 @@ impl PackageManager { println!("Installing {} dependencies...", manifest.dependencies.len()); println!(); - for (name, dep) in &manifest.dependencies { + for (_name, dep) in &manifest.dependencies { self.install_dependency(dep)?; } @@ -351,6 +351,7 @@ impl PackageManager { } /// Get the search paths for installed packages + #[allow(dead_code)] pub fn get_package_paths(&self) -> Vec { let mut paths = vec![];