1use anyhow::Result;
  2use db::{
  3    query,
  4    sqlez::{
  5        bindable::Column, domain::Domain, statement::Statement,
  6        thread_safe_connection::ThreadSafeConnection,
  7    },
  8    sqlez_macros::sql,
  9};
 10use serde::{Deserialize, Serialize};
 11use time::OffsetDateTime;
 12
 13#[cfg(test)]
 14#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
 15pub(crate) struct SerializedCommandInvocation {
 16    pub(crate) command_name: String,
 17    pub(crate) user_query: String,
 18    pub(crate) last_invoked: OffsetDateTime,
 19}
 20
 21#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
 22pub(crate) struct SerializedCommandUsage {
 23    pub(crate) command_name: String,
 24    pub(crate) invocations: u16,
 25    pub(crate) last_invoked: OffsetDateTime,
 26}
 27
 28impl Column for SerializedCommandUsage {
 29    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 30        let (command_name, next_index): (String, i32) = Column::column(statement, start_index)?;
 31        let (invocations, next_index): (u16, i32) = Column::column(statement, next_index)?;
 32        let (last_invoked_raw, next_index): (i64, i32) = Column::column(statement, next_index)?;
 33
 34        let usage = Self {
 35            command_name,
 36            invocations,
 37            last_invoked: OffsetDateTime::from_unix_timestamp(last_invoked_raw)?,
 38        };
 39        Ok((usage, next_index))
 40    }
 41}
 42
 43#[cfg(test)]
 44impl Column for SerializedCommandInvocation {
 45    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 46        let (command_name, next_index): (String, i32) = Column::column(statement, start_index)?;
 47        let (user_query, next_index): (String, i32) = Column::column(statement, next_index)?;
 48        let (last_invoked_raw, next_index): (i64, i32) = Column::column(statement, next_index)?;
 49        let command_invocation = Self {
 50            command_name,
 51            user_query,
 52            last_invoked: OffsetDateTime::from_unix_timestamp(last_invoked_raw)?,
 53        };
 54        Ok((command_invocation, next_index))
 55    }
 56}
 57
 58pub struct CommandPaletteDB(ThreadSafeConnection);
 59
 60impl Domain for CommandPaletteDB {
 61    const NAME: &str = stringify!(CommandPaletteDB);
 62    const MIGRATIONS: &[&str] = &[sql!(
 63        CREATE TABLE IF NOT EXISTS command_invocations(
 64            id INTEGER PRIMARY KEY AUTOINCREMENT,
 65            command_name TEXT NOT NULL,
 66            user_query TEXT NOT NULL,
 67            last_invoked INTEGER DEFAULT (unixepoch())  NOT NULL
 68        ) STRICT;
 69    )];
 70}
 71
 72db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
 73
 74impl CommandPaletteDB {
 75    pub async fn write_command_invocation(
 76        &self,
 77        command_name: impl Into<String>,
 78        user_query: impl Into<String>,
 79    ) -> Result<()> {
 80        let command_name = command_name.into();
 81        let user_query = user_query.into();
 82        log::debug!(
 83            "Writing command invocation: command_name={command_name}, user_query={user_query}"
 84        );
 85        self.write_command_invocation_internal(command_name, user_query)
 86            .await
 87    }
 88
 89    #[cfg(test)]
 90    query! {
 91        pub(crate) fn get_last_invoked(command: &str) -> Result<Option<SerializedCommandInvocation>> {
 92            SELECT
 93            command_name,
 94            user_query,
 95            last_invoked FROM command_invocations
 96            WHERE command_name=(?)
 97            ORDER BY last_invoked DESC
 98            LIMIT 1
 99        }
100    }
101
102    query! {
103        pub fn get_command_usage(command: &str) -> Result<Option<SerializedCommandUsage>> {
104            SELECT command_name, COUNT(1), MAX(last_invoked)
105            FROM command_invocations
106            WHERE command_name=(?)
107            GROUP BY command_name
108        }
109    }
110
111    query! {
112        async fn write_command_invocation_internal(command_name: String, user_query: String) -> Result<()> {
113            INSERT INTO command_invocations (command_name, user_query) VALUES ((?), (?));
114            DELETE FROM command_invocations WHERE id IN (SELECT MIN(id) FROM command_invocations HAVING COUNT(1) > 1000);
115        }
116    }
117
118    query! {
119        pub fn list_commands_used() -> Result<Vec<SerializedCommandUsage>> {
120            SELECT command_name, COUNT(1), MAX(last_invoked)
121            FROM command_invocations
122            GROUP BY command_name
123            ORDER BY COUNT(1) DESC
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130
131    use crate::persistence::{CommandPaletteDB, SerializedCommandUsage};
132
133    #[gpui::test]
134    async fn test_saves_and_retrieves_command_invocation() {
135        let db =
136            CommandPaletteDB::open_test_db("test_saves_and_retrieves_command_invocation").await;
137
138        let retrieved_cmd = db.get_last_invoked("editor: backspace").unwrap();
139
140        assert!(retrieved_cmd.is_none());
141
142        db.write_command_invocation("editor: backspace", "")
143            .await
144            .unwrap();
145
146        let retrieved_cmd = db.get_last_invoked("editor: backspace").unwrap();
147
148        assert!(retrieved_cmd.is_some());
149        let retrieved_cmd = retrieved_cmd.expect("is some");
150        assert_eq!(retrieved_cmd.command_name, "editor: backspace".to_string());
151        assert_eq!(retrieved_cmd.user_query, "".to_string());
152    }
153
154    #[gpui::test]
155    async fn test_gets_usage_history() {
156        let db = CommandPaletteDB::open_test_db("test_gets_usage_history").await;
157        db.write_command_invocation("go to line: toggle", "200")
158            .await
159            .unwrap();
160        db.write_command_invocation("go to line: toggle", "201")
161            .await
162            .unwrap();
163
164        let retrieved_cmd = db.get_last_invoked("go to line: toggle").unwrap();
165
166        assert!(retrieved_cmd.is_some());
167        let retrieved_cmd = retrieved_cmd.expect("is some");
168
169        let command_usage = db.get_command_usage("go to line: toggle").unwrap();
170
171        assert!(command_usage.is_some());
172        let command_usage: SerializedCommandUsage = command_usage.expect("is some");
173
174        assert_eq!(command_usage.command_name, "go to line: toggle");
175        assert_eq!(command_usage.invocations, 2);
176        assert_eq!(command_usage.last_invoked, retrieved_cmd.last_invoked);
177    }
178
179    #[gpui::test]
180    async fn test_lists_ordered_by_usage() {
181        let db = CommandPaletteDB::open_test_db("test_lists_ordered_by_usage").await;
182
183        let empty_commands = db.list_commands_used();
184        match &empty_commands {
185            Ok(_) => (),
186            Err(e) => println!("Error: {:?}", e),
187        }
188        assert!(empty_commands.is_ok());
189        assert_eq!(empty_commands.expect("is ok").len(), 0);
190
191        db.write_command_invocation("go to line: toggle", "200")
192            .await
193            .unwrap();
194        db.write_command_invocation("editor: backspace", "")
195            .await
196            .unwrap();
197        db.write_command_invocation("editor: backspace", "")
198            .await
199            .unwrap();
200
201        let commands = db.list_commands_used();
202
203        assert!(commands.is_ok());
204        let commands = commands.expect("is ok");
205        assert_eq!(commands.len(), 2);
206        assert_eq!(commands.as_slice()[0].command_name, "editor: backspace");
207        assert_eq!(commands.as_slice()[0].invocations, 2);
208        assert_eq!(commands.as_slice()[1].command_name, "go to line: toggle");
209        assert_eq!(commands.as_slice()[1].invocations, 1);
210    }
211
212    #[gpui::test]
213    async fn test_handles_max_invocation_entries() {
214        let db = CommandPaletteDB::open_test_db("test_handles_max_invocation_entries").await;
215
216        for i in 1..=1001 {
217            db.write_command_invocation("some-command", &i.to_string())
218                .await
219                .unwrap();
220        }
221        let some_command = db.get_command_usage("some-command").unwrap();
222
223        assert!(some_command.is_some());
224        assert_eq!(some_command.expect("is some").invocations, 1000);
225    }
226}