mirror of https://github.com/TaKO8Ki/gobang
Compare commits
No commits in common. 'main' and 'v0.1.0-alpha.1' have entirely different histories.
main
...
v0.1.0-alp
@ -1 +0,0 @@
|
||||
github: TaKO8Ki
|
@ -1,15 +0,0 @@
|
||||
Thank you for making gobang better!
|
||||
|
||||
Here's a checklist for things that will be checked during review or continuous integration.
|
||||
|
||||
- \[ ] Added passing unit tests
|
||||
- \[ ] `cargo test` passes locally. It takes much time.
|
||||
- \[ ] Run `cargo fmt`
|
||||
|
||||
Delete this line and everything above before opening your PR.
|
||||
|
||||
---
|
||||
|
||||
*Please write a short comment explaining your change (or "none" for internal only changes)*
|
||||
|
||||
changelog:
|
File diff suppressed because it is too large
Load Diff
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Takayuki Maeda
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
Binary file not shown.
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 8.3 MiB |
Binary file not shown.
Before Width: | Height: | Size: 9.5 KiB |
@ -1,191 +0,0 @@
|
||||
use super::{Component, EventState, MovableComponent};
|
||||
use crate::components::command::CommandInfo;
|
||||
use crate::config::KeyConfig;
|
||||
use crate::event::Key;
|
||||
use anyhow::Result;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Clear, List, ListItem, ListState},
|
||||
Frame,
|
||||
};
|
||||
|
||||
const RESERVED_WORDS_IN_WHERE_CLAUSE: &[&str] = &["IN", "AND", "OR", "NOT", "NULL", "IS"];
|
||||
const ALL_RESERVED_WORDS: &[&str] = &[
|
||||
"IN", "AND", "OR", "NOT", "NULL", "IS", "SELECT", "UPDATE", "DELETE", "FROM", "LIMIT", "WHERE",
|
||||
];
|
||||
|
||||
pub struct CompletionComponent {
|
||||
key_config: KeyConfig,
|
||||
state: ListState,
|
||||
word: String,
|
||||
candidates: Vec<String>,
|
||||
}
|
||||
|
||||
impl CompletionComponent {
|
||||
pub fn new(key_config: KeyConfig, word: impl Into<String>, all: bool) -> Self {
|
||||
Self {
|
||||
key_config,
|
||||
state: ListState::default(),
|
||||
word: word.into(),
|
||||
candidates: if all {
|
||||
ALL_RESERVED_WORDS.iter().map(|w| w.to_string()).collect()
|
||||
} else {
|
||||
RESERVED_WORDS_IN_WHERE_CLAUSE
|
||||
.iter()
|
||||
.map(|w| w.to_string())
|
||||
.collect()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, word: impl Into<String>) {
|
||||
self.word = word.into();
|
||||
self.state.select(None);
|
||||
self.state.select(Some(0))
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.filterd_candidates().count() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.filterd_candidates().count() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn filterd_candidates(&self) -> impl Iterator<Item = &String> {
|
||||
self.candidates.iter().filter(move |c| {
|
||||
(c.starts_with(self.word.to_lowercase().as_str())
|
||||
|| c.starts_with(self.word.to_uppercase().as_str()))
|
||||
&& !self.word.is_empty()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn selected_candidate(&self) -> Option<String> {
|
||||
self.filterd_candidates()
|
||||
.collect::<Vec<&String>>()
|
||||
.get(self.state.selected()?)
|
||||
.map(|c| c.to_string())
|
||||
}
|
||||
|
||||
pub fn word(&self) -> String {
|
||||
self.word.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl MovableComponent for CompletionComponent {
|
||||
fn draw<B: Backend>(
|
||||
&mut self,
|
||||
f: &mut Frame<B>,
|
||||
area: Rect,
|
||||
_focused: bool,
|
||||
x: u16,
|
||||
y: u16,
|
||||
) -> Result<()> {
|
||||
if !self.word.is_empty() {
|
||||
let width = 30;
|
||||
let candidates = self
|
||||
.filterd_candidates()
|
||||
.map(|c| ListItem::new(c.to_string()))
|
||||
.collect::<Vec<ListItem>>();
|
||||
if candidates.clone().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let candidate_list = List::new(candidates.clone())
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.highlight_style(Style::default().bg(Color::Blue))
|
||||
.style(Style::default());
|
||||
|
||||
let area = Rect::new(
|
||||
area.x + x,
|
||||
area.y + y + 2,
|
||||
width
|
||||
.min(f.size().width)
|
||||
.min(f.size().right().saturating_sub(area.x + x)),
|
||||
(candidates.len().min(5) as u16 + 2)
|
||||
.min(f.size().bottom().saturating_sub(area.y + y + 2)),
|
||||
);
|
||||
f.render_widget(Clear, area);
|
||||
f.render_stateful_widget(candidate_list, area, &mut self.state);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CompletionComponent {
|
||||
fn commands(&self, _out: &mut Vec<CommandInfo>) {}
|
||||
|
||||
fn event(&mut self, key: Key) -> Result<EventState> {
|
||||
if key == self.key_config.move_down {
|
||||
self.next();
|
||||
return Ok(EventState::Consumed);
|
||||
} else if key == self.key_config.move_up {
|
||||
self.previous();
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
Ok(EventState::NotConsumed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{CompletionComponent, KeyConfig};
|
||||
|
||||
#[test]
|
||||
fn test_filterd_candidates_lowercase() {
|
||||
assert_eq!(
|
||||
CompletionComponent::new(KeyConfig::default(), "an", false)
|
||||
.filterd_candidates()
|
||||
.collect::<Vec<&String>>(),
|
||||
vec![&"AND".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filterd_candidates_uppercase() {
|
||||
assert_eq!(
|
||||
CompletionComponent::new(KeyConfig::default(), "AN", false)
|
||||
.filterd_candidates()
|
||||
.collect::<Vec<&String>>(),
|
||||
vec![&"AND".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filterd_candidates_multiple_candidates() {
|
||||
assert_eq!(
|
||||
CompletionComponent::new(KeyConfig::default(), "n", false)
|
||||
.filterd_candidates()
|
||||
.collect::<Vec<&String>>(),
|
||||
vec![&"NOT".to_string(), &"NULL".to_string()]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
CompletionComponent::new(KeyConfig::default(), "N", false)
|
||||
.filterd_candidates()
|
||||
.collect::<Vec<&String>>(),
|
||||
vec![&"NOT".to_string(), &"NULL".to_string()]
|
||||
);
|
||||
}
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
use super::{compute_character_width, Component, DrawableComponent, EventState};
|
||||
use crate::components::command::CommandInfo;
|
||||
use crate::event::Key;
|
||||
use anyhow::Result;
|
||||
use database_tree::Table;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
text::Spans,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub struct DatabaseFilterComponent {
|
||||
pub table: Option<Table>,
|
||||
input: Vec<char>,
|
||||
input_idx: usize,
|
||||
input_cursor_position: u16,
|
||||
}
|
||||
|
||||
impl DatabaseFilterComponent {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
table: None,
|
||||
input: Vec::new(),
|
||||
input_idx: 0,
|
||||
input_cursor_position: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_str(&self) -> String {
|
||||
self.input.iter().collect()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.table = None;
|
||||
self.input = Vec::new();
|
||||
self.input_idx = 0;
|
||||
self.input_cursor_position = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for DatabaseFilterComponent {
|
||||
fn draw<B: Backend>(&self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
|
||||
let query = Paragraph::new(Spans::from(format!(
|
||||
"{:w$}",
|
||||
if self.input.is_empty() && !focused {
|
||||
"Filter tables".to_string()
|
||||
} else {
|
||||
self.input_str()
|
||||
},
|
||||
w = area.width as usize
|
||||
)))
|
||||
.style(if focused {
|
||||
Style::default()
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
})
|
||||
.block(Block::default().borders(Borders::BOTTOM));
|
||||
f.render_widget(query, area);
|
||||
|
||||
if focused {
|
||||
f.set_cursor(
|
||||
(area.x + self.input_cursor_position).min(area.right().saturating_sub(1)),
|
||||
area.y,
|
||||
)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for DatabaseFilterComponent {
|
||||
fn commands(&self, _out: &mut Vec<CommandInfo>) {}
|
||||
|
||||
fn event(&mut self, key: Key) -> Result<EventState> {
|
||||
let input_str: String = self.input.iter().collect();
|
||||
|
||||
match key {
|
||||
Key::Char(c) => {
|
||||
self.input.insert(self.input_idx, c);
|
||||
self.input_idx += 1;
|
||||
self.input_cursor_position += compute_character_width(c);
|
||||
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
Key::Delete | Key::Backspace => {
|
||||
if input_str.width() > 0 && !self.input.is_empty() && self.input_idx > 0 {
|
||||
let last_c = self.input.remove(self.input_idx - 1);
|
||||
self.input_idx -= 1;
|
||||
self.input_cursor_position -= compute_character_width(last_c);
|
||||
}
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
Key::Left => {
|
||||
if !self.input.is_empty() && self.input_idx > 0 {
|
||||
self.input_idx -= 1;
|
||||
self.input_cursor_position = self
|
||||
.input_cursor_position
|
||||
.saturating_sub(compute_character_width(self.input[self.input_idx]));
|
||||
}
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
Key::Ctrl('a') => {
|
||||
if !self.input.is_empty() && self.input_idx > 0 {
|
||||
self.input_idx = 0;
|
||||
self.input_cursor_position = 0
|
||||
}
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
Key::Right => {
|
||||
if self.input_idx < self.input.len() {
|
||||
let next_c = self.input[self.input_idx];
|
||||
self.input_idx += 1;
|
||||
self.input_cursor_position += compute_character_width(next_c);
|
||||
}
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
Key::Ctrl('e') => {
|
||||
if self.input_idx < self.input.len() {
|
||||
self.input_idx = self.input.len();
|
||||
self.input_cursor_position = self.input_str().width() as u16;
|
||||
}
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(EventState::NotConsumed)
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
use super::{Component, DrawableComponent, EventState};
|
||||
use crate::components::command::CommandInfo;
|
||||
use crate::config::KeyConfig;
|
||||
use crate::event::Key;
|
||||
use anyhow::Result;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Alignment, Rect},
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
|
||||
pub struct DebugComponent {
|
||||
msg: String,
|
||||
visible: bool,
|
||||
key_config: KeyConfig,
|
||||
}
|
||||
|
||||
impl DebugComponent {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(key_config: KeyConfig, msg: String) -> Self {
|
||||
Self {
|
||||
msg,
|
||||
visible: false,
|
||||
key_config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for DebugComponent {
|
||||
fn draw<B: Backend>(&self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
|
||||
if true {
|
||||
let width = 65;
|
||||
let height = 10;
|
||||
let error = Paragraph::new(self.msg.to_string())
|
||||
.block(Block::default().title("Debug").borders(Borders::ALL))
|
||||
.alignment(Alignment::Left)
|
||||
.wrap(Wrap { trim: true });
|
||||
let area = Rect::new(
|
||||
(f.size().width.saturating_sub(width)) / 2,
|
||||
(f.size().height.saturating_sub(height)) / 2,
|
||||
width.min(f.size().width),
|
||||
height.min(f.size().height),
|
||||
);
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(error, area);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for DebugComponent {
|
||||
fn commands(&self, _out: &mut Vec<CommandInfo>) {}
|
||||
|
||||
fn event(&mut self, key: Key) -> Result<EventState> {
|
||||
if self.visible {
|
||||
if key == self.key_config.exit_popup {
|
||||
self.msg = String::new();
|
||||
self.hide();
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
return Ok(EventState::NotConsumed);
|
||||
}
|
||||
Ok(EventState::NotConsumed)
|
||||
}
|
||||
}
|
@ -1,200 +0,0 @@
|
||||
use super::{Component, EventState, StatefulDrawableComponent};
|
||||
use crate::clipboard::copy_to_clipboard;
|
||||
use crate::components::command::{self, CommandInfo};
|
||||
use crate::components::TableComponent;
|
||||
use crate::config::KeyConfig;
|
||||
use crate::database::Pool;
|
||||
use crate::event::Key;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use database_tree::{Database, Table};
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, List, ListItem},
|
||||
Frame,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Focus {
|
||||
Column,
|
||||
Constraint,
|
||||
ForeignKey,
|
||||
Index,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Focus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PropertiesComponent {
|
||||
column_table: TableComponent,
|
||||
constraint_table: TableComponent,
|
||||
foreign_key_table: TableComponent,
|
||||
index_table: TableComponent,
|
||||
focus: Focus,
|
||||
key_config: KeyConfig,
|
||||
}
|
||||
|
||||
impl PropertiesComponent {
|
||||
pub fn new(key_config: KeyConfig) -> Self {
|
||||
Self {
|
||||
column_table: TableComponent::new(key_config.clone()),
|
||||
constraint_table: TableComponent::new(key_config.clone()),
|
||||
foreign_key_table: TableComponent::new(key_config.clone()),
|
||||
index_table: TableComponent::new(key_config.clone()),
|
||||
focus: Focus::Column,
|
||||
key_config,
|
||||
}
|
||||
}
|
||||
|
||||
fn focused_component(&mut self) -> &mut TableComponent {
|
||||
match self.focus {
|
||||
Focus::Column => &mut self.column_table,
|
||||
Focus::Constraint => &mut self.constraint_table,
|
||||
Focus::ForeignKey => &mut self.foreign_key_table,
|
||||
Focus::Index => &mut self.index_table,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
database: Database,
|
||||
table: Table,
|
||||
pool: &Box<dyn Pool>,
|
||||
) -> Result<()> {
|
||||
self.column_table.reset();
|
||||
let columns = pool.get_columns(&database, &table).await?;
|
||||
if !columns.is_empty() {
|
||||
self.column_table.update(
|
||||
columns
|
||||
.iter()
|
||||
.map(|c| c.columns())
|
||||
.collect::<Vec<Vec<String>>>(),
|
||||
columns.get(0).unwrap().fields(),
|
||||
database.clone(),
|
||||
table.clone(),
|
||||
);
|
||||
}
|
||||
self.constraint_table.reset();
|
||||
let constraints = pool.get_constraints(&database, &table).await?;
|
||||
if !constraints.is_empty() {
|
||||
self.constraint_table.update(
|
||||
constraints
|
||||
.iter()
|
||||
.map(|c| c.columns())
|
||||
.collect::<Vec<Vec<String>>>(),
|
||||
constraints.get(0).unwrap().fields(),
|
||||
database.clone(),
|
||||
table.clone(),
|
||||
);
|
||||
}
|
||||
self.foreign_key_table.reset();
|
||||
let foreign_keys = pool.get_foreign_keys(&database, &table).await?;
|
||||
if !foreign_keys.is_empty() {
|
||||
self.foreign_key_table.update(
|
||||
foreign_keys
|
||||
.iter()
|
||||
.map(|c| c.columns())
|
||||
.collect::<Vec<Vec<String>>>(),
|
||||
foreign_keys.get(0).unwrap().fields(),
|
||||
database.clone(),
|
||||
table.clone(),
|
||||
);
|
||||
}
|
||||
self.index_table.reset();
|
||||
let indexes = pool.get_indexes(&database, &table).await?;
|
||||
if !indexes.is_empty() {
|
||||
self.index_table.update(
|
||||
indexes
|
||||
.iter()
|
||||
.map(|c| c.columns())
|
||||
.collect::<Vec<Vec<String>>>(),
|
||||
indexes.get(0).unwrap().fields(),
|
||||
database.clone(),
|
||||
table.clone(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tab_names(&self) -> Vec<(Focus, String)> {
|
||||
vec![
|
||||
(Focus::Column, command::tab_columns(&self.key_config).name),
|
||||
(
|
||||
Focus::Constraint,
|
||||
command::tab_constraints(&self.key_config).name,
|
||||
),
|
||||
(
|
||||
Focus::ForeignKey,
|
||||
command::tab_foreign_keys(&self.key_config).name,
|
||||
),
|
||||
(Focus::Index, command::tab_indexes(&self.key_config).name),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulDrawableComponent for PropertiesComponent {
|
||||
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Length(20), Constraint::Min(1)])
|
||||
.split(area);
|
||||
|
||||
let tab_names = self
|
||||
.tab_names()
|
||||
.iter()
|
||||
.map(|(f, c)| {
|
||||
ListItem::new(c.to_string()).style(if *f == self.focus {
|
||||
Style::default().bg(Color::Blue)
|
||||
} else {
|
||||
Style::default()
|
||||
})
|
||||
})
|
||||
.collect::<Vec<ListItem>>();
|
||||
|
||||
let tab_list = List::new(tab_names)
|
||||
.block(Block::default().borders(Borders::ALL).style(if focused {
|
||||
Style::default()
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
}))
|
||||
.style(Style::default());
|
||||
|
||||
f.render_widget(tab_list, layout[0]);
|
||||
|
||||
self.focused_component().draw(f, layout[1], focused)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for PropertiesComponent {
|
||||
fn commands(&self, out: &mut Vec<CommandInfo>) {
|
||||
out.push(CommandInfo::new(command::toggle_property_tabs(
|
||||
&self.key_config,
|
||||
)));
|
||||
}
|
||||
|
||||
fn event(&mut self, key: Key) -> Result<EventState> {
|
||||
self.focused_component().event(key)?;
|
||||
|
||||
if key == self.key_config.copy {
|
||||
if let Some(text) = self.focused_component().selected_cells() {
|
||||
copy_to_clipboard(text.as_str())?
|
||||
}
|
||||
} else if key == self.key_config.tab_columns {
|
||||
self.focus = Focus::Column;
|
||||
} else if key == self.key_config.tab_constraints {
|
||||
self.focus = Focus::Constraint;
|
||||
} else if key == self.key_config.tab_foreign_keys {
|
||||
self.focus = Focus::ForeignKey;
|
||||
} else if key == self.key_config.tab_indexes {
|
||||
self.focus = Focus::Index;
|
||||
}
|
||||
Ok(EventState::NotConsumed)
|
||||
}
|
||||
}
|
@ -1,285 +0,0 @@
|
||||
use super::{
|
||||
compute_character_width, CompletionComponent, Component, EventState, MovableComponent,
|
||||
StatefulDrawableComponent, TableComponent,
|
||||
};
|
||||
use crate::components::command::CommandInfo;
|
||||
use crate::config::KeyConfig;
|
||||
use crate::database::{ExecuteResult, Pool};
|
||||
use crate::event::Key;
|
||||
use crate::ui::stateful_paragraph::{ParagraphState, StatefulParagraph};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
struct QueryResult {
|
||||
updated_rows: u64,
|
||||
}
|
||||
|
||||
impl QueryResult {
|
||||
fn result_str(&self) -> String {
|
||||
format!("Query OK, {} row affected", self.updated_rows)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Focus {
|
||||
Editor,
|
||||
Table,
|
||||
}
|
||||
|
||||
pub struct SqlEditorComponent {
|
||||
input: Vec<char>,
|
||||
input_cursor_position_x: u16,
|
||||
input_idx: usize,
|
||||
table: TableComponent,
|
||||
query_result: Option<QueryResult>,
|
||||
completion: CompletionComponent,
|
||||
key_config: KeyConfig,
|
||||
paragraph_state: ParagraphState,
|
||||
focus: Focus,
|
||||
}
|
||||
|
||||
impl SqlEditorComponent {
|
||||
pub fn new(key_config: KeyConfig) -> Self {
|
||||
Self {
|
||||
input: Vec::new(),
|
||||
input_idx: 0,
|
||||
input_cursor_position_x: 0,
|
||||
table: TableComponent::new(key_config.clone()),
|
||||
completion: CompletionComponent::new(key_config.clone(), "", true),
|
||||
focus: Focus::Editor,
|
||||
paragraph_state: ParagraphState::default(),
|
||||
query_result: None,
|
||||
key_config,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_completion(&mut self) {
|
||||
let input = &self
|
||||
.input
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| i < &self.input_idx)
|
||||
.map(|(_, i)| i)
|
||||
.collect::<String>()
|
||||
.split(' ')
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
self.completion
|
||||
.update(input.last().unwrap_or(&String::new()));
|
||||
}
|
||||
|
||||
fn complete(&mut self) -> anyhow::Result<EventState> {
|
||||
if let Some(candidate) = self.completion.selected_candidate() {
|
||||
let mut input = Vec::new();
|
||||
let first = self
|
||||
.input
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| i < &self.input_idx.saturating_sub(self.completion.word().len()))
|
||||
.map(|(_, c)| c.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
let last = self
|
||||
.input
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| i >= &self.input_idx)
|
||||
.map(|(_, c)| c.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let is_last_word = last.first().map_or(false, |c| c == &" ".to_string());
|
||||
|
||||
let middle = if is_last_word {
|
||||
candidate
|
||||
.chars()
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
} else {
|
||||
let mut c = candidate
|
||||
.chars()
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
c.push(" ".to_string());
|
||||
c
|
||||
};
|
||||
|
||||
input.extend(first);
|
||||
input.extend(middle.clone());
|
||||
input.extend(last);
|
||||
|
||||
self.input = input.join("").chars().collect();
|
||||
self.input_idx += &middle.len();
|
||||
if is_last_word {
|
||||
self.input_idx += 1;
|
||||
}
|
||||
self.input_idx -= self.completion.word().len();
|
||||
self.input_cursor_position_x += middle
|
||||
.join("")
|
||||
.chars()
|
||||
.map(compute_character_width)
|
||||
.sum::<u16>();
|
||||
if is_last_word {
|
||||
self.input_cursor_position_x += " ".to_string().width() as u16
|
||||
}
|
||||
self.input_cursor_position_x -= self
|
||||
.completion
|
||||
.word()
|
||||
.chars()
|
||||
.map(compute_character_width)
|
||||
.sum::<u16>();
|
||||
self.update_completion();
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
Ok(EventState::NotConsumed)
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulDrawableComponent for SqlEditorComponent {
|
||||
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(if matches!(self.focus, Focus::Table) {
|
||||
vec![Constraint::Length(7), Constraint::Min(1)]
|
||||
} else {
|
||||
vec![Constraint::Percentage(50), Constraint::Min(1)]
|
||||
})
|
||||
.split(area);
|
||||
|
||||
let editor = StatefulParagraph::new(self.input.iter().collect::<String>())
|
||||
.wrap(Wrap { trim: true })
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
|
||||
f.render_stateful_widget(editor, layout[0], &mut self.paragraph_state);
|
||||
|
||||
if let Some(result) = self.query_result.as_ref() {
|
||||
let result = Paragraph::new(result.result_str())
|
||||
.block(Block::default().borders(Borders::ALL).style(
|
||||
if focused && matches!(self.focus, Focus::Editor) {
|
||||
Style::default()
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
},
|
||||
))
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(result, layout[1]);
|
||||
} else {
|
||||
self.table
|
||||
.draw(f, layout[1], focused && matches!(self.focus, Focus::Table))?;
|
||||
}
|
||||
|
||||
if focused && matches!(self.focus, Focus::Editor) {
|
||||
f.set_cursor(
|
||||
(layout[0].x + 1)
|
||||
.saturating_add(
|
||||
self.input_cursor_position_x % layout[0].width.saturating_sub(2),
|
||||
)
|
||||
.min(area.right().saturating_sub(2)),
|
||||
(layout[0].y
|
||||
+ 1
|
||||
+ self.input_cursor_position_x / layout[0].width.saturating_sub(2))
|
||||
.min(layout[0].bottom()),
|
||||
)
|
||||
}
|
||||
|
||||
if focused && matches!(self.focus, Focus::Editor) {
|
||||
self.completion.draw(
|
||||
f,
|
||||
area,
|
||||
false,
|
||||
self.input_cursor_position_x % layout[0].width.saturating_sub(2) + 1,
|
||||
self.input_cursor_position_x / layout[0].width.saturating_sub(2),
|
||||
)?;
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for SqlEditorComponent {
|
||||
fn commands(&self, _out: &mut Vec<CommandInfo>) {}
|
||||
|
||||
fn event(&mut self, key: Key) -> Result<EventState> {
|
||||
let input_str: String = self.input.iter().collect();
|
||||
|
||||
if key == self.key_config.focus_above && matches!(self.focus, Focus::Table) {
|
||||
self.focus = Focus::Editor
|
||||
} else if key == self.key_config.enter {
|
||||
return self.complete();
|
||||
}
|
||||
|
||||
match key {
|
||||
Key::Char(c) if matches!(self.focus, Focus::Editor) => {
|
||||
self.input.insert(self.input_idx, c);
|
||||
self.input_idx += 1;
|
||||
self.input_cursor_position_x += compute_character_width(c);
|
||||
self.update_completion();
|
||||
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
Key::Esc if matches!(self.focus, Focus::Editor) => self.focus = Focus::Table,
|
||||
Key::Delete | Key::Backspace if matches!(self.focus, Focus::Editor) => {
|
||||
if input_str.width() > 0 && !self.input.is_empty() && self.input_idx > 0 {
|
||||
let last_c = self.input.remove(self.input_idx - 1);
|
||||
self.input_idx -= 1;
|
||||
self.input_cursor_position_x -= compute_character_width(last_c);
|
||||
self.completion.update("");
|
||||
}
|
||||
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
Key::Left if matches!(self.focus, Focus::Editor) => {
|
||||
if !self.input.is_empty() && self.input_idx > 0 {
|
||||
self.input_idx -= 1;
|
||||
self.input_cursor_position_x = self
|
||||
.input_cursor_position_x
|
||||
.saturating_sub(compute_character_width(self.input[self.input_idx]));
|
||||
self.completion.update("");
|
||||
}
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
Key::Right if matches!(self.focus, Focus::Editor) => {
|
||||
if self.input_idx < self.input.len() {
|
||||
let next_c = self.input[self.input_idx];
|
||||
self.input_idx += 1;
|
||||
self.input_cursor_position_x += compute_character_width(next_c);
|
||||
self.completion.update("");
|
||||
}
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
key if matches!(self.focus, Focus::Table) => return self.table.event(key),
|
||||
_ => (),
|
||||
}
|
||||
return Ok(EventState::NotConsumed);
|
||||
}
|
||||
|
||||
async fn async_event(&mut self, key: Key, pool: &Box<dyn Pool>) -> Result<EventState> {
|
||||
if key == self.key_config.enter && matches!(self.focus, Focus::Editor) {
|
||||
let query = self.input.iter().collect();
|
||||
let result = pool.execute(&query).await?;
|
||||
match result {
|
||||
ExecuteResult::Read {
|
||||
headers,
|
||||
rows,
|
||||
database,
|
||||
table,
|
||||
} => {
|
||||
self.table.update(rows, headers, database, table);
|
||||
self.focus = Focus::Table;
|
||||
self.query_result = None;
|
||||
}
|
||||
ExecuteResult::Write { updated_rows } => {
|
||||
self.query_result = Some(QueryResult { updated_rows })
|
||||
}
|
||||
}
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
|
||||
Ok(EventState::NotConsumed)
|
||||
}
|
||||
}
|
@ -1,431 +0,0 @@
|
||||
use crate::get_or_null;
|
||||
|
||||
use super::{ExecuteResult, Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
|
||||
use async_trait::async_trait;
|
||||
use chrono::NaiveDateTime;
|
||||
use database_tree::{Child, Database, Table};
|
||||
use futures::TryStreamExt;
|
||||
use sqlx::sqlite::{SqliteColumn, SqlitePoolOptions, SqliteRow};
|
||||
use sqlx::{Column as _, Row as _, TypeInfo as _};
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct SqlitePool {
|
||||
pool: sqlx::sqlite::SqlitePool,
|
||||
}
|
||||
|
||||
impl SqlitePool {
|
||||
pub async fn new(database_url: &str) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
pool: SqlitePoolOptions::new()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.connect(database_url)
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Constraint {
|
||||
name: String,
|
||||
column_name: String,
|
||||
origin: String,
|
||||
}
|
||||
|
||||
impl TableRow for Constraint {
|
||||
fn fields(&self) -> Vec<String> {
|
||||
vec![
|
||||
"name".to_string(),
|
||||
"column_name".to_string(),
|
||||
"origin".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
fn columns(&self) -> Vec<String> {
|
||||
vec![
|
||||
self.name.to_string(),
|
||||
self.column_name.to_string(),
|
||||
self.origin.to_string(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Column {
|
||||
name: Option<String>,
|
||||
r#type: Option<String>,
|
||||
null: Option<String>,
|
||||
default: Option<String>,
|
||||
comment: Option<String>,
|
||||
}
|
||||
|
||||
impl TableRow for Column {
|
||||
fn fields(&self) -> Vec<String> {
|
||||
vec![
|
||||
"name".to_string(),
|
||||
"type".to_string(),
|
||||
"null".to_string(),
|
||||
"default".to_string(),
|
||||
"comment".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
fn columns(&self) -> Vec<String> {
|
||||
vec![
|
||||
self.name
|
||||
.as_ref()
|
||||
.map_or(String::new(), |name| name.to_string()),
|
||||
self.r#type
|
||||
.as_ref()
|
||||
.map_or(String::new(), |r#type| r#type.to_string()),
|
||||
self.null
|
||||
.as_ref()
|
||||
.map_or(String::new(), |null| null.to_string()),
|
||||
self.default
|
||||
.as_ref()
|
||||
.map_or(String::new(), |default| default.to_string()),
|
||||
self.comment
|
||||
.as_ref()
|
||||
.map_or(String::new(), |comment| comment.to_string()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ForeignKey {
|
||||
column_name: Option<String>,
|
||||
ref_table: Option<String>,
|
||||
ref_column: Option<String>,
|
||||
}
|
||||
|
||||
impl TableRow for ForeignKey {
|
||||
fn fields(&self) -> Vec<String> {
|
||||
vec![
|
||||
"column_name".to_string(),
|
||||
"ref_table".to_string(),
|
||||
"ref_column".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
fn columns(&self) -> Vec<String> {
|
||||
vec![
|
||||
self.column_name
|
||||
.as_ref()
|
||||
.map_or(String::new(), |r#type| r#type.to_string()),
|
||||
self.ref_table
|
||||
.as_ref()
|
||||
.map_or(String::new(), |r#type| r#type.to_string()),
|
||||
self.ref_column
|
||||
.as_ref()
|
||||
.map_or(String::new(), |r#type| r#type.to_string()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Index {
|
||||
name: Option<String>,
|
||||
column_name: Option<String>,
|
||||
r#type: Option<String>,
|
||||
}
|
||||
|
||||
impl TableRow for Index {
|
||||
fn fields(&self) -> Vec<String> {
|
||||
vec![
|
||||
"name".to_string(),
|
||||
"column_name".to_string(),
|
||||
"type".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
fn columns(&self) -> Vec<String> {
|
||||
vec![
|
||||
self.name
|
||||
.as_ref()
|
||||
.map_or(String::new(), |name| name.to_string()),
|
||||
self.column_name
|
||||
.as_ref()
|
||||
.map_or(String::new(), |column_name| column_name.to_string()),
|
||||
self.r#type
|
||||
.as_ref()
|
||||
.map_or(String::new(), |r#type| r#type.to_string()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Pool for SqlitePool {
|
||||
async fn execute(&self, query: &String) -> anyhow::Result<ExecuteResult> {
|
||||
let query = query.trim();
|
||||
if query.to_uppercase().starts_with("SELECT") {
|
||||
let mut rows = sqlx::query(query).fetch(&self.pool);
|
||||
let mut headers = vec![];
|
||||
let mut records = vec![];
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
headers = row
|
||||
.columns()
|
||||
.iter()
|
||||
.map(|column| column.name().to_string())
|
||||
.collect();
|
||||
let mut new_row = vec![];
|
||||
for column in row.columns() {
|
||||
new_row.push(convert_column_value_to_string(&row, column)?)
|
||||
}
|
||||
records.push(new_row)
|
||||
}
|
||||
return Ok(ExecuteResult::Read {
|
||||
headers,
|
||||
rows: records,
|
||||
database: Database {
|
||||
name: "-".to_string(),
|
||||
children: Vec::new(),
|
||||
},
|
||||
table: Table {
|
||||
name: "-".to_string(),
|
||||
create_time: None,
|
||||
update_time: None,
|
||||
engine: None,
|
||||
schema: None,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let result = sqlx::query(query).execute(&self.pool).await?;
|
||||
Ok(ExecuteResult::Write {
|
||||
updated_rows: result.rows_affected(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_databases(&self) -> anyhow::Result<Vec<Database>> {
|
||||
let databases = sqlx::query("SELECT name FROM pragma_database_list")
|
||||
.fetch_all(&self.pool)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|table| table.get(0))
|
||||
.collect::<Vec<String>>();
|
||||
let mut list = vec![];
|
||||
for db in databases {
|
||||
list.push(Database::new(
|
||||
db.clone(),
|
||||
self.get_tables(db.clone()).await?,
|
||||
))
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
async fn get_tables(&self, _database: String) -> anyhow::Result<Vec<Child>> {
|
||||
let mut rows =
|
||||
sqlx::query("SELECT name FROM sqlite_master WHERE type = 'table'").fetch(&self.pool);
|
||||
let mut tables = Vec::new();
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
tables.push(Table {
|
||||
name: row.try_get("name")?,
|
||||
create_time: None,
|
||||
update_time: None,
|
||||
engine: None,
|
||||
schema: None,
|
||||
})
|
||||
}
|
||||
Ok(tables.into_iter().map(|table| table.into()).collect())
|
||||
}
|
||||
|
||||
async fn get_records(
|
||||
&self,
|
||||
_database: &Database,
|
||||
table: &Table,
|
||||
page: u16,
|
||||
filter: Option<String>,
|
||||
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
|
||||
let query = if let Some(filter) = filter {
|
||||
format!(
|
||||
"SELECT * FROM `{table}` WHERE {filter} LIMIT {page}, {limit}",
|
||||
table = table.name,
|
||||
filter = filter,
|
||||
page = page,
|
||||
limit = RECORDS_LIMIT_PER_PAGE
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"SELECT * FROM `{}` LIMIT {page}, {limit}",
|
||||
table.name,
|
||||
page = page,
|
||||
limit = RECORDS_LIMIT_PER_PAGE
|
||||
)
|
||||
};
|
||||
let mut rows = sqlx::query(query.as_str()).fetch(&self.pool);
|
||||
let mut headers = vec![];
|
||||
let mut records = vec![];
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
headers = row
|
||||
.columns()
|
||||
.iter()
|
||||
.map(|column| column.name().to_string())
|
||||
.collect();
|
||||
let mut new_row = vec![];
|
||||
for column in row.columns() {
|
||||
new_row.push(convert_column_value_to_string(&row, column)?)
|
||||
}
|
||||
records.push(new_row)
|
||||
}
|
||||
Ok((headers, records))
|
||||
}
|
||||
|
||||
async fn get_columns(
|
||||
&self,
|
||||
_database: &Database,
|
||||
table: &Table,
|
||||
) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
|
||||
let query = format!("SELECT * FROM pragma_table_info('{}');", table.name);
|
||||
let mut rows = sqlx::query(query.as_str()).fetch(&self.pool);
|
||||
let mut columns: Vec<Box<dyn TableRow>> = vec![];
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
let null: Option<i16> = row.try_get("notnull")?;
|
||||
columns.push(Box::new(Column {
|
||||
name: row.try_get("name")?,
|
||||
r#type: row.try_get("type")?,
|
||||
null: if matches!(null, Some(null) if null == 1) {
|
||||
Some("✔︎".to_string())
|
||||
} else {
|
||||
Some("".to_string())
|
||||
},
|
||||
default: row.try_get("dflt_value")?,
|
||||
comment: None,
|
||||
}))
|
||||
}
|
||||
Ok(columns)
|
||||
}
|
||||
|
||||
async fn get_constraints(
|
||||
&self,
|
||||
_database: &Database,
|
||||
table: &Table,
|
||||
) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
|
||||
let mut rows = sqlx::query(
|
||||
"
|
||||
SELECT
|
||||
p.origin,
|
||||
s.name AS index_name,
|
||||
i.name AS column_name
|
||||
FROM
|
||||
sqlite_master s
|
||||
JOIN pragma_index_list(s.tbl_name) p ON s.name = p.name,
|
||||
pragma_index_info(s.name) i
|
||||
WHERE
|
||||
s.type = 'index'
|
||||
AND tbl_name = ?
|
||||
AND NOT p.origin = 'c'
|
||||
",
|
||||
)
|
||||
.bind(&table.name)
|
||||
.fetch(&self.pool);
|
||||
let mut constraints: Vec<Box<dyn TableRow>> = vec![];
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
constraints.push(Box::new(Constraint {
|
||||
name: row.try_get("index_name")?,
|
||||
column_name: row.try_get("column_name")?,
|
||||
origin: row.try_get("origin")?,
|
||||
}))
|
||||
}
|
||||
Ok(constraints)
|
||||
}
|
||||
|
||||
async fn get_foreign_keys(
|
||||
&self,
|
||||
_database: &Database,
|
||||
table: &Table,
|
||||
) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
|
||||
let query = format!(
|
||||
"SELECT p.`from`, p.`to`, p.`table` FROM pragma_foreign_key_list('{}') p",
|
||||
&table.name
|
||||
);
|
||||
let mut rows = sqlx::query(query.as_str())
|
||||
.bind(&table.name)
|
||||
.fetch(&self.pool);
|
||||
let mut foreign_keys: Vec<Box<dyn TableRow>> = vec![];
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
foreign_keys.push(Box::new(ForeignKey {
|
||||
column_name: row.try_get("from")?,
|
||||
ref_table: row.try_get("table")?,
|
||||
ref_column: row.try_get("to")?,
|
||||
}))
|
||||
}
|
||||
Ok(foreign_keys)
|
||||
}
|
||||
|
||||
async fn get_indexes(
|
||||
&self,
|
||||
_database: &Database,
|
||||
table: &Table,
|
||||
) -> anyhow::Result<Vec<Box<dyn TableRow>>> {
|
||||
let mut rows = sqlx::query(
|
||||
"
|
||||
SELECT
|
||||
m.name AS index_name,
|
||||
p.*
|
||||
FROM
|
||||
sqlite_master m,
|
||||
pragma_index_info(m.name) p
|
||||
WHERE
|
||||
m.type = 'index'
|
||||
AND m.tbl_name = ?
|
||||
",
|
||||
)
|
||||
.bind(&table.name)
|
||||
.fetch(&self.pool);
|
||||
let mut foreign_keys: Vec<Box<dyn TableRow>> = vec![];
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
foreign_keys.push(Box::new(Index {
|
||||
name: row.try_get("index_name")?,
|
||||
column_name: row.try_get("name")?,
|
||||
r#type: Some(String::new()),
|
||||
}))
|
||||
}
|
||||
Ok(foreign_keys)
|
||||
}
|
||||
|
||||
async fn close(&self) {
|
||||
self.pool.close().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_column_value_to_string(
|
||||
row: &SqliteRow,
|
||||
column: &SqliteColumn,
|
||||
) -> anyhow::Result<String> {
|
||||
let column_name = column.name();
|
||||
if let Ok(value) = row.try_get(column_name) {
|
||||
let value: Option<String> = value;
|
||||
Ok(value.unwrap_or_else(|| "NULL".to_string()))
|
||||
} else if let Ok(value) = row.try_get(column_name) {
|
||||
let value: Option<&str> = value;
|
||||
Ok(get_or_null!(value))
|
||||
} else if let Ok(value) = row.try_get(column_name) {
|
||||
let value: Option<i16> = value;
|
||||
Ok(get_or_null!(value))
|
||||
} else if let Ok(value) = row.try_get(column_name) {
|
||||
let value: Option<i32> = value;
|
||||
Ok(get_or_null!(value))
|
||||
} else if let Ok(value) = row.try_get(column_name) {
|
||||
let value: Option<i64> = value;
|
||||
Ok(get_or_null!(value))
|
||||
} else if let Ok(value) = row.try_get(column_name) {
|
||||
let value: Option<f32> = value;
|
||||
Ok(get_or_null!(value))
|
||||
} else if let Ok(value) = row.try_get(column_name) {
|
||||
let value: Option<f64> = value;
|
||||
Ok(get_or_null!(value))
|
||||
} else if let Ok(value) = row.try_get(column_name) {
|
||||
let value: Option<chrono::DateTime<chrono::Utc>> = value;
|
||||
Ok(get_or_null!(value))
|
||||
} else if let Ok(value) = row.try_get(column_name) {
|
||||
let value: Option<chrono::DateTime<chrono::Local>> = value;
|
||||
Ok(get_or_null!(value))
|
||||
} else if let Ok(value) = row.try_get(column_name) {
|
||||
let value: Option<NaiveDateTime> = value;
|
||||
Ok(get_or_null!(value))
|
||||
} else if let Ok(value) = row.try_get(column_name) {
|
||||
let value: Option<bool> = value;
|
||||
Ok(get_or_null!(value))
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"column type not implemented: `{}` {}",
|
||||
column_name,
|
||||
column.type_info().clone().name()
|
||||
)
|
||||
}
|
||||
}
|
@ -1,545 +0,0 @@
|
||||
use easy_cast::Cast;
|
||||
use tui::text::StyledGrapheme;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
const NBSP: &str = "\u{00a0}";
|
||||
|
||||
/// A state machine to pack styled symbols into lines.
|
||||
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
|
||||
/// iterators for that).
|
||||
pub trait LineComposer<'a> {
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>;
|
||||
}
|
||||
|
||||
/// A state machine that wraps lines on word boundaries.
|
||||
pub struct WordWrapper<'a, 'b> {
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
max_line_width: u16,
|
||||
current_line: Vec<StyledGrapheme<'a>>,
|
||||
next_line: Vec<StyledGrapheme<'a>>,
|
||||
/// Removes the leading whitespace from lines
|
||||
trim: bool,
|
||||
}
|
||||
|
||||
impl<'a, 'b> WordWrapper<'a, 'b> {
|
||||
pub fn new(
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
max_line_width: u16,
|
||||
trim: bool,
|
||||
) -> WordWrapper<'a, 'b> {
|
||||
WordWrapper {
|
||||
symbols,
|
||||
max_line_width,
|
||||
current_line: vec![],
|
||||
next_line: vec![],
|
||||
trim,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
std::mem::swap(&mut self.current_line, &mut self.next_line);
|
||||
self.next_line.truncate(0);
|
||||
|
||||
let mut current_line_width = self
|
||||
.current_line
|
||||
.iter()
|
||||
.map(|StyledGrapheme { symbol, .. }| -> u16 { symbol.width().cast() })
|
||||
.sum();
|
||||
|
||||
let mut symbols_to_last_word_end: usize = 0;
|
||||
let mut width_to_last_word_end: u16 = 0;
|
||||
let mut prev_whitespace = false;
|
||||
let mut symbols_exhausted = true;
|
||||
for StyledGrapheme { symbol, style } in &mut self.symbols {
|
||||
symbols_exhausted = false;
|
||||
let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
|
||||
|
||||
// Ignore characters wider that the total max width.
|
||||
if Cast::<u16>::cast(symbol.width()) > self.max_line_width
|
||||
// Skip leading whitespace when trim is enabled.
|
||||
|| self.trim && symbol_whitespace && symbol != "\n" && current_line_width == 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break on newline and discard it.
|
||||
if symbol == "\n" {
|
||||
if prev_whitespace {
|
||||
current_line_width = width_to_last_word_end;
|
||||
self.current_line.truncate(symbols_to_last_word_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Mark the previous symbol as word end.
|
||||
if symbol_whitespace && !prev_whitespace {
|
||||
symbols_to_last_word_end = self.current_line.len();
|
||||
width_to_last_word_end = current_line_width;
|
||||
}
|
||||
|
||||
self.current_line.push(StyledGrapheme { symbol, style });
|
||||
current_line_width += Cast::<u16>::cast(symbol.width());
|
||||
|
||||
if current_line_width > self.max_line_width {
|
||||
// If there was no word break in the text, wrap at the end of the line.
|
||||
let (truncate_at, truncated_width) = if symbols_to_last_word_end == 0 {
|
||||
(self.current_line.len() - 1, self.max_line_width)
|
||||
} else {
|
||||
(self.current_line.len() - 1, width_to_last_word_end)
|
||||
};
|
||||
|
||||
// Push the remainder to the next line but strip leading whitespace:
|
||||
{
|
||||
let remainder = &self.current_line[truncate_at..];
|
||||
if !remainder.is_empty() {
|
||||
self.next_line.extend_from_slice(&remainder);
|
||||
}
|
||||
}
|
||||
self.current_line.truncate(truncate_at);
|
||||
current_line_width = truncated_width;
|
||||
break;
|
||||
}
|
||||
|
||||
prev_whitespace = symbol_whitespace;
|
||||
}
|
||||
|
||||
// Even if the iterator is exhausted, pass the previous remainder.
|
||||
if symbols_exhausted && self.current_line.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((&self.current_line[..], current_line_width))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A state machine that truncates overhanging lines.
|
||||
pub struct LineTruncator<'a, 'b> {
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
max_line_width: u16,
|
||||
current_line: Vec<StyledGrapheme<'a>>,
|
||||
/// Record the offet to skip render
|
||||
horizontal_offset: u16,
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineTruncator<'a, 'b> {
|
||||
pub fn new(
|
||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
||||
max_line_width: u16,
|
||||
) -> LineTruncator<'a, 'b> {
|
||||
LineTruncator {
|
||||
symbols,
|
||||
max_line_width,
|
||||
horizontal_offset: 0,
|
||||
current_line: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) {
|
||||
self.horizontal_offset = horizontal_offset;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.current_line.truncate(0);
|
||||
let mut current_line_width = 0;
|
||||
|
||||
let mut skip_rest = false;
|
||||
let mut symbols_exhausted = true;
|
||||
let mut horizontal_offset = self.horizontal_offset as usize;
|
||||
for StyledGrapheme { symbol, style } in &mut self.symbols {
|
||||
symbols_exhausted = false;
|
||||
|
||||
// Ignore characters wider that the total max width.
|
||||
if Cast::<u16>::cast(symbol.width()) > self.max_line_width {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break on newline and discard it.
|
||||
if symbol == "\n" {
|
||||
break;
|
||||
}
|
||||
|
||||
if current_line_width + Cast::<u16>::cast(symbol.width()) > self.max_line_width {
|
||||
// Exhaust the remainder of the line.
|
||||
skip_rest = true;
|
||||
break;
|
||||
}
|
||||
|
||||
let symbol = if horizontal_offset == 0 {
|
||||
symbol
|
||||
} else {
|
||||
let w = symbol.width();
|
||||
if w > horizontal_offset {
|
||||
let t = trim_offset(symbol, horizontal_offset);
|
||||
horizontal_offset = 0;
|
||||
t
|
||||
} else {
|
||||
horizontal_offset -= w;
|
||||
""
|
||||
}
|
||||
};
|
||||
current_line_width += Cast::<u16>::cast(symbol.width());
|
||||
self.current_line.push(StyledGrapheme { symbol, style });
|
||||
}
|
||||
|
||||
if skip_rest {
|
||||
for StyledGrapheme { symbol, .. } in &mut self.symbols {
|
||||
if symbol == "\n" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if symbols_exhausted && self.current_line.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((&self.current_line[..], current_line_width))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function will return a str slice which start at specified offset.
|
||||
/// As src is a unicode str, start offset has to be calculated with each character.
|
||||
fn trim_offset(src: &str, mut offset: usize) -> &str {
|
||||
let mut start = 0;
|
||||
for c in UnicodeSegmentation::graphemes(src, true) {
|
||||
let w = c.width();
|
||||
if w <= offset {
|
||||
offset -= w;
|
||||
start += c.len();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
&src[start..]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
enum Composer {
|
||||
WordWrapper { trim: bool },
|
||||
LineTruncator,
|
||||
}
|
||||
|
||||
fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
|
||||
let style = Default::default();
|
||||
let mut styled =
|
||||
UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
|
||||
let mut composer: Box<dyn LineComposer> = match which {
|
||||
Composer::WordWrapper { trim } => {
|
||||
Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
|
||||
}
|
||||
Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
|
||||
};
|
||||
let mut lines = vec![];
|
||||
let mut widths = vec![];
|
||||
while let Some((styled, width)) = composer.next_line() {
|
||||
let line = styled
|
||||
.iter()
|
||||
.map(|StyledGrapheme { symbol, .. }| *symbol)
|
||||
.collect::<String>();
|
||||
assert!(width <= text_area_width);
|
||||
lines.push(line);
|
||||
widths.push(width);
|
||||
}
|
||||
(lines, widths)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_one_line() {
|
||||
let width = 40;
|
||||
for i in 1..width {
|
||||
let text = "a".repeat(i);
|
||||
let (word_wrapper, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, &text, width as u16);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
|
||||
let expected = vec![text];
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_short_lines() {
|
||||
let width = 20;
|
||||
let text =
|
||||
"abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let wrapped: Vec<&str> = text.split('\n').collect();
|
||||
assert_eq!(word_wrapper, wrapped);
|
||||
assert_eq!(line_truncator, wrapped);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_long_word() {
|
||||
let width = 20;
|
||||
let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
|
||||
let (word_wrapper, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let wrapped = vec![
|
||||
&text[..width],
|
||||
&text[width..width * 2],
|
||||
&text[width * 2..width * 3],
|
||||
&text[width * 3..],
|
||||
];
|
||||
assert_eq!(
|
||||
word_wrapper, wrapped,
|
||||
"WordWrapper should detect the line cannot be broken on word boundary and \
|
||||
break it at line width limit."
|
||||
);
|
||||
assert_eq!(line_truncator, vec![&text[..width]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_long_sentence() {
|
||||
let width = 20;
|
||||
let text =
|
||||
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
|
||||
let text_multi_space =
|
||||
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
|
||||
m n o";
|
||||
let (word_wrapper_single_space, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
||||
let (word_wrapper_multi_space, _) = run_composer(
|
||||
Composer::WordWrapper { trim: true },
|
||||
text_multi_space,
|
||||
width as u16,
|
||||
);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
assert_eq!(
|
||||
word_wrapper_single_space,
|
||||
vec![
|
||||
"abcd efghij klmnopab",
|
||||
"cd efgh ijklmnopabcd",
|
||||
"efg hijkl mnopab c d",
|
||||
" e f g h i j k l m n",
|
||||
" o",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
word_wrapper_multi_space,
|
||||
vec![
|
||||
"abcd efghij klmno",
|
||||
"pabcd efgh ijklm",
|
||||
"nopabcdefg hijkl mno",
|
||||
"pab c d e f g h i j ",
|
||||
"k l m n o"
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(line_truncator, vec![&text[..width]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_zero_width() {
|
||||
let width = 0;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = Vec::new();
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_max_line_width_of_1() {
|
||||
let width = 1;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true).collect();
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, vec!["a"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_max_line_width_of_1_double_width_characters() {
|
||||
let width = 1;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
|
||||
両端点では、";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
|
||||
assert_eq!(line_truncator, vec!["", "a"]);
|
||||
}
|
||||
|
||||
/// Tests WordWrapper with words some of which exceed line length and some not.
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_mixed_length() {
|
||||
let width = 20;
|
||||
let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
"abcd efghij klmnopab",
|
||||
"cdefghijklmnopabcdef",
|
||||
"ghijkl mnopab cdefgh",
|
||||
"i j klmno"
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_double_width_chars() {
|
||||
let width = 20;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
|
||||
では、";
|
||||
let (word_wrapper, word_wrapper_width) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, &text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
|
||||
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
|
||||
let wrapped = vec![
|
||||
"コンピュータ上で文字",
|
||||
"を扱う場合、典型的に",
|
||||
"は文字による通信を行",
|
||||
"う場合にその両端点で",
|
||||
"は、",
|
||||
];
|
||||
assert_eq!(word_wrapper, wrapped);
|
||||
assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_leading_whitespace_removal() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
|
||||
assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
|
||||
}
|
||||
|
||||
/// Tests truncation of leading whitespace.
|
||||
#[test]
|
||||
fn line_composer_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = " ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec![""]);
|
||||
assert_eq!(line_truncator, vec![" "]);
|
||||
}
|
||||
|
||||
/// Tests an input starting with a letter, folowed by spaces - some of the behaviour is
|
||||
/// incidental.
|
||||
#[test]
|
||||
fn line_composer_char_plus_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = "a ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
// What's happening below is: the first line gets consumed, trailing spaces discarded,
|
||||
// after 20 of which a word break occurs (probably shouldn't). The second line break
|
||||
// discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
|
||||
// that much.
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
"a ",
|
||||
" ",
|
||||
" ",
|
||||
" "
|
||||
]
|
||||
);
|
||||
assert_eq!(line_truncator, vec!["a "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
|
||||
let width = 20;
|
||||
// Japanese seems not to use spaces but we should break on spaces anyway... We're using it
|
||||
// to test double-width chars.
|
||||
// You are more than welcome to add word boundary detection based of alterations of
|
||||
// hiragana and katakana...
|
||||
// This happens to also be a test case for mixed width because regular spaces are single width.
|
||||
let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
|
||||
let (word_wrapper, word_wrapper_width) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
"コンピュ ータ上で文",
|
||||
"字を扱う場合、 典型",
|
||||
"的には文 字による 通",
|
||||
"信を行 う場合にその",
|
||||
"両端点では、"
|
||||
]
|
||||
);
|
||||
// Odd-sized lines have a space in them.
|
||||
assert_eq!(word_wrapper_width, vec![8, 14, 17, 6, 12]);
|
||||
}
|
||||
|
||||
/// Ensure words separated by nbsp are wrapped as if they were a single one.
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_nbsp() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA AAAA", "\u{a0}AAA"]);
|
||||
|
||||
// Ensure that if the character was a regular space, it would be wrapped differently.
|
||||
let text_space = text.replace("\u{00a0}", " ");
|
||||
let (word_wrapper_space, _) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
|
||||
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", " AAA"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_preserve_indentation() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
|
||||
let width = 10;
|
||||
let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec!["AAA AAA AA", "AAA AA AAA", "AAA", " B", " C", " D"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
|
||||
let width = 10;
|
||||
let text = " 4 Indent\n must wrap!";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
" ",
|
||||
" 4 Ind",
|
||||
"ent",
|
||||
" ",
|
||||
" mus",
|
||||
"t wrap!"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
@ -1,182 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use easy_cast::Cast;
|
||||
use std::iter;
|
||||
use tui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::Style,
|
||||
text::{StyledGrapheme, Text},
|
||||
widgets::{Block, StatefulWidget, Widget, Wrap},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::reflow::{LineComposer, LineTruncator, WordWrapper};
|
||||
|
||||
const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
|
||||
match alignment {
|
||||
Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
|
||||
Alignment::Right => text_area_width.saturating_sub(line_width),
|
||||
Alignment::Left => 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StatefulParagraph<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Widget style
|
||||
style: Style,
|
||||
/// How to wrap the text
|
||||
wrap: Option<Wrap>,
|
||||
/// The text to display
|
||||
text: Text<'a>,
|
||||
/// Alignment of the text
|
||||
alignment: Alignment,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct ScrollPos {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
}
|
||||
|
||||
impl ScrollPos {
|
||||
pub const fn new(x: u16, y: u16) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub struct ParagraphState {
|
||||
/// Scroll
|
||||
scroll: ScrollPos,
|
||||
/// after all wrapping this is the amount of lines
|
||||
lines: u16,
|
||||
/// last visible height
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl ParagraphState {
|
||||
pub const fn lines(self) -> u16 {
|
||||
self.lines
|
||||
}
|
||||
|
||||
pub const fn height(self) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
pub const fn scroll(self) -> ScrollPos {
|
||||
self.scroll
|
||||
}
|
||||
|
||||
pub fn set_scroll(&mut self, scroll: ScrollPos) {
|
||||
self.scroll = scroll;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StatefulParagraph<'a> {
|
||||
pub fn new<T>(text: T) -> Self
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
Self {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
wrap: None,
|
||||
text: text.into(),
|
||||
alignment: Alignment::Left,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn wrap(mut self, wrap: Wrap) -> Self {
|
||||
self.wrap = Some(wrap);
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn alignment(mut self, alignment: Alignment) -> Self {
|
||||
self.alignment = alignment;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StatefulWidget for StatefulParagraph<'a> {
|
||||
type State = ParagraphState;
|
||||
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
buf.set_style(area, self.style);
|
||||
let text_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if text_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let style = self.style;
|
||||
let mut styled = self.text.lines.iter().flat_map(|spans| {
|
||||
spans
|
||||
.0
|
||||
.iter()
|
||||
.flat_map(|span| span.styled_graphemes(style))
|
||||
// Required given the way composers work but might be refactored out if we change
|
||||
// composers to operate on lines instead of a stream of graphemes.
|
||||
.chain(iter::once(StyledGrapheme {
|
||||
symbol: "\n",
|
||||
style: self.style,
|
||||
}))
|
||||
});
|
||||
|
||||
let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
|
||||
Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
|
||||
} else {
|
||||
let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
|
||||
if let Alignment::Left = self.alignment {
|
||||
line_composer.set_horizontal_offset(state.scroll.x);
|
||||
}
|
||||
line_composer
|
||||
};
|
||||
let mut y = 0;
|
||||
let mut end_reached = false;
|
||||
while let Some((current_line, current_line_width)) = line_composer.next_line() {
|
||||
if !end_reached && y >= state.scroll.y {
|
||||
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
|
||||
for StyledGrapheme { symbol, style } in current_line {
|
||||
buf.get_mut(text_area.left() + x, text_area.top() + y - state.scroll.y)
|
||||
.set_symbol(if symbol.is_empty() {
|
||||
// If the symbol is empty, the last char which rendered last time will
|
||||
// leave on the line. It's a quick fix.
|
||||
" "
|
||||
} else {
|
||||
symbol
|
||||
})
|
||||
.set_style(*style);
|
||||
x += Cast::<u16>::cast(symbol.width());
|
||||
}
|
||||
}
|
||||
y += 1;
|
||||
if y >= text_area.height + state.scroll.y {
|
||||
end_reached = true;
|
||||
}
|
||||
}
|
||||
|
||||
state.lines = y;
|
||||
state.height = area.height;
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
use std::ops::Range;
|
||||
use syntect::{
|
||||
highlighting::{
|
||||
FontStyle, HighlightState, Highlighter, RangedHighlightIterator, Style, ThemeSet,
|
||||
},
|
||||
parsing::{ParseState, ScopeStack, SyntaxSet},
|
||||
};
|
||||
use tui::text::{Span, Spans};
|
||||
|
||||
struct SyntaxLine {
|
||||
items: Vec<(Style, usize, Range<usize>)>,
|
||||
}
|
||||
|
||||
pub struct SyntaxText {
|
||||
text: String,
|
||||
lines: Vec<SyntaxLine>,
|
||||
}
|
||||
|
||||
impl SyntaxText {
|
||||
pub fn new(text: String) -> Self {
|
||||
let syntax_set: SyntaxSet = SyntaxSet::load_defaults_nonewlines();
|
||||
let theme_set: ThemeSet = ThemeSet::load_defaults();
|
||||
|
||||
let mut state = ParseState::new(syntax_set.find_syntax_by_extension("sql").unwrap());
|
||||
let highlighter = Highlighter::new(&theme_set.themes["base16-eighties.dark"]);
|
||||
let mut syntax_lines: Vec<SyntaxLine> = Vec::new();
|
||||
let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
|
||||
|
||||
for (number, line) in text.lines().enumerate() {
|
||||
let ops = state.parse_line(line, &syntax_set);
|
||||
let iter =
|
||||
RangedHighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
|
||||
|
||||
syntax_lines.push(SyntaxLine {
|
||||
items: iter
|
||||
.map(|(style, _, range)| (style, number, range))
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
text,
|
||||
lines: syntax_lines,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert(&self) -> tui::text::Text<'_> {
|
||||
let mut result_lines: Vec<Spans> = Vec::with_capacity(self.lines.len());
|
||||
|
||||
for (syntax_line, line_content) in self.lines.iter().zip(self.text.lines()) {
|
||||
let mut line_span = Spans(Vec::with_capacity(syntax_line.items.len()));
|
||||
|
||||
for (style, _, range) in &syntax_line.items {
|
||||
let item_content = &line_content[range.clone()];
|
||||
let item_style = syntact_style_to_tui(style);
|
||||
|
||||
line_span.0.push(Span::styled(item_content, item_style));
|
||||
}
|
||||
|
||||
result_lines.push(line_span);
|
||||
}
|
||||
|
||||
result_lines.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a SyntaxText> for tui::text::Text<'a> {
|
||||
fn from(v: &'a SyntaxText) -> Self {
|
||||
let mut result_lines: Vec<Spans> = Vec::with_capacity(v.lines.len());
|
||||
|
||||
for (syntax_line, line_content) in v.lines.iter().zip(v.text.lines()) {
|
||||
let mut line_span = Spans(Vec::with_capacity(syntax_line.items.len()));
|
||||
|
||||
for (style, _, range) in &syntax_line.items {
|
||||
let item_content = &line_content[range.clone()];
|
||||
let item_style = syntact_style_to_tui(style);
|
||||
|
||||
line_span.0.push(Span::styled(item_content, item_style));
|
||||
}
|
||||
|
||||
result_lines.push(line_span);
|
||||
}
|
||||
|
||||
result_lines.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn syntact_style_to_tui(style: &Style) -> tui::style::Style {
|
||||
let mut res = tui::style::Style::default().fg(tui::style::Color::Rgb(
|
||||
style.foreground.r,
|
||||
style.foreground.g,
|
||||
style.foreground.b,
|
||||
));
|
||||
|
||||
if style.font_style.contains(FontStyle::BOLD) {
|
||||
res = res.add_modifier(tui::style::Modifier::BOLD);
|
||||
}
|
||||
if style.font_style.contains(FontStyle::ITALIC) {
|
||||
res = res.add_modifier(tui::style::Modifier::ITALIC);
|
||||
}
|
||||
if style.font_style.contains(FontStyle::UNDERLINE) {
|
||||
res = res.add_modifier(tui::style::Modifier::UNDERLINED);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
use std::{env, fmt};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Version {
|
||||
major: u32,
|
||||
minor: u32,
|
||||
patch: u32,
|
||||
pre: Option<String>,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
/// read version at compile time from env variables
|
||||
pub fn new() -> Self {
|
||||
let mut res = Self::default();
|
||||
let major_str = env!("CARGO_PKG_VERSION_MAJOR");
|
||||
if let Ok(major) = major_str.parse::<u32>() {
|
||||
res.major = major;
|
||||
}
|
||||
let minor_str = env!("CARGO_PKG_VERSION_MINOR");
|
||||
if let Ok(minor) = minor_str.parse::<u32>() {
|
||||
res.minor = minor;
|
||||
}
|
||||
let patch_str = env!("CARGO_PKG_VERSION_PATCH");
|
||||
if let Ok(patch) = patch_str.parse::<u32>() {
|
||||
res.patch = patch;
|
||||
}
|
||||
let pre_str = env!("CARGO_PKG_VERSION_PRE");
|
||||
res.pre = if pre_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(pre_str.to_string())
|
||||
};
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Version {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"v{}.{}.{}{}",
|
||||
self.major,
|
||||
self.minor,
|
||||
self.patch,
|
||||
self.pre
|
||||
.as_ref()
|
||||
.map_or(String::new(), |pre| format!("-{}", pre.to_string()))
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue