1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
//! SQL query editor pane.
//!
//! A code editor with SQL highlighting that hands the statement under the
//! caret, the selection, or the whole buffer to the session, which runs it and
//! can cancel it. Completion from live introspection is still ahead (TODO.md
//! section 3).
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::input::{Editor, EditorState};
use gpui_kit::component::{ActiveTheme, Disableable, IconName, Sizable, h_flex, v_flex};
use gpui_kit::prelude::*;
use gpui_kit::{Context, Entity, EventEmitter, Window, actions, div};
use gpui_kit::assets::IconName as AssetIcon;
use std::cell::RefCell;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use crate::db::{Engine, statement};
use crate::settings;
use crate::ui::session::{OpenFile, SaveFile};
actions!(zippa_db, [RunQuery, RunScript, Explain, ExplainAnalyze]);
pub enum QueryEditorEvent {
/// The user asked to run one statement: what is selected, or the one the
/// caret is in.
Run(String),
/// The user asked to run the whole buffer, statement by statement.
RunScript(String),
/// The user asked for the statement's plan. `analyze` asks the server to
/// run it so the plan carries actual times.
Explain { sql: String, analyze: bool },
/// The user asked to read a SQL file into a tab.
Open,
/// The user asked to write this buffer to its file.
Save,
}
pub struct QueryEditor {
state: Entity<EditorState>,
running: bool,
/// The engine this buffer runs against, so the analyze button can say when
/// the server has no `EXPLAIN ANALYZE`.
engine: Engine,
/// Memoized `statement` plus its write/read judgement. Both clone the
/// whole buffer and re-split its statements, a hitch on every render
/// with a multi-MB buffer; the key is a hash of the buffer, cursor, and
/// selection, so an untouched buffer answers without touching the text.
statement_cache: RefCell<Option<StatementCache>>,
}
struct StatementCache {
key: u64,
statement: Option<String>,
writes: bool,
}
impl EventEmitter<QueryEditorEvent> for QueryEditor {}
impl QueryEditor {
/// Open an editor that already holds `sql`, as when a table is opened from
/// the sidebar.
pub fn with_text(
sql: impl Into<String>,
engine: Engine,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let sql = sql.into();
let state = cx.new(|cx| {
EditorState::new(window, cx)
.language("sql")
.placeholder("SELECT * FROM …")
.default_value(sql)
});
Self {
state,
running: false,
engine,
statement_cache: RefCell::new(None),
}
}
/// Put the caret in the editor, as clicking it does.
#[cfg(test)]
pub fn focus(&self, window: &mut Window, cx: &mut Context<Self>) {
let handle = self.focus_handle(cx);
handle.focus(window, cx);
}
/// The editor's own focus handle, so its owner can hand it to a dock
/// panel without going through a window.
pub fn focus_handle(&self, cx: &gpui_kit::App) -> gpui_kit::FocusHandle {
use gpui_kit::Focusable as _;
self.state.read(cx).focus_handle(cx)
}
/// Replace the buffer contents.
#[cfg(test)]
pub fn set_sql(&self, sql: &str, window: &mut Window, cx: &mut Context<Self>) {
self.state
.update(cx, |state, cx| state.set_value(sql.to_string(), window, cx));
}
/// Put the caret at a byte offset, the way clicking in the buffer does.
#[cfg(test)]
pub(crate) fn set_cursor_for_test(&self, offset: usize, cx: &mut Context<Self>) {
self.state
.update(cx, |state, cx| state.set_selected_range(offset..offset, cx));
}
/// Select a range, the way dragging across the buffer does.
#[cfg(test)]
pub(crate) fn select_for_test(&self, range: std::ops::Range<usize>, cx: &mut Context<Self>) {
self.state
.update(cx, |state, cx| state.set_selected_range(range, cx));
}
/// The statement a run would send, for a test to read.
#[cfg(test)]
pub(crate) fn statement_for_test(&self, cx: &gpui_kit::App) -> Option<String> {
self.statement(cx)
}
/// The statement currently in the buffer.
pub fn sql(&self, cx: &gpui_kit::App) -> String {
self.state.read(cx).value().to_string()
}
/// Whether the analyze button would be disabled, so a test can check the
/// gate without reading button state out of the window.
#[cfg(test)]
pub(crate) fn analyze_disabled_for_test(&self, cx: &gpui_kit::App) -> bool {
self.running || self.statement_writes(cx) || !self.analyze_supported()
}
/// Whether this engine can report actual times. SQLite's planner cannot, so
/// its analyze button is disabled rather than showing the plain plan under
/// a button that promises timings.
pub(crate) fn analyze_supported(&self) -> bool {
!matches!(self.engine, Engine::Sqlite)
}
pub fn set_running(&mut self, running: bool, cx: &mut Context<Self>) {
self.running = running;
cx.notify();
}
fn run(&mut self, _: &RunQuery, _window: &mut Window, cx: &mut Context<Self>) {
self.emit_run(cx);
}
fn run_script(&mut self, _: &RunScript, _window: &mut Window, cx: &mut Context<Self>) {
self.emit_run_script(cx);
}
fn explain(&mut self, _: &Explain, _window: &mut Window, cx: &mut Context<Self>) {
self.emit_explain(false, cx);
}
fn explain_analyze(
&mut self,
_: &ExplainAnalyze,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.emit_explain(true, cx);
}
/// Run what is selected, or the statement the caret is in.
fn emit_run(&mut self, cx: &mut Context<Self>) {
if self.running {
return;
}
let Some(sql) = self.statement(cx) else {
return;
};
cx.emit(QueryEditorEvent::Run(sql));
}
/// Run the whole buffer, statement by statement.
fn emit_run_script(&mut self, cx: &mut Context<Self>) {
if self.running {
return;
}
let sql = self.state.read(cx).value().to_string();
if sql.trim().is_empty() {
return;
}
cx.emit(QueryEditorEvent::RunScript(sql));
}
/// Ask for the current statement's plan.
///
/// Also how the session reaches this from outside the editor's own
/// shortcut — the quick switcher's Explain items.
pub(crate) fn emit_explain(&mut self, analyze: bool, cx: &mut Context<Self>) {
if self.running {
return;
}
let Some(sql) = self.statement(cx) else {
return;
};
cx.emit(QueryEditorEvent::Explain { sql, analyze });
}
/// Whether the statement a run would send is one that changes data.
///
/// The plan of a write can be read, but asking the server to run it for
/// actual times cannot, so the analyze button is disabled and says why.
pub(crate) fn statement_writes(&self, cx: &gpui_kit::App) -> bool {
self.cached_statement(cx).1
}
/// The one statement a run should send.
///
/// A selection is taken as written — it may be a fragment, or several
/// statements, and running it verbatim is what selecting it asked for.
/// Otherwise the statement the caret is in is the one that runs.
fn statement(&self, cx: &gpui_kit::App) -> Option<String> {
self.cached_statement(cx).0
}
/// The statement a run would send, plus whether it writes — memoized.
///
/// Finding the statement clones the whole buffer and re-splits it, a
/// hitch on every render with a multi-MB buffer. The key hashes the
/// buffer's chunks (borrowed, never copied) together with the cursor
/// and selection, so an untouched buffer answers from the cache.
fn cached_statement(&self, cx: &gpui_kit::App) -> (Option<String>, bool) {
let key = {
let state = self.state.read(cx);
let mut hasher = DefaultHasher::new();
for chunk in state.text().chunks() {
chunk.hash(&mut hasher);
}
state.cursor().hash(&mut hasher);
state.selected_range().hash(&mut hasher);
hasher.finish()
};
if let Some(cached) = self.statement_cache.borrow().as_ref()
&& cached.key == key
{
return (cached.statement.clone(), cached.writes);
}
let statement = self.compute_statement(cx);
let writes = statement.as_ref().is_some_and(|sql| {
// A statement with its own `EXPLAIN` header is judged by what it
// explains, so `EXPLAIN ANALYZE SELECT` still reads as a read.
let inner = statement::explained(sql)
.map(|(inner, _)| inner)
.unwrap_or_else(|| sql.clone());
statement::first_write(&inner).is_some()
});
*self.statement_cache.borrow_mut() = Some(StatementCache {
key,
statement: statement.clone(),
writes,
});
(statement, writes)
}
/// The uncached statement: the selection verbatim, else the statement
/// the caret is in.
fn compute_statement(&self, cx: &gpui_kit::App) -> Option<String> {
let state = self.state.read(cx);
let selected = state.selected_value().to_string();
if !selected.trim().is_empty() {
return Some(selected);
}
let sql = state.value().to_string();
statement::at_cursor(&sql, state.cursor()).map(|statement| statement.text)
}
}
impl Render for QueryEditor {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let writes = self.statement_writes(cx);
let can_analyze = self.analyze_supported();
v_flex()
.key_context("QueryEditor")
.on_action(cx.listener(Self::run))
.on_action(cx.listener(Self::run_script))
.on_action(cx.listener(Self::explain))
.on_action(cx.listener(Self::explain_analyze))
.size_full()
.border_b_1()
.border_color(cx.theme().border)
.child(
h_flex()
.px_2()
.py_1()
.gap_2()
.justify_between()
.child(
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("QUERY"),
)
.child(
h_flex()
.gap_1()
.child(
Button::new("open-file")
.ghost()
.small()
.icon(IconName::FolderOpen)
.accessibility_label("Open a SQL file")
.tooltip_with_action(
"Open a SQL file",
&OpenFile,
Some("Session"),
)
.on_click(cx.listener(|_this, _, _window, cx| {
cx.emit(QueryEditorEvent::Open)
})),
)
.child(
Button::new("save-file")
.ghost()
.small()
.icon(IconName::HardDrive)
.accessibility_label("Save to a SQL file")
.tooltip_with_action(
"Save to a SQL file",
&SaveFile,
Some("Session"),
)
.on_click(cx.listener(|_this, _, _window, cx| {
cx.emit(QueryEditorEvent::Save)
})),
)
.child(
Button::new("explain")
.ghost()
.small()
.icon(AssetIcon::Route)
.accessibility_label("Explain the statement")
.tooltip_with_action(
"Show the statement's plan without running it",
&Explain,
Some("QueryEditor"),
)
.disabled(self.running)
.on_click(cx.listener(|this, _, _window, cx| {
this.emit_explain(false, cx)
})),
)
.child(
Button::new("explain-analyze")
.ghost()
.small()
.icon(AssetIcon::Gauge)
.accessibility_label("Explain the statement and run it")
.tooltip_with_action(
if !can_analyze {
"SQLite has no EXPLAIN ANALYZE; actual times are not available"
} else if writes {
"Analyze runs the query; this statement changes data"
} else {
"Explain the statement and run it for actual times"
},
&ExplainAnalyze,
Some("QueryEditor"),
)
.disabled(self.running || writes || !can_analyze)
.on_click(cx.listener(|this, _, _window, cx| {
this.emit_explain(true, cx)
})),
)
.child(
Button::new("run-script")
.ghost()
.small()
.icon(IconName::SquareTerminal)
.accessibility_label("Run every statement in the buffer")
.tooltip_with_action(
"Run every statement in the buffer",
&RunScript,
Some("QueryEditor"),
)
.disabled(self.running)
.on_click(
cx.listener(|this, _, _window, cx| {
this.emit_run_script(cx)
}),
),
)
.child(
Button::new("run")
.primary()
.small()
.icon(IconName::Play)
.label(if self.running { "Running…" } else { "Run" })
.tooltip_with_action(
"Run the selection, or the statement the caret is in",
&RunQuery,
Some("QueryEditor"),
)
.disabled(self.running)
.on_click(
cx.listener(|this, _, _window, cx| this.emit_run(cx)),
),
),
),
)
.child(
Editor::new(&self.state)
.h_full()
.appearance(false)
.border_t_1()
.border_color(cx.theme().border)
// Refines over the editor's own monospace default.
.font_family(settings::editor_font(cx)),
)
}
}