feat: show SQL editor line numbers
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
- [ ] feature: ability to give the connection configs a custom name (control how they're displayed in the sidebar)
|
- [x] feature: ability to give the connection configs a custom name (control how they're displayed in the sidebar)
|
||||||
- [ ] feature: ability to remove a database connection
|
- [x] feature: ability to remove a database connection
|
||||||
- [ ] feature: line numbers in gutter of sql editor
|
- [x] feature: line numbers in gutter of sql editor
|
||||||
|
- [ ] bug: database type should display nothing instead of `-` when no database is connected. (it should only show the type when it's available)
|
||||||
+5
-5
@@ -723,14 +723,14 @@ mod tests {
|
|||||||
app.paste_query("SELECT '界', 42;\n-- comment");
|
app.paste_query("SELECT '界', 42;\n-- comment");
|
||||||
terminal.draw(|frame| app.draw(frame, frame.area(), Focus::Query))?;
|
terminal.draw(|frame| app.draw(frame, frame.area(), Focus::Query))?;
|
||||||
let buffer = terminal.backend().buffer();
|
let buffer = terminal.backend().buffer();
|
||||||
assert_eq!(buffer[(1, 1)].fg, Color::Cyan);
|
assert_eq!(buffer[(3, 1)].fg, Color::Cyan);
|
||||||
assert_eq!(buffer[(8, 1)].fg, Color::Green);
|
assert_eq!(buffer[(10, 1)].fg, Color::Green);
|
||||||
assert_eq!(buffer[(1, 2)].fg, Color::DarkGray);
|
assert_eq!(buffer[(3, 2)].fg, Color::DarkGray);
|
||||||
app.handle_query_key(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
|
app.handle_query_key(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
|
||||||
app.handle_query_key(KeyEvent::new(KeyCode::Char('\''), KeyModifiers::NONE));
|
app.handle_query_key(KeyEvent::new(KeyCode::Char('\''), KeyModifiers::NONE));
|
||||||
app.paste_query("unfinished\nstring");
|
app.paste_query("unfinished\nstring");
|
||||||
terminal.draw(|frame| app.draw(frame, frame.area(), Focus::Query))?;
|
terminal.draw(|frame| app.draw(frame, frame.area(), Focus::Query))?;
|
||||||
assert_eq!(terminal.backend().buffer()[(1, 2)].fg, Color::Green);
|
assert_eq!(terminal.backend().buffer()[(3, 2)].fg, Color::Green);
|
||||||
app.handle_query_key(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
|
app.handle_query_key(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
|
||||||
app.paste_query(&format!("{}--{}", "-- line\n".repeat(12), "x".repeat(90)));
|
app.paste_query(&format!("{}--{}", "-- line\n".repeat(12), "x".repeat(90)));
|
||||||
terminal.draw(|frame| app.draw(frame, frame.area(), Focus::Query))?;
|
terminal.draw(|frame| app.draw(frame, frame.area(), Focus::Query))?;
|
||||||
@@ -738,7 +738,7 @@ mod tests {
|
|||||||
let buffer = terminal.backend().buffer();
|
let buffer = terminal.backend().buffer();
|
||||||
assert_eq!(cursor.x, 58);
|
assert_eq!(cursor.x, 58);
|
||||||
assert_eq!(buffer[(cursor.x - 1, cursor.y)].symbol(), "x");
|
assert_eq!(buffer[(cursor.x - 1, cursor.y)].symbol(), "x");
|
||||||
assert_eq!(buffer[(1, cursor.y)].fg, Color::DarkGray);
|
assert_eq!(buffer[(4, cursor.y)].fg, Color::DarkGray);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+180
-39
@@ -101,9 +101,9 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
|||||||
workspace.y + (workspace.height - height) / 2, width, height);
|
workspace.y + (workspace.height - height) / 2, width, height);
|
||||||
frame.render_widget(Clear, area);
|
frame.render_widget(Clear, area);
|
||||||
frame.render_widget(Paragraph::new(format!(
|
frame.render_widget(Paragraph::new(format!(
|
||||||
"Remove saved connection \"{}\"?\nOnly the configuration is removed. The database and active session are kept.\n\ny: remove · n/Esc: cancel",
|
"Remove saved connection \"{}\"?\nOnly the configuration is removed. The database and active session are kept.",
|
||||||
profiles.entries[index].label()))
|
profiles.entries[index].label()))
|
||||||
.wrap(Wrap { trim: false }).block(Block::bordered().title(" Remove connection ")), area);
|
.wrap(Wrap { trim: false }).block(Block::bordered().title(" Remove connection ").title_bottom(" y: remove · n/Esc: cancel ")), area);
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
match event::read()? {
|
match event::read()? {
|
||||||
@@ -117,11 +117,21 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
|||||||
match profiles.remove(index) {
|
match profiles.remove(index) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
active = active.and_then(|active| {
|
active = active.and_then(|active| {
|
||||||
if active == index { None } else { Some(active - usize::from(active > index)) }
|
if active == index {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(active - usize::from(active > index))
|
||||||
|
}
|
||||||
});
|
});
|
||||||
let updated = explorer_entries(profiles.entries.len(), active,
|
let updated = explorer_entries(
|
||||||
browser.as_ref().map(|app| app.relations.len()), expanded);
|
profiles.entries.len(),
|
||||||
selection.select((!updated.is_empty()).then(|| selection.selected().unwrap_or(0).min(updated.len() - 1)));
|
active,
|
||||||
|
browser.as_ref().map(|app| app.relations.len()),
|
||||||
|
expanded,
|
||||||
|
);
|
||||||
|
selection.select((!updated.is_empty()).then(|| {
|
||||||
|
selection.selected().unwrap_or(0).min(updated.len() - 1)
|
||||||
|
}));
|
||||||
error = None;
|
error = None;
|
||||||
}
|
}
|
||||||
Err(failure) => error = Some(error_text(failure.as_ref())),
|
Err(failure) => error = Some(error_text(failure.as_ref())),
|
||||||
@@ -181,7 +191,10 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
profiles.save(&form).map(|()| {
|
profiles.save(&form).map(|()| {
|
||||||
let saved_index = profiles.entries.iter().position(|entry| entry.id == form.id)
|
let saved_index = profiles
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.position(|entry| entry.id == form.id)
|
||||||
.unwrap_or(profiles.entries.len() - 1);
|
.unwrap_or(profiles.entries.len() - 1);
|
||||||
let updated = explorer_entries(
|
let updated = explorer_entries(
|
||||||
profiles.entries.len(),
|
profiles.entries.len(),
|
||||||
@@ -190,10 +203,7 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
|||||||
expanded,
|
expanded,
|
||||||
);
|
);
|
||||||
selection.select(updated.iter().position(|entry| {
|
selection.select(updated.iter().position(|entry| {
|
||||||
*entry
|
*entry == ExplorerEntry::Database(Some(saved_index))
|
||||||
== ExplorerEntry::Database(Some(
|
|
||||||
saved_index,
|
|
||||||
))
|
|
||||||
}));
|
}));
|
||||||
focus = Focus::Explorer;
|
focus = Focus::Explorer;
|
||||||
})
|
})
|
||||||
@@ -242,13 +252,19 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
|||||||
show_form = true;
|
show_form = true;
|
||||||
}
|
}
|
||||||
KeyCode::Char('d') | KeyCode::Delete if focus == Focus::Explorer => {
|
KeyCode::Char('d') | KeyCode::Delete if focus == Focus::Explorer => {
|
||||||
remove = selection.selected().and_then(|index| match entries.get(index) {
|
remove =
|
||||||
|
selection
|
||||||
|
.selected()
|
||||||
|
.and_then(|index| match entries.get(index) {
|
||||||
Some(ExplorerEntry::Database(Some(index))) => Some(*index),
|
Some(ExplorerEntry::Database(Some(index))) => Some(*index),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
KeyCode::Char('e') if focus == Focus::Explorer => {
|
KeyCode::Char('e') if focus == Focus::Explorer => {
|
||||||
let index = selection.selected().and_then(|index| match entries.get(index) {
|
let index =
|
||||||
|
selection
|
||||||
|
.selected()
|
||||||
|
.and_then(|index| match entries.get(index) {
|
||||||
Some(ExplorerEntry::Database(Some(index))) => Some(*index),
|
Some(ExplorerEntry::Database(Some(index))) => Some(*index),
|
||||||
Some(ExplorerEntry::Relation(_)) => active,
|
Some(ExplorerEntry::Relation(_)) => active,
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -394,6 +410,8 @@ fn draw_help(frame: &mut Frame, screen: Rect, offset: &mut u16) {
|
|||||||
Left/Right, h/l Collapse/expand explorer or select cells\n\
|
Left/Right, h/l Collapse/expand explorer or select cells\n\
|
||||||
Enter Connect/expand database or open relation\n\
|
Enter Connect/expand database or open relation\n\
|
||||||
i Saved connection info on database or child\n\
|
i Saved connection info on database or child\n\
|
||||||
|
e Edit saved configuration/name on database or child\n\
|
||||||
|
d / Delete Remove saved database (y confirms, n/Esc cancels)\n\
|
||||||
s Toggle rows / column schema\n\
|
s Toggle rows / column schema\n\
|
||||||
n/p, PgDn/PgUp Next / previous 100-row relation page\n\
|
n/p, PgDn/PgUp Next / previous 100-row relation page\n\
|
||||||
r Refresh relations and active relation\n\n\
|
r Refresh relations and active relation\n\n\
|
||||||
@@ -414,10 +432,11 @@ fn draw_help(frame: &mut Frame, screen: Rect, offset: &mut u16) {
|
|||||||
Connection / password windows\n\
|
Connection / password windows\n\
|
||||||
Tab / Shift-Tab Change fields\n\
|
Tab / Shift-Tab Change fields\n\
|
||||||
Arrows Choose provider; Left/Right changes TLS\n\
|
Arrows Choose provider; Left/Right changes TLS\n\
|
||||||
Enter Edit provider / save new / connect saved\n\
|
Enter Edit provider / save settings / connect\n\
|
||||||
Ctrl-U / paste Clear / enter text in a focused field\n\
|
Ctrl-U / paste Clear / enter text in a focused field\n\
|
||||||
Esc Cancel and discard edits\n\
|
Esc Cancel and discard edits\n\
|
||||||
Saved settings update only after a successful connection.\n\
|
Optional Name sets the sidebar label; blank uses the default.\n\
|
||||||
|
Connect saves settings only on success; e edits save without connecting.\n\
|
||||||
Passwords are never saved.",
|
Passwords are never saved.",
|
||||||
)
|
)
|
||||||
.wrap(Wrap { trim: false });
|
.wrap(Wrap { trim: false });
|
||||||
@@ -596,8 +615,13 @@ impl Profiles {
|
|||||||
path TEXT NOT NULL
|
path TEXT NOT NULL
|
||||||
)",
|
)",
|
||||||
)?;
|
)?;
|
||||||
if !storage.prepare("SELECT name FROM pragma_table_info('connections') WHERE name = 'name'")?.exists([])? {
|
if !storage
|
||||||
storage.execute_batch("ALTER TABLE connections ADD COLUMN name TEXT NOT NULL DEFAULT ''")?;
|
.prepare("SELECT name FROM pragma_table_info('connections') WHERE name = 'name'")?
|
||||||
|
.exists([])?
|
||||||
|
{
|
||||||
|
storage.execute_batch(
|
||||||
|
"ALTER TABLE connections ADD COLUMN name TEXT NOT NULL DEFAULT ''",
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
let entries = storage.prepare("SELECT provider, host, port, database_name, username, tls, path, id, name FROM connections ORDER BY id")?
|
let entries = storage.prepare("SELECT provider, host, port, database_name, username, tls, path, id, name FROM connections ORDER BY id")?
|
||||||
.query_map([], |row| {
|
.query_map([], |row| {
|
||||||
@@ -666,7 +690,10 @@ impl Profiles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&mut self, index: usize) -> Result<()> {
|
fn remove(&mut self, index: usize) -> Result<()> {
|
||||||
self.storage.execute("DELETE FROM connections WHERE id = ?1", [self.entries[index].id])?;
|
self.storage.execute(
|
||||||
|
"DELETE FROM connections WHERE id = ?1",
|
||||||
|
[self.entries[index].id],
|
||||||
|
)?;
|
||||||
self.entries.remove(index);
|
self.entries.remove(index);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -703,7 +730,11 @@ impl Default for Form {
|
|||||||
|
|
||||||
impl Form {
|
impl Form {
|
||||||
fn name_focus(&self) -> usize {
|
fn name_focus(&self) -> usize {
|
||||||
if self.provider == 1 { 2 } else { 6 + usize::from(self.reconnect) }
|
if self.provider == 1 {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
6 + usize::from(self.reconnect)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect(&self) -> Result<Database> {
|
fn connect(&self) -> Result<Database> {
|
||||||
@@ -910,8 +941,16 @@ impl Form {
|
|||||||
.style(Style::default().fg(Color::Cyan)),
|
.style(Style::default().fg(Color::Cyan)),
|
||||||
header,
|
header,
|
||||||
);
|
);
|
||||||
let [name_area, body] = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).areas(body);
|
let [name_area, body] =
|
||||||
self.name.draw(frame, name_area, " Name (optional) ", false, self.focus == self.name_focus(), None);
|
Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).areas(body);
|
||||||
|
self.name.draw(
|
||||||
|
frame,
|
||||||
|
name_area,
|
||||||
|
" Name (optional) ",
|
||||||
|
false,
|
||||||
|
self.focus == self.name_focus(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
let [providers, details] =
|
let [providers, details] =
|
||||||
Layout::horizontal([Constraint::Length(24), Constraint::Min(1)]).areas(body);
|
Layout::horizontal([Constraint::Length(24), Constraint::Min(1)]).areas(body);
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
@@ -1245,36 +1284,62 @@ impl Input {
|
|||||||
focused: bool,
|
focused: bool,
|
||||||
highlighted: Option<Text<'_>>,
|
highlighted: Option<Text<'_>>,
|
||||||
) {
|
) {
|
||||||
|
let numbered = highlighted.is_some();
|
||||||
let text = if secret {
|
let text = if secret {
|
||||||
Text::raw("•".repeat(self.value.chars().count()))
|
Text::raw("•".repeat(self.value.chars().count()))
|
||||||
} else {
|
} else {
|
||||||
highlighted.unwrap_or_else(|| Text::raw(&self.value))
|
highlighted.unwrap_or_else(|| Text::raw(&self.value))
|
||||||
};
|
};
|
||||||
|
let line_count = text.lines.len().max(1);
|
||||||
|
let block = Block::bordered()
|
||||||
|
.title(label)
|
||||||
|
.border_style(Style::default().fg(if focused {
|
||||||
|
Color::Cyan
|
||||||
|
} else {
|
||||||
|
Color::DarkGray
|
||||||
|
}));
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
let gutter_width = if numbered {
|
||||||
|
(line_count.ilog10() as u16 + 2).min(inner.width.saturating_sub(1))
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let content = Rect::new(
|
||||||
|
inner.x + gutter_width,
|
||||||
|
inner.y,
|
||||||
|
inner.width - gutter_width,
|
||||||
|
inner.height,
|
||||||
|
);
|
||||||
let prefix = &self.value[..self.cursor];
|
let prefix = &self.value[..self.cursor];
|
||||||
let row = u16::try_from(prefix.matches('\n').count()).unwrap_or(u16::MAX);
|
let row = u16::try_from(prefix.matches('\n').count()).unwrap_or(u16::MAX);
|
||||||
let row_offset = row.saturating_sub(area.height.saturating_sub(3));
|
let row_offset = row.saturating_sub(content.height.saturating_sub(1));
|
||||||
let before_cursor = if secret {
|
let before_cursor = if secret {
|
||||||
self.value[..self.cursor].chars().count()
|
self.value[..self.cursor].chars().count()
|
||||||
} else {
|
} else {
|
||||||
Line::raw(prefix.rsplit('\n').next().unwrap_or("")).width()
|
Line::raw(prefix.rsplit('\n').next().unwrap_or("")).width()
|
||||||
};
|
};
|
||||||
let cursor = u16::try_from(before_cursor).unwrap_or(u16::MAX);
|
let cursor = u16::try_from(before_cursor).unwrap_or(u16::MAX);
|
||||||
let offset = cursor.saturating_sub(area.width.saturating_sub(3));
|
let offset = cursor.saturating_sub(content.width.saturating_sub(1));
|
||||||
|
frame.render_widget(Paragraph::new(text).scroll((row_offset, offset)), content);
|
||||||
|
if gutter_width > 0 {
|
||||||
|
let lines = (usize::from(row_offset)..line_count)
|
||||||
|
.take(usize::from(inner.height))
|
||||||
|
.map(|line| {
|
||||||
|
Line::raw(format!(
|
||||||
|
"{:>width$} ",
|
||||||
|
line + 1,
|
||||||
|
width = usize::from(gutter_width - 1)
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(text).scroll((row_offset, offset)).block(
|
Paragraph::new(lines).style(Style::default().fg(Color::DarkGray)),
|
||||||
Block::bordered()
|
Rect::new(inner.x, inner.y, gutter_width, inner.height),
|
||||||
.title(label)
|
|
||||||
.border_style(Style::default().fg(if focused {
|
|
||||||
Color::Cyan
|
|
||||||
} else {
|
|
||||||
Color::DarkGray
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
area,
|
|
||||||
);
|
);
|
||||||
if focused && area.width > 2 && area.height > 2 {
|
}
|
||||||
frame
|
if focused && content.width > 0 && content.height > 0 {
|
||||||
.set_cursor_position((area.x + 1 + cursor - offset, area.y + 1 + row - row_offset));
|
frame.set_cursor_position((content.x + cursor - offset, content.y + row - row_offset));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1282,6 +1347,71 @@ impl Input {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn sql_gutter_tracks_scrolling_digits_and_cursor_without_entering_sql() -> Result<()> {
|
||||||
|
use ratatui::{Terminal, backend::TestBackend};
|
||||||
|
let source = (1..=12)
|
||||||
|
.map(|line| format!("SELECT {line}, '界abcdefghijklmnop';"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
let mut input = Input::from(source.as_str());
|
||||||
|
let mut terminal = Terminal::new(TestBackend::new(22, 5))?;
|
||||||
|
terminal.draw(|frame| {
|
||||||
|
input.draw(
|
||||||
|
frame,
|
||||||
|
frame.area(),
|
||||||
|
"Query",
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
Some(crate::sql::text(
|
||||||
|
&input.value,
|
||||||
|
&crate::sql::highlight(&input.value, true),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let buffer = terminal.backend().buffer();
|
||||||
|
for (row, number) in [(1, "10"), (2, "11"), (3, "12")] {
|
||||||
|
let gutter: String = (1..=2).map(|x| buffer[(x, row)].symbol()).collect();
|
||||||
|
assert_eq!(gutter, number);
|
||||||
|
}
|
||||||
|
assert_eq!(terminal.get_cursor_position()?.x, 20);
|
||||||
|
input.paste_multiline("\n");
|
||||||
|
terminal.draw(|frame| {
|
||||||
|
input.draw(
|
||||||
|
frame,
|
||||||
|
frame.area(),
|
||||||
|
"Query",
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
Some(crate::sql::text(
|
||||||
|
&input.value,
|
||||||
|
&crate::sql::highlight(&input.value, true),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
assert_eq!(terminal.backend().buffer()[(1, 3)].symbol(), "1");
|
||||||
|
assert_eq!(terminal.backend().buffer()[(2, 3)].symbol(), "3");
|
||||||
|
assert_eq!(terminal.get_cursor_position()?, (4, 3).into());
|
||||||
|
input.vim_motion('g');
|
||||||
|
terminal.draw(|frame| {
|
||||||
|
input.draw(
|
||||||
|
frame,
|
||||||
|
frame.area(),
|
||||||
|
"Query",
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
Some(crate::sql::text(
|
||||||
|
&input.value,
|
||||||
|
&crate::sql::highlight(&input.value, true),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
assert_eq!(terminal.backend().buffer()[(2, 1)].symbol(), "1");
|
||||||
|
assert_eq!(terminal.backend().buffer()[(4, 1)].symbol(), "S");
|
||||||
|
assert_eq!(terminal.get_cursor_position()?, (4, 1).into());
|
||||||
|
assert_eq!(input.value, format!("{source}\n"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keybindings_scroll_to_the_end_and_clamp_after_resize() -> Result<()> {
|
fn keybindings_scroll_to_the_end_and_clamp_after_resize() -> Result<()> {
|
||||||
@@ -1629,7 +1759,10 @@ mod tests {
|
|||||||
let restored = &profiles.entries[0];
|
let restored = &profiles.entries[0];
|
||||||
assert_eq!(restored.tls, 2);
|
assert_eq!(restored.tls, 2);
|
||||||
assert_eq!(restored.label(), "Warehouse");
|
assert_eq!(restored.label(), "Warehouse");
|
||||||
assert_eq!(restored.connection_label(), "warehouse · inspector@db.example:5440");
|
assert_eq!(
|
||||||
|
restored.connection_label(),
|
||||||
|
"warehouse · inspector@db.example:5440"
|
||||||
|
);
|
||||||
for (input, expected) in
|
for (input, expected) in
|
||||||
restored
|
restored
|
||||||
.postgres
|
.postgres
|
||||||
@@ -1651,7 +1784,11 @@ mod tests {
|
|||||||
let directory = tempfile::tempdir()?;
|
let directory = tempfile::tempdir()?;
|
||||||
let path = directory.path().join("connections.sqlite3");
|
let path = directory.path().join("connections.sqlite3");
|
||||||
let mut profiles = Profiles::open(&path)?;
|
let mut profiles = Profiles::open(&path)?;
|
||||||
let mut form = Form { provider: 1, path: Input::from("/tmp/example.sqlite"), ..Form::default() };
|
let mut form = Form {
|
||||||
|
provider: 1,
|
||||||
|
path: Input::from("/tmp/example.sqlite"),
|
||||||
|
..Form::default()
|
||||||
|
};
|
||||||
form.focus = form.name_focus();
|
form.focus = form.name_focus();
|
||||||
form.input_mut().unwrap().paste(" Local data ");
|
form.input_mut().unwrap().paste(" Local data ");
|
||||||
profiles.save(&form)?;
|
profiles.save(&form)?;
|
||||||
@@ -1676,7 +1813,11 @@ mod tests {
|
|||||||
std::fs::write(&database, contents)?;
|
std::fs::write(&database, contents)?;
|
||||||
let path = directory.path().join("profiles.sqlite");
|
let path = directory.path().join("profiles.sqlite");
|
||||||
let mut profiles = Profiles::open(&path)?;
|
let mut profiles = Profiles::open(&path)?;
|
||||||
let form = Form { provider: 1, path: Input::from(database.to_str().unwrap()), ..Form::default() };
|
let form = Form {
|
||||||
|
provider: 1,
|
||||||
|
path: Input::from(database.to_str().unwrap()),
|
||||||
|
..Form::default()
|
||||||
|
};
|
||||||
profiles.save(&form)?;
|
profiles.save(&form)?;
|
||||||
profiles.save(&form)?;
|
profiles.save(&form)?;
|
||||||
let survivor = profiles.entries[1].id;
|
let survivor = profiles.entries[1].id;
|
||||||
|
|||||||
+4
-2
@@ -47,18 +47,20 @@ Navigation:
|
|||||||
Works on a saved database entry or any of its table/view children.
|
Works on a saved database entry or any of its table/view children.
|
||||||
r Refresh relations and the active relation
|
r Refresh relations and the active relation
|
||||||
Connected databases have a trailing * and show their tables/views beneath them.
|
Connected databases have a trailing * and show their tables/views beneath them.
|
||||||
SQLite sidebar entries show only the filename; the info window shows the full path.
|
Unnamed SQLite entries show only the filename; the info window shows the full path.
|
||||||
Enter on the connected database collapses/expands it without reconnecting.
|
Enter on the connected database collapses/expands it without reconnecting.
|
||||||
Keybindings float above the workspace; Up/Down/jk scroll, Home/End/g/G jump.
|
Keybindings float above the workspace; Up/Down/jk scroll, Home/End/g/G jump.
|
||||||
Query Insert mode and connection/password fields keep ? as literal text.
|
Query Insert mode and connection/password fields keep ? as literal text.
|
||||||
Full-width bars show dbdash/READ ONLY above and mode/database below.
|
Full-width bars show dbdash/READ ONLY above and mode/database below.
|
||||||
Saved PostgreSQL connections show database · user@host:port beside the mode,
|
Saved PostgreSQL connections show database · user@host:port beside the mode,
|
||||||
matching the sidebar; passwords are never included.
|
regardless of custom sidebar names; passwords are never included.
|
||||||
The database provider sits immediately left of the bottom-right position badge.
|
The database provider sits immediately left of the bottom-right position badge.
|
||||||
Only badges have colored backgrounds; the remaining bar area uses terminal defaults.
|
Only badges have colored backgrounds; the remaining bar area uses terminal defaults.
|
||||||
Focused inputs show one-based line:column (Unicode characters, not screen cells).
|
Focused inputs show one-based line:column (Unicode characters, not screen cells).
|
||||||
The position badge shows N/A outside focused inputs, including in info/help popups.
|
The position badge shows N/A outside focused inputs, including in info/help popups.
|
||||||
The query editor sits above rows/results; the explorer spans both panes.
|
The query editor sits above rows/results; the explorer spans both panes.
|
||||||
|
The SQL editor has a one-based line-number gutter that follows vertical scrolling;
|
||||||
|
horizontal scrolling moves only SQL, not the numbers or logical cursor position.
|
||||||
Explorer and rows/schema bottom borders align; table names appear in the
|
Explorer and rows/schema bottom borders align; table names appear in the
|
||||||
top-right border, with page/column status below. Read errors appear above the query editor.
|
top-right border, with page/column status below. Read errors appear above the query editor.
|
||||||
Query editor starts in NORMAL: i/a/I/A enters INSERT; hjkl/arrows move.
|
Query editor starts in NORMAL: i/a/I/A enters INSERT; hjkl/arrows move.
|
||||||
|
|||||||
Reference in New Issue
Block a user