lc/
completion.rs

1//! Shell completion support for the lc CLI
2//! 
3//! This module provides both static completion generation and dynamic completion
4//! support for values that depend on the current configuration.
5
6use anyhow::Result;
7use clap::CommandFactory;
8use clap_complete::{generate, Shell};
9use std::io;
10
11use crate::cli::{Cli, CompletionShell};
12use crate::config::Config;
13
14/// Generate shell completions for the specified shell
15pub async fn generate_completions(shell: CompletionShell) -> Result<()> {
16    let mut cmd = Cli::command();
17    let shell_type = match shell {
18        CompletionShell::Bash => Shell::Bash,
19        CompletionShell::Zsh => Shell::Zsh,
20        CompletionShell::Fish => Shell::Fish,
21        CompletionShell::PowerShell => Shell::PowerShell,
22        CompletionShell::Elvish => Shell::Elvish,
23    };
24
25    // Generate basic completions
26    generate(shell_type, &mut cmd, "lc", &mut io::stdout());
27    
28    // Add custom completion functions for dynamic values
29    match shell {
30        CompletionShell::Bash => generate_bash_dynamic_completions(),
31        CompletionShell::Zsh => generate_zsh_dynamic_completions(),
32        CompletionShell::Fish => generate_fish_dynamic_completions(),
33        _ => {
34            eprintln!("Note: Dynamic completions for providers are not yet supported for {:?}", shell);
35            eprintln!("Basic command completions have been generated.");
36        }
37    }
38    
39    Ok(())
40}
41
42/// Generate dynamic completion functions for Bash
43fn generate_bash_dynamic_completions() {
44    println!(r#"
45# Dynamic completion functions for lc (Bash)
46_lc_complete_providers() {{
47    local providers
48    providers=$(lc providers list 2>/dev/null | grep "  •" | awk '{{print $2}}' 2>/dev/null || echo "")
49    COMPREPLY=($(compgen -W "$providers" -- "${{COMP_WORDS[COMP_CWORD]}}"))
50}}
51
52_lc_complete_models() {{
53    local models provider
54    # Check if a provider was specified with -p or --provider
55    for ((i=1; i<COMP_CWORD; i++)); do
56        if [[ "${{COMP_WORDS[i]}}" == "-p" || "${{COMP_WORDS[i]}}" == "--provider" ]]; then
57            provider="${{COMP_WORDS[i+1]}}"
58            break
59        elif [[ "${{COMP_WORDS[i]}}" =~ ^--provider= ]]; then
60            provider="${{COMP_WORDS[i]#--provider=}}"
61            break
62        elif [[ "${{COMP_WORDS[i]}}" =~ ^-p.+ ]]; then
63            provider="${{COMP_WORDS[i]#-p}}"
64            break
65        fi
66    done
67    
68    if [[ -n "$provider" ]]; then
69        # Get models for specific provider (extract full model name including colons)
70        models=$(lc providers models "$provider" 2>/dev/null | grep "  •" | awk -F' ' '{{gsub(/^  • /, "", $0); gsub(/ \(.*$/, "", $0); gsub(/ \[.*$/, "", $0); print $1}}' 2>/dev/null || echo "")
71    else
72        # Get all models in provider:model format
73        models=$(lc models 2>/dev/null | awk '
74            /^[a-zA-Z0-9_-]+:$/ {{ provider = substr($0, 1, length($0)-1) }}
75            /^  •/ {{
76                gsub(/^  • /, "")
77                gsub(/ \(.*$/, "")
78                gsub(/ \[.*$/, "")
79                if (provider != "") print provider ":" $0
80            }}
81        ' 2>/dev/null || echo "")
82    fi
83    COMPREPLY=($(compgen -W "$models" -- "${{COMP_WORDS[COMP_CWORD]}}"))
84}}
85
86_lc_complete_vectordbs() {{
87    local vectordbs
88    vectordbs=$(lc vectors list 2>/dev/null | grep "  •" | awk '{{print $2}}' 2>/dev/null || echo "")
89    COMPREPLY=($(compgen -W "$vectordbs" -- "${{COMP_WORDS[COMP_CWORD]}}"))
90}}
91
92# Enhanced completion function with alias support
93_lc_enhanced() {{
94    local cur prev opts cmd
95    COMPREPLY=()
96    cur="${{COMP_WORDS[COMP_CWORD]}}"
97    prev="${{COMP_WORDS[COMP_CWORD-1]}}"
98    
99    # Handle command aliases by expanding them
100    if [[ COMP_CWORD -ge 1 ]]; then
101        cmd="${{COMP_WORDS[1]}}"
102        case "$cmd" in
103            p)
104                COMP_WORDS[1]="providers"
105                ;;
106            k)
107                COMP_WORDS[1]="keys"
108                ;;
109            l)
110                COMP_WORDS[1]="logs"
111                ;;
112            co)
113                COMP_WORDS[1]="config"
114                ;;
115            c)
116                COMP_WORDS[1]="chat"
117                ;;
118            m)
119                COMP_WORDS[1]="models"
120                ;;
121            a)
122                COMP_WORDS[1]="alias"
123                ;;
124            t)
125                COMP_WORDS[1]="templates"
126                ;;
127            pr)
128                COMP_WORDS[1]="proxy"
129                ;;
130            e)
131                COMP_WORDS[1]="embed"
132                ;;
133            s)
134                COMP_WORDS[1]="similar"
135                ;;
136            v)
137                COMP_WORDS[1]="vectors"
138                ;;
139            w)
140                COMP_WORDS[1]="web-chat-proxy"
141                ;;
142            sy)
143                COMP_WORDS[1]="sync"
144                ;;
145            se)
146                COMP_WORDS[1]="search"
147                ;;
148            img)
149                COMP_WORDS[1]="image"
150                ;;
151            dump)
152                COMP_WORDS[1]="dump-metadata"
153                ;;
154        esac
155    fi
156    
157    case "$prev" in
158        -p|--provider)
159            _lc_complete_providers
160            return 0
161            ;;
162        -m|--model)
163            _lc_complete_models
164            return 0
165            ;;
166        -v|--vectordb|--database)
167            _lc_complete_vectordbs
168            return 0
169            ;;
170    esac
171    
172    # Fall back to default completion
173    _lc "$@"
174}}
175
176# Register the enhanced completion
177complete -F _lc_enhanced lc
178
179# Instructions for setup
180# Add the above to your ~/.bashrc or ~/.bash_completion to enable dynamic completions
181# Then run: source ~/.bashrc
182"#);
183}
184
185/// Generate dynamic completion functions for Zsh
186fn generate_zsh_dynamic_completions() {
187    println!(r#"
188# Dynamic completion functions for lc (Zsh)
189_lc_providers() {{
190    local providers
191    providers=($(lc providers list 2>/dev/null | grep "  •" | awk '{{print $2}}' 2>/dev/null || echo ""))
192    _describe 'providers' providers
193}}
194
195_lc_models() {{
196    local models provider
197    # Check if a provider was specified with -p or --provider in the current command line
198    local -a words
199    words=(${{(z)BUFFER}})
200    
201    for ((i=1; i<=${{#words}}; i++)); do
202        if [[ "${{words[i]}}" == "-p" || "${{words[i]}}" == "--provider" ]]; then
203            provider="${{words[i+1]}}"
204            break
205        elif [[ "${{words[i]}}" =~ ^--provider= ]]; then
206            provider="${{words[i]#--provider=}}"
207            break
208        elif [[ "${{words[i]}}" =~ ^-p.+ ]]; then
209            provider="${{words[i]#-p}}"
210            break
211        fi
212    done
213    
214    if [[ -n "$provider" ]]; then
215        # Get models for specific provider (extract full model name including colons)
216        models=($(lc providers models "$provider" 2>/dev/null | grep "  •" | awk -F' ' '{{gsub(/^  • /, "", $0); gsub(/ \(.*$/, "", $0); gsub(/ \[.*$/, "", $0); print $1}}' 2>/dev/null || echo ""))
217        # For provider-specific models, just use the model names directly
218        _describe 'models' models
219    else
220        # Get all models in provider:model format
221        local raw_models
222        raw_models=($(lc models 2>/dev/null | awk '
223            /^[a-zA-Z0-9_-]+:$/ {{ provider = substr($0, 1, length($0)-1) }}
224            /^  •/ {{
225                gsub(/^  • /, "")
226                gsub(/ \(.*$/, "")
227                gsub(/ \[.*$/, "")
228                if (provider != "") print provider ":" $0
229            }}
230        ' 2>/dev/null || echo ""))
231        
232        # Use compadd with proper display format: "provider -- model"
233        local -a completions descriptions
234        for model in $raw_models; do
235            local provider_part="${{model%%:*}}"
236            local model_part="${{model#*:}}"
237            completions+=("$model")
238            descriptions+=("$provider_part -- $model_part")
239        done
240        
241        if [[ ${{#completions}} -gt 0 ]]; then
242            compadd -d descriptions -a completions
243        fi
244    fi
245}}
246
247_lc_vectordbs() {{
248    local vectordbs
249    vectordbs=($(lc vectors list 2>/dev/null | grep "  •" | awk '{{print $2}}' 2>/dev/null || echo ""))
250    _describe 'vectordbs' vectordbs
251}}
252
253# Override the default completion to use our dynamic functions
254# This replaces _default with our custom functions in the generated completion
255if (( $+functions[_lc] )); then
256    # Modify the existing _lc function to use our dynamic completions
257    eval "$(declare -f _lc | sed \
258        -e "s/:PROVIDER:_default/:PROVIDER:_lc_providers/g" \
259        -e "s/:MODEL:_default/:MODEL:_lc_models/g" \
260        -e "s/:VECTORDB:_default/:VECTORDB:_lc_vectordbs/g" \
261        -e "s/:DATABASE:_default/:DATABASE:_lc_vectordbs/g")"
262fi
263
264# Custom wrapper function to handle command aliases
265_lc_with_aliases() {{
266    local context curcontext="$curcontext" state line
267    typeset -A opt_args
268    typeset -a _arguments_options
269    local ret=1
270
271    if is-at-least 5.2; then
272        _arguments_options=(-s -S -C)
273    else
274        _arguments_options=(-s -C)
275    fi
276
277    # First, let the original _lc function handle most of the work
278    _lc "$@"
279    ret=$?
280    
281    # If we're in a command context and have an alias, handle it specially
282    if [[ $state == "lc" && -n $line[2] ]]; then
283        case $line[2] in
284            (p)
285                # Redirect 'p' alias to 'providers' subcommand completion
286                words=("providers" "${{words[@]:2}}")
287                (( CURRENT -= 1 ))
288                curcontext="${{curcontext%:*:*}}:lc-command-providers:"
289                
290                # Handle special case for 'lc p m <TAB>' (providers models command)
291                if [[ ${{#words}} -ge 3 && "${{words[2]}}" == "m" ]]; then
292                    # This is 'lc p m <TAB>' - should complete with provider names
293                    _lc_providers
294                    ret=$?
295                elif [[ ${{#words}} -ge 3 && "${{words[2]}}" == "models" ]]; then
296                    # This is 'lc p models <TAB>' - should complete with provider names
297                    _lc_providers
298                    ret=$?
299                else
300                    _lc__providers_commands
301                    ret=$?
302                fi
303                ;;
304            (k)
305                # Redirect 'k' alias to 'keys' subcommand completion
306                words=("keys" "${{words[@]:2}}")
307                (( CURRENT -= 1 ))
308                curcontext="${{curcontext%:*:*}}:lc-command-keys:"
309                _lc__keys_commands
310                ret=$?
311                ;;
312            (l)
313                # Redirect 'l' alias to 'logs' subcommand completion
314                words=("logs" "${{words[@]:2}}")
315                (( CURRENT -= 1 ))
316                curcontext="${{curcontext%:*:*}}:lc-command-logs:"
317                _lc__logs_commands
318                ret=$?
319                ;;
320            (co)
321                # Redirect 'co' alias to 'config' subcommand completion
322                words=("config" "${{words[@]:2}}")
323                (( CURRENT -= 1 ))
324                curcontext="${{curcontext%:*:*}}:lc-command-config:"
325                _lc__config_commands
326                ret=$?
327                ;;
328            (c)
329                # Redirect 'c' alias to 'chat' subcommand completion
330                words=("chat" "${{words[@]:2}}")
331                (( CURRENT -= 1 ))
332                curcontext="${{curcontext%:*:*}}:lc-command-chat:"
333                # Chat command has no subcommands, so just complete its options
334                _arguments "${{_arguments_options[@]}}" : \
335                    '-m+[Model to use for the chat]:MODEL:_lc_models' \
336                    '--model=[Model to use for the chat]:MODEL:_lc_models' \
337                    '-p+[Provider to use for the chat]:PROVIDER:_lc_providers' \
338                    '--provider=[Provider to use for the chat]:PROVIDER:_lc_providers' \
339                    '--cid=[Chat ID to use or continue]:CHAT_ID:_default' \
340                    '-t+[Include tools from MCP server(s)]:TOOLS:_default' \
341                    '--tools=[Include tools from MCP server(s)]:TOOLS:_default' \
342                    '-v+[Vector database name for RAG]:DATABASE:_lc_vectordbs' \
343                    '--vectordb=[Vector database name for RAG]:DATABASE:_lc_vectordbs' \
344                    '-d[Enable debug/verbose logging]' \
345                    '--debug[Enable debug/verbose logging]' \
346                    '*-i+[Attach image(s) to the chat]:IMAGES:_default' \
347                    '*--image=[Attach image(s) to the chat]:IMAGES:_default' \
348                    '-h[Print help]' \
349                    '--help[Print help]'
350                ret=$?
351                ;;
352            (m)
353                # Redirect 'm' alias to 'models' subcommand completion
354                words=("models" "${{words[@]:2}}")
355                (( CURRENT -= 1 ))
356                curcontext="${{curcontext%:*:*}}:lc-command-models:"
357                _lc__models_commands
358                ret=$?
359                ;;
360            (a)
361                # Redirect 'a' alias to 'alias' subcommand completion
362                words=("alias" "${{words[@]:2}}")
363                (( CURRENT -= 1 ))
364                curcontext="${{curcontext%:*:*}}:lc-command-alias:"
365                _lc__alias_commands
366                ret=$?
367                ;;
368            (t)
369                # Redirect 't' alias to 'templates' subcommand completion
370                words=("templates" "${{words[@]:2}}")
371                (( CURRENT -= 1 ))
372                curcontext="${{curcontext%:*:*}}:lc-command-templates:"
373                _lc__templates_commands
374                ret=$?
375                ;;
376            (pr)
377                # Redirect 'pr' alias to 'proxy' subcommand completion
378                words=("proxy" "${{words[@]:2}}")
379                (( CURRENT -= 1 ))
380                curcontext="${{curcontext%:*:*}}:lc-command-proxy:"
381                # Proxy command has no subcommands, so just complete its options
382                _arguments "${{_arguments_options[@]}}" : \
383                    '-p+[Port to listen on]:PORT:_default' \
384                    '--port=[Port to listen on]:PORT:_default' \
385                    '--host=[Host to bind to]:HOST:_default' \
386                    '--provider=[Filter by provider]:PROVIDER:_lc_providers' \
387                    '-m+[Filter by specific model]:MODEL:_lc_models' \
388                    '--model=[Filter by specific model]:MODEL:_lc_models' \
389                    '-k+[API key for authentication]:API_KEY:_default' \
390                    '--key=[API key for authentication]:API_KEY:_default' \
391                    '-g[Generate a random API key]' \
392                    '--generate-key[Generate a random API key]' \
393                    '-h[Print help]' \
394                    '--help[Print help]'
395                ret=$?
396                ;;
397            (e)
398                # Redirect 'e' alias to 'embed' subcommand completion
399                words=("embed" "${{words[@]:2}}")
400                (( CURRENT -= 1 ))
401                curcontext="${{curcontext%:*:*}}:lc-command-embed:"
402                # Embed command has no subcommands, so just complete its options
403                _arguments "${{_arguments_options[@]}}" : \
404                    '-m+[Model to use for embeddings]:MODEL:_lc_models' \
405                    '--model=[Model to use for embeddings]:MODEL:_lc_models' \
406                    '-p+[Provider to use for embeddings]:PROVIDER:_lc_providers' \
407                    '--provider=[Provider to use for embeddings]:PROVIDER:_lc_providers' \
408                    '-v+[Vector database name to store embeddings]:DATABASE:_lc_vectordbs' \
409                    '--vectordb=[Vector database name to store embeddings]:DATABASE:_lc_vectordbs' \
410                    '*-f+[Files to embed]:FILES:_files' \
411                    '*--files=[Files to embed]:FILES:_files' \
412                    '-d[Enable debug/verbose logging]' \
413                    '--debug[Enable debug/verbose logging]' \
414                    '-h[Print help]' \
415                    '--help[Print help]' \
416                    '::text -- Text to embed:_default'
417                ret=$?
418                ;;
419            (s)
420                # Redirect 's' alias to 'similar' subcommand completion
421                words=("similar" "${{words[@]:2}}")
422                (( CURRENT -= 1 ))
423                curcontext="${{curcontext%:*:*}}:lc-command-similar:"
424                # Similar command has no subcommands, so just complete its options
425                _arguments "${{_arguments_options[@]}}" : \
426                    '-m+[Model to use for embeddings]:MODEL:_lc_models' \
427                    '--model=[Model to use for embeddings]:MODEL:_lc_models' \
428                    '-p+[Provider to use for embeddings]:PROVIDER:_lc_providers' \
429                    '--provider=[Provider to use for embeddings]:PROVIDER:_lc_providers' \
430                    '-v+[Vector database name to search]:DATABASE:_lc_vectordbs' \
431                    '--vectordb=[Vector database name to search]:DATABASE:_lc_vectordbs' \
432                    '-l+[Number of similar results to return]:LIMIT:_default' \
433                    '--limit=[Number of similar results to return]:LIMIT:_default' \
434                    '-h[Print help]' \
435                    '--help[Print help]' \
436                    ':query -- Query text to find similar content:_default'
437                ret=$?
438                ;;
439            (v)
440                # Redirect 'v' alias to 'vectors' subcommand completion
441                words=("vectors" "${{words[@]:2}}")
442                (( CURRENT -= 1 ))
443                curcontext="${{curcontext%:*:*}}:lc-command-vectors:"
444                _lc__vectors_commands
445                ret=$?
446                ;;
447            (w)
448                # Redirect 'w' alias to 'web-chat-proxy' subcommand completion
449                words=("web-chat-proxy" "${{words[@]:2}}")
450                (( CURRENT -= 1 ))
451                curcontext="${{curcontext%:*:*}}:lc-command-web-chat-proxy:"
452                _lc__web_chat_proxy_commands
453                ret=$?
454                ;;
455            (sy)
456                # Redirect 'sy' alias to 'sync' subcommand completion
457                words=("sync" "${{words[@]:2}}")
458                (( CURRENT -= 1 ))
459                curcontext="${{curcontext%:*:*}}:lc-command-sync:"
460                _lc__sync_commands
461                ret=$?
462                ;;
463            (se)
464                # Redirect 'se' alias to 'search' subcommand completion
465                words=("search" "${{words[@]:2}}")
466                (( CURRENT -= 1 ))
467                curcontext="${{curcontext%:*:*}}:lc-command-search:"
468                _lc__search_commands
469                ret=$?
470                ;;
471            (img)
472                # Redirect 'img' alias to 'image' subcommand completion
473                words=("image" "${{words[@]:2}}")
474                (( CURRENT -= 1 ))
475                curcontext="${{curcontext%:*:*}}:lc-command-image:"
476                # Image command has no subcommands, so just complete its options
477                _arguments "${{_arguments_options[@]}}" : \
478                    '-m+[Model to use for image generation]:MODEL:_lc_models' \
479                    '--model=[Model to use for image generation]:MODEL:_lc_models' \
480                    '-p+[Provider to use for image generation]:PROVIDER:_lc_providers' \
481                    '--provider=[Provider to use for image generation]:PROVIDER:_lc_providers' \
482                    '-s+[Image size]:SIZE:_default' \
483                    '--size=[Image size]:SIZE:_default' \
484                    '-n+[Number of images to generate]:COUNT:_default' \
485                    '--count=[Number of images to generate]:COUNT:_default' \
486                    '-o+[Output directory for generated images]:OUTPUT:_directories' \
487                    '--output=[Output directory for generated images]:OUTPUT:_directories' \
488                    '-d[Enable debug/verbose logging]' \
489                    '--debug[Enable debug/verbose logging]' \
490                    '-h[Print help]' \
491                    '--help[Print help]' \
492                    ':prompt -- Text prompt for image generation:_default'
493                ret=$?
494                ;;
495            (dump)
496                # Redirect 'dump' alias to 'dump-metadata' subcommand completion
497                words=("dump-metadata" "${{words[@]:2}}")
498                (( CURRENT -= 1 ))
499                curcontext="${{curcontext%:*:*}}:lc-command-dump-metadata:"
500                # Dump-metadata command has no subcommands, so just complete its options
501                _arguments "${{_arguments_options[@]}}" : \
502                    '-l[List available cached metadata files]' \
503                    '--list[List available cached metadata files]' \
504                    '-h[Print help]' \
505                    '--help[Print help]' \
506                    '::provider -- Specific provider to dump:_lc_providers'
507                ret=$?
508                ;;
509        esac
510    fi
511    
512    return ret
513}}
514
515# Replace the main completion function with our alias-aware version
516compdef _lc_with_aliases lc
517
518# Instructions for setup
519# Add the above to your ~/.zshrc or a file in your fpath to enable dynamic provider completion
520# Then run: source ~/.zshrc
521"#);
522}
523
524/// Generate dynamic completion functions for Fish
525fn generate_fish_dynamic_completions() {
526    println!(r#"
527# Dynamic completion functions for lc (Fish)
528function __lc_complete_providers
529    lc providers list 2>/dev/null | grep "  •" | awk '{{print $2}}' 2>/dev/null
530end
531
532# Add dynamic provider completion
533complete -c lc -s p -l provider -f -a "(__lc_complete_providers)" -d "Provider to use"
534
535# Instructions for setup
536# Add the above to ~/.config/fish/completions/lc.fish to enable dynamic provider completion
537# The file will be loaded automatically by Fish
538"#);
539}
540
541/// Get list of available providers for completion
542#[allow(dead_code)]
543pub fn get_available_providers() -> Vec<String> {
544    match Config::load() {
545        Ok(config) => {
546            let mut providers: Vec<String> = config.providers.keys().cloned().collect();
547            providers.sort();
548            providers
549        }
550        Err(_) => Vec::new(),
551    }
552}
553
554/// Get list of available models for completion (simplified version)
555#[allow(dead_code)]
556pub fn get_available_models() -> Vec<String> {
557    // For now, return common model names
558    // In a full implementation, this would load from cache
559    vec![
560        "gpt-4".to_string(),
561        "gpt-4-turbo".to_string(),
562        "gpt-3.5-turbo".to_string(),
563        "claude-3-sonnet".to_string(),
564        "claude-3-haiku".to_string(),
565        "gemini-pro".to_string(),
566    ]
567}
568
569/// Get list of available vector databases for completion
570#[allow(dead_code)]
571pub fn get_available_vectordbs() -> Vec<String> {
572    match crate::vector_db::VectorDatabase::list_databases() {
573        Ok(databases) => databases,
574        Err(_) => Vec::new(),
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581
582    #[test]
583    fn test_get_available_models() {
584        let models = get_available_models();
585        assert!(!models.is_empty());
586        assert!(models.contains(&"gpt-4".to_string()));
587    }
588}