persistence.rs

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