Skip to main content

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