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 self.write_command_invocation_internal(command_name.into(), user_query.into())
71 .await
72 }
73
74 query! {
75 pub fn get_last_invoked(command: &str) -> Result<Option<SerializedCommandInvocation>> {
76 SELECT
77 command_name,
78 user_query,
79 last_invoked FROM command_invocations
80 WHERE command_name=(?)
81 ORDER BY last_invoked DESC
82 LIMIT 1
83 }
84 }
85
86 query! {
87 pub fn get_command_usage(command: &str) -> Result<Option<SerializedCommandUsage>> {
88 SELECT command_name, COUNT(1), MAX(last_invoked)
89 FROM command_invocations
90 WHERE command_name=(?)
91 GROUP BY command_name
92 }
93 }
94
95 query! {
96 async fn write_command_invocation_internal(command_name: String, user_query: String) -> Result<()> {
97 INSERT INTO command_invocations (command_name, user_query) VALUES ((?), (?));
98 DELETE FROM command_invocations WHERE id IN (SELECT MIN(id) FROM command_invocations HAVING COUNT(1) > 1000);
99 }
100 }
101
102 query! {
103 pub fn list_commands_used() -> Result<Vec<SerializedCommandUsage>> {
104 SELECT command_name, COUNT(1), MAX(last_invoked)
105 FROM command_invocations
106 GROUP BY command_name
107 ORDER BY COUNT(1) DESC
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114
115 use crate::persistence::{CommandPaletteDB, SerializedCommandUsage};
116
117 #[gpui::test]
118 async fn test_saves_and_retrieves_command_invocation() {
119 let db =
120 CommandPaletteDB(db::open_test_db("test_saves_and_retrieves_command_invocation").await);
121
122 let retrieved_cmd = db.get_last_invoked("editor: backspace").unwrap();
123
124 assert!(retrieved_cmd.is_none());
125
126 db.write_command_invocation("editor: backspace", "")
127 .await
128 .unwrap();
129
130 let retrieved_cmd = db.get_last_invoked("editor: backspace").unwrap();
131
132 assert!(retrieved_cmd.is_some());
133 let retrieved_cmd = retrieved_cmd.expect("is some");
134 assert_eq!(retrieved_cmd.command_name, "editor: backspace".to_string());
135 assert_eq!(retrieved_cmd.user_query, "".to_string());
136 }
137
138 #[gpui::test]
139 async fn test_gets_usage_history() {
140 let db = CommandPaletteDB(db::open_test_db("test_gets_usage_history").await);
141 db.write_command_invocation("go to line: toggle", "200")
142 .await
143 .unwrap();
144 db.write_command_invocation("go to line: toggle", "201")
145 .await
146 .unwrap();
147
148 let retrieved_cmd = db.get_last_invoked("go to line: toggle").unwrap();
149
150 assert!(retrieved_cmd.is_some());
151 let retrieved_cmd = retrieved_cmd.expect("is some");
152
153 let command_usage = db.get_command_usage("go to line: toggle").unwrap();
154
155 assert!(command_usage.is_some());
156 let command_usage: SerializedCommandUsage = command_usage.expect("is some");
157
158 assert_eq!(command_usage.command_name, "go to line: toggle");
159 assert_eq!(command_usage.invocations, 2);
160 assert_eq!(command_usage.last_invoked, retrieved_cmd.last_invoked);
161 }
162
163 #[gpui::test]
164 async fn test_lists_ordered_by_usage() {
165 let db = CommandPaletteDB(db::open_test_db("test_lists_ordered_by_usage").await);
166
167 let empty_commands = db.list_commands_used();
168 match &empty_commands {
169 Ok(_) => (),
170 Err(e) => println!("Error: {:?}", e),
171 }
172 assert!(empty_commands.is_ok());
173 assert_eq!(empty_commands.expect("is ok").len(), 0);
174
175 db.write_command_invocation("go to line: toggle", "200")
176 .await
177 .unwrap();
178 db.write_command_invocation("editor: backspace", "")
179 .await
180 .unwrap();
181 db.write_command_invocation("editor: backspace", "")
182 .await
183 .unwrap();
184
185 let commands = db.list_commands_used();
186
187 assert!(commands.is_ok());
188 let commands = commands.expect("is ok");
189 assert_eq!(commands.len(), 2);
190 assert_eq!(commands.as_slice()[0].command_name, "editor: backspace");
191 assert_eq!(commands.as_slice()[0].invocations, 2);
192 assert_eq!(commands.as_slice()[1].command_name, "go to line: toggle");
193 assert_eq!(commands.as_slice()[1].invocations, 1);
194 }
195
196 #[gpui::test]
197 async fn test_handles_max_invocation_entries() {
198 let db = CommandPaletteDB(db::open_test_db("test_handles_max_invocation_entries").await);
199
200 for i in 1..=1001 {
201 db.write_command_invocation("some-command", &i.to_string())
202 .await
203 .unwrap();
204 }
205 let some_command = db.get_command_usage("some-command").unwrap();
206
207 assert!(some_command.is_some());
208 assert_eq!(some_command.expect("is some").invocations, 1000);
209 }
210}