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