reovim_plugin_completion/
commands.rs

1//! Completion-related commands (unified command-event types)
2
3use reovim_core::{
4    buffer::TextOps,
5    command::traits::*,
6    declare_event_command,
7    event_bus::{DynEvent, Event},
8};
9
10use crate::CompletionRequest;
11
12// === Trigger Command (custom impl for context data + priority) ===
13
14/// Trigger completion at current cursor position (command)
15#[derive(Debug, Clone, Copy)]
16pub struct CompletionTrigger {
17    /// Buffer ID where completion was triggered
18    pub buffer_id: usize,
19}
20
21impl CompletionTrigger {
22    /// Create instance from buffer ID
23    #[must_use]
24    pub const fn new(buffer_id: usize) -> Self {
25        Self { buffer_id }
26    }
27}
28
29impl CommandTrait for CompletionTrigger {
30    fn name(&self) -> &'static str {
31        "completion_trigger"
32    }
33
34    fn description(&self) -> &'static str {
35        "Trigger completion at cursor position"
36    }
37
38    fn execute(&self, ctx: &mut ExecutionContext) -> CommandResult {
39        // Extract buffer content and cursor position
40        let content = ctx.buffer.content_to_string();
41        let cursor_row = ctx.buffer.cur.y as usize;
42        let cursor_col = ctx.buffer.cur.x as usize;
43
44        // Get current line and extract word prefix
45        let line = ctx
46            .buffer
47            .contents
48            .get(cursor_row)
49            .map(|l| l.inner.clone())
50            .unwrap_or_default();
51
52        // Find word start by scanning backwards from cursor
53        // Stop at word boundary characters (common programming delimiters)
54        let line_chars: Vec<char> = line.chars().collect();
55        let mut word_start_col = cursor_col;
56        while word_start_col > 0 {
57            let ch = line_chars.get(word_start_col - 1).copied().unwrap_or(' ');
58            if is_word_boundary(ch) {
59                break;
60            }
61            word_start_col -= 1;
62        }
63
64        // Extract prefix (text already typed)
65        let prefix: String = line_chars[word_start_col..cursor_col].iter().collect();
66
67        // Create completion request event
68        let request = CompletionRequest {
69            buffer_id: ctx.buffer_id,
70            file_path: ctx.buffer.file_path.clone(),
71            content,
72            cursor_row: cursor_row as u32,
73            cursor_col: cursor_col as u32,
74            line,
75            prefix,
76            word_start_col: word_start_col as u32,
77            trigger_char: None,
78        };
79
80        // Emit CompletionTriggered event with full context
81        CommandResult::EmitEvent(DynEvent::new(CompletionTriggered { request }))
82    }
83
84    fn clone_box(&self) -> Box<dyn CommandTrait> {
85        Box::new(*self)
86    }
87
88    fn as_any(&self) -> &dyn std::any::Any {
89        self
90    }
91}
92
93/// Event emitted when completion is triggered (contains full context)
94#[derive(Debug, Clone)]
95pub struct CompletionTriggered {
96    /// The completion request with all context
97    pub request: CompletionRequest,
98}
99
100impl Event for CompletionTriggered {
101    fn priority(&self) -> u32 {
102        50 // High priority for UI updates
103    }
104}
105
106// === Navigation & Action Commands (using macro) ===
107
108declare_event_command! {
109    CompletionSelectNext,
110    id: "completion_next",
111    description: "Select next completion item",
112}
113
114declare_event_command! {
115    CompletionSelectPrev,
116    id: "completion_prev",
117    description: "Select previous completion item",
118}
119
120declare_event_command! {
121    CompletionConfirm,
122    id: "completion_confirm",
123    description: "Confirm and insert selected completion",
124}
125
126declare_event_command! {
127    CompletionDismiss,
128    id: "completion_dismiss",
129    description: "Dismiss completion popup",
130}
131
132/// Check if a character is a word boundary for completion prefix extraction
133///
134/// Word boundaries include whitespace and common programming delimiters.
135/// This allows proper prefix extraction for cases like `foo.bar.hel` -> prefix = "hel"
136#[must_use]
137const fn is_word_boundary(c: char) -> bool {
138    matches!(
139        c,
140        ' ' | '\t'
141            | '\n'
142            | '\r'
143            | '.'
144            | ':'
145            | '('
146            | ')'
147            | '['
148            | ']'
149            | '{'
150            | '}'
151            | '<'
152            | '>'
153            | ','
154            | ';'
155            | '+'
156            | '-'
157            | '*'
158            | '/'
159            | '='
160            | '!'
161            | '@'
162            | '#'
163            | '$'
164            | '%'
165            | '^'
166            | '&'
167            | '|'
168            | '~'
169            | '`'
170            | '"'
171            | '\''
172            | '\\'
173    )
174}