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 query! {
128 pub fn list_recent_queries() -> Result<Vec<String>> {
129 SELECT user_query
130 FROM command_invocations
131 WHERE user_query != ""
132 GROUP BY user_query
133 ORDER BY MAX(last_invoked) ASC
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140
141 use crate::persistence::{CommandPaletteDB, SerializedCommandUsage};
142
143 #[gpui::test]
144 async fn test_saves_and_retrieves_command_invocation() {
145 let db =
146 CommandPaletteDB::open_test_db("test_saves_and_retrieves_command_invocation").await;
147
148 let retrieved_cmd = db.get_last_invoked("editor: backspace").unwrap();
149
150 assert!(retrieved_cmd.is_none());
151
152 db.write_command_invocation("editor: backspace", "")
153 .await
154 .unwrap();
155
156 let retrieved_cmd = db.get_last_invoked("editor: backspace").unwrap();
157
158 assert!(retrieved_cmd.is_some());
159 let retrieved_cmd = retrieved_cmd.expect("is some");
160 assert_eq!(retrieved_cmd.command_name, "editor: backspace".to_string());
161 assert_eq!(retrieved_cmd.user_query, "".to_string());
162 }
163
164 #[gpui::test]
165 async fn test_gets_usage_history() {
166 let db = CommandPaletteDB::open_test_db("test_gets_usage_history").await;
167 db.write_command_invocation("go to line: toggle", "200")
168 .await
169 .unwrap();
170 db.write_command_invocation("go to line: toggle", "201")
171 .await
172 .unwrap();
173
174 let retrieved_cmd = db.get_last_invoked("go to line: toggle").unwrap();
175
176 assert!(retrieved_cmd.is_some());
177 let retrieved_cmd = retrieved_cmd.expect("is some");
178
179 let command_usage = db.get_command_usage("go to line: toggle").unwrap();
180
181 assert!(command_usage.is_some());
182 let command_usage: SerializedCommandUsage = command_usage.expect("is some");
183
184 assert_eq!(command_usage.command_name, "go to line: toggle");
185 assert_eq!(command_usage.invocations, 2);
186 assert_eq!(command_usage.last_invoked, retrieved_cmd.last_invoked);
187 }
188
189 #[gpui::test]
190 async fn test_lists_ordered_by_usage() {
191 let db = CommandPaletteDB::open_test_db("test_lists_ordered_by_usage").await;
192
193 let empty_commands = db.list_commands_used();
194 match &empty_commands {
195 Ok(_) => (),
196 Err(e) => println!("Error: {:?}", e),
197 }
198 assert!(empty_commands.is_ok());
199 assert_eq!(empty_commands.expect("is ok").len(), 0);
200
201 db.write_command_invocation("go to line: toggle", "200")
202 .await
203 .unwrap();
204 db.write_command_invocation("editor: backspace", "")
205 .await
206 .unwrap();
207 db.write_command_invocation("editor: backspace", "")
208 .await
209 .unwrap();
210
211 let commands = db.list_commands_used();
212
213 assert!(commands.is_ok());
214 let commands = commands.expect("is ok");
215 assert_eq!(commands.len(), 2);
216 assert_eq!(commands.as_slice()[0].command_name, "editor: backspace");
217 assert_eq!(commands.as_slice()[0].invocations, 2);
218 assert_eq!(commands.as_slice()[1].command_name, "go to line: toggle");
219 assert_eq!(commands.as_slice()[1].invocations, 1);
220 }
221
222 #[gpui::test]
223 async fn test_handles_max_invocation_entries() {
224 let db = CommandPaletteDB::open_test_db("test_handles_max_invocation_entries").await;
225
226 for i in 1..=1001 {
227 db.write_command_invocation("some-command", &i.to_string())
228 .await
229 .unwrap();
230 }
231 let some_command = db.get_command_usage("some-command").unwrap();
232
233 assert!(some_command.is_some());
234 assert_eq!(some_command.expect("is some").invocations, 1000);
235 }
236}