diff --git a/src/bin/edit/draw_statusbar.rs b/src/bin/edit/draw_statusbar.rs index 474d7dd383b0..4dc08a74d75a 100644 --- a/src/bin/edit/draw_statusbar.rs +++ b/src/bin/edit/draw_statusbar.rs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use edit::arena::scratch_arena; use edit::framebuffer::{Attributes, IndexedColor}; +use edit::fuzzy::score_fuzzy; use edit::helpers::*; use edit::input::vk; use edit::tui::*; @@ -194,42 +196,62 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) { } pub fn draw_dialog_encoding_change(ctx: &mut Context, state: &mut State) { - let doc = state.documents.active_mut().unwrap(); + let encoding = state.documents.active_mut().map_or("", |doc| doc.buffer.borrow().encoding()); let reopen = state.wants_encoding_change == StateEncodingChange::Reopen; let width = (ctx.size().width - 20).max(10); let height = (ctx.size().height - 10).max(10); let mut change = None; + let mut done = encoding.is_empty(); ctx.modal_begin( "encode", if reopen { loc(LocId::EncodingReopen) } else { loc(LocId::EncodingConvert) }, ); { - ctx.scrollarea_begin("scrollarea", Size { width, height }); - ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4)); + ctx.table_begin("encoding-search"); + ctx.table_set_columns(&[0, COORD_TYPE_SAFE_MAX]); + ctx.table_set_cell_gap(Size { width: 1, height: 0 }); ctx.inherit_focus(); { - let encodings = icu::get_available_encodings(); + ctx.table_next_row(); + ctx.inherit_focus(); + + ctx.label("needle-label", loc(LocId::SearchNeedleLabel)); + + if ctx.editline("needle", &mut state.encoding_picker_needle) { + encoding_picker_update_list(state); + } + ctx.inherit_focus(); + } + ctx.table_end(); + ctx.scrollarea_begin("scrollarea", Size { width, height }); + ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4)); + { ctx.list_begin("encodings"); ctx.inherit_focus(); - for &encoding in encodings { - if ctx.list_item(encoding == doc.buffer.borrow().encoding(), encoding) - == ListSelection::Activated - { - change = Some(encoding); + + for enc in state + .encoding_picker_results + .as_deref() + .unwrap_or_else(|| icu::get_available_encodings().preferred) + { + if ctx.list_item(enc.canonical == encoding, enc.label) == ListSelection::Activated { + change = Some(enc.canonical); break; } + ctx.attr_overflow(Overflow::TruncateTail); } ctx.list_end(); } ctx.scrollarea_end(); } - if ctx.modal_end() { - state.wants_encoding_change = StateEncodingChange::None; - } + done |= ctx.modal_end(); + done |= change.is_some(); - if let Some(encoding) = change { + if let Some(encoding) = change + && let Some(doc) = state.documents.active_mut() + { if reopen && doc.path.is_some() { let mut res = Ok(()); if doc.buffer.borrow().is_dirty() { @@ -244,12 +266,41 @@ pub fn draw_dialog_encoding_change(ctx: &mut Context, state: &mut State) { } else { doc.buffer.borrow_mut().set_encoding(encoding); } + } + if done { state.wants_encoding_change = StateEncodingChange::None; + state.encoding_picker_needle.clear(); + state.encoding_picker_results = None; ctx.needs_rerender(); } } +fn encoding_picker_update_list(state: &mut State) { + state.encoding_picker_results = None; + + let needle = state.encoding_picker_needle.trim_ascii(); + if needle.is_empty() { + return; + } + + let encodings = icu::get_available_encodings(); + let scratch = scratch_arena(None); + let mut matches = Vec::new_in(&*scratch); + + for enc in encodings.all { + let local_scratch = scratch_arena(Some(&scratch)); + let (score, _) = score_fuzzy(&local_scratch, enc.label, needle, true); + + if score > 0 { + matches.push((score, *enc)); + } + } + + matches.sort_by(|a, b| b.0.cmp(&a.0)); + state.encoding_picker_results = Some(Vec::from_iter(matches.iter().map(|(_, enc)| *enc))); +} + pub fn draw_document_picker(ctx: &mut Context, state: &mut State) { ctx.modal_begin("document-picker", ""); { diff --git a/src/bin/edit/state.rs b/src/bin/edit/state.rs index 90689e59a6d5..ab02a2b6e24e 100644 --- a/src/bin/edit/state.rs +++ b/src/bin/edit/state.rs @@ -144,9 +144,12 @@ pub struct State { pub search_options: buffer::SearchOptions, pub search_success: bool, + pub wants_encoding_picker: bool, + pub encoding_picker_needle: String, + pub encoding_picker_results: Option>, + pub wants_save: bool, pub wants_statusbar_focus: bool, - pub wants_encoding_picker: bool, pub wants_encoding_change: StateEncodingChange, pub wants_indentation_picker: bool, pub wants_document_picker: bool, @@ -189,9 +192,12 @@ impl State { search_options: Default::default(), search_success: true, + wants_encoding_picker: false, + encoding_picker_needle: Default::default(), + encoding_picker_results: Default::default(), + wants_save: false, wants_statusbar_focus: false, - wants_encoding_picker: false, wants_encoding_change: StateEncodingChange::None, wants_indentation_picker: false, wants_document_picker: false, diff --git a/src/icu.rs b/src/icu.rs index a82b653d8253..c96c86b7a537 100644 --- a/src/icu.rs +++ b/src/icu.rs @@ -15,16 +15,32 @@ use crate::buffer::TextBuffer; use crate::unicode::Utf8Chars; use crate::{apperr, arena_format, sys}; -static mut ENCODINGS: Vec<&'static str> = Vec::new(); +#[derive(Clone, Copy)] +pub struct Encoding { + pub label: &'static str, + pub canonical: &'static str, +} + +pub struct Encodings { + pub preferred: &'static [Encoding], + pub all: &'static [Encoding], +} + +static mut ENCODINGS: Encodings = Encodings { preferred: &[], all: &[] }; /// Returns a list of encodings ICU supports. -pub fn get_available_encodings() -> &'static [&'static str] { +pub fn get_available_encodings() -> &'static Encodings { // OnceCell for people that want to put it into a static. #[allow(static_mut_refs)] unsafe { - if ENCODINGS.is_empty() { - ENCODINGS.push("UTF-8"); - ENCODINGS.push("UTF-8 BOM"); + if ENCODINGS.all.is_empty() { + let scratch = scratch_arena(None); + let mut preferred = Vec::new_in(&*scratch); + let mut alternative = Vec::new_in(&*scratch); + + // These encodings are always available. + preferred.push(Encoding { label: "UTF-8", canonical: "UTF-8" }); + preferred.push(Encoding { label: "UTF-8 BOM", canonical: "UTF-8 BOM" }); if let Ok(f) = init_if_needed() { let mut n = 0; @@ -34,17 +50,43 @@ pub fn get_available_encodings() -> &'static [&'static str] { break; } + n += 1; + let name = CStr::from_ptr(name).to_str().unwrap_unchecked(); - // We have already pushed UTF-8 above. - // There is no need to filter UTF-8 BOM here, since ICU does not distinguish it from UTF-8. - if name != "UTF-8" { - ENCODINGS.push(name); + // We have already pushed UTF-8 above and can skip it. + // There is no need to filter UTF-8 BOM here, + // since ICU does not distinguish it from UTF-8. + if name.is_empty() || name == "UTF-8" { + continue; } - n += 1; + let mut status = icu_ffi::U_ZERO_ERROR; + let mime = (f.ucnv_getStandardName)( + name.as_ptr(), + c"MIME".as_ptr() as *const _, + &mut status, + ); + if !mime.is_null() && status.is_success() { + let mime = CStr::from_ptr(mime).to_str().unwrap_unchecked(); + preferred.push(Encoding { label: mime, canonical: name }); + } else { + alternative.push(Encoding { label: name, canonical: name }); + } } } + + let preferred_len = preferred.len(); + + // Combine the preferred and alternative encodings into a single list. + let mut all = Vec::with_capacity(preferred.len() + alternative.len()); + all.extend(preferred); + all.extend(alternative); + + let all = all.leak(); + ENCODINGS.preferred = &all[..preferred_len]; + ENCODINGS.all = &all[..]; } + &ENCODINGS } } @@ -827,6 +869,15 @@ pub fn fold_case<'a>(arena: &'a Arena, input: &str) -> ArenaString<'a> { result } +// NOTE: +// To keep this neat, fields are ordered by prefix (= `ucol_` before `uregex_`), +// followed by functions in this order: +// * Static methods (e.g. `ucnv_getAvailableName`) +// * Constructors (e.g. `ucnv_open`) +// * Destructors (e.g. `ucnv_close`) +// * Methods, grouped by relationship +// (e.g. `uregex_start64` and `uregex_end64` are near each other) +// // WARNING: // The order of the fields MUST match the order of strings in the following two arrays. #[allow(non_snake_case)] @@ -834,16 +885,19 @@ pub fn fold_case<'a>(arena: &'a Arena, input: &str) -> ArenaString<'a> { struct LibraryFunctions { // LIBICUUC_PROC_NAMES u_errorName: icu_ffi::u_errorName, + ucasemap_open: icu_ffi::ucasemap_open, + ucasemap_utf8FoldCase: icu_ffi::ucasemap_utf8FoldCase, ucnv_getAvailableName: icu_ffi::ucnv_getAvailableName, + ucnv_getStandardName: icu_ffi::ucnv_getStandardName, ucnv_open: icu_ffi::ucnv_open, ucnv_close: icu_ffi::ucnv_close, ucnv_convertEx: icu_ffi::ucnv_convertEx, - ucasemap_open: icu_ffi::ucasemap_open, - ucasemap_utf8FoldCase: icu_ffi::ucasemap_utf8FoldCase, utext_setup: icu_ffi::utext_setup, utext_close: icu_ffi::utext_close, // LIBICUI18N_PROC_NAMES + ucol_open: icu_ffi::ucol_open, + ucol_strcollUTF8: icu_ffi::ucol_strcollUTF8, uregex_open: icu_ffi::uregex_open, uregex_close: icu_ffi::uregex_close, uregex_setTimeLimit: icu_ffi::uregex_setTimeLimit, @@ -852,25 +906,26 @@ struct LibraryFunctions { uregex_findNext: icu_ffi::uregex_findNext, uregex_start64: icu_ffi::uregex_start64, uregex_end64: icu_ffi::uregex_end64, - ucol_open: icu_ffi::ucol_open, - ucol_strcollUTF8: icu_ffi::ucol_strcollUTF8, } -const LIBICUUC_PROC_NAMES: [&CStr; 9] = [ - // Found in libicuuc.so on UNIX, icuuc.dll/icu.dll on Windows. +// Found in libicuuc.so on UNIX, icuuc.dll/icu.dll on Windows. +const LIBICUUC_PROC_NAMES: [&CStr; 10] = [ c"u_errorName", + c"ucasemap_open", + c"ucasemap_utf8FoldCase", c"ucnv_getAvailableName", + c"ucnv_getStandardName", c"ucnv_open", c"ucnv_close", c"ucnv_convertEx", - c"ucasemap_open", - c"ucasemap_utf8FoldCase", c"utext_setup", c"utext_close", ]; +// Found in libicui18n.so on UNIX, icuin.dll/icu.dll on Windows. const LIBICUI18N_PROC_NAMES: [&CStr; 10] = [ - // Found in libicui18n.so on UNIX, icuin.dll/icu.dll on Windows. + c"ucol_open", + c"ucol_strcollUTF8", c"uregex_open", c"uregex_close", c"uregex_setTimeLimit", @@ -879,8 +934,6 @@ const LIBICUI18N_PROC_NAMES: [&CStr; 10] = [ c"uregex_findNext", c"uregex_start64", c"uregex_end64", - c"ucol_open", - c"ucol_strcollUTF8", ]; enum LibraryFunctionsState { @@ -1020,7 +1073,13 @@ mod icu_ffi { pub struct UConverter; - pub type ucnv_getAvailableName = unsafe extern "C" fn(n: i32) -> *mut c_char; + pub type ucnv_getAvailableName = unsafe extern "C" fn(n: i32) -> *const c_char; + + pub type ucnv_getStandardName = unsafe extern "C" fn( + name: *const u8, + standard: *const u8, + status: &mut UErrorCode, + ) -> *const c_char; pub type ucnv_open = unsafe extern "C" fn(converter_name: *const u8, status: &mut UErrorCode) -> *mut UConverter; diff --git a/src/lib.rs b/src/lib.rs index 38a972a19bb5..db7790dcf263 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ pub mod buffer; pub mod cell; pub mod document; pub mod framebuffer; +pub mod fuzzy; pub mod hash; pub mod helpers; pub mod icu;