mod lsp;
mod sync;
use clap::{Parser, Subcommand};
use derive_more::{Display, From, Into};
use lsp::LspServerCommand;
use std::{path::PathBuf, str::FromStr};
use tracing::{error, info};
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
#[derive(Clone, Debug, Display, From, Into)]
struct Host(String);
#[derive(Clone, Copy, Debug, Display, From, Into)]
struct Port(u16);
impl Default for Port {
fn default() -> Self {
Self(22)
}
}
#[derive(Clone, Debug, From, Into)]
struct Workspace(PathBuf);
impl Workspace {
fn namespace(&self) -> eyre::Result<Namespace> {
let username = std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.or_else(|_| std::env::var("LOGNAME"))
.unwrap_or("unknown".to_string());
let hostname = std::env::var("HOST")
.or_else(|_| std::env::var("HOSTNAME"))
.unwrap_or("unknown".to_string());
let folder = self
.0
.display()
.to_string()
.replacen("/", "", 1)
.replace("/", "-");
let path = PathBuf::from(format!("{username}@{hostname}/{folder}"));
Ok(Namespace::from(path))
}
fn display(&self) -> std::path::Display<'_> {
self.0.display()
}
}
impl FromStr for Workspace {
type Err = eyre::Error;
fn from_str(s: &str) -> eyre::Result<Self> {
let path = PathBuf::from(s).canonicalize()?;
Ok(Self::from(path))
}
}
#[derive(Clone, Debug, From, Into)]
struct Namespace(PathBuf);
impl Namespace {
fn display(&self) -> std::path::Display<'_> {
self.0.display()
}
}
#[derive(Clone, Debug, Display, From, Into)]
#[display("{host}:{port}")]
struct Remote {
host: Host,
port: Port,
}
impl Remote {
fn host(&self) -> &Host {
&self.host
}
fn port(&self) -> Port {
self.port
}
}
impl FromStr for Remote {
type Err = eyre::Error;
fn from_str(s: &str) -> eyre::Result<Self> {
let remote = match s.split_once(':') {
Some((h, p)) => Remote {
host: h.to_string().into(),
port: u16::from_str_radix(p, 10)?.into(),
},
None => Remote {
host: s.to_string().into(),
port: Port::default(),
},
};
Ok(remote)
}
}
#[derive(Parser)]
#[command(name = "mirage")]
#[command(about = "Remote LSP proxy for Neovim", long_about = None)]
struct Args {
#[arg(short = 'r', long = "remote", value_name = "root@127.0.0.1:22", value_parser = Remote::from_str)]
remote: Remote,
#[arg(short = 'w', long = "workspace", env = "PWD", value_name = "/some/directory", value_parser = Workspace::from_str)]
workspace: Workspace,
#[arg(short = 'e', long = "exclude", default_values = ["target", ".git"])]
exclude: Vec<String>,
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Lsp {
#[command(subcommand)]
server: LspServerCommand,
},
}
#[tokio::main]
async fn main() {
let cli = Args::parse();
let level = match &cli.command {
Commands::Lsp { .. } => {
if cli.verbose == 0 {
"off"
} else {
match cli.verbose {
1 => "info",
2 => "debug",
_ => "trace",
}
}
}
};
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level)))
.init();
let remote = cli.remote;
let workspace = cli.workspace;
let namespace = workspace
.namespace()
.expect("something went wrong while determening namespace");
let session = sync::Session::start(&remote, &workspace, &namespace)
.expect("failed to start mutagen session");
match &cli.command {
Commands::Lsp { server } => {
info!("Starting mirage LSP proxy");
let lsp_server = lsp::LspServer::from(server);
if let Err(e) = lsp::start(
remote.host().clone(),
remote.port(),
workspace,
&cli.exclude,
&lsp_server,
)
.await
{
error!("Failed to start remote LSP: {}", e);
}
}
}
if let Err(e) = session.terminate() {
error!("Failed to terminate sync session: {}", e);
std::process::exit(1);
}
}