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:
373
src/main.rs
373
src/main.rs
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user