Add SQL Editor component (#84)

* syntax text

* add sql editor

* remove sytaxx_text module

* remove sytax_text

* add a focus above key

* add a event for movining focus and table component

* add syntax text component

* add a key for executing queries

* add completion

* add reserved words

* update completion when key is delete or left/right

* use reserved words in where clause

* editor

* add a macro for debugging

* stop inserting a new line in the sentence

* remove run key

* enter to execute a query

* fix tests for stateful paragraph

* change const to let
pull/137/head
Takayuki Maeda 3 years ago committed by GitHub
parent 566f9ebb43
commit 4bcd4802fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

186
Cargo.lock generated

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.4"
@ -92,6 +98,30 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bit-set"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.2.1"
@ -192,6 +222,15 @@ dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.1"
@ -359,6 +398,34 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "fancy-regex"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf"
dependencies = [
"bit-set",
"regex",
]
[[package]]
name = "flate2"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80edafed416a46fb378521624fab1cfa2eb514784fd8921adbe8a8d8321da811"
dependencies = [
"cfg-if",
"crc32fast",
"libc",
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
@ -522,9 +589,11 @@ dependencies = [
"structopt",
"strum",
"strum_macros",
"syntect",
"tokio",
"toml",
"tui",
"unicode-segmentation",
"unicode-width",
"which",
]
@ -644,6 +713,12 @@ dependencies = [
"spin",
]
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "lexical-core"
version = "0.7.6"
@ -680,6 +755,21 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "line-wrap"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
dependencies = [
"safemem",
]
[[package]]
name = "linked-hash-map"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
name = "lock_api"
version = "0.4.4"
@ -727,6 +817,16 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "miniz_oxide"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
dependencies = [
"adler",
"autocfg 1.0.1",
]
[[package]]
name = "mio"
version = "0.7.13"
@ -924,6 +1024,20 @@ version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
[[package]]
name = "plist"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38d026d73eeaf2ade76309d0c65db5a35ecf649e3cec428db316243ea9d6711"
dependencies = [
"base64",
"chrono",
"indexmap",
"line-wrap",
"serde",
"xml-rs",
]
[[package]]
name = "ppv-lite86"
version = "0.2.10"
@ -1131,6 +1245,21 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "safemem"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -1480,6 +1609,28 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "syntect"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b20815bbe80ee0be06e6957450a841185fcf690fe0178f14d77a05ce2caa031"
dependencies = [
"bincode",
"bitflags",
"fancy-regex",
"flate2",
"fnv",
"lazy_static",
"lazycell",
"plist",
"regex-syntax",
"serde",
"serde_derive",
"serde_json",
"walkdir",
"yaml-rust",
]
[[package]]
name = "tap"
version = "1.0.1"
@ -1699,6 +1850,17 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
@ -1825,6 +1987,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@ -1837,6 +2008,21 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]]
name = "xml-rs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "zeroize"
version = "1.3.0"

@ -37,6 +37,8 @@ rust_decimal = "1.15"
dirs-next = "2.0"
clap = "2.33.3"
structopt = "0.3.22"
syntect = { version = "4.5", default-features = false, features = ["metadata", "default-fancy"]}
unicode-segmentation = "1.7"
[target.'cfg(all(target_family="unix",not(target_os="macos")))'.dependencies]
which = "4.1"

@ -5,12 +5,27 @@ host = "localhost"
database = "world"
port = 3306
[[conn]]
type = "mysql"
user = "root"
host = "'127.0.0.1'"
database = "world"
port = 3306
[[conn]]
type = "mysql"
user = "root"
host = "localhost"
port = 3306
[[conn]]
type = "mysql"
user = "tako8ki"
host = "localhost"
port = 3306
database = "world"
password = "password"
[[conn]]
type = "postgres"
user = "postgres"
@ -22,6 +37,22 @@ type = "postgres"
user = "postgres"
host = "localhost"
port = 5432
password = "password"
database = "dvdrental"
[[conn]]
type = "postgres"
user = "hoge"
host = "localhost"
port = 5432
password = "hoge"
database = "dvdrental"
[[conn]]
type = "postgres"
user = "hoge"
host = "localhost"
port = 5432
database = "dvdrental"
[[conn]]

@ -8,7 +8,7 @@ use crate::{
components::tab::Tab,
components::{
command, ConnectionsComponent, DatabasesComponent, ErrorComponent, HelpComponent,
RecordTableComponent, TabComponent, TableComponent,
RecordTableComponent, SqlEditorComponent, TabComponent, TableComponent,
},
config::Config,
};
@ -30,6 +30,7 @@ pub struct App {
constraint_table: TableComponent,
foreign_key_table: TableComponent,
index_table: TableComponent,
sql_editor: SqlEditorComponent,
focus: Focus,
tab: TabComponent,
help: HelpComponent,
@ -51,6 +52,7 @@ impl App {
constraint_table: TableComponent::new(config.key_config.clone()),
foreign_key_table: TableComponent::new(config.key_config.clone()),
index_table: TableComponent::new(config.key_config.clone()),
sql_editor: SqlEditorComponent::new(config.key_config.clone()),
tab: TabComponent::new(config.key_config.clone()),
help: HelpComponent::new(config.key_config.clone()),
databases: DatabasesComponent::new(config.key_config.clone()),
@ -84,8 +86,7 @@ impl App {
.split(f.size());
self.databases
.draw(f, main_chunks[0], matches!(self.focus, Focus::DabataseList))
.unwrap();
.draw(f, main_chunks[0], matches!(self.focus, Focus::DabataseList))?;
let right_chunks = Layout::default()
.direction(Direction::Vertical)
@ -117,6 +118,10 @@ impl App {
self.index_table
.draw(f, right_chunks[1], matches!(self.focus, Focus::Table))?
}
Tab::Sql => {
self.sql_editor
.draw(f, right_chunks[1], matches!(self.focus, Focus::Table))?;
}
}
self.error.draw(f, Rect::default(), false)?;
self.help.draw(f, Rect::default(), false)?;
@ -437,6 +442,17 @@ impl App {
}
};
}
Tab::Sql => {
if self.sql_editor.event(key)?.is_consumed()
|| self
.sql_editor
.async_event(key, self.pool.as_ref().unwrap())
.await?
.is_consumed()
{
return Ok(EventState::Consumed);
};
}
};
}
}

@ -131,6 +131,10 @@ pub fn tab_indexes(key: &KeyConfig) -> CommandText {
CommandText::new(format!("Indexes [{}]", key.tab_indexes), CMD_GROUP_TABLE)
}
pub fn tab_sql_editor(key: &KeyConfig) -> CommandText {
CommandText::new(format!("SQL [{}]", key.tab_sql_editor), CMD_GROUP_TABLE)
}
pub fn toggle_tabs(key_config: &KeyConfig) -> CommandText {
CommandText::new(
format!(

@ -11,7 +11,10 @@ use tui::{
Frame,
};
const RESERVED_WORDS: &[&str] = &["IN", "AND", "OR", "NOT", "NULL", "IS"];
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,
@ -21,12 +24,19 @@ pub struct CompletionComponent {
}
impl CompletionComponent {
pub fn new(key_config: KeyConfig, word: impl Into<String>) -> Self {
pub fn new(key_config: KeyConfig, word: impl Into<String>, all: bool) -> Self {
Self {
key_config,
state: ListState::default(),
word: word.into(),
candidates: RESERVED_WORDS.iter().map(|w| w.to_string()).collect(),
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()
},
}
}
@ -145,7 +155,7 @@ mod test {
#[test]
fn test_filterd_candidates_lowercase() {
assert_eq!(
CompletionComponent::new(KeyConfig::default(), "an")
CompletionComponent::new(KeyConfig::default(), "an", false)
.filterd_candidates()
.collect::<Vec<&String>>(),
vec![&"AND".to_string()]
@ -155,7 +165,7 @@ mod test {
#[test]
fn test_filterd_candidates_uppercase() {
assert_eq!(
CompletionComponent::new(KeyConfig::default(), "AN")
CompletionComponent::new(KeyConfig::default(), "AN", false)
.filterd_candidates()
.collect::<Vec<&String>>(),
vec![&"AND".to_string()]
@ -165,14 +175,14 @@ mod test {
#[test]
fn test_filterd_candidates_multiple_candidates() {
assert_eq!(
CompletionComponent::new(KeyConfig::default(), "n")
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")
CompletionComponent::new(KeyConfig::default(), "N", false)
.filterd_candidates()
.collect::<Vec<&String>>(),
vec![&"NOT".to_string(), &"NULL".to_string()]

@ -6,6 +6,7 @@ pub mod databases;
pub mod error;
pub mod help;
pub mod record_table;
pub mod sql_editor;
pub mod tab;
pub mod table;
pub mod table_filter;
@ -24,6 +25,7 @@ pub use databases::DatabasesComponent;
pub use error::ErrorComponent;
pub use help::HelpComponent;
pub use record_table::RecordTableComponent;
pub use sql_editor::SqlEditorComponent;
pub use tab::TabComponent;
pub use table::TableComponent;
pub use table_filter::TableFilterComponent;
@ -33,6 +35,7 @@ pub use table_value::TableValueComponent;
#[cfg(debug_assertions)]
pub use debug::DebugComponent;
use crate::database::Pool;
use anyhow::Result;
use async_trait::async_trait;
use std::convert::TryInto;
@ -87,6 +90,14 @@ pub trait Component {
fn event(&mut self, key: crate::event::Key) -> Result<EventState>;
async fn async_event(
&mut self,
_key: crate::event::Key,
_pool: &Box<dyn Pool>,
) -> Result<EventState> {
Ok(EventState::NotConsumed)
}
fn focused(&self) -> bool {
false
}

@ -0,0 +1,289 @@
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,
query: String,
}
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,
query: query.to_string(),
})
}
}
return Ok(EventState::Consumed);
}
Ok(EventState::NotConsumed)
}
}

@ -20,6 +20,7 @@ pub enum Tab {
Constraints,
ForeignKeys,
Indexes,
Sql,
}
impl std::fmt::Display for Tab {
@ -52,6 +53,7 @@ impl TabComponent {
command::tab_constraints(&self.key_config).name,
command::tab_foreign_keys(&self.key_config).name,
command::tab_indexes(&self.key_config).name,
command::tab_sql_editor(&self.key_config).name,
]
}
}
@ -90,7 +92,7 @@ impl Component for TabComponent {
self.selected_tab = Tab::ForeignKeys;
return Ok(EventState::Consumed);
} else if key == self.key_config.tab_indexes {
self.selected_tab = Tab::Indexes;
self.selected_tab = Tab::Sql;
return Ok(EventState::Consumed);
}
Ok(EventState::NotConsumed)

@ -34,7 +34,7 @@ impl TableFilterComponent {
input: Vec::new(),
input_idx: 0,
input_cursor_position: 0,
completion: CompletionComponent::new(key_config, ""),
completion: CompletionComponent::new(key_config, "", false),
}
}

@ -87,6 +87,7 @@ pub struct KeyConfig {
pub exit_popup: Key,
pub focus_right: Key,
pub focus_left: Key,
pub focus_above: Key,
pub focus_connections: Key,
pub open_help: Key,
pub filter: Key,
@ -105,6 +106,7 @@ pub struct KeyConfig {
pub tab_indexes: Key,
pub extend_or_shorten_widget_width_to_right: Key,
pub extend_or_shorten_widget_width_to_left: Key,
pub tab_sql_editor: Key,
}
impl Default for KeyConfig {
@ -123,6 +125,7 @@ impl Default for KeyConfig {
exit_popup: Key::Esc,
focus_right: Key::Right,
focus_left: Key::Left,
focus_above: Key::Up,
focus_connections: Key::Char('c'),
open_help: Key::Char('?'),
filter: Key::Char('/'),
@ -141,6 +144,7 @@ impl Default for KeyConfig {
tab_indexes: Key::Char('5'),
extend_or_shorten_widget_width_to_right: Key::Char('>'),
extend_or_shorten_widget_width_to_left: Key::Char('<'),
tab_sql_editor: Key::Char('6'),
}
}
}

@ -12,7 +12,8 @@ use database_tree::{Child, Database, Table};
pub const RECORDS_LIMIT_PER_PAGE: u8 = 200;
#[async_trait]
pub trait Pool {
pub trait Pool: Send + Sync {
async fn execute(&self, query: &String) -> anyhow::Result<ExecuteResult>;
async fn get_databases(&self) -> anyhow::Result<Vec<Database>>;
async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Child>>;
async fn get_records(
@ -45,6 +46,18 @@ pub trait Pool {
async fn close(&self);
}
pub enum ExecuteResult {
Read {
headers: Vec<String>,
rows: Vec<Vec<String>>,
database: Database,
table: Table,
},
Write {
updated_rows: u64,
},
}
pub trait TableRow: std::marker::Send {
fn fields(&self) -> Vec<String>;
fn columns(&self) -> Vec<String>;

@ -1,6 +1,6 @@
use crate::get_or_null;
use super::{Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use super::{ExecuteResult, Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use async_trait::async_trait;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use database_tree::{Child, Database, Table};
@ -146,6 +146,49 @@ impl TableRow for Index {
#[async_trait]
impl Pool for MySqlPool {
async fn execute(&self, query: &String) -> anyhow::Result<ExecuteResult> {
let query = query.trim();
if query.starts_with("SELECT") || query.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("SHOW DATABASES")
.fetch_all(&self.pool)

@ -1,6 +1,6 @@
use crate::get_or_null;
use super::{Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use super::{ExecuteResult, Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use async_trait::async_trait;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use database_tree::{Child, Database, Schema, Table};
@ -147,6 +147,47 @@ impl TableRow for Index {
#[async_trait]
impl Pool for PostgresPool {
async fn execute(&self, query: &String) -> anyhow::Result<ExecuteResult> {
let query = query.trim();
if query.starts_with("SELECT") || query.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 datname FROM pg_database")
.fetch_all(&self.pool)

@ -1,6 +1,6 @@
use crate::get_or_null;
use super::{Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use super::{ExecuteResult, Pool, TableRow, RECORDS_LIMIT_PER_PAGE};
use async_trait::async_trait;
use chrono::NaiveDateTime;
use database_tree::{Child, Database, Table};
@ -150,6 +150,47 @@ impl TableRow for Index {
#[async_trait]
impl Pool for SqlitePool {
async fn execute(&self, query: &String) -> anyhow::Result<ExecuteResult> {
let query = query.trim();
if query.starts_with("SELECT") || query.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)

@ -61,3 +61,21 @@ macro_rules! outln {
writeln!($config.log_level.write(&$level), $($expr),+).expect("Can't write output");
}}
}
#[macro_export]
macro_rules! debug {
($($expr:expr),+) => {
#[cfg(debug_assertions)]
{
use std::io::{Write};
use std::fs::OpenOptions;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.append(true)
.open("gobang.log")
.unwrap();
writeln!(file, $($expr),+).expect("Can't write output");
}
}
}

@ -2,8 +2,11 @@ use crate::config::KeyConfig;
use crate::event::Key;
use database_tree::MoveSelection;
pub mod reflow;
pub mod scrollbar;
pub mod scrolllist;
pub mod stateful_paragraph;
pub mod syntax_text;
pub fn common_nav(key: Key, key_config: &KeyConfig) -> Option<MoveSelection> {
if key == key_config.scroll_down {

@ -0,0 +1,545 @@
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!"
]
);
}
}

@ -0,0 +1,182 @@
#![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;
}
}

@ -0,0 +1,106 @@
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…
Cancel
Save