diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/jvn_comp.rs | 245 | ||||
| -rw-r--r-- | src/cmds.rs | 1 | ||||
| -rw-r--r-- | src/cmds/comp/helpdoc.rs | 13 | ||||
| -rw-r--r-- | src/systems.rs | 1 | ||||
| -rw-r--r-- | src/systems/comp.rs | 2 | ||||
| -rw-r--r-- | src/systems/comp/context.rs | 33 | ||||
| -rw-r--r-- | src/systems/render/renderer.rs | 2 |
7 files changed, 266 insertions, 31 deletions
diff --git a/src/bin/jvn_comp.rs b/src/bin/jvn_comp.rs index d00916c..30a9f81 100644 --- a/src/bin/jvn_comp.rs +++ b/src/bin/jvn_comp.rs @@ -1,54 +1,237 @@ +use std::{fs::OpenOptions, process::exit}; + use clap::Parser; +use env_logger::Target; +use just_enough_vcs_cli::systems::{ + cmd::_commands::jv_cmd_nodes, + comp::{ + _comps::{jv_cmd_comp_nodes, match_comp}, + context::CompletionContext, + }, + render::renderer::jv_override_renderers, +}; +#[cfg(debug_assertions)] +use log::debug; +use log::{LevelFilter, error, trace}; + +fn main() { + // If not in release mode, initialize env_logger to capture logs + #[cfg(debug_assertions)] + init_env_logger(); + + // Get context parameters from clap + let ctx = match CompletionContext::try_parse() { + Ok(args) => CompletionContext { + // In completion scripts, "-" is replaced with "^", need to convert back here + command_line: args.command_line.replace('^', "-"), + cursor_position: args.cursor_position, + current_word: args.current_word.replace('^', "-"), + previous_word: args.previous_word.replace('^', "-"), + command_name: args.command_name.replace('^', "-"), + word_index: args.word_index, + all_words: args.all_words.iter().map(|w| w.replace('^', "-")).collect(), + }, + Err(e) => { + // An error occurred, collecting information for output + error!( + "Error: {}, origin=\"{}\"", + e, + std::env::args().collect::<Vec<String>>().join(" ") + ); + std::process::exit(1); + } + }; + + // Trace context information + #[cfg(debug_assertions)] + trace_ctx(&ctx); + + // Perform pre-completion for common flags; + // if completion fails, start matching command nodes for completion + let result = pre_comp(&ctx); + if let Some(suggestions) = result { + handle_comp_result(&Some(suggestions)); + } else { + trace!("Using specific completion"); + let result = comp(ctx); + handle_comp_result(&result); + } +} + +fn pre_comp(ctx: &CompletionContext) -> Option<Vec<String>> { + // Match and comp Override Renderers + if ctx.previous_word == "--renderer" { + return Some(jv_override_renderers()); + } -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct CompletionContext { - /// The full command line - #[arg(short = 'f', long)] - command_line: String, + None +} + +fn comp(ctx: CompletionContext) -> Option<Vec<String>> { + let args: Vec<String> = ctx.all_words.iter().skip(1).cloned().collect(); + let nodes = jv_cmd_comp_nodes(); + let command = format!("{} ", args.join(" ")); - /// Cursor position - #[arg(short = 'C', long)] - cursor_position: usize, + #[cfg(debug_assertions)] + debug!("Arguments: `{}`", command); - /// Current word - #[arg(short = 'w', long)] - current_word: String, + // Find all nodes that match the command prefix + let matching_nodes: Vec<&String> = nodes + .iter() + .filter(|node| { + let matches = command.starts_with(&format!("{} ", node)); + #[cfg(debug_assertions)] + debug!("Checking node '{}': matches = {}", node, matches); + matches + }) + .collect(); - /// Previous word - #[arg(short = 'p', long)] - previous_word: String, + let match_node: Option<String> = match matching_nodes.len() { + 0 => { + if let Some(result) = try_comp_cmd_nodes(&ctx) { + return Some(result); + } + // No matching node found + None + } + 1 => { + // Single matching node found + Some(matching_nodes[0].clone()) + } + _ => { + // Multiple matching nodes found + // Find the node with the longest length (most specific match) + matching_nodes + .iter() + .max_by_key(|node| node.len()) + .map(|node| node.to_string()) + } + }; - /// Command name - #[arg(short = 'c', long)] - command_name: String, + #[cfg(debug_assertions)] + match &match_node { + Some(node) => trace!("Matched `{}`", node), + None => trace!("No competions matched."), + } - /// Word index - #[arg(short = 'i', long)] - word_index: usize, + let Some(match_node) = match_node else { + return None; + }; - /// All words - #[arg(short = 'a', long, num_args = 1..)] - all_words: Vec<String>, + match_comp(match_node, ctx) } -fn main() { - let args = match CompletionContext::try_parse() { - Ok(args) => args, - Err(_) => std::process::exit(1), +fn try_comp_cmd_nodes(ctx: &CompletionContext) -> Option<Vec<String>> { + let cmd_nodes = jv_cmd_nodes(); + + // If the current position is less than 1, do not perform completion + if ctx.word_index < 1 { + return None; }; - match comp(args) { + + // Get the current input path + let input_path: Vec<&str> = ctx.all_words[1..ctx.word_index] + .iter() + .filter(|s| !s.is_empty()) + .map(|s| s.as_str()) + .collect(); + + #[cfg(debug_assertions)] + debug!( + "try_comp_cmd_nodes: input_path = {:?}, word_index = {}, all_words = {:?}", + input_path, ctx.word_index, ctx.all_words + ); + + // Filter command nodes that match the input path + let mut suggestions = Vec::new(); + + for node in cmd_nodes { + let node_parts: Vec<&str> = node.split(' ').collect(); + + #[cfg(debug_assertions)] + debug!("Checking node: '{}', parts: {:?}", node, node_parts); + + // If input path is longer than node parts, skip + if input_path.len() > node_parts.len() { + continue; + } + + // Check if input path matches the beginning of node parts + let mut matches = true; + for i in 0..input_path.len() { + if i >= node_parts.len() || input_path[i] != node_parts[i] { + matches = false; + break; + } + } + + if matches && input_path.len() < node_parts.len() { + // Add the next part as suggestion + suggestions.push(node_parts[input_path.len()].to_string()); + } + } + + // Remove duplicates and sort + suggestions.sort(); + suggestions.dedup(); + + #[cfg(debug_assertions)] + debug!("try_comp_cmd_nodes: suggestions = {:?}", suggestions); + + if suggestions.is_empty() { + None + } else { + Some(suggestions) + } +} + +fn handle_comp_result(r: &Option<Vec<String>>) { + match r { Some(suggestions) => { suggestions .iter() .for_each(|suggest| println!("{}", suggest)); + exit(0) } None => { + // Output "_file_" to notify the completion script to perform "file completion" println!("_file_"); + exit(0) } } } -fn comp(_args: CompletionContext) -> Option<Vec<String>> { - None +#[cfg(debug_assertions)] +fn trace_ctx(ctx: &CompletionContext) { + log::trace!("command_line={}", ctx.command_line); + log::trace!("cursor_position={}", ctx.cursor_position); + log::trace!("current_word={}", ctx.current_word); + log::trace!("previous_word={}", ctx.previous_word); + log::trace!("command_name={}", ctx.command_name); + log::trace!("word_index={}", ctx.word_index); + log::trace!("all_words={:?}", ctx.all_words); +} + +#[cfg(debug_assertions)] +fn init_env_logger() { + let mut log_path = std::env::current_exe() + .expect("Failed to get current executable path") + .parent() + .expect("Failed to get parent directory") + .to_path_buf(); + log_path.push("../../jv_comp_log.txt"); + + // Only initialize logger if log file exists + if log_path.exists() { + let log_file = OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + .expect("Failed to open log file"); + + env_logger::Builder::new() + .filter_level(LevelFilter::Trace) + .target(Target::Pipe(Box::new(log_file))) + .init(); + } } diff --git a/src/cmds.rs b/src/cmds.rs index d3c7a27..aa41aec 100644 --- a/src/cmds.rs +++ b/src/cmds.rs @@ -1,6 +1,7 @@ pub mod arg; pub mod cmd; pub mod collect; +pub mod comp; pub mod converter; pub mod r#in; pub mod out; diff --git a/src/cmds/comp/helpdoc.rs b/src/cmds/comp/helpdoc.rs new file mode 100644 index 0000000..7f07cad --- /dev/null +++ b/src/cmds/comp/helpdoc.rs @@ -0,0 +1,13 @@ +use crate::systems::{comp::context::CompletionContext, helpdoc}; + +pub fn comp(ctx: CompletionContext) -> Option<Vec<String>> { + if ctx.previous_word == "helpdoc" { + return Some( + helpdoc::get_helpdoc_list() + .iter() + .map(|s| s.to_string()) + .collect(), + ); + } + None +} diff --git a/src/systems.rs b/src/systems.rs index e0d4491..5ca0801 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,4 +1,5 @@ pub mod cmd; +pub mod comp; pub mod debug; pub mod helpdoc; pub mod render; diff --git a/src/systems/comp.rs b/src/systems/comp.rs new file mode 100644 index 0000000..c7c6577 --- /dev/null +++ b/src/systems/comp.rs @@ -0,0 +1,2 @@ +pub mod _comps; +pub mod context; diff --git a/src/systems/comp/context.rs b/src/systems/comp/context.rs new file mode 100644 index 0000000..36d1840 --- /dev/null +++ b/src/systems/comp/context.rs @@ -0,0 +1,33 @@ +use clap::{Parser, command}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct CompletionContext { + /// The full command line + #[arg(short = 'f', long)] + pub command_line: String, + + /// Cursor position + #[arg(short = 'C', long)] + pub cursor_position: usize, + + /// Current word + #[arg(short = 'w', long)] + pub current_word: String, + + /// Previous word + #[arg(short = 'p', long)] + pub previous_word: String, + + /// Command name + #[arg(short = 'c', long)] + pub command_name: String, + + /// Word index + #[arg(short = 'i', long)] + pub word_index: usize, + + /// All words + #[arg(short = 'a', long, num_args = 1..)] + pub all_words: Vec<String>, +} diff --git a/src/systems/render/renderer.rs b/src/systems/render/renderer.rs index dab4c23..13de8c6 100644 --- a/src/systems/render/renderer.rs +++ b/src/systems/render/renderer.rs @@ -60,3 +60,5 @@ macro_rules! r_println { $result.println(&format!($($arg)*)) }; } + +include!("_override_renderers.rs"); |
