Add sqlformat support

This commit is contained in:
Spxg
2025-05-27 01:04:02 +08:00
parent 9bc627488a
commit 5f88554db9
13 changed files with 359 additions and 143 deletions

30
Cargo.lock generated
View File

@@ -212,7 +212,7 @@ dependencies = [
"pathdiff",
"serde",
"toml",
"winnow",
"winnow 0.7.10",
]
[[package]]
@@ -1807,6 +1807,16 @@ dependencies = [
"web-sys",
]
[[package]]
name = "sqlformat"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d7b3e8a3b6f2ee93ac391a0f757c13790caa0147892e3545cd549dd5b54bc0"
dependencies = [
"unicode_categories",
"winnow 0.6.26",
]
[[package]]
name = "sqlight"
version = "0.1.0"
@@ -1829,6 +1839,7 @@ dependencies = [
"serde-wasm-bindgen",
"serde_json",
"split-grid",
"sqlformat",
"sqlite-wasm-rs",
"thiserror 2.0.12",
"tokio",
@@ -2041,7 +2052,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
"winnow 0.7.10",
]
[[package]]
@@ -2088,6 +2099,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "url"
version = "2.5.4"
@@ -2349,6 +2366,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.7.10"

View File

@@ -38,3 +38,4 @@ log = "0.4.27"
fragile = "2.0.1"
hex = "0.4.3"
prettytable-rs = "0.10.0"
sqlformat = "0.3.5"

View File

@@ -0,0 +1,38 @@
.-buttonReset {
color: var(--font-color);
border: none;
background: inherit;
background-color: transparent; /* IE 11 */
padding: 0;
font: inherit;
line-height: inherit;
text-align: inherit;
}
.-menuItemFullButton {
composes: -buttonReset;
transition: color var(--header-transition);
width: 100%;
user-select: text;
}
.container {
composes: -menuItemFullButton;
&:hover {
color: var(--header-tint);
}
}
.-menuItemTitle {
font-weight: 600;
}
.name {
composes: -menuItemTitle;
margin: 0;
}
.description {
margin: 0;
}

View File

@@ -0,0 +1,4 @@
.aside {
margin: 0.25em 0 0;
color: #888;
}

View File

@@ -28,6 +28,9 @@ mod bindgen {
#[wasm_bindgen(method, js_name = getValue)]
pub fn get_value(this: &Editor) -> String;
#[wasm_bindgen(method, js_name = setValue)]
pub fn set_value(this: &Editor, value: String);
#[wasm_bindgen(method, js_name = getSelectedText)]
pub fn get_selected_text(this: &Editor) -> String;
}
@@ -201,6 +204,10 @@ impl Editor {
self.js.get_value()
}
pub fn set_value(&self, value: String) {
self.js.set_value(value);
}
pub fn get_selected_value(&self) -> String {
self.js.get_selected_text()
}

View File

@@ -45,6 +45,8 @@
<link data-trunk href="./assets/module.postcss/selectable_menu_item.module.css" rel="css">
<link data-trunk href="./assets/module.postcss/loader.module.css" rel="css">
<link data-trunk href="./assets/module.postcss/config_element.module.css" rel="css">
<link data-trunk href="./assets/module.postcss/menu_aside.module.css" rel="css">
<link data-trunk href="./assets/module.postcss/button_menu_item.module.css" rel="css">
<link data-trunk href="./assets/module.postcss/output/execute.module.css" rel="css">
<link data-trunk href="./assets/module.postcss/output/header.module.css" rel="css">

View File

@@ -0,0 +1,25 @@
use istyles::istyles;
use leptos::prelude::*;
use web_sys::MouseEvent;
use crate::app::menu_item::MenuItem;
istyles!(
styles,
"assets/module.postcss/button_menu_item.module.css.map"
);
#[component]
pub fn ButtonMenuItem<C>(name: String, on_click: C, children: Children) -> impl IntoView
where
C: Fn(MouseEvent) + Send + 'static,
{
view! {
<MenuItem>
<button class=styles::container on:click=on_click>
<div class=styles::name>{name}</div>
<div class=styles::description>{children()}</div>
</button>
</MenuItem>
}
}

22
src/app/database_menu.rs Normal file
View File

@@ -0,0 +1,22 @@
use leptos::prelude::*;
use web_sys::MouseEvent;
use crate::app::{button_menu_item::ButtonMenuItem, menu_aside::MenuAside, menu_group::MenuGroup};
#[component]
pub fn DatabaseMenu<L, D>(load: L, download: D) -> impl IntoView
where
L: Fn(MouseEvent) + Send + 'static,
D: Fn(MouseEvent) + Send + 'static,
{
view! {
<MenuGroup title="Database".into()>
<ButtonMenuItem name="Load".into() on_click=load>
<MenuAside>"The KEEP CONTEXT will be automatically enabled."</MenuAside>
</ButtonMenuItem>
<ButtonMenuItem name="Download".into() on_click=download>
<MenuAside>"Will be downloaded as test.db"</MenuAside>
</ButtonMenuItem>
</MenuGroup>
}
}

View File

@@ -3,7 +3,7 @@ use leptos::{html::Input, prelude::*, tachys::html};
use prettytable::{Cell, Row, Table};
use reactive_stores::Store;
use wasm_bindgen::{JsCast, prelude::Closure};
use web_sys::{Blob, Event, FileReader, HtmlInputElement, Url, UrlSearchParams};
use web_sys::{Blob, Event, FileReader, HtmlInputElement, MouseEvent, Url, UrlSearchParams};
use crate::{
FragileComfirmed, LoadDbOptions, PrepareOptions, SQLightError, SQLiteStatementResult,
@@ -14,10 +14,12 @@ use crate::{
button_set::{Button, ButtonSet, IconButton, LinkButton, Rule},
config_menu::ConfigMenu,
context_menu::ContextMenu,
database_menu::DatabaseMenu,
icon::{build_icon, config_icon, expandable_icon, github_icon, more_options_icon},
output::change_focus,
pop_button::PopButton,
state::{Focus, GlobalState, GlobalStateStoreFields},
tools_menu::ToolsMenu,
vfs_menu::VfsMenu,
},
};
@@ -48,14 +50,16 @@ pub fn Header() -> impl IntoView {
</div>
<div class=styles::right>
<input type="file" node_ref=input_ref style="display: none" />
<ButtonSet>
<LoadButton input_ref=input_ref />
<Rule />
<DownloadButton />
</ButtonSet>
<ButtonSet>
<ShareButton />
</ButtonSet>
<ButtonSet>
<DatabaseButton input_ref=input_ref menu_container=menu_container />
</ButtonSet>
<ButtonSet>
<ToolsButton menu_container=menu_container />
</ButtonSet>
<ButtonSet>
<ConfigMenuButton menu_container=menu_container />
</ButtonSet>
@@ -114,140 +118,6 @@ fn ExecuteButton() -> impl IntoView {
}
}
#[component]
fn DownloadButton() -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
Effect::new(move || {
state.exported().track();
let Some(downloaded) = state.exported().write_untracked().take() else {
return;
};
let filename = downloaded.filename;
let buffer = downloaded.data;
let array = js_sys::Array::new();
array.push(&buffer);
let blob = Blob::new_with_u8_array_sequence(&array).unwrap();
let url = Url::create_object_url_with_blob(&blob).unwrap();
let document = document();
let a = document
.create_element("a")
.unwrap()
.dyn_into::<web_sys::HtmlAnchorElement>()
.unwrap();
a.set_href(&url);
a.set_download(&filename);
a.click();
Url::revoke_object_url(&url).unwrap();
});
let on_click = move |_| {
if let Some(worker) = &*state.worker().read() {
worker.send_task(WorkerRequest::DownloadDb);
}
};
view! { <Button on_click=on_click>"Download DB"</Button> }
}
#[component]
fn LoadButton(input_ref: NodeRef<Input>) -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
let (file, set_file) = signal::<Option<FragileComfirmed<web_sys::File>>>(None);
Effect::new(move || {
if let Some(file) = &*file.read() {
let filename = file.name();
if let Ok(reader) = FileReader::new() {
let on_progress = FragileComfirmed::new(Closure::wrap(Box::new(
move |ev: web_sys::ProgressEvent| {
if ev.length_computable() {
state.import_progress().set(Some(ImportProgress {
filename: filename.clone(),
loaded: ev.loaded(),
total: ev.total(),
opened: None,
}));
}
},
)
as Box<dyn FnMut(_)>));
let on_error = FragileComfirmed::new(Closure::wrap(Box::new(
move |ev: web_sys::ProgressEvent| {
let target = ev.target().unwrap();
let reader = target.unchecked_into::<FileReader>();
let dom_error = reader.error().unwrap();
state.last_error().set(Some(FragileComfirmed::new(
SQLightError::ImportDb(dom_error.message().to_string()),
)));
},
)
as Box<dyn FnMut(_)>));
let on_load =
FragileComfirmed::new(Closure::wrap(Box::new(move |ev: web_sys::Event| {
let target = ev.target().unwrap();
let reader = target.unchecked_into::<FileReader>();
let result = reader.result().unwrap();
let array_buffer = result.unchecked_into::<js_sys::ArrayBuffer>();
let data = js_sys::Uint8Array::new(&array_buffer);
if let Some(worker) = &*state.worker().read() {
worker.send_task(WorkerRequest::LoadDb(LoadDbOptions { data }));
}
})
as Box<dyn FnMut(_)>));
reader.set_onprogress(Some(on_progress.as_ref().unchecked_ref()));
reader.set_onerror(Some(on_error.as_ref().unchecked_ref()));
reader.set_onload(Some(on_load.as_ref().unchecked_ref()));
reader.read_as_array_buffer(file).unwrap();
on_cleanup(move || {
drop(on_progress);
drop(on_error);
drop(on_load);
});
}
}
});
let on_change = move |ev: Event| {
if let Some(target) = ev.target() {
let input = target.unchecked_into::<HtmlInputElement>();
if let Some(files) = input.files() {
if files.length() > 0 {
let file = FragileComfirmed::new(files.get(0).unwrap());
set_file.set(Some(file));
} else {
set_file.set(None);
}
}
}
};
let on_click = move |_| {
if let Some(input) = &*input_ref.read() {
if input.onchange().is_none() {
let callback = Closure::wrap(Box::new(on_change) as Box<dyn Fn(Event)>);
input.set_onchange(Some(callback.as_ref().unchecked_ref::<js_sys::Function>()));
callback.forget();
}
input.set_value("");
input.click();
}
};
view! { <Button on_click=on_click>"Load DB"</Button> }
}
#[component]
fn VfsMenuButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
@@ -337,6 +207,173 @@ fn AdvancedOptionsMenuButton(menu_container: NodeRef<html::element::Div>) -> imp
}
}
#[component]
fn ToolsButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
let button = |toggle, node_ref| {
view! {
<Button icon_right=expandable_icon() on_click=toggle node_ref=node_ref>
"Tools"
</Button>
}
.into_any()
};
view! {
<PopButton
button=button
menu=Box::new(|_close| { view! { <ToolsMenu /> }.into_any() })
menu_container=menu_container
></PopButton>
}
}
#[component]
fn DatabaseButton(
input_ref: NodeRef<Input>,
menu_container: NodeRef<html::element::Div>,
) -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
let button = |toggle, node_ref| {
view! {
<Button icon_right=expandable_icon() on_click=toggle node_ref=node_ref>
"Database"
</Button>
}
.into_any()
};
let (file, set_file) = signal::<Option<FragileComfirmed<web_sys::File>>>(None);
Effect::new(move || {
if let Some(file) = &*file.read() {
let filename = file.name();
if let Ok(reader) = FileReader::new() {
let on_progress = FragileComfirmed::new(Closure::wrap(Box::new(
move |ev: web_sys::ProgressEvent| {
if ev.length_computable() {
state.import_progress().set(Some(ImportProgress {
filename: filename.clone(),
loaded: ev.loaded(),
total: ev.total(),
opened: None,
}));
}
},
)
as Box<dyn FnMut(_)>));
let on_error = FragileComfirmed::new(Closure::wrap(Box::new(
move |ev: web_sys::ProgressEvent| {
let target = ev.target().unwrap();
let reader = target.unchecked_into::<FileReader>();
let dom_error = reader.error().unwrap();
state.last_error().set(Some(FragileComfirmed::new(
SQLightError::ImportDb(dom_error.message().to_string()),
)));
},
)
as Box<dyn FnMut(_)>));
let on_load =
FragileComfirmed::new(Closure::wrap(Box::new(move |ev: web_sys::Event| {
let target = ev.target().unwrap();
let reader = target.unchecked_into::<FileReader>();
let result = reader.result().unwrap();
let array_buffer = result.unchecked_into::<js_sys::ArrayBuffer>();
let data = js_sys::Uint8Array::new(&array_buffer);
if let Some(worker) = &*state.worker().read_untracked() {
worker.send_task(WorkerRequest::LoadDb(LoadDbOptions { data }));
}
})
as Box<dyn FnMut(_)>));
reader.set_onprogress(Some(on_progress.as_ref().unchecked_ref()));
reader.set_onerror(Some(on_error.as_ref().unchecked_ref()));
reader.set_onload(Some(on_load.as_ref().unchecked_ref()));
reader.read_as_array_buffer(file).unwrap();
on_cleanup(move || {
drop(on_progress);
drop(on_error);
drop(on_load);
});
}
}
});
let on_change = move |ev: Event| {
if let Some(target) = ev.target() {
let input = target.unchecked_into::<HtmlInputElement>();
if let Some(files) = input.files() {
if files.length() > 0 {
let file = FragileComfirmed::new(files.get(0).unwrap());
set_file.set(Some(file));
} else {
set_file.set(None);
}
}
}
};
let on_load = move |_: MouseEvent| {
if let Some(input) = &*input_ref.read() {
if input.onchange().is_none() {
let callback = Closure::wrap(Box::new(on_change) as Box<dyn Fn(Event)>);
input.set_onchange(Some(callback.as_ref().unchecked_ref::<js_sys::Function>()));
callback.forget();
}
input.set_value("");
input.click();
}
};
Effect::new(move || {
state.exported().track();
let Some(downloaded) = state.exported().write_untracked().take() else {
return;
};
let filename = downloaded.filename;
let buffer = downloaded.data;
let array = js_sys::Array::new();
array.push(&buffer);
let blob = Blob::new_with_u8_array_sequence(&array).unwrap();
let url = Url::create_object_url_with_blob(&blob).unwrap();
let document = document();
let a = document
.create_element("a")
.unwrap()
.dyn_into::<web_sys::HtmlAnchorElement>()
.unwrap();
a.set_href(&url);
a.set_download(&filename);
a.click();
Url::revoke_object_url(&url).unwrap();
});
let on_download = move |_| {
if let Some(worker) = &*state.worker().read() {
worker.send_task(WorkerRequest::DownloadDb);
}
};
view! {
<PopButton
button=button
menu=Box::new(move |_close| {
view! { <DatabaseMenu load=on_load download=on_download /> }.into_any()
})
menu_container=menu_container
></PopButton>
}
}
#[component]
fn ShareButton() -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();

9
src/app/menu_aside.rs Normal file
View File

@@ -0,0 +1,9 @@
use istyles::istyles;
use leptos::prelude::*;
istyles!(styles, "assets/module.postcss/menu_aside.module.css.map");
#[component]
pub fn MenuAside(children: Children) -> impl IntoView {
view! { <p class=styles::aside>{children()}</p> }
}

View File

@@ -1,12 +1,15 @@
mod advanced_options_menu;
mod button_menu_item;
mod button_set;
mod config_element;
mod config_menu;
mod context_menu;
mod database_menu;
mod editor;
mod header;
mod icon;
mod loader;
mod menu_aside;
mod menu_group;
mod menu_item;
mod output;
@@ -15,6 +18,7 @@ mod pop_button;
mod select_one;
mod selectable_menu_item;
mod state;
mod tools_menu;
mod vfs_menu;
pub use playground::playground;

View File

@@ -20,7 +20,7 @@ INSERT INTO blobs(data) VALUES (randomblob(12));
SELECT 'Hello World!',
datetime('now','localtime') AS TM,
x'73716c69676874' AS BLOB_VAL,
unhex('73716c69676874') AS BLOB_VAL,
NULL as NULL_VAL;
SELECT * FROM blobs;";

41
src/app/tools_menu.rs Normal file
View File

@@ -0,0 +1,41 @@
use leptos::prelude::*;
use reactive_stores::Store;
use sqlformat::{FormatOptions, QueryParams};
use crate::app::{
GlobalState, GlobalStateStoreFields, button_menu_item::ButtonMenuItem, menu_aside::MenuAside,
menu_group::MenuGroup,
};
#[component]
pub fn ToolsMenu() -> impl IntoView {
let state = expect_context::<Store<GlobalState>>();
let format = move |_event| {
let Some(editor) = &*state.editor().read() else {
return;
};
let format_options = FormatOptions {
uppercase: Some(true),
lines_between_queries: 2,
..Default::default()
};
let sql = sqlformat::format(
&editor.get_value(),
&QueryParams::default(),
&format_options,
);
editor.set_value(sql);
};
view! {
<MenuGroup title="Tools".into()>
<ButtonMenuItem name="SQL Format".into() on_click=format>
<div>Format this code with sqlformat.</div>
<MenuAside>"https://crates.io/crates/sqlformat"</MenuAside>
</ButtonMenuItem>
</MenuGroup>
}
}