Skip to content
77 changes: 64 additions & 13 deletions src/bin/edit/draw_statusbar.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -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() {
Expand All @@ -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", "");
{
Expand Down
10 changes: 8 additions & 2 deletions src/bin/edit/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<icu::Encoding>>,

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,
Expand Down Expand Up @@ -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,
Expand Down
103 changes: 81 additions & 22 deletions src/icu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
}
}
Expand Down Expand Up @@ -827,23 +869,35 @@ 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)]
#[repr(C)]
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,
Expand All @@ -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",
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down