diff --git a/crates/edit/Cargo.toml b/crates/edit/Cargo.toml index c7bc03f0afb..fcf8edeb79d 100644 --- a/crates/edit/Cargo.toml +++ b/crates/edit/Cargo.toml @@ -40,7 +40,6 @@ features = [ "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", - "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Threading", diff --git a/crates/edit/src/apperr.rs b/crates/edit/src/apperr.rs deleted file mode 100644 index 8427f2f19e9..00000000000 --- a/crates/edit/src/apperr.rs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Provides a transparent error type for edit. - -use std::alloc::AllocError; -use std::{io, result}; - -use crate::sys; - -pub const APP_ICU_MISSING: Error = Error::new_app(0); - -/// Edit's transparent `Result` type. -pub type Result = result::Result; - -/// Edit's transparent `Error` type. -/// Abstracts over system and application errors. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Error { - App(u32), - Icu(u32), - Sys(u32), -} - -impl Error { - pub const fn new_app(code: u32) -> Self { - Self::App(code) - } - - pub const fn new_icu(code: u32) -> Self { - Self::Icu(code) - } - - pub const fn new_sys(code: u32) -> Self { - Self::Sys(code) - } -} - -impl From for Error { - fn from(err: io::Error) -> Self { - sys::io_error_to_apperr(err) - } -} - -impl From for Error { - fn from(_: AllocError) -> Self { - // TODO: Technically this breaks if the AllocError isn't recent. By then, the errno may - // have been tained. But the stdlib AllocError is a bad type with no way to carry info. - sys::get_last_error() - } -} diff --git a/crates/edit/src/bin/edit/documents.rs b/crates/edit/src/bin/edit/documents.rs index 4366417af62..eabf798678f 100644 --- a/crates/edit/src/bin/edit/documents.rs +++ b/crates/edit/src/bin/edit/documents.rs @@ -3,14 +3,15 @@ use std::collections::LinkedList; use std::ffi::OsStr; -use std::fs; use std::fs::File; use std::path::{Path, PathBuf}; +use std::{fs, io}; use edit::buffer::{RcTextBuffer, TextBuffer}; use edit::helpers::{CoordType, Point}; -use edit::{apperr, path, sys}; +use edit::{path, sys}; +use crate::apperr; use crate::state::DisplayablePathBuf; pub struct Document { @@ -144,10 +145,10 @@ impl DocumentManager { let (path, goto) = Self::parse_filename_goto(path); let path = path::normalize(path); - let mut file = match Self::open_for_reading(&path) { + let mut file = match File::open(&path) { Ok(file) => Some(file), - Err(err) if sys::apperr_is_not_found(err) => None, - Err(err) => return Err(err), + Err(err) if err.kind() == io::ErrorKind::NotFound => None, + Err(err) => return Err(err.into()), }; let file_id = if file.is_some() { Some(sys::file_id(file.as_ref(), &path)?) } else { None }; diff --git a/crates/edit/src/bin/edit/draw_editor.rs b/crates/edit/src/bin/edit/draw_editor.rs index 94f7dbfc50f..479c053ac4d 100644 --- a/crates/edit/src/bin/edit/draw_editor.rs +++ b/crates/edit/src/bin/edit/draw_editor.rs @@ -38,7 +38,7 @@ pub fn draw_editor(ctx: &mut Context, state: &mut State) { fn draw_search(ctx: &mut Context, state: &mut State) { if let Err(err) = icu::init() { - error_log_add(ctx, state, err); + error_log_add(ctx, state, err.into()); state.wants_search.kind = StateSearchKind::Disabled; return; } diff --git a/crates/edit/src/bin/edit/draw_filepicker.rs b/crates/edit/src/bin/edit/draw_filepicker.rs index 94adb033129..d6e9d3b73f3 100644 --- a/crates/edit/src/bin/edit/draw_filepicker.rs +++ b/crates/edit/src/bin/edit/draw_filepicker.rs @@ -341,7 +341,7 @@ fn draw_dialog_saveas_refresh_files(state: &mut State) { } for entries in &mut dirs_files[1..] { - entries.sort_by(|a, b| { + entries.sort_unstable_by(|a, b| { let a = a.as_bytes(); let b = b.as_bytes(); diff --git a/crates/edit/src/bin/edit/draw_statusbar.rs b/crates/edit/src/bin/edit/draw_statusbar.rs index 8250e44daed..935e158e176 100644 --- a/crates/edit/src/bin/edit/draw_statusbar.rs +++ b/crates/edit/src/bin/edit/draw_statusbar.rs @@ -302,7 +302,7 @@ fn encoding_picker_update_list(state: &mut State) { } } - matches.sort_by(|a, b| b.0.cmp(&a.0)); + matches.sort_unstable_by(|a, b| b.0.cmp(&a.0)); state.encoding_picker_results = Some(Vec::from_iter(matches.iter().map(|(_, enc)| *enc))); } diff --git a/crates/edit/src/bin/edit/localization.rs b/crates/edit/src/bin/edit/localization.rs index 2e3eed8943e..fa22dc89c45 100644 --- a/crates/edit/src/bin/edit/localization.rs +++ b/crates/edit/src/bin/edit/localization.rs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use edit::helpers::AsciiStringHelpers; use edit::sys; +use stdext::AsciiStringHelpers as _; use stdext::arena::scratch_arena; include!(concat!(env!("OUT_DIR"), "/i18n_edit.rs")); diff --git a/crates/edit/src/bin/edit/main.rs b/crates/edit/src/bin/edit/main.rs index 7e02ec82b41..a05756a009c 100644 --- a/crates/edit/src/bin/edit/main.rs +++ b/crates/edit/src/bin/edit/main.rs @@ -3,6 +3,7 @@ #![feature(allocator_api, linked_list_cursors, string_from_utf8_lossy_owned)] +mod apperr; mod documents; mod draw_editor; mod draw_filepicker; @@ -26,7 +27,7 @@ use edit::input::{self, kbmod, vk}; use edit::oklab::StraightRgba; use edit::tui::*; use edit::vt::{self, Token}; -use edit::{apperr, base64, path, sys, unicode}; +use edit::{base64, path, sys, unicode}; use localization::*; use state::*; use stdext::arena::{self, Arena, ArenaString, scratch_arena}; @@ -37,6 +38,9 @@ const SCRATCH_ARENA_CAPACITY: usize = 128 * MEBI; #[cfg(target_pointer_width = "64")] const SCRATCH_ARENA_CAPACITY: usize = 512 * MEBI; +// NOTE: Before our main() gets called, Rust initializes its stdlib. This pulls in the entire +// std::io::{stdin, stdout, stderr} machinery, and probably some more, which amounts to about 20KB. +// It can technically be avoided nowadays with `#![no_main]`. Maybe a fun project for later? :) fn main() -> process::ExitCode { if cfg!(debug_assertions) { let hook = std::panic::take_hook(); diff --git a/crates/edit/src/bin/edit/state.rs b/crates/edit/src/bin/edit/state.rs index 451060bf6db..c8d45bd8ca6 100644 --- a/crates/edit/src/bin/edit/state.rs +++ b/crates/edit/src/bin/edit/state.rs @@ -10,8 +10,9 @@ use edit::framebuffer::IndexedColor; use edit::helpers::*; use edit::oklab::StraightRgba; use edit::tui::*; -use edit::{apperr, buffer, icu, sys}; +use edit::{buffer, icu}; +use crate::apperr; use crate::documents::DocumentManager; use crate::localization::*; @@ -27,10 +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::APP_ICU_MISSING => f.write_str(loc(LocId::ErrorIcuMissing)), - apperr::Error::App(code) => write!(f, "Unknown app error code: {code}"), - apperr::Error::Icu(code) => icu::apperr_format(f, code), - apperr::Error::Sys(code) => sys::apperr_format(f, code), + 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), } } } diff --git a/crates/edit/src/buffer/gap_buffer.rs b/crates/edit/src/buffer/gap_buffer.rs index 4f84d712638..401d0f08442 100644 --- a/crates/edit/src/buffer/gap_buffer.rs +++ b/crates/edit/src/buffer/gap_buffer.rs @@ -3,11 +3,11 @@ use std::ops::Range; use std::ptr::{self, NonNull}; -use std::slice; +use std::{io, slice}; use stdext::sys::{virtual_commit, virtual_release, virtual_reserve}; +use stdext::{ReplaceRange as _, slice_copy_safe}; -use crate::apperr; use crate::document::{ReadableDocument, WriteableDocument}; use crate::helpers::*; @@ -64,7 +64,7 @@ pub struct GapBuffer { } impl GapBuffer { - pub fn new(small: bool) -> apperr::Result { + pub fn new(small: bool) -> io::Result { let reserve; let buffer; let text; diff --git a/crates/edit/src/buffer/mod.rs b/crates/edit/src/buffer/mod.rs index 5f4ca052f24..251fa401d14 100644 --- a/crates/edit/src/buffer/mod.rs +++ b/crates/edit/src/buffer/mod.rs @@ -28,7 +28,7 @@ use std::cell::UnsafeCell; use std::collections::LinkedList; use std::fmt::Write as _; use std::fs::File; -use std::io::{Read as _, Write as _}; +use std::io::{self, Read as _, Write as _}; use std::mem::{self, MaybeUninit}; use std::ops::Range; use std::rc::Rc; @@ -36,6 +36,7 @@ use std::str; pub use gap_buffer::GapBuffer; use stdext::arena::{Arena, ArenaString, scratch_arena}; +use stdext::{ReplaceRange as _, minmax, slice_as_uninit_mut, slice_copy_safe}; use crate::cell::SemiRefCell; use crate::clipboard::Clipboard; @@ -45,7 +46,7 @@ use crate::helpers::*; use crate::oklab::StraightRgba; use crate::simd::memchr2; use crate::unicode::{self, Cursor, MeasurementConfig, Utf8Chars}; -use crate::{apperr, icu, simd}; +use crate::{icu, simd}; /// The margin template is used for line numbers. /// The max. line number we should ever expect is probably 64-bit, @@ -59,6 +60,25 @@ const VISUAL_SPACE_PREFIX_ADD: usize = '・'.len_utf8() - 1; const VISUAL_TAB: &str = "→ "; const VISUAL_TAB_PREFIX_ADD: usize = '→'.len_utf8() - 1; +pub enum IoError { + Io(io::Error), + Icu(icu::Error), +} + +pub type IoResult = std::result::Result; + +impl From for IoError { + fn from(err: io::Error) -> Self { + Self::Io(err) + } +} + +impl From for IoError { + fn from(err: icu::Error) -> Self { + Self::Icu(err) + } +} + /// Stores statistics about the whole document. #[derive(Copy, Clone)] pub struct TextBufferStatistics { @@ -250,14 +270,14 @@ pub struct TextBuffer { impl TextBuffer { /// Creates a new text buffer inside an [`Rc`]. /// See [`TextBuffer::new()`]. - pub fn new_rc(small: bool) -> apperr::Result { + pub fn new_rc(small: bool) -> io::Result { let buffer = Self::new(small)?; Ok(Rc::new(SemiRefCell::new(buffer))) } /// Creates a new text buffer. With `small` you can control /// if the buffer is optimized for <1MiB contents. - pub fn new(small: bool) -> apperr::Result { + pub fn new(small: bool) -> io::Result { Ok(Self { buffer: GapBuffer::new(small)?, @@ -665,11 +685,7 @@ impl TextBuffer { } /// Reads a file from disk into the text buffer, detecting encoding and BOM. - pub fn read_file( - &mut self, - file: &mut File, - encoding: Option<&'static str>, - ) -> apperr::Result<()> { + pub fn read_file(&mut self, file: &mut File, encoding: Option<&'static str>) -> IoResult<()> { let scratch = scratch_arena(None); let mut buf = scratch.alloc_uninit().transpose(); let mut first_chunk_len = 0; @@ -818,7 +834,7 @@ impl TextBuffer { buf: &mut [MaybeUninit; 4 * KIBI], first_chunk_len: usize, done: bool, - ) -> apperr::Result<()> { + ) -> io::Result<()> { { let mut first_chunk = unsafe { buf[..first_chunk_len].assume_init_ref() }; if first_chunk.starts_with(b"\xEF\xBB\xBF") { @@ -872,7 +888,7 @@ impl TextBuffer { buf: &mut [MaybeUninit; 4 * KIBI], first_chunk_len: usize, mut done: bool, - ) -> apperr::Result<()> { + ) -> IoResult<()> { let scratch = scratch_arena(None); let pivot_buffer = scratch.alloc_uninit_slice(4 * KIBI); let mut c = icu::Converter::new(pivot_buffer, self.encoding, "UTF-8")?; @@ -931,7 +947,7 @@ impl TextBuffer { } /// Writes the text buffer contents to a file, handling BOM and encoding. - pub fn write_file(&mut self, file: &mut File) -> apperr::Result<()> { + pub fn write_file(&mut self, file: &mut File) -> IoResult<()> { let mut offset = 0; if self.encoding.starts_with("UTF-8") { @@ -954,7 +970,7 @@ impl TextBuffer { Ok(()) } - fn write_file_with_icu(&mut self, file: &mut File) -> apperr::Result<()> { + fn write_file_with_icu(&mut self, file: &mut File) -> IoResult<()> { let scratch = scratch_arena(None); let pivot_buffer = scratch.alloc_uninit_slice(4 * KIBI); let buf = scratch.alloc_uninit_slice(4 * KIBI); @@ -1080,7 +1096,7 @@ impl TextBuffer { } /// Find the next occurrence of the given `pattern` and select it. - pub fn find_and_select(&mut self, pattern: &str, options: SearchOptions) -> apperr::Result<()> { + pub fn find_and_select(&mut self, pattern: &str, options: SearchOptions) -> icu::Result<()> { if let Some(search) = &mut self.search { let search = search.get_mut(); // When the search input changes we must reset the search. @@ -1138,7 +1154,7 @@ impl TextBuffer { pattern: &str, options: SearchOptions, replacement: &[u8], - ) -> apperr::Result<()> { + ) -> icu::Result<()> { // Editors traditionally replace the previous search hit, not the next possible one. if let (Some(search), Some(..)) = (&self.search, &self.selection) { let search = unsafe { &mut *search.get() }; @@ -1161,7 +1177,7 @@ impl TextBuffer { pattern: &str, options: SearchOptions, replacement: &[u8], - ) -> apperr::Result<()> { + ) -> icu::Result<()> { let scratch = scratch_arena(None); let mut search = self.find_construct_search(pattern, options)?; let mut offset = 0; @@ -1186,9 +1202,9 @@ impl TextBuffer { &self, pattern: &str, options: SearchOptions, - ) -> apperr::Result { + ) -> icu::Result { if pattern.is_empty() { - return Err(apperr::Error::Icu(1)); // U_ILLEGAL_ARGUMENT_ERROR + return Err(icu::ILLEGAL_ARGUMENT_ERROR); } let sanitized_pattern = if options.whole_word && options.use_regex { @@ -1714,13 +1730,11 @@ impl TextBuffer { return None; } - let scratch = scratch_arena(None); let width = destination.width(); let height = destination.height(); let line_number_width = self.margin_width.max(3) as usize - 3; let text_width = width - self.margin_width; let mut visualizer_buf = [0xE2, 0x90, 0x80]; // U+2400 in UTF8 - let mut line = ArenaString::new_in(&scratch); let mut visual_pos_x_max = 0; // Pick the cursor closer to the `origin.y`. @@ -1737,10 +1751,10 @@ impl TextBuffer { Some(TextBufferSelection { beg, end }) => minmax(beg, end), }; - line.reserve(width as usize * 2); - for y in 0..height { - line.clear(); + let scratch = scratch_arena(None); + let mut line = ArenaString::new_in(&scratch); + line.reserve(width as usize * 2); let visual_line = origin.y + y; let mut cursor_beg = @@ -2717,6 +2731,7 @@ impl TextBuffer { fn undo_redo(&mut self, undo: bool) { let buffer_generation = self.buffer.generation(); let mut entry_buffer_generation = None; + let mut damage_start = CoordType::MAX; loop { // Transfer the last entry from the undo stack to the redo stack or vice versa. @@ -2761,6 +2776,8 @@ impl TextBuffer { cursor }; + damage_start = damage_start.min(cursor.logical_pos.y); + { let mut change = change.borrow_mut(); let change = &mut *change; @@ -2830,6 +2847,11 @@ impl TextBuffer { } } + if damage_start == CoordType::MAX { + // There weren't any undo/redo entries. + return; + } + if entry_buffer_generation.is_some() { self.recalc_after_content_changed(); } diff --git a/crates/edit/src/clipboard.rs b/crates/edit/src/clipboard.rs index 413de71fbd9..741931e25bb 100644 --- a/crates/edit/src/clipboard.rs +++ b/crates/edit/src/clipboard.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + //! Clipboard facilities for the editor. /// The builtin, internal clipboard of the editor. diff --git a/crates/edit/src/document.rs b/crates/edit/src/document.rs index c7def366d30..0059a959772 100644 --- a/crates/edit/src/document.rs +++ b/crates/edit/src/document.rs @@ -8,10 +8,9 @@ use std::mem; use std::ops::Range; use std::path::PathBuf; +use stdext::ReplaceRange as _; use stdext::arena::{ArenaString, scratch_arena}; -use crate::helpers::ReplaceRange as _; - /// An abstraction over reading from text containers. pub trait ReadableDocument { /// Read some bytes starting at (including) the given absolute offset. diff --git a/crates/edit/src/helpers.rs b/crates/edit/src/helpers.rs index 40590733da9..756292b8575 100644 --- a/crates/edit/src/helpers.rs +++ b/crates/edit/src/helpers.rs @@ -3,14 +3,10 @@ //! Random assortment of helpers I didn't know where to put. -use std::alloc::Allocator; use std::cmp::Ordering; -use std::io::Read; -use std::mem::{self, MaybeUninit}; -use std::ops::{Bound, Range, RangeBounds}; -use std::{fmt, ptr, slice, str}; - -use crate::apperr; +use std::io::{self, Read}; +use std::mem::MaybeUninit; +use std::{fmt, slice}; pub const KILO: usize = 1000; pub const MEGA: usize = 1000 * 1000; @@ -161,136 +157,11 @@ where if v2 < v1 { [v2, v1] } else { [v1, v2] } } -#[inline(always)] -#[allow(clippy::ptr_eq)] -pub fn opt_ptr(a: Option<&T>) -> *const T { - unsafe { mem::transmute(a) } -} - -/// Surprisingly, there's no way in Rust to do a `ptr::eq` on `Option<&T>`. -/// Uses `unsafe` so that the debug performance isn't too bad. -#[inline(always)] -#[allow(clippy::ptr_eq)] -pub fn opt_ptr_eq(a: Option<&T>, b: Option<&T>) -> bool { - opt_ptr(a) == opt_ptr(b) -} - -/// Creates a `&str` from a pointer and a length. -/// Exists, because `std::str::from_raw_parts` is unstable, par for the course. -/// -/// # Safety -/// -/// The given data must be valid UTF-8. -/// The given data must outlive the returned reference. -#[inline] -#[must_use] -pub const unsafe fn str_from_raw_parts<'a>(ptr: *const u8, len: usize) -> &'a str { - unsafe { str::from_utf8_unchecked(slice::from_raw_parts(ptr, len)) } -} - -/// [`<[T]>::copy_from_slice`] panics if the two slices have different lengths. -/// This one just returns the copied amount. -pub fn slice_copy_safe(dst: &mut [T], src: &[T]) -> usize { - let len = src.len().min(dst.len()); - unsafe { ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), len) }; - len -} - -/// [`Vec::splice`] results in really bad assembly. -/// This doesn't. Don't use [`Vec::splice`]. -pub trait ReplaceRange { - fn replace_range>(&mut self, range: R, src: &[T]); -} - -impl ReplaceRange for Vec { - fn replace_range>(&mut self, range: R, src: &[T]) { - let start = match range.start_bound() { - Bound::Included(&start) => start, - Bound::Excluded(start) => start + 1, - Bound::Unbounded => 0, - }; - let end = match range.end_bound() { - Bound::Included(end) => end + 1, - Bound::Excluded(&end) => end, - Bound::Unbounded => usize::MAX, - }; - vec_replace_impl(self, start..end, src); - } -} - -fn vec_replace_impl(dst: &mut Vec, range: Range, src: &[T]) { - unsafe { - let dst_len = dst.len(); - let src_len = src.len(); - let off = range.start.min(dst_len); - let del_len = range.end.saturating_sub(off).min(dst_len - off); - - if del_len == 0 && src_len == 0 { - return; // nothing to do - } - - let tail_len = dst_len - off - del_len; - let new_len = dst_len - del_len + src_len; - - if src_len > del_len { - dst.reserve(src_len - del_len); - } - - // NOTE: drop_in_place() is not needed here, because T is constrained to Copy. - - // SAFETY: as_mut_ptr() must called after reserve() to ensure that the pointer is valid. - let ptr = dst.as_mut_ptr().add(off); - - // Shift the tail. - if tail_len > 0 && src_len != del_len { - ptr::copy(ptr.add(del_len), ptr.add(src_len), tail_len); - } - - // Copy in the replacement. - ptr::copy_nonoverlapping(src.as_ptr(), ptr, src_len); - dst.set_len(new_len); - } -} - /// [`Read`] but with [`MaybeUninit`] buffers. -pub fn file_read_uninit( - file: &mut T, - buf: &mut [MaybeUninit], -) -> apperr::Result { +pub fn file_read_uninit(file: &mut T, buf: &mut [MaybeUninit]) -> io::Result { unsafe { let buf_slice = slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, buf.len()); let n = file.read(buf_slice)?; Ok(n) } } - -/// Turns a [`&[u8]`] into a [`&[MaybeUninit]`]. -#[inline(always)] -pub const fn slice_as_uninit_ref(slice: &[T]) -> &[MaybeUninit] { - unsafe { slice::from_raw_parts(slice.as_ptr() as *const MaybeUninit, slice.len()) } -} - -/// Turns a [`&mut [T]`] into a [`&mut [MaybeUninit]`]. -#[inline(always)] -pub const fn slice_as_uninit_mut(slice: &mut [T]) -> &mut [MaybeUninit] { - unsafe { slice::from_raw_parts_mut(slice.as_mut_ptr() as *mut MaybeUninit, slice.len()) } -} - -/// Helpers for ASCII string comparisons. -pub trait AsciiStringHelpers { - /// Tests if a string starts with a given ASCII prefix. - /// - /// This function name really is a mouthful, but it's a combination - /// of [`str::starts_with`] and [`str::eq_ignore_ascii_case`]. - fn starts_with_ignore_ascii_case(&self, prefix: &str) -> bool; -} - -impl AsciiStringHelpers for str { - fn starts_with_ignore_ascii_case(&self, prefix: &str) -> bool { - // Casting to bytes first ensures we skip any UTF8 boundary checks. - // Since the comparison is ASCII, we don't need to worry about that. - let s = self.as_bytes(); - let p = prefix.as_bytes(); - p.len() <= s.len() && s[..p.len()].eq_ignore_ascii_case(p) - } -} diff --git a/crates/edit/src/icu.rs b/crates/edit/src/icu.rs index 1be796019f7..8b8f783eeee 100644 --- a/crates/edit/src/icu.rs +++ b/crates/edit/src/icu.rs @@ -5,17 +5,54 @@ use std::cmp::Ordering; use std::ffi::{CStr, c_char}; -use std::mem; use std::mem::MaybeUninit; use std::ops::Range; use std::ptr::{null, null_mut}; +use std::{fmt, mem}; use stdext::arena::{Arena, ArenaString, scratch_arena}; use stdext::arena_format; use crate::buffer::TextBuffer; +use crate::sys; use crate::unicode::Utf8Chars; -use crate::{apperr, sys}; + +pub(crate) const ILLEGAL_ARGUMENT_ERROR: Error = Error(1); // U_ILLEGAL_ARGUMENT_ERROR +pub const ICU_MISSING_ERROR: Error = Error(0); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Error(u32); + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn format(code: u32) -> &'static str { + let Ok(f) = init_if_needed() else { + return ""; + }; + + let status = icu_ffi::UErrorCode::new(code); + let ptr = unsafe { (f.u_errorName)(status) }; + if ptr.is_null() { + return ""; + } + + let str = unsafe { CStr::from_ptr(ptr) }; + str.to_str().unwrap_or("") + } + + let code = self.0; + if code != 0 + && let msg = format(code) + && !msg.is_empty() + { + write!(f, "ICU Error: {msg}") + } else { + write!(f, "ICU Error: {code:#08x}") + } + } +} + +pub type Result = std::result::Result; #[derive(Clone, Copy)] pub struct Encoding { @@ -93,31 +130,6 @@ pub fn get_available_encodings() -> &'static Encodings { } } -/// Formats the given ICU error code into a human-readable string. -pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result { - fn format(code: u32) -> &'static str { - let Ok(f) = init_if_needed() else { - return ""; - }; - - let status = icu_ffi::UErrorCode::new(code); - let ptr = unsafe { (f.u_errorName)(status) }; - if ptr.is_null() { - return ""; - } - - let str = unsafe { CStr::from_ptr(ptr) }; - str.to_str().unwrap_or("") - } - - let msg = format(code); - if !msg.is_empty() { - write!(f, "ICU Error: {msg}") - } else { - write!(f, "ICU Error: {code:#08x}") - } -} - /// Converts between two encodings using ICU. pub struct Converter<'pivot> { source: *mut icu_ffi::UConverter, @@ -149,7 +161,7 @@ impl<'pivot> Converter<'pivot> { pivot_buffer: &'pivot mut [MaybeUninit], source_encoding: &str, target_encoding: &str, - ) -> apperr::Result { + ) -> Result { let f = init_if_needed()?; let arena = scratch_arena(None); @@ -197,7 +209,7 @@ impl<'pivot> Converter<'pivot> { &mut self, input: &[u8], output: &mut [MaybeUninit], - ) -> apperr::Result<(usize, usize)> { + ) -> Result<(usize, usize)> { let f = assume_loaded(); let input_beg = input.as_ptr(); @@ -303,7 +315,7 @@ impl Text { /// /// The caller must ensure that the given [`TextBuffer`] /// outlives the returned `Text` instance. - pub unsafe fn new(tb: &TextBuffer) -> apperr::Result { + pub unsafe fn new(tb: &TextBuffer) -> Result { let f = init_if_needed()?; let mut status = icu_ffi::U_ZERO_ERROR; @@ -623,7 +635,7 @@ impl Regex { /// # Safety /// /// The caller must ensure that the given `Text` outlives the returned `Regex` instance. - pub unsafe fn new(pattern: &str, flags: i32, text: &Text) -> apperr::Result { + pub unsafe fn new(pattern: &str, flags: i32, text: &Text) -> Result { let f = init_if_needed()?; unsafe { let scratch = scratch_arena(None); @@ -968,13 +980,13 @@ enum LibraryFunctionsState { static mut LIBRARY_FUNCTIONS: LibraryFunctionsState = LibraryFunctionsState::Uninitialized; -pub fn init() -> apperr::Result<()> { +pub fn init() -> Result<()> { init_if_needed()?; Ok(()) } #[allow(static_mut_refs)] -fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> { +fn init_if_needed() -> Result<&'static LibraryFunctions> { #[cold] fn load() { unsafe { @@ -1045,7 +1057,7 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> { match unsafe { &LIBRARY_FUNCTIONS } { LibraryFunctionsState::Loaded(f) => Ok(f), - _ => Err(apperr::APP_ICU_MISSING), + _ => Err(ICU_MISSING_ERROR), } } @@ -1062,7 +1074,7 @@ mod icu_ffi { use std::ffi::{c_char, c_int, c_void}; - use crate::apperr; + use super::Error; #[derive(Copy, Clone, Eq, PartialEq)] #[repr(transparent)] @@ -1081,9 +1093,9 @@ mod icu_ffi { self.0 > 0 } - pub fn as_error(&self) -> apperr::Error { + pub fn as_error(&self) -> Error { debug_assert!(self.0 > 0); - apperr::Error::new_icu(self.0 as u32) + Error(self.0 as u32) } } diff --git a/crates/edit/src/lib.rs b/crates/edit/src/lib.rs index 126cbce525f..90229b48a26 100644 --- a/crates/edit/src/lib.rs +++ b/crates/edit/src/lib.rs @@ -17,7 +17,6 @@ )] #![allow(clippy::missing_transmute_annotations, clippy::new_without_default, stable_features)] -pub mod apperr; pub mod base64; pub mod buffer; pub mod cell; diff --git a/crates/edit/src/sys/unix.rs b/crates/edit/src/sys/unix.rs index 07514325d5c..ba9d06fde64 100644 --- a/crates/edit/src/sys/unix.rs +++ b/crates/edit/src/sys/unix.rs @@ -12,12 +12,11 @@ use std::mem::{self, ManuallyDrop, MaybeUninit}; use std::os::fd::{AsRawFd as _, FromRawFd as _}; use std::path::Path; use std::ptr::{NonNull, null_mut}; -use std::{thread, time}; +use std::{io, thread, time}; use stdext::arena::{Arena, ArenaString, scratch_arena}; use stdext::arena_format; -use crate::apperr; use crate::helpers::*; struct State { @@ -51,7 +50,7 @@ pub fn init() -> Deinit { Deinit } -pub fn switch_modes() -> apperr::Result<()> { +pub fn switch_modes() -> io::Result<()> { unsafe { // Reopen stdin if it's redirected (= piped input). if libc::isatty(STATE.stdin) == 0 { @@ -352,7 +351,7 @@ pub struct FileId { } /// Returns a unique identifier for the given file by handle or path. -pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result { +pub fn file_id(file: Option<&File>, path: &Path) -> io::Result { let file = match file { Some(f) => f, None => &File::open(path)?, @@ -366,10 +365,10 @@ pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result { } } -unsafe fn load_library(name: *const c_char) -> apperr::Result> { +unsafe fn load_library(name: *const c_char) -> io::Result> { unsafe { NonNull::new(libc::dlopen(name, libc::RTLD_LAZY)) - .ok_or_else(|| errno_to_apperr(libc::ENOENT)) + .ok_or_else(|| from_raw_os_error(libc::ENOENT)) } } @@ -381,14 +380,11 @@ unsafe fn load_library(name: *const c_char) -> apperr::Result> { /// of the function you're loading. No type checks whatsoever are performed. // // It'd be nice to constrain T to std::marker::FnPtr, but that's unstable. -pub unsafe fn get_proc_address( - handle: NonNull, - name: *const c_char, -) -> apperr::Result { +pub unsafe fn get_proc_address(handle: NonNull, name: *const c_char) -> io::Result { unsafe { let sym = libc::dlsym(handle.as_ptr(), name); if sym.is_null() { - Err(errno_to_apperr(libc::ENOENT)) + Err(from_raw_os_error(libc::ENOENT)) } else { Ok(mem::transmute_copy(&sym)) } @@ -400,7 +396,7 @@ pub struct LibIcu { pub libicui18n: NonNull, } -pub fn load_icu() -> apperr::Result { +pub fn load_icu() -> io::Result { const fn const_str_eq(a: &str, b: &str) -> bool { let a = a.as_bytes(); let b = b.as_bytes(); @@ -540,45 +536,29 @@ pub fn preferred_languages(arena: &Arena) -> Vec, &Arena> { } #[inline] -fn errno() -> i32 { +#[cold] +fn errno() -> c_int { + // libc unfortunately doesn't export an alias for `errno` (WHY?). + // As such we (ab)use the stdlib and use its internal errno implementation. + // // Under `-O -Copt-level=s` the 1.87 compiler fails to fully inline and // remove the raw_os_error() call. This leaves us with the drop() call. // ManuallyDrop fixes that and results in a direct `std::sys::os::errno` call. - ManuallyDrop::new(std::io::Error::last_os_error()).raw_os_error().unwrap_or(0) + ManuallyDrop::new(io::Error::last_os_error()).raw_os_error().unwrap_or(0) } +#[inline] #[cold] -pub fn get_last_error() -> apperr::Error { - errno_to_apperr(errno()) +fn last_os_error() -> io::Error { + io::Error::last_os_error() } #[inline] -pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error { - errno_to_apperr(err.raw_os_error().unwrap_or(0)) -} - -pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result { - write!(f, "Error {code}")?; - - unsafe { - let ptr = libc::strerror(code as i32); - if !ptr.is_null() { - let msg = CStr::from_ptr(ptr).to_string_lossy(); - write!(f, ": {msg}")?; - } - } - - Ok(()) -} - -pub fn apperr_is_not_found(err: apperr::Error) -> bool { - err == errno_to_apperr(libc::ENOENT) -} - -const fn errno_to_apperr(no: c_int) -> apperr::Error { - apperr::Error::new_sys(if no < 0 { 0 } else { no as u32 }) +#[cold] +fn from_raw_os_error(code: c_int) -> io::Error { + io::Error::from_raw_os_error(code) } -fn check_int_return(ret: libc::c_int) -> apperr::Result { - if ret < 0 { Err(get_last_error()) } else { Ok(ret) } +fn check_int_return(ret: libc::c_int) -> io::Result { + if ret < 0 { Err(last_os_error()) } else { Ok(ret) } } diff --git a/crates/edit/src/sys/windows.rs b/crates/edit/src/sys/windows.rs index 380153b407e..b5475486280 100644 --- a/crates/edit/src/sys/windows.rs +++ b/crates/edit/src/sys/windows.rs @@ -8,17 +8,14 @@ use std::mem::MaybeUninit; use std::os::windows::io::{AsRawHandle as _, FromRawHandle}; use std::path::{Path, PathBuf}; use std::ptr::{self, NonNull, null, null_mut}; -use std::{mem, time}; +use std::{io, mem, time}; use stdext::arena::{Arena, ArenaString, scratch_arena}; -use windows_sys::Win32::Foundation::ERROR_INVALID_PARAMETER; use windows_sys::Win32::Storage::FileSystem; -use windows_sys::Win32::System::Diagnostics::Debug; use windows_sys::Win32::System::{Console, IO, LibraryLoader, Threading}; use windows_sys::Win32::{Foundation, Globalization}; use windows_sys::core::*; -use crate::apperr; use crate::helpers::*; macro_rules! w_env { @@ -69,12 +66,8 @@ unsafe extern "system" fn read_console_input_ex_placeholder( } const CONSOLE_READ_NOWAIT: u16 = 0x0002; - const INVALID_CONSOLE_MODE: u32 = u32::MAX; -// Locally-defined error codes follow the HRESULT format, but they have bit 29 set to indicate that they are Customer error codes. -const ERROR_UNSUPPORTED_LEGACY_CONSOLE: u32 = 0xE0010001; - struct State { read_console_input_ex: ReadConsoleInputExW, stdin: Foundation::HANDLE, @@ -122,7 +115,7 @@ pub fn init() -> Deinit { } /// Switches the terminal into raw mode, etc. -pub fn switch_modes() -> apperr::Result<()> { +pub fn switch_modes() -> io::Result<()> { unsafe { // `kernel32.dll` doesn't exist on OneCore variants of Windows. // NOTE: `kernelbase.dll` is NOT a stable API to rely on. In our case it's the best option though. @@ -130,7 +123,7 @@ pub fn switch_modes() -> apperr::Result<()> { // This is written as two nested `match` statements so that we can return the error from the first // `load_read_func` call if it fails. The kernel32.dll lookup may contain some valid information, // while the kernelbase.dll lookup may not, since it's not a stable API. - unsafe fn load_read_func(module: *const u16) -> apperr::Result { + unsafe fn load_read_func(module: *const u16) -> io::Result { unsafe { get_module(module) .and_then(|m| get_proc_address(m, c"ReadConsoleInputExW".as_ptr())) @@ -161,7 +154,7 @@ pub fn switch_modes() -> apperr::Result<()> { if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE) || ptr::eq(STATE.stdout, Foundation::INVALID_HANDLE_VALUE) { - return Err(get_last_error()); + return Err(last_os_error()); } check_bool_return(Console::GetConsoleMode(STATE.stdin, &raw mut STATE.stdin_mode_old))?; @@ -173,8 +166,8 @@ pub fn switch_modes() -> apperr::Result<()> { | Console::ENABLE_EXTENDED_FLAGS | Console::ENABLE_VIRTUAL_TERMINAL_INPUT, )) { - Err(e) if e == gle_to_apperr(ERROR_INVALID_PARAMETER) => { - Err(apperr::Error::Sys(ERROR_UNSUPPORTED_LEGACY_CONSOLE)) + Err(e) if e.kind() == io::ErrorKind::InvalidInput => { + Err(io::Error::other("This application does not support the legacy console.")) } other => other, }?; @@ -475,7 +468,7 @@ impl PartialEq for FileId { impl Eq for FileId {} /// Returns a unique identifier for the given file by handle or path. -pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result { +pub fn file_id(file: Option<&File>, path: &Path) -> io::Result { let file = match file { Some(f) => f, None => &File::open(path)?, @@ -484,7 +477,7 @@ pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result { file_id_from_handle(file).or_else(|_| Ok(FileId::Path(std::fs::canonicalize(path)?))) } -fn file_id_from_handle(file: &File) -> apperr::Result { +fn file_id_from_handle(file: &File) -> io::Result { unsafe { let mut info = MaybeUninit::::uninit(); check_bool_return(FileSystem::GetFileInformationByHandleEx( @@ -516,11 +509,11 @@ pub fn canonicalize(path: &Path) -> std::io::Result { Ok(path) } -unsafe fn get_module(name: *const u16) -> apperr::Result> { +unsafe fn get_module(name: *const u16) -> io::Result> { unsafe { check_ptr_return(LibraryLoader::GetModuleHandleW(name)) } } -unsafe fn load_library(name: *const u16) -> apperr::Result> { +unsafe fn load_library(name: *const u16) -> io::Result> { unsafe { check_ptr_return(LibraryLoader::LoadLibraryExW( name, @@ -538,13 +531,10 @@ unsafe fn load_library(name: *const u16) -> apperr::Result> { /// of the function you're loading. No type checks whatsoever are performed. // // It'd be nice to constrain T to std::marker::FnPtr, but that's unstable. -pub unsafe fn get_proc_address( - handle: NonNull, - name: *const c_char, -) -> apperr::Result { +pub unsafe fn get_proc_address(handle: NonNull, name: *const c_char) -> io::Result { unsafe { let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name as *const u8); - if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(get_last_error()) } + if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(last_os_error()) } } } @@ -553,7 +543,7 @@ pub struct LibIcu { pub libicui18n: NonNull, } -pub fn load_icu() -> apperr::Result { +pub fn load_icu() -> io::Result { const fn const_ptr_u16_eq(a: *const u16, b: *const u16) -> bool { unsafe { let mut a = a; @@ -654,67 +644,16 @@ fn wide_to_utf8<'a>(arena: &'a Arena, wide: &[u16]) -> ArenaString<'a> { res } -#[cold] -pub fn get_last_error() -> apperr::Error { - unsafe { gle_to_apperr(Foundation::GetLastError()) } -} - #[inline] -const fn gle_to_apperr(gle: u32) -> apperr::Error { - apperr::Error::new_sys(if gle == 0 { 0x8000FFFF } else { 0x80070000 | gle }) -} - -#[inline] -pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error { - gle_to_apperr(err.raw_os_error().unwrap_or(0) as u32) -} - -/// Formats a platform error code into a human-readable string. -pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result { - match code { - ERROR_UNSUPPORTED_LEGACY_CONSOLE => { - write!(f, "This application does not support the legacy console.") - } - _ => unsafe { - let mut ptr: *mut u8 = null_mut(); - let len = Debug::FormatMessageA( - Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER - | Debug::FORMAT_MESSAGE_FROM_SYSTEM - | Debug::FORMAT_MESSAGE_IGNORE_INSERTS, - null(), - code, - 0, - &mut ptr as *mut *mut _ as *mut _, - 0, - null_mut(), - ); - - write!(f, "Error {code:#08x}")?; - - if len > 0 { - let msg = str_from_raw_parts(ptr, len as usize); - let msg = msg.trim_ascii(); - let msg = msg.replace(['\r', '\n'], " "); - write!(f, ": {msg}")?; - Foundation::LocalFree(ptr as *mut _); - } - - Ok(()) - }, - } -} - -/// Checks if the given error is a "file not found" error. -pub fn apperr_is_not_found(err: apperr::Error) -> bool { - const FNF: apperr::Error = gle_to_apperr(Foundation::ERROR_FILE_NOT_FOUND); - const PNF: apperr::Error = gle_to_apperr(Foundation::ERROR_PATH_NOT_FOUND); - err == FNF || err == PNF +#[cold] +fn last_os_error() -> io::Error { + io::Error::last_os_error() } -fn check_bool_return(ret: BOOL) -> apperr::Result<()> { - if ret == 0 { Err(get_last_error()) } else { Ok(()) } +fn check_bool_return(ret: BOOL) -> io::Result<()> { + if ret == 0 { Err(last_os_error()) } else { Ok(()) } } -fn check_ptr_return(ret: *mut T) -> apperr::Result> { - NonNull::new(ret).ok_or_else(get_last_error) +fn check_ptr_return(ret: *mut T) -> io::Result> { + NonNull::new(ret).ok_or_else(last_os_error) } diff --git a/crates/edit/src/tui.rs b/crates/edit/src/tui.rs index 93f42da5ffa..247505a6041 100644 --- a/crates/edit/src/tui.rs +++ b/crates/edit/src/tui.rs @@ -147,10 +147,10 @@ use std::arch::breakpoint; #[cfg(debug_assertions)] use std::collections::HashSet; use std::fmt::Write as _; -use std::{iter, mem, ptr, time}; +use std::{io, iter, mem, ptr, time}; use stdext::arena::{Arena, ArenaString, scratch_arena}; -use stdext::arena_format; +use stdext::{arena_format, opt_ptr_eq, str_from_raw_parts}; use crate::buffer::{CursorMovement, MoveLineDirection, RcTextBuffer, TextBuffer, TextBufferCell}; use crate::cell::*; @@ -161,12 +161,12 @@ use crate::hash::*; use crate::helpers::*; use crate::input::{InputKeyMod, kbmod, vk}; use crate::oklab::StraightRgba; -use crate::{apperr, input, simd, unicode}; +use crate::{input, simd, unicode}; const ROOT_ID: u64 = 0x14057B7EF767814F; // Knuth's MMIX constant const SHIFT_TAB: InputKey = vk::TAB.with_modifiers(kbmod::SHIFT); const KBMOD_FOR_WORD_NAV: InputKeyMod = - if cfg!(target_os = "macos") { kbmod::ALT } else { kbmod::CTRL }; + if cfg!(any(target_os = "macos", target_os = "ios")) { kbmod::ALT } else { kbmod::CTRL }; type Input<'input> = input::Input<'input>; type InputKey = input::InputKey; @@ -375,7 +375,7 @@ pub struct Tui { impl Tui { /// Creates a new [`Tui`] instance for storing state across frames. - pub fn new() -> apperr::Result { + pub fn new() -> io::Result { let arena_prev = Arena::new(128 * MEBI)?; let arena_next = Arena::new(128 * MEBI)?; // SAFETY: Since `prev_tree` refers to `arena_prev`/`arena_next`, from its POV the lifetime @@ -2652,7 +2652,7 @@ impl<'a> Context<'a, '_> { _ => return false, }, vk::B => match modifiers { - kbmod::ALT if cfg!(target_os = "macos") => { + kbmod::ALT if cfg!(any(target_os = "macos", target_os = "ios")) => { // On macOS, terminals commonly emit the Emacs style // Alt+B (ESC b) sequence for Alt+Left. tb.cursor_move_delta(CursorMovement::Word, -1); @@ -2660,7 +2660,7 @@ impl<'a> Context<'a, '_> { _ => return false, }, vk::F => match modifiers { - kbmod::ALT if cfg!(target_os = "macos") => { + kbmod::ALT if cfg!(any(target_os = "macos", target_os = "ios")) => { // On macOS, terminals commonly emit the Emacs style // Alt+F (ESC f) sequence for Alt+Right. tb.cursor_move_delta(CursorMovement::Word, 1); @@ -3120,7 +3120,8 @@ impl<'a> Context<'a, '_> { /// /// Returns true if the menu is open. Continue appending items to it in that case. pub fn menubar_menu_begin(&mut self, text: &str, accelerator: char) -> bool { - let accelerator = if cfg!(target_os = "macos") { '\0' } else { accelerator }; + let accelerator = + if cfg!(any(target_os = "macos", target_os = "ios")) { '\0' } else { accelerator }; let mixin = self.tree.current_node.borrow().child_count as u64; self.next_block_id_mixin(mixin);