use crate::types::Cli;
use std::io::{self, BufRead, Write};
pub struct CliRunner {
pub commands: Vec<(String, Cli)>,
pub extension: String,
pub lang_name: String,
}
impl CliRunner {
pub fn new(lang_name: &str, extension: &str) -> Self {
Self {
commands: vec![],
extension: extension.to_string(),
lang_name: lang_name.to_string(),
}
}
pub fn register(&mut self, name: &str, cli: Cli) {
self.commands.push((name.to_string(), cli));
}
pub fn run_with_args(
&self,
args: Vec<String>,
run_source: &dyn Fn(&str) -> Result<(), String>,
run_path: &dyn Fn(&str) -> Result<(), String>,
check_path: &dyn Fn(&str) -> Result<(), String>,
build_path: &dyn Fn(&str) -> Result<String, String>,
lsp_analyzer: &dyn Fn(Option<&str>, &str) -> Result<(), String>,
) {
let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("help");
let file = args.get(2).map(|s| s.as_str()).unwrap_or("");
let lsp_mode = args.iter().any(|a| a == "--lsp");
match cmd {
"lsp" => {
self.run_lsp_server(lsp_analyzer);
}
"help" => self.print_help(),
"version" => println!("{} language toolkit", self.lang_name),
c => {
let found = self.commands.iter().find(|(name, _)| name == c);
match found {
Some((_, Cli::Run)) => {
if file.is_empty() {
eprintln!("Usage: {} run <file>", self.lang_name);
return;
}
if let Err(e) = run_path(file) {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
Some((_, Cli::Repl)) => self.run_repl(run_source),
Some((_, Cli::Check)) => {
if file.is_empty() {
eprintln!("Usage: {} check <file>", self.lang_name);
return;
}
match check_path(file) {
Ok(_) => {
if lsp_mode {
println!("{}", lsp_ok_json(file));
} else {
println!("OK");
}
}
Err(e) => {
if lsp_mode {
println!("{}", lsp_error_json(file, &e));
} else {
eprintln!("Error: {}", e);
}
}
}
}
Some((_, Cli::New)) => {
if file.is_empty() {
eprintln!("Usage: {} new <name>", self.lang_name);
return;
}
std::fs::create_dir_all(file).ok();
std::fs::write(
format!("{}/main{}", file, self.extension),
format!("// {} project\n", file),
)
.ok();
let _ = std::fs::write(
format!("{}/langkit.toml", file),
format!(
"name = \"{}\"\nextension = \"{}\"\nbuild_dir = \"build\"\nlibs_dir = \"libs\"\n",
self.lang_name, self.extension
),
);
println!("Created project '{}'", file);
}
Some((_, Cli::Format)) => println!("Formatter not yet implemented"),
Some((_, Cli::Build)) => {
if file.is_empty() {
eprintln!("Usage: {} build <file>", self.lang_name);
return;
}
match build_path(file) {
Ok(out) => println!("Built {}", out),
Err(e) => eprintln!("Build error: {}", e),
}
}
Some((_, Cli::Update)) => println!("Update not yet implemented"),
Some((_, Cli::Docs)) => println!("Docs not yet implemented"),
Some((_, Cli::Cancel)) => println!("Nothing to cancel"),
Some((_, Cli::Lsp)) => self.run_lsp_server(lsp_analyzer),
Some((_, Cli::Custom(f))) => f(args),
None => eprintln!("Unknown command: '{}'. Run '{} help'", c, self.lang_name),
}
}
}
}
fn run_repl(&self, runner: &dyn Fn(&str) -> Result<(), String>) {
println!("{} REPL - type 'exit' to quit", self.lang_name);
loop {
print!("> ");
io::stdout().flush().ok();
let mut line = String::new();
if io::stdin().read_line(&mut line).is_err() {
break;
}
let line = line.trim();
if line == "exit" || line == "quit" {
break;
}
if line.is_empty() {
continue;
}
if let Err(e) = runner(line) {
eprintln!("Error: {}", e);
}
}
}
fn run_lsp_server(&self, analyzer: &dyn Fn(Option<&str>, &str) -> Result<(), String>) {
let stdin = io::stdin();
let mut reader = io::BufReader::new(stdin.lock());
let mut docs: std::collections::HashMap<String, String> = std::collections::HashMap::new();
loop {
let msg = match read_lsp_message(&mut reader) {
Ok(Some(m)) => m,
Ok(None) => break,
Err(_) => break,
};
let parsed: serde_json::Value = match serde_json::from_str(&msg) {
Ok(v) => v,
Err(_) => continue,
};
let method = parsed.get("method").and_then(|m| m.as_str());
if method.is_none() {
continue;
}
let method = method.unwrap();
match method {
"initialize" => {
if let Some(id) = parsed.get("id") {
let result = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"capabilities": {
"textDocumentSync": 1
}
}
});
send_lsp_message(&result.to_string());
}
}
"shutdown" => {
if let Some(id) = parsed.get("id") {
let result = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"result": null
});
send_lsp_message(&result.to_string());
}
}
"exit" => break,
"textDocument/didOpen" => {
if let Some(params) = parsed.get("params") {
let uri = params
.get("textDocument")
.and_then(|d| d.get("uri"))
.and_then(|u| u.as_str());
let text = params
.get("textDocument")
.and_then(|d| d.get("text"))
.and_then(|t| t.as_str());
if let (Some(uri), Some(text)) = (uri, text) {
docs.insert(uri.to_string(), text.to_string());
publish_diagnostics(uri, text, analyzer);
}
}
}
"textDocument/didChange" => {
if let Some(params) = parsed.get("params") {
let uri = params
.get("textDocument")
.and_then(|d| d.get("uri"))
.and_then(|u| u.as_str());
let text = params
.get("contentChanges")
.and_then(|c| c.get(0))
.and_then(|c| c.get("text"))
.and_then(|t| t.as_str());
if let (Some(uri), Some(text)) = (uri, text) {
docs.insert(uri.to_string(), text.to_string());
publish_diagnostics(uri, text, analyzer);
}
}
}
"textDocument/didSave" => {
if let Some(params) = parsed.get("params") {
let uri = params
.get("textDocument")
.and_then(|d| d.get("uri"))
.and_then(|u| u.as_str());
if let Some(uri) = uri {
if let Some(text) = docs.get(uri) {
publish_diagnostics(uri, text, analyzer);
}
}
}
}
_ => {}
}
}
}
fn print_help(&self) {
println!("Usage: {} <command> [file]\n\nCommands:", self.lang_name);
for (name, _) in &self.commands {
println!(" {}", name);
}
println!(" help\n version\n lsp");
println!("\nOptions:\n --lsp Output LSP-style JSON diagnostics (check only)");
}
}
fn lsp_ok_json(path: &str) -> String {
let uri = path_to_uri(path);
format!(
"{{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/publishDiagnostics\",\"params\":{{\"uri\":\"{}\",\"diagnostics\":[]}}}}",
uri
)
}
fn lsp_error_json(path: &str, err: &str) -> String {
let uri = path_to_uri(path);
let (line, col, msg) = parse_line_col(err);
let msg = json_escape(&msg);
format!(
"{{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/publishDiagnostics\",\"params\":{{\"uri\":\"{}\",\"diagnostics\":[{{\"range\":{{\"start\":{{\"line\":{},\"character\":{}}},\"end\":{{\"line\":{},\"character\":{}}}}},\"severity\":1,\"source\":\"langkit\",\"message\":\"{}\"}}]}}}}",
uri,
line,
col,
line,
col.saturating_add(1),
msg
)
}
fn parse_line_col(err: &str) -> (usize, usize, String) {
let mut line = 0usize;
let mut col = 0usize;
let mut msg = err.to_string();
if let Some(idx) = err.find("Line ") {
let rest = &err[idx + 5..];
let mut parts = rest.splitn(2, ':');
if let Some(line_str) = parts.next() {
if let Ok(l) = line_str.trim().parse::<usize>() {
line = l.saturating_sub(1);
}
}
if let Some(after_line) = parts.next() {
let mut col_parts = after_line.splitn(2, ':');
if let Some(col_str) = col_parts.next() {
if let Ok(c) = col_str.trim().parse::<usize>() {
col = c.saturating_sub(1);
}
}
if let Some(rest_msg) = col_parts.next() {
msg = rest_msg.trim().to_string();
}
}
}
(line, col, msg)
}
fn json_escape(s: &str) -> String {
let mut out = String::new();
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out
}
fn path_to_uri(path: &str) -> String {
let abs = std::fs::canonicalize(path).unwrap_or_else(|_| path.into());
let mut s = abs.to_string_lossy().replace('\\', "/");
if !s.starts_with('/') {
s = format!("/{}", s);
}
format!("file://{}", s)
}
fn read_lsp_message(reader: &mut dyn BufRead) -> io::Result<Option<String>> {
let mut content_length = None;
let mut line = String::new();
loop {
line.clear();
if reader.read_line(&mut line)? == 0 {
return Ok(None);
}
let trimmed = line.trim();
if trimmed.is_empty() {
break;
}
if let Some(rest) = trimmed.strip_prefix("Content-Length:") {
content_length = rest.trim().parse::<usize>().ok();
}
}
let len = match content_length {
Some(l) => l,
None => return Ok(None),
};
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf)?;
Ok(Some(String::from_utf8_lossy(&buf).to_string()))
}
fn send_lsp_message(payload: &str) {
let mut out = io::stdout();
let header = format!("Content-Length: {}\r\n\r\n", payload.as_bytes().len());
let _ = out.write_all(header.as_bytes());
let _ = out.write_all(payload.as_bytes());
let _ = out.flush();
}
fn publish_diagnostics(
uri: &str,
text: &str,
analyzer: &dyn Fn(Option<&str>, &str) -> Result<(), String>,
) {
let path = uri_to_path(uri);
let result = analyzer(path.as_deref(), text);
let diag = match result {
Ok(()) => serde_json::json!([]),
Err(e) => {
let (line, col, msg) = parse_line_col(&e);
serde_json::json!([{
"range": {
"start": { "line": line, "character": col },
"end": { "line": line, "character": col + 1 }
},
"severity": 1,
"source": "langkit",
"message": msg
}])
}
};
let payload = serde_json::json!({
"jsonrpc": "2.0",
"method": "textDocument/publishDiagnostics",
"params": {
"uri": uri,
"diagnostics": diag
}
});
send_lsp_message(&payload.to_string());
}
fn uri_to_path(uri: &str) -> Option<String> {
if let Some(path) = uri.strip_prefix("file://") {
let mut decoded = path.replace("%20", " ");
if decoded.len() > 2 && decoded.as_bytes()[0] == b'/' && decoded.as_bytes()[2] == b':' {
decoded = decoded[1..].to_string();
}
return Some(decoded);
}
None
}