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}