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}