use anyhow::{anyhow, Result};
use nvim_rs::{create::tokio as create, rpc::handler::Dummy};
use url::Url;
use crate::socket;
#[derive(Debug)]
struct OpenParams {
file: String,
line: Option<u64>,
col: Option<u64>,
split: Option<String>,
tab: bool,
}
fn parse_uri(raw: &str) -> Result<OpenParams> {
let url = Url::parse(raw)
.map_err(|e| anyhow!("URI invalide : {}", e))?;
if url.scheme() != "nvim" {
return Err(anyhow!(
"schéma URI incorrect : attendu 'nvim', reçu '{}'",
url.scheme()
));
}
let host = url.host_str().unwrap_or("");
if host == "file" {
return parse_file_uri(&url);
}
let command = if host.is_empty() || host == "open" {
"open"
} else {
host
};
if command != "open" {
return Err(anyhow!(
"commande inconnue : '{}' (seul 'open' est supporté en v0.1)",
command
));
}
let mut file = String::new();
let mut line = None::<u64>;
let mut col = None::<u64>;
let mut split = None::<String>;
let mut tab = false;
for (key, value) in url.query_pairs() {
match key.as_ref() {
"file" => file = value.into_owned(),
"line" => line = value.parse().ok(),
"col" => col = value.parse().ok(),
"split" => split = Some(value.into_owned()),
"tab" => tab = value == "true" || value == "1",
_ => {} }
}
if file.is_empty() {
return Err(anyhow!("paramètre 'file' manquant dans l'URI"));
}
validate_file_path(&file)?;
Ok(OpenParams { file, line, col, split, tab })
}
fn parse_file_uri(url: &Url) -> Result<OpenParams> {
let raw_path = url.path().trim_start_matches('/');
let full_path = format!("/{}", raw_path);
let parts: Vec<&str> = full_path.split(':').collect();
let n = parts.len();
let (file, line, col) = match n {
0 => return Err(anyhow!("chemin de fichier manquant dans l'URI nvim://file/")),
1 => (parts[0].to_string(), None, None),
2 => {
if parts[1].chars().all(|c| c.is_ascii_digit()) {
let line: u64 = parts[1].parse().map_err(|_| anyhow!("valeur line invalide"))?;
(parts[0].to_string(), Some(line), None)
} else {
(full_path.clone(), None, None)
}
}
_ => {
let maybe_col = parts[n - 1];
let maybe_line = parts[n - 2];
if maybe_col.chars().all(|c| c.is_ascii_digit())
&& maybe_line.chars().all(|c| c.is_ascii_digit())
{
let file_str = parts[..n - 2].join(":");
let line_val: u64 = maybe_line.parse().map_err(|_| anyhow!("valeur line invalide"))?;
let col_val: u64 = maybe_col.parse().map_err(|_| anyhow!("valeur col invalide"))?;
(file_str, Some(line_val), Some(col_val))
} else if maybe_col.chars().all(|c| c.is_ascii_digit()) {
let file_str = parts[..n - 1].join(":");
let line_val: u64 = maybe_col.parse().map_err(|_| anyhow!("valeur line invalide"))?;
(file_str, Some(line_val), None)
} else {
(full_path.clone(), None, None)
}
}
};
if file.is_empty() {
return Err(anyhow!("chemin de fichier manquant dans l'URI nvim://file/"));
}
validate_file_path(&file)?;
Ok(OpenParams { file, line, col, split: None, tab: false })
}
fn validate_file_path(file: &str) -> Result<()> {
if file.contains('\0') {
return Err(anyhow!("chemin invalide : contient un null byte"));
}
if file.contains(';') || file.contains('|') || file.contains('`')
|| file.contains('&') || file.contains('$')
|| file.contains('<') || file.contains('>')
{
return Err(anyhow!("chemin invalide : contient des métacaractères dangereux"));
}
Ok(())
}
fn build_lua(params: &OpenParams) -> String {
let mut uri = format!("nvim://open?file={}", url_encode(¶ms.file));
if let Some(ln) = params.line {
uri.push_str(&format!("&line={}", ln));
}
if let Some(c) = params.col {
uri.push_str(&format!("&col={}", c));
}
if let Some(ref s) = params.split {
uri.push_str(&format!("&split={}", s));
}
if params.tab {
uri.push_str("&tab=true");
}
format!("require('portal').handle('{}')", uri.replace('\'', "\\'"))
}
fn url_encode(s: &str) -> String {
let mut encoded = String::with_capacity(s.len() * 3);
for byte in s.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
| b'-' | b'_' | b'.' | b'~' | b'/' => {
encoded.push(byte as char);
}
_ => {
encoded.push_str(&format!("%{:02X}", byte));
}
}
}
encoded
}
pub async fn handle(raw_uri: &str, explicit_socket: Option<&str>) -> Result<()> {
let params = parse_uri(raw_uri)?;
let socket_path = if let Some(s) = explicit_socket {
std::path::PathBuf::from(s)
} else {
socket::find_best().ok_or_else(|| {
anyhow!(
"Aucune instance Neovim active trouvée.\n\
Astuce : démarrer Neovim avec `nvim --listen /tmp/nvim.sock`\n\
ou ajouter `auto_server = true` dans votre setup()."
)
})?
};
let lua_code = build_lua(¶ms);
let (nvim, _io) = create::new_path(&socket_path, Dummy::new())
.await
.map_err(|e| anyhow!(
"connexion au socket '{}' échouée : {}",
socket_path.display(), e
))?;
nvim.exec_lua(&lua_code, vec![])
.await
.map_err(|e| anyhow!("nvim_exec_lua échoué : {}", e))?;
Ok(())
}