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

View File

@@ -3,6 +3,7 @@
mod ast;
mod diagnostics;
mod exhaustiveness;
mod formatter;
mod interpreter;
mod lexer;
mod lsp;
@@ -77,17 +78,49 @@ fn main() {
}
}
"--help" | "-h" => {
println!("Lux {} - A functional language with first-class effects", VERSION);
println!();
println!("Usage:");
println!(" lux Start the REPL");
println!(" lux <file.lux> Run a file");
println!(" lux --lsp Start LSP server (for IDE integration)");
println!(" lux --help Show this help");
print_help();
}
"--version" | "-v" => {
println!("Lux {}", VERSION);
}
"fmt" => {
// Format files
if args.len() < 3 {
eprintln!("Usage: lux fmt <file.lux> [--check]");
std::process::exit(1);
}
let check_only = args.iter().any(|a| a == "--check");
for arg in &args[2..] {
if arg.starts_with('-') {
continue;
}
format_file(arg, check_only);
}
}
"test" => {
// Run tests
run_tests(&args[2..]);
}
"watch" => {
// Watch mode
if args.len() < 3 {
eprintln!("Usage: lux watch <file.lux>");
std::process::exit(1);
}
watch_file(&args[2]);
}
"init" => {
// Initialize a new project
init_project(args.get(2).map(|s| s.as_str()));
}
"check" => {
// Type check without running
if args.len() < 3 {
eprintln!("Usage: lux check <file.lux>");
std::process::exit(1);
}
check_file(&args[2]);
}
path => {
// Run a file
run_file(path);
@@ -99,6 +132,332 @@ fn main() {
}
}
fn print_help() {
println!("Lux {} - A functional language with first-class effects", VERSION);
println!();
println!("Usage:");
println!(" lux Start the REPL");
println!(" lux <file.lux> Run a file");
println!(" lux fmt <file.lux> Format a file (--check to verify only)");
println!(" lux check <file.lux> Type check without running");
println!(" lux test [pattern] Run tests (optional pattern filter)");
println!(" lux watch <file.lux> Watch and re-run on changes");
println!(" lux init [name] Initialize a new project");
println!(" lux --lsp Start LSP server (for IDE integration)");
println!(" lux --help Show this help");
println!(" lux --version Show version");
}
fn format_file(path: &str, check_only: bool) {
use formatter::{format, FormatConfig};
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file '{}': {}", path, e);
std::process::exit(1);
}
};
let config = FormatConfig::default();
let formatted = match format(&source, &config) {
Ok(f) => f,
Err(e) => {
eprintln!("Error formatting '{}': {}", path, e);
std::process::exit(1);
}
};
if check_only {
if source != formatted {
eprintln!("{} would be reformatted", path);
std::process::exit(1);
} else {
println!("{} is correctly formatted", path);
}
} else {
if source != formatted {
if let Err(e) = std::fs::write(path, &formatted) {
eprintln!("Error writing file '{}': {}", path, e);
std::process::exit(1);
}
println!("Formatted {}", path);
} else {
println!("{} unchanged", path);
}
}
}
fn check_file(path: &str) {
use modules::ModuleLoader;
use std::path::Path;
let file_path = Path::new(path);
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading file '{}': {}", path, e);
std::process::exit(1);
}
};
let mut loader = ModuleLoader::new();
if let Some(parent) = file_path.parent() {
loader.add_search_path(parent.to_path_buf());
}
let program = match loader.load_source(&source, Some(file_path)) {
Ok(p) => p,
Err(e) => {
eprintln!("Module error: {}", e);
std::process::exit(1);
}
};
let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
for error in errors {
let diagnostic = error.to_diagnostic();
eprint!("{}", render(&diagnostic, &source, Some(path)));
}
std::process::exit(1);
}
println!("{}: OK", path);
}
fn run_tests(args: &[String]) {
use std::path::Path;
use std::fs;
// Find test files
let pattern = args.first().map(|s| s.as_str());
// Look for test files in current directory and tests/ subdirectory
let mut test_files = Vec::new();
for entry in fs::read_dir(".").into_iter().flatten().flatten() {
let path = entry.path();
if path.extension().map(|e| e == "lux").unwrap_or(false) {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.starts_with("test_") || name.ends_with("_test.lux") {
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
test_files.push(path);
}
}
}
}
}
if Path::new("tests").is_dir() {
for entry in fs::read_dir("tests").into_iter().flatten().flatten() {
let path = entry.path();
if path.extension().map(|e| e == "lux").unwrap_or(false) {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
test_files.push(path);
}
}
}
}
}
if test_files.is_empty() {
println!("No test files found.");
println!("Test files should be named test_*.lux or *_test.lux");
return;
}
let mut passed = 0;
let mut failed = 0;
for test_file in &test_files {
let path_str = test_file.to_string_lossy().to_string();
print!("Testing {}... ", path_str);
// Run the test file
let result = std::process::Command::new(std::env::current_exe().unwrap())
.arg(&path_str)
.output();
match result {
Ok(output) if output.status.success() => {
println!("OK");
passed += 1;
}
Ok(output) => {
println!("FAILED");
if !output.stderr.is_empty() {
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
}
failed += 1;
}
Err(e) => {
println!("ERROR: {}", e);
failed += 1;
}
}
}
println!();
println!("Results: {} passed, {} failed", passed, failed);
if failed > 0 {
std::process::exit(1);
}
}
fn watch_file(path: &str) {
use std::time::{Duration, SystemTime};
use std::path::Path;
let file_path = Path::new(path);
if !file_path.exists() {
eprintln!("File not found: {}", path);
std::process::exit(1);
}
println!("Watching {} for changes (Ctrl+C to stop)...", path);
println!();
let mut last_modified = SystemTime::UNIX_EPOCH;
loop {
let metadata = match std::fs::metadata(file_path) {
Ok(m) => m,
Err(_) => {
std::thread::sleep(Duration::from_millis(500));
continue;
}
};
let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
if modified > last_modified {
last_modified = modified;
// Clear screen
print!("\x1B[2J\x1B[H");
println!("=== Running {} ===", path);
println!();
// Run the file
let result = std::process::Command::new(std::env::current_exe().unwrap())
.arg(path)
.status();
match result {
Ok(status) if status.success() => {
println!();
println!("=== Success ===");
}
Ok(_) => {
println!();
println!("=== Failed ===");
}
Err(e) => {
eprintln!("Error running file: {}", e);
}
}
println!();
println!("Watching for changes...");
}
std::thread::sleep(Duration::from_millis(500));
}
}
fn init_project(name: Option<&str>) {
use std::fs;
use std::path::Path;
let project_name = name.unwrap_or("my-lux-project");
let project_dir = Path::new(project_name);
if project_dir.exists() {
eprintln!("Directory '{}' already exists", project_name);
std::process::exit(1);
}
// Create project structure
fs::create_dir_all(project_dir.join("src")).unwrap();
fs::create_dir_all(project_dir.join("tests")).unwrap();
// Create lux.toml
let toml_content = format!(
r#"[project]
name = "{}"
version = "0.1.0"
description = "A Lux project"
[dependencies]
# Add dependencies here
# example = "1.0.0"
"#,
project_name
);
fs::write(project_dir.join("lux.toml"), toml_content).unwrap();
// Create main.lux
let main_content = r#"// Main entry point
fn main(): Unit with {Console} = {
Console.print("Hello from Lux!")
}
let output = run main() with {}
"#;
fs::write(project_dir.join("src").join("main.lux"), main_content).unwrap();
// Create a test file
let test_content = r#"// Example test file
fn testAddition(): Bool = {
let result = 2 + 2
result == 4
}
fn main(): Unit with {Console} = {
if testAddition() then
Console.print("Test passed!")
else
Console.print("Test failed!")
}
let output = run main() with {}
"#;
fs::write(project_dir.join("tests").join("test_example.lux"), test_content).unwrap();
// Create .gitignore
let gitignore_content = r#"# Lux build artifacts
/target/
*.luxc
# Editor files
.vscode/
.idea/
*.swp
*~
"#;
fs::write(project_dir.join(".gitignore"), gitignore_content).unwrap();
println!("Created new Lux project: {}", project_name);
println!();
println!("Project structure:");
println!(" {}/", project_name);
println!(" ├── lux.toml");
println!(" ├── src/");
println!(" │ └── main.lux");
println!(" └── tests/");
println!(" └── test_example.lux");
println!();
println!("To get started:");
println!(" cd {}", project_name);
println!(" lux src/main.lux");
}
fn run_file(path: &str) {
use modules::ModuleLoader;
use std::path::Path;