From bda45ac7a0f8110339cea0bb90d19d6d0175e75b Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 25 Mar 2026 14:06:01 +0100 Subject: [PATCH 1/4] Add rudimentary settings.json support --- crates/edit/src/bin/edit/apperr.rs | 1 + crates/edit/src/bin/edit/documents.rs | 13 ++- crates/edit/src/bin/edit/draw_menubar.rs | 23 ++++- crates/edit/src/bin/edit/main.rs | 7 ++ crates/edit/src/bin/edit/settings.rs | 119 +++++++++++++++++++++++ crates/edit/src/bin/edit/state.rs | 21 +++- crates/edit/src/buffer/mod.rs | 6 +- i18n/edit.toml | 27 +++++ 8 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 crates/edit/src/bin/edit/settings.rs diff --git a/crates/edit/src/bin/edit/apperr.rs b/crates/edit/src/bin/edit/apperr.rs index baa8d071a8e..0f916a20e19 100644 --- a/crates/edit/src/bin/edit/apperr.rs +++ b/crates/edit/src/bin/edit/apperr.rs @@ -7,6 +7,7 @@ use edit::{buffer, icu}; #[derive(Debug)] pub enum Error { + SettingsInvalid(&'static str), Io(io::Error), Icu(icu::Error), } diff --git a/crates/edit/src/bin/edit/documents.rs b/crates/edit/src/bin/edit/documents.rs index 974963db871..1bbdde61afd 100644 --- a/crates/edit/src/bin/edit/documents.rs +++ b/crates/edit/src/bin/edit/documents.rs @@ -12,6 +12,7 @@ use edit::lsh::{FILE_ASSOCIATIONS, Language, process_file_associations}; use edit::{path, sys}; use crate::apperr; +use crate::settings::Settings; use crate::state::DisplayablePathBuf; pub struct Document { @@ -92,10 +93,14 @@ impl Document { return lang; } - if let Some(path) = &self.path - && let Some(lang) = process_file_associations(FILE_ASSOCIATIONS, path) - { - return Some(lang); + if let Some(path) = &self.path { + let settings = Settings::borrow(); + if let Some(lang) = process_file_associations(&settings.file_associations, path) { + return Some(lang); + } + if let Some(lang) = process_file_associations(FILE_ASSOCIATIONS, path) { + return Some(lang); + } } None diff --git a/crates/edit/src/bin/edit/draw_menubar.rs b/crates/edit/src/bin/edit/draw_menubar.rs index a8e1da78c24..1ab702b6464 100644 --- a/crates/edit/src/bin/edit/draw_menubar.rs +++ b/crates/edit/src/bin/edit/draw_menubar.rs @@ -7,6 +7,7 @@ use edit::tui::*; use stdext::arena_format; use crate::localization::*; +use crate::settings::Settings; use crate::state::*; pub fn draw_menubar(ctx: &mut Context, state: &mut State) { @@ -51,10 +52,28 @@ fn draw_menu_file(ctx: &mut Context, state: &mut State) { if ctx.menubar_menu_button(loc(LocId::FileSaveAs), 'A', vk::NULL) { state.wants_file_picker = StateFilePicker::SaveAs; } - if ctx.menubar_menu_button(loc(LocId::FileClose), 'C', kbmod::CTRL | vk::W) { - state.wants_close = true; + } + #[allow(irrefutable_let_patterns)] + if let path = Settings::borrow().path.as_path() + && !path.as_os_str().is_empty() + && ctx.menubar_menu_button(loc(LocId::FilePreferences), 'P', vk::NULL) + { + match state.documents.add_file_path(path) { + Ok(doc) => { + if let mut tb = doc.buffer.borrow_mut() + && tb.text_length() == 0 + { + Settings::bootstrap(&mut tb); + } + } + Err(err) => error_log_add(ctx, state, err), } } + if state.documents.active().is_some() + && ctx.menubar_menu_button(loc(LocId::FileClose), 'C', kbmod::CTRL | vk::W) + { + state.wants_close = true; + } if ctx.menubar_menu_button(loc(LocId::FileExit), 'X', kbmod::CTRL | vk::Q) { state.wants_exit = true; } diff --git a/crates/edit/src/bin/edit/main.rs b/crates/edit/src/bin/edit/main.rs index 8444df43dee..30ad149b20b 100644 --- a/crates/edit/src/bin/edit/main.rs +++ b/crates/edit/src/bin/edit/main.rs @@ -8,6 +8,7 @@ mod draw_filepicker; mod draw_menubar; mod draw_statusbar; mod localization; +mod settings; mod state; use std::borrow::Cow; @@ -32,6 +33,8 @@ use stdext::arena::{self, Arena, scratch_arena}; use stdext::arena_format; use stdext::collections::{BString, BVec}; +use crate::settings::Settings; + #[cfg(target_pointer_width = "32")] const SCRATCH_ARENA_CAPACITY: usize = 128 * MEBI; #[cfg(target_pointer_width = "64")] @@ -72,6 +75,10 @@ fn run() -> apperr::Result<()> { return Ok(()); } + if let Err(err) = Settings::reload() { + state.add_error(err); + } + // This will reopen stdin if it's redirected (which may fail) and switch // the terminal to raw mode which prevents the user from pressing Ctrl+C. // `handle_args` may want to print a help message (must not fail), diff --git a/crates/edit/src/bin/edit/settings.rs b/crates/edit/src/bin/edit/settings.rs new file mode 100644 index 00000000000..9dd9cdc70f7 --- /dev/null +++ b/crates/edit/src/bin/edit/settings.rs @@ -0,0 +1,119 @@ +use std::path::PathBuf; + +use edit::buffer::TextBuffer; +use edit::cell::{Ref, SemiRefCell}; +use edit::json; +use edit::lsh::{LANGUAGES, Language}; +use stdext::arena::{read_to_string, scratch_arena}; +use stdext::arena_format; + +use crate::apperr; + +pub struct Settings { + pub path: PathBuf, + pub file_associations: Vec<(String, &'static Language)>, +} + +struct SettingsCell(SemiRefCell); +unsafe impl Sync for SettingsCell {} +static SETTINGS: SettingsCell = SettingsCell(SemiRefCell::new(Settings::new())); + +impl Settings { + /// Fills the given settings.json text buffer with some initial contents for convenience. + pub fn bootstrap(tb: &mut TextBuffer) { + tb.set_crlf(false); + tb.write_raw(b"{\n}\n"); + tb.cursor_move_to_logical(Default::default()); + tb.mark_as_clean(); + } + + const fn new() -> Self { + Settings { path: PathBuf::new(), file_associations: Vec::new() } + } + + pub fn borrow() -> Ref<'static, Settings> { + SETTINGS.0.borrow() + } + + pub fn reload() -> apperr::Result<()> { + let s = &mut *SETTINGS.0.borrow_mut(); + + // Reset all members if we had been loaded previously. + if !s.path.as_os_str().is_empty() { + *s = Settings::new(); + } + + s.load() + } + + fn load(&mut self) -> apperr::Result<()> { + self.path = match settings_json_path() { + Some(p) => p, + None => return Ok(()), + }; + + let scratch = scratch_arena(None); + let str = match read_to_string(&scratch, &self.path) { + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(err) => return Err(err.into()), + Ok(str) => str, + }; + let Ok(json) = json::parse(&scratch, &str) else { + return Err(apperr::Error::SettingsInvalid("Invalid JSON")); + }; + let Some(root) = json.as_object() else { + return Err(apperr::Error::SettingsInvalid("Non-object root")); + }; + + if let Some(f) = root.get_object("files.associations") { + for &(mut key, ref value) in f.iter() { + if !key.contains('/') { + key = arena_format!(&*scratch, "**/{key}").leak(); + } + + let Some(id) = value.as_str() else { + return Err(apperr::Error::SettingsInvalid("files.associations")); + }; + let Some(language) = LANGUAGES.iter().find(|lang| lang.id == id) else { + return Err(apperr::Error::SettingsInvalid("language ID")); + }; + + self.file_associations.push((key.to_string(), language)); + } + } + + Ok(()) + } +} + +fn settings_json_path() -> Option { + let mut config_dir = config_dir()?; + config_dir.push("settings.json"); + Some(config_dir) +} + +fn config_dir() -> Option { + fn var_path(key: &str) -> Option { + std::env::var_os(key).map(PathBuf::from) + } + + fn push(mut path: PathBuf, suffix: &str) -> PathBuf { + path.push(suffix); + path + } + + #[cfg(target_os = "windows")] + { + var_path("APPDATA").map(|p| push(p, "Microsoft/Edit")) + } + #[cfg(any(target_os = "macos", target_os = "ios"))] + { + var_path("HOME").map(|p| push(p, "Library/Application Support/com.microsoft.edit")) + } + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "ios")))] + { + var_path("XDG_CONFIG_HOME") + .or_else(|| var_path("HOME").map(|p| push(p, ".config"))) + .map(|p| push(p, "msedit")) + } +} diff --git a/crates/edit/src/bin/edit/state.rs b/crates/edit/src/bin/edit/state.rs index b3ac2806b42..13a1cefbbea 100644 --- a/crates/edit/src/bin/edit/state.rs +++ b/crates/edit/src/bin/edit/state.rs @@ -28,6 +28,9 @@ impl From for FormatApperr { impl std::fmt::Display for FormatApperr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.0 { + apperr::Error::SettingsInvalid(what) => { + write!(f, "{}{}", loc(LocId::SettingsInvalid), what) + } apperr::Error::Icu(icu::ICU_MISSING_ERROR) => f.write_str(loc(LocId::ErrorIcuMissing)), apperr::Error::Icu(ref err) => err.fmt(f), apperr::Error::Io(ref err) => err.fmt(f), @@ -226,6 +229,18 @@ impl State { exit: false, }) } + + pub fn add_error(&mut self, err: apperr::Error) -> bool { + let msg = format!("{}", FormatApperr::from(err)); + if msg.is_empty() { + return false; + } + + self.error_log[self.error_log_index] = msg; + self.error_log_index = (self.error_log_index + 1) % self.error_log.len(); + self.error_log_count = self.error_log.len().min(self.error_log_count + 1); + true + } } pub fn draw_add_untitled_document(ctx: &mut Context, state: &mut State) { @@ -235,11 +250,7 @@ pub fn draw_add_untitled_document(ctx: &mut Context, state: &mut State) { } pub fn error_log_add(ctx: &mut Context, state: &mut State, err: apperr::Error) { - let msg = format!("{}", FormatApperr::from(err)); - if !msg.is_empty() { - state.error_log[state.error_log_index] = msg; - state.error_log_index = (state.error_log_index + 1) % state.error_log.len(); - state.error_log_count = state.error_log.len().min(state.error_log_count + 1); + if state.add_error(err) { ctx.needs_rerender(); } } diff --git a/crates/edit/src/buffer/mod.rs b/crates/edit/src/buffer/mod.rs index b63e72eac08..1e2b4611001 100644 --- a/crates/edit/src/buffer/mod.rs +++ b/crates/edit/src/buffer/mod.rs @@ -352,12 +352,14 @@ impl TextBuffer { self.buffer.generation() } - /// Force the buffer to be dirty. + /// Force the buffer to be dirty (needs to be saved to disk). pub fn mark_as_dirty(&mut self) { self.last_save_generation = self.buffer.generation().wrapping_sub(1); } - fn mark_as_clean(&mut self) { + /// Force the buffer to be clean (has been saved to disk). + /// Use this with caution. It's called automatically on write(). + pub fn mark_as_clean(&mut self) { self.last_save_generation = self.buffer.generation(); } diff --git a/i18n/edit.toml b/i18n/edit.toml index f0eaff969a1..9e5b061615f 100644 --- a/i18n/edit.toml +++ b/i18n/edit.toml @@ -206,6 +206,20 @@ vi = "Luôn" zh_hans = "总是" zh_hant = "總是" +# A generic settings load failue, e.g. "Invalid Settings: files.associations" +[SettingsInvalid] +en = "Invalid Settings: " +de = "Ungültige Einstellungen: " +es = "Configuración no válida: " +fr = "Paramètres non valides : " +it = "Impostazioni non valide: " +ja = "無効な設定: " +ko = "잘못된 설정: " +pt_br = "Configurações inválidas: " +ru = "Недопустимые параметры: " +zh_hans = "无效设置: " +zh_hant = "無效設定: " + # A menu bar item [File] en = "File" @@ -376,6 +390,19 @@ vi = "Lưu thành…" zh_hans = "另存为…" zh_hant = "另存新檔…" +[FilePreferences] +en = "Preferences" +de = "Einstellungen" +es = "Configuración" +fr = "Paramètres" +it = "Impostazioni" +ja = "設定" +ko = "설정" +pt_br = "Configurações" +ru = "Параметры" +zh_hans = "设置" +zh_hant = "設定" + [FileClose] en = "Close File" ar = "إغلاق الملف" From e3b465f589d95cb1521d8007a8774fa01cca6f07 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 25 Mar 2026 14:08:33 +0100 Subject: [PATCH 2/4] Cleanup --- crates/edit/src/bin/edit/draw_menubar.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/edit/src/bin/edit/draw_menubar.rs b/crates/edit/src/bin/edit/draw_menubar.rs index 1ab702b6464..5d1acc2114a 100644 --- a/crates/edit/src/bin/edit/draw_menubar.rs +++ b/crates/edit/src/bin/edit/draw_menubar.rs @@ -53,7 +53,6 @@ fn draw_menu_file(ctx: &mut Context, state: &mut State) { state.wants_file_picker = StateFilePicker::SaveAs; } } - #[allow(irrefutable_let_patterns)] if let path = Settings::borrow().path.as_path() && !path.as_os_str().is_empty() && ctx.menubar_menu_button(loc(LocId::FilePreferences), 'P', vk::NULL) From 1b21589c57f0c4d123a41bce4ecb1a97ffa0f3da Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 25 Mar 2026 18:44:30 +0100 Subject: [PATCH 3/4] Windows path style --- crates/edit/src/bin/edit/settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/edit/src/bin/edit/settings.rs b/crates/edit/src/bin/edit/settings.rs index 9dd9cdc70f7..e29b0970a9b 100644 --- a/crates/edit/src/bin/edit/settings.rs +++ b/crates/edit/src/bin/edit/settings.rs @@ -104,7 +104,7 @@ fn config_dir() -> Option { #[cfg(target_os = "windows")] { - var_path("APPDATA").map(|p| push(p, "Microsoft/Edit")) + var_path("APPDATA").map(|p| push(p, "Microsoft\\Edit")) } #[cfg(any(target_os = "macos", target_os = "ios"))] { From 20fd9ee527c350030ec1e15ff7fcb65c09441f61 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 25 Mar 2026 18:44:44 +0100 Subject: [PATCH 4/4] Our own float parser with... --- crates/edit/benches/lib.rs | 9 ++ crates/edit/src/json.rs | 2 +- crates/stdext/src/float.rs | 244 +++++++++++++++++++++++++++++++++++++ crates/stdext/src/lib.rs | 1 + 4 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 crates/stdext/src/float.rs diff --git a/crates/edit/benches/lib.rs b/crates/edit/benches/lib.rs index af1639c27b2..45c4d52793b 100644 --- a/crates/edit/benches/lib.rs +++ b/crates/edit/benches/lib.rs @@ -10,6 +10,7 @@ use edit::helpers::*; use edit::{buffer, hash, json, oklab, simd, unicode}; use stdext::arena::{self, scratch_arena}; use stdext::collections::BVec; +use stdext::float::parse_f64_approx; use stdext::glob; use stdext::unicode::Utf8Chars; @@ -136,6 +137,13 @@ fn bench_buffer(c: &mut Criterion) { }); } +fn bench_float(c: &mut Criterion) { + c.benchmark_group("float::parse_f64_approx") + .bench_function("123", |b| b.iter(|| parse_f64_approx(black_box(b"123")))) + .bench_function("123.456", |b| b.iter(|| parse_f64_approx(black_box(b"123.456")))) + .bench_function("123.456e3", |b| b.iter(|| parse_f64_approx(black_box(b"123.456e3")))); +} + fn bench_glob(c: &mut Criterion) { // Same benchmark as in glob-match const PATH: &str = "foo/bar/foo/bar/foo/bar/foo/bar/foo/bar.txt"; @@ -282,6 +290,7 @@ fn bench(c: &mut Criterion) { arena::init(128 * MEBI).unwrap(); bench_buffer(c); + bench_float(c); bench_glob(c); bench_hash(c); bench_json(c); diff --git a/crates/edit/src/json.rs b/crates/edit/src/json.rs index 299b2f357d6..54489ec4cc9 100644 --- a/crates/edit/src/json.rs +++ b/crates/edit/src/json.rs @@ -213,7 +213,7 @@ impl<'a, 'i> Parser<'a, 'i> { self.pos += 1; } - if let Ok(num) = self.input[start..self.pos].parse::() + if let Some(num) = stdext::float::parse_f64_approx(&self.bytes[start..self.pos]) && num.is_finite() { Ok(Value::Number(num)) diff --git a/crates/stdext/src/float.rs b/crates/stdext/src/float.rs new file mode 100644 index 00000000000..52a4de2246f --- /dev/null +++ b/crates/stdext/src/float.rs @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! A simple, tiny, approximate (!) float parser. +//! It's a good fit when you're fine with a few ULP of error. +//! +//! It implements the same grammar accepted by `f64::from_str`. + +/// Parse an ASCII byte string into an `f64`. +/// +/// Accepts the same grammar as `f64::from_str`. +/// The result may differ from `f64::from_str` by a few ULP. +pub fn parse_f64_approx(input: &[u8]) -> Option { + if input.is_empty() { + return None; + } + + let mut pos = 0; + + // Sign + let negative = match input[pos] { + b'+' => { + pos += 1; + false + } + b'-' => { + pos += 1; + true + } + _ => false, + }; + + if pos >= input.len() { + return None; + } + + // Special values + let remaining = &input[pos..]; + if remaining.eq_ignore_ascii_case(b"inf") || remaining.eq_ignore_ascii_case(b"infinity") { + return Some(if negative { f64::NEG_INFINITY } else { f64::INFINITY }); + } + if remaining.eq_ignore_ascii_case(b"nan") { + return Some(f64::NAN); + } + + let mut mantissa: u64 = 0; + let mut exponent: i32 = 0; + let mut has_digits = false; + + // Integer part + while pos < input.len() && input[pos].is_ascii_digit() { + has_digits = true; + let d = (input[pos] - b'0') as u64; + if mantissa < 1_000_000_000_000_000_000 { + mantissa = mantissa * 10 + d; + } else { + exponent += 1; + } + pos += 1; + } + + // Fractional part + if pos < input.len() && input[pos] == b'.' { + pos += 1; + while pos < input.len() && input[pos].is_ascii_digit() { + has_digits = true; + let d = (input[pos] - b'0') as u64; + if mantissa < 1_000_000_000_000_000_000 { + mantissa = mantissa * 10 + d; + exponent -= 1; + } + pos += 1; + } + } + + // Must have had at least one digit + if !has_digits { + return None; + } + + // Explicit exponent + if pos < input.len() && (input[pos] == b'e' || input[pos] == b'E') { + pos += 1; + + let exp_negative = match input.get(pos) { + Some(b'+') => { + pos += 1; + false + } + Some(b'-') => { + pos += 1; + true + } + _ => false, + }; + + // Must have at least one exponent digit + if pos >= input.len() || !input[pos].is_ascii_digit() { + return None; + } + + let mut exp_val: i32 = 0; + while pos < input.len() && input[pos].is_ascii_digit() { + exp_val = exp_val.saturating_mul(10).saturating_add((input[pos] - b'0') as i32); + pos += 1; + } + + if exp_negative { + exponent = exponent.saturating_sub(exp_val); + } else { + exponent = exponent.saturating_add(exp_val); + } + } + + // Must have consumed the entire input + if pos != input.len() { + return None; + } + + let mut value = mantissa as f64; + + if exponent != 0 { + const TABLE: [f64; 8] = [1e-3, 1e-2, 1e-1, 1e0, 1e1, 1e2, 1e3, 1e4]; + value *= match TABLE.get((exponent + 3) as usize) { + Some(&v) => v, + None => 10f64.powi(exponent), + }; + } + + if negative { + value = -value; + } + + Some(value) +} + +#[cfg(test)] +#[allow(clippy::approx_constant)] +mod tests { + use super::*; + + /// Helper: parse and unwrap. + fn p(s: &str) -> f64 { + parse_f64_approx(s.as_bytes()) + .unwrap_or_else(|| panic!("parse_f64 returned None for {:?}", s)) + } + + /// Helper: assert parse fails. + fn fail(s: &str) { + assert!(parse_f64_approx(s.as_bytes()).is_none(), "expected None for {:?}", s); + } + + /// Helper: assert result is within 1 ULP of expected. + fn approx(s: &str, expected: f64) { + let got = p(s); + let diff = (got.to_bits() as i64).wrapping_sub(expected.to_bits() as i64).unsigned_abs(); + assert!(diff <= 1, "more than 1 ULP off for {:?}: got={}, expected={}", s, got, expected); + } + + // ---- Integers ---- + #[test] + fn integers() { + assert_eq!(p("0"), 0.0); + assert_eq!(p("1"), 1.0); + assert_eq!(p("123"), 123.0); + assert_eq!(p("007"), 7.0); + assert_eq!(p("-456"), -456.0); + assert_eq!(p("+42"), 42.0); + } + + // ---- Decimals ---- + #[test] + fn decimals() { + assert_eq!(p("3.14"), 3.14); + assert_eq!(p("0.5"), 0.5); + assert_eq!(p(".5"), 0.5); + assert_eq!(p("5."), 5.0); + assert_eq!(p("-3.14"), -3.14); + assert_eq!(p("0.0"), 0.0); + } + + // ---- Scientific notation ---- + #[test] + fn scientific() { + assert_eq!(p("1e3"), 1e3); + assert_eq!(p("2.5E10"), 2.5e10); + approx("2.5e-10", 2.5e-10); + approx("1.5e-3", 0.0015); + assert_eq!(p("1e0"), 1.0); + assert_eq!(p("1e+2"), 100.0); + } + + // ---- Special values ---- + #[test] + fn special_values() { + assert_eq!(p("inf"), f64::INFINITY); + assert_eq!(p("-inf"), f64::NEG_INFINITY); + assert_eq!(p("+infinity"), f64::INFINITY); + assert_eq!(p("Inf"), f64::INFINITY); + assert_eq!(p("INFINITY"), f64::INFINITY); + assert!(p("NaN").is_nan()); + assert!(p("nan").is_nan()); + assert!(p("NAN").is_nan()); + } + + // ---- Edge: many digits ---- + #[test] + fn many_digits() { + // 19+ digit integer — truncation kicks in but result is close + let v = p("12345678901234567890"); + assert!((v - 12345678901234567890.0f64).abs() / v < 1e-15); + } + + // ---- Errors ---- + #[test] + fn errors() { + fail(""); + fail("+"); + fail("-"); + fail("."); + fail("e5"); + fail("1e"); + fail("1e+"); + fail("1e-"); + fail("abc"); + fail(" 1"); + fail("1 "); + fail("1.2.3"); + fail("--1"); + fail("1e2e3"); + } + + // ---- Cross-check against stdlib for common config values ---- + #[test] + fn cross_check_stdlib() { + let cases = [ + "0", "1", "-1", "0.5", "123.456", "1e10", "1e-10", "3.14", "2.5E10", "2.5e-10", + "0.0015", "1000000", "99.99", "0.001", "1e22", "-0.0", "0.1", "0.2", "0.3", + ]; + for s in cases { + approx(s, s.parse().unwrap()); + } + } +} diff --git a/crates/stdext/src/lib.rs b/crates/stdext/src/lib.rs index e009494acf9..c85378bb73e 100644 --- a/crates/stdext/src/lib.rs +++ b/crates/stdext/src/lib.rs @@ -6,6 +6,7 @@ pub mod alloc; pub mod arena; pub mod collections; +pub mod float; pub mod glob; mod helpers; pub mod simd;