mirror of https://github.com/TaKO8Ki/gobang
Compare commits
No commits in common. 'main' and 'v0.1.0-alpha.3' have entirely different histories.
main
...
v0.1.0-alp
@ -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:
|
Binary file not shown.
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 18 MiB |
@ -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,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
|
||||
}
|
Loading…
Reference in New Issue