diff --git a/src/assets.rs b/src/assets.rs index 598a087..10f5c29 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -135,14 +135,17 @@ pub const LONG_ARRAY_GHOST_UV: Vec2u = Vec2u::new(0, 48); pub const CHUNK_GHOST_UV: Vec2u = Vec2u::new(64, 48); pub const ALERT_UV: Vec2u = Vec2u::new(112, 144); pub const BACKDROP_UV: Vec2u = Vec2u::new(32, 160); -pub const ADD_SEARCH_BOOKMARKS: Vec2u = Vec2u::new(48, 160); -pub const REMOVE_SEARCH_BOOKMARKS: Vec2u = Vec2u::new(64, 160); -pub const SEARCH_KEYS: Vec2u = Vec2u::new(80, 160); -pub const SEARCH_VALUES: Vec2u = Vec2u::new(80, 176); -pub const SEARCH_KEYS_AND_VALUES: Vec2u = Vec2u::new(80, 192); -pub const STRING_SEARCH_MODE: Vec2u = Vec2u::new(96, 160); -pub const REGEX_SEARCH_MODE: Vec2u = Vec2u::new(96, 176); -pub const SNBT_SEARCH_MODE: Vec2u = Vec2u::new(96, 192); +pub const ADD_SEARCH_BOOKMARKS_UV: Vec2u = Vec2u::new(48, 160); +pub const REMOVE_SEARCH_BOOKMARKS_UV: Vec2u = Vec2u::new(64, 160); +pub const SEARCH_KEYS_UV: Vec2u = Vec2u::new(80, 160); +pub const SEARCH_VALUES_UV: Vec2u = Vec2u::new(80, 176); +pub const SEARCH_KEYS_AND_VALUES_UV: Vec2u = Vec2u::new(80, 192); +pub const STRING_SEARCH_MODE_UV: Vec2u = Vec2u::new(96, 160); +pub const REGEX_SEARCH_MODE_UV: Vec2u = Vec2u::new(96, 176); +pub const SNBT_SEARCH_MODE_UV: Vec2u = Vec2u::new(96, 192); +pub const NEW_FILE_UV: Vec2u = Vec2u::new(96, 48); +pub const REFRESH_UV: Vec2u = Vec2u::new(152, 144); +pub const DISABLED_REFRESH_UV: Vec2u = Vec2u::new(168, 144); pub const BASE_Z: u8 = 5; pub const JUST_OVERLAPPING_BASE_Z: u8 = BASE_Z + 1; @@ -153,10 +156,9 @@ pub const LINE_NUMBER_Z: u8 = 60; pub const LINE_NUMBER_CONNECTOR_Z: u8 = LINE_NUMBER_Z + 1; pub const BOOKMARK_Z: u8 = 80; pub const SELECTED_TEXT_Z: u8 = 130; -pub const ELEMENT_HIGHLIGHT_Z: u8 = SELECTED_TEXT_Z; pub const ACTION_WHEEL_Z: u8 = 190; -pub const SCROLLBAR_Z: u8 = 200; pub const SCROLLBAR_BOOKMARK_Z: u8 = SCROLLBAR_Z - 1; +pub const SCROLLBAR_Z: u8 = 200; pub const HELD_ENTRY_Z: u8 = 210; pub const ALERT_Z: u8 = 240; pub const ALERT_TEXT_Z: u8 = ALERT_Z + 1; diff --git a/src/assets/atlas.hex b/src/assets/atlas.hex index 8dfa91d..9793451 100644 Binary files a/src/assets/atlas.hex and b/src/assets/atlas.hex differ diff --git a/src/assets/build/atlas.png b/src/assets/build/atlas.png index 590a8a5..be8a5b4 100644 Binary files a/src/assets/build/atlas.png and b/src/assets/build/atlas.png differ diff --git a/src/bookmark.rs b/src/bookmark.rs new file mode 100644 index 0000000..663c279 --- /dev/null +++ b/src/bookmark.rs @@ -0,0 +1,291 @@ +use std::cmp::Ordering; +use std::collections::Bound; +use std::convert::identity; +use std::ops::{Deref, DerefMut, Index, IndexMut, RangeBounds}; +use crate::assets::{BOOKMARK_UV, HIDDEN_BOOKMARK_UV}; +use crate::vertex_buffer_builder::Vec2u; + +macro_rules! slice { + ($($t:tt)*) => { + unsafe { core::mem::transmute::<&[Bookmark], &BookmarkSlice>(&$($t)*) } + }; +} + +macro_rules! slice_mut { + ($($t:tt)*) => { + unsafe { core::mem::transmute::<&mut [Bookmark], &mut BookmarkSlice>(&mut $($t)*) } + }; +} + +#[derive(Copy, Clone, Debug)] +pub struct Bookmark { + true_line_number: usize, + line_number: usize, + uv: Vec2u, +} + +impl Bookmark { + #[inline] + pub const fn new(true_line_number: usize, line_number: usize) -> Self { + Self { + true_line_number, + line_number, + uv: BOOKMARK_UV, + } + } + + #[inline] + pub const fn with_uv(true_line_number: usize, line_number: usize, uv: Vec2u) -> Self { + Self { + true_line_number, + line_number, + uv, + } + } + + #[inline] + pub const fn true_line_number(self) -> usize { self.true_line_number } + + #[inline] + pub const fn line_number(self) -> usize { self.line_number } + + #[inline] + pub const fn uv(self) -> Vec2u { self.uv } + + #[inline] + pub const fn hidden(self, line_number: usize) -> Self { + Self { + true_line_number: self.true_line_number, + line_number, + uv: HIDDEN_BOOKMARK_UV, + } + } + + #[inline] + pub const fn open(self, line_number: usize) -> Self { + Self { + true_line_number: self.true_line_number, + line_number, + uv: BOOKMARK_UV, + } + } + + #[inline] + pub const fn offset(self, offset: usize, true_offset: usize) -> Self { + Self { + true_line_number: self.true_line_number.wrapping_add(true_offset), + line_number: self.line_number.wrapping_add(offset), + uv: self.uv, + } + } +} + +impl PartialEq for Bookmark { + fn eq(&self, other: &Self) -> bool { self.true_line_number == other.true_line_number } +} + +impl Eq for Bookmark {} + +impl PartialOrd for Bookmark { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } +} +impl Ord for Bookmark { + fn cmp(&self, other: &Self) -> Ordering { self.true_line_number.cmp(&other.true_line_number) } +} + +pub struct Bookmarks { + inner: Vec, +} + +impl Bookmarks { + #[inline] + pub const fn new() -> Self { + Self { + inner: vec![] + } + } + + #[inline] + pub fn toggle(&mut self, bookmark: Bookmark) -> Result<(), Bookmark> { + match self.inner.binary_search(&bookmark) { + Ok(idx) => Err(self.inner.remove(idx)), + Err(idx) => { + self.inner.insert(idx, bookmark); + Ok(()) + } + } + } + + #[inline] + pub fn clear(&mut self) { + self.inner.clear(); + } + + /// # Safety + /// `inner` must be sorted least to greatest, i.e. it is up to the caller to assure `inner.is_sorted()` + #[inline] + pub unsafe fn from_raw(inner: Vec) -> Self { + Self { + inner + } + } + + #[inline] + pub fn into_raw(self) -> Box<[Bookmark]> { + self.inner.into_boxed_slice() + } + + #[inline] + pub fn remove>(&mut self, range: R) -> Vec { + match (range.start_bound().map(|&x| Bookmark::new(x, 0)), range.end_bound().map(|&x| Bookmark::new(x, 0))) { + (Bound::Unbounded, Bound::Unbounded) => self.inner.drain(..), + (Bound::Unbounded, Bound::Included(ref end)) => self.inner.drain(..=self.binary_search(end).unwrap_or_else(identity)), + (Bound::Unbounded, Bound::Excluded(ref end)) => self.inner.drain(..self.binary_search(end).unwrap_or_else(identity)), + (Bound::Included(ref start), Bound::Unbounded) => self.inner.drain(self.binary_search(start).unwrap_or_else(identity)..), + (Bound::Included(ref start), Bound::Included(ref end)) => self.inner.drain(self.binary_search(start).unwrap_or_else(identity)..=self.binary_search(end).unwrap_or_else(identity)), + (Bound::Included(ref start), Bound::Excluded(ref end)) => self.inner.drain(self.binary_search(start).unwrap_or_else(identity)..self.binary_search(end).unwrap_or_else(identity)), + (Bound::Excluded(ref start), Bound::Unbounded) => self.inner.drain(self.binary_search(start).map_or_else(identity, |x| x + 1)..), + (Bound::Excluded(ref start), Bound::Included(ref end)) => self.inner.drain(self.binary_search(start).map_or_else(identity, |x| x + 1)..=self.binary_search(end).unwrap_or_else(identity)), + (Bound::Excluded(ref start), Bound::Excluded(ref end)) => self.inner.drain(self.binary_search(start).map_or_else(identity, |x| x + 1)..self.binary_search(end).unwrap_or_else(identity)), + }.collect() + } +} + +#[repr(transparent)] +pub struct BookmarkSlice([Bookmark]); + +impl BookmarkSlice { + pub const EMPTY: &'static BookmarkSlice = unsafe { core::mem::transmute::<&[Bookmark], &Self>(&[]) }; + pub const EMPTY_MUT: &'static mut BookmarkSlice = unsafe { core::mem::transmute::<&mut [Bookmark], &mut Self>(&mut []) }; + + #[inline] + pub fn increment(&mut self, value: usize, true_value: usize) { + for bookmark in &mut self.0 { + bookmark.line_number = bookmark.line_number.wrapping_add(value); + bookmark.true_line_number = bookmark.true_line_number.wrapping_add(true_value); + } + } + + #[inline] + pub fn decrement(&mut self, value: usize, true_value: usize) { + for bookmark in &mut self.0 { + bookmark.line_number -= value; + bookmark.true_line_number -= true_value; + } + } + + #[inline] + pub fn split_first(&self) -> Option<(Bookmark, &BookmarkSlice)> { + if let [head, rest @ ..] = &self.0 { + Some((*head, slice!(rest))) + } else { + None + } + } + + #[inline] + pub fn iter(&self) -> Iter<'_> { + Iter(&self.0) + } + + #[inline] + pub fn iter_mut(&mut self) -> IterMut<'_> { + IterMut(&mut self.0) + } +} + +impl Deref for Bookmarks { + type Target = BookmarkSlice; + + fn deref(&self) -> &Self::Target { + unsafe { core::mem::transmute(self.inner.as_slice()) } + } +} + +impl DerefMut for Bookmarks { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { core::mem::transmute(self.inner.as_mut_slice()) } + } +} + +impl Deref for BookmarkSlice { + type Target = [Bookmark]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for BookmarkSlice { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub struct Iter<'a>(&'a [Bookmark]); + +impl<'a> Iterator for Iter<'a> { + type Item = Bookmark; + + fn next(&mut self) -> Option { + if let Some((&item, rest)) = self.0.split_first() { + self.0 = rest; + Some(item) + } else { + None + } + } +} + +pub struct IterMut<'a>(&'a mut [Bookmark]); + +impl<'a> Iterator for IterMut<'a> { + type Item = &'a mut Bookmark; + + fn next(&mut self) -> Option { + if self.0.is_empty() { + None + } else { + unsafe { + let ptr = self.0.as_mut_ptr(); + let len = self.0.len(); + self.0 = core::slice::from_raw_parts_mut(ptr.add(1), len - 1); + Some(core::mem::transmute(ptr)) + } + } + } +} + +impl> Index for BookmarkSlice { + type Output = BookmarkSlice; + + fn index(&self, index: R) -> &Self::Output { + match (index.start_bound().map(|&x| Bookmark::new(x, 0)), index.end_bound().map(|&x| Bookmark::new(x, 0))) { + (Bound::Unbounded, Bound::Unbounded) => self, + (Bound::Unbounded, Bound::Included(ref end)) => { let end = self.binary_search(end).unwrap_or_else(identity); if end >= self.len() { BookmarkSlice::EMPTY } else { slice!(self.0[..=end]) } }, + (Bound::Unbounded, Bound::Excluded(ref end)) => { let end = self.binary_search(end).unwrap_or_else(identity); slice!(self.0[..end]) }, + (Bound::Included(ref start), Bound::Unbounded) => { let start = self.binary_search(start).unwrap_or_else(identity); slice!(self.0[start..]) }, + (Bound::Included(ref start), Bound::Included(ref end)) => { let start = self.binary_search(start).unwrap_or_else(identity); let end = self.binary_search(end).unwrap_or_else(identity); if end >= self.len() { BookmarkSlice::EMPTY } else { slice!(self.0[start..=end]) } }, + (Bound::Included(ref start), Bound::Excluded(ref end)) => { let start = self.binary_search(start).unwrap_or_else(identity); let end = self.binary_search(end).unwrap_or_else(identity); slice!(self.0[start..end]) }, + (Bound::Excluded(ref start), Bound::Unbounded) => { let start = self.binary_search(start).map_or_else(identity, |x| x + 1); slice!(self.0[start..]) }, + (Bound::Excluded(ref start), Bound::Included(ref end)) => { let start = self.binary_search(start).map_or_else(identity, |x| x + 1); let end = self.binary_search(end).unwrap_or_else(identity); if end >= self.len() { BookmarkSlice::EMPTY } else { slice!(self.0[start..=end]) } }, + (Bound::Excluded(ref start), Bound::Excluded(ref end)) => { let start = self.binary_search(start).map_or_else(identity, |x| x + 1); let end = self.binary_search(end).unwrap_or_else(identity); slice!(self.0[start..end]) }, + } + } +} + +impl> IndexMut for BookmarkSlice { + fn index_mut(&mut self, index: R) -> &mut Self::Output { + match (index.start_bound().map(|&x| Bookmark::new(x, 0)), index.end_bound().map(|&x| Bookmark::new(x, 0))) { + (Bound::Unbounded, Bound::Unbounded) => self, + (Bound::Unbounded, Bound::Included(ref end)) => { let end = self.binary_search(end).unwrap_or_else(identity); if end >= self.len() { BookmarkSlice::EMPTY_MUT } else { slice_mut!(self.0[..=end]) } }, + (Bound::Unbounded, Bound::Excluded(ref end)) => { let end = self.binary_search(end).unwrap_or_else(identity); slice_mut!(self.0[..end]) }, + (Bound::Included(ref start), Bound::Unbounded) => { let start = self.binary_search(start).unwrap_or_else(identity); slice_mut!(self.0[start..]) }, + (Bound::Included(ref start), Bound::Included(ref end)) => { let start = self.binary_search(start).unwrap_or_else(identity); let end = self.binary_search(end).unwrap_or_else(identity); if end >= self.len() { BookmarkSlice::EMPTY_MUT } else { slice_mut!(self.0[start..=end]) } }, + (Bound::Included(ref start), Bound::Excluded(ref end)) => { let start = self.binary_search(start).unwrap_or_else(identity); let end = self.binary_search(end).unwrap_or_else(identity); slice_mut!(self.0[start..end]) }, + (Bound::Excluded(ref start), Bound::Unbounded) => { let start = self.binary_search(start).map_or_else(identity, |x| x + 1); slice_mut!(self.0[start..]) }, + (Bound::Excluded(ref start), Bound::Included(ref end)) => { let start = self.binary_search(start).map_or_else(identity, |x| x + 1); let end = self.binary_search(end).unwrap_or_else(identity); if end >= self.len() { BookmarkSlice::EMPTY_MUT } else { slice_mut!(self.0[start..=end]) } }, + (Bound::Excluded(ref start), Bound::Excluded(ref end)) => { let start = self.binary_search(start).map_or_else(identity, |x| x + 1); let end = self.binary_search(end).unwrap_or_else(identity); slice_mut!(self.0[start..end]) }, + } + } +} diff --git a/src/cli.rs b/src/cli.rs index b45b35c..1c5e494 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -160,7 +160,7 @@ pub fn find() -> ! { if !bookmarks.is_empty() { Some(SearchResult { path, - lines: bookmarks.into_iter().map(|x| x.true_line_number).collect(), + lines: bookmarks.into_iter().map(|x| x.true_line_number()).collect(), }) } else { None diff --git a/src/element_action.rs b/src/element_action.rs index 6645b84..302be89 100644 --- a/src/element_action.rs +++ b/src/element_action.rs @@ -1,16 +1,13 @@ use std::cmp::Ordering; -use std::convert::identity; #[cfg(not(target_arch = "wasm32"))] -use std::fs::OpenOptions; -#[cfg(not(target_arch = "wasm32"))] -use std::process::Command; +use std::{fs::OpenOptions, process::Command}; use compact_str::CompactString; #[cfg(not(target_arch = "wasm32"))] use notify::{EventKind, PollWatcher, RecursiveMode, Watcher}; use uuid::Uuid; -use crate::{Bookmark, panic_unchecked, set_clipboard, FileUpdateSubscription, since_epoch}; +use crate::{panic_unchecked, set_clipboard, FileUpdateSubscription, since_epoch}; #[cfg(not(target_arch = "wasm32"))] use crate::{FileUpdateSubscriptionType, assets::{OPEN_ARRAY_IN_HEX_UV, OPEN_IN_TXT}}; use crate::assets::{ACTION_WHEEL_Z, COPY_FORMATTED_UV, COPY_RAW_UV, SORT_COMPOUND_BY_NAME, SORT_COMPOUND_BY_TYPE}; @@ -21,6 +18,7 @@ use crate::elements::list::NbtList; use crate::elements::string::NbtString; use crate::vertex_buffer_builder::VertexBufferBuilder; use crate::workbench_action::WorkbenchAction; +use crate::bookmark::Bookmarks; #[derive(Copy, Clone)] pub enum ElementAction { @@ -112,7 +110,7 @@ impl ElementAction { } #[allow(clippy::too_many_lines)] - pub fn apply(self, key: Option, indices: Box<[usize]>, tab_uuid: Uuid, true_line_number: usize, line_number: usize, element: &mut NbtElement, bookmarks: &mut Vec, subscription: &mut Option) -> Option { + pub fn apply(self, key: Option, indices: Box<[usize]>, tab_uuid: Uuid, true_line_number: usize, line_number: usize, element: &mut NbtElement, bookmarks: &mut Bookmarks, subscription: &mut Option) -> Option { #[must_use] #[cfg(not(target_arch = "wasm32"))] fn open_file(str: &str) -> bool { @@ -271,9 +269,7 @@ impl ElementAction { Self::SortCompoundByName => { let open = element.open(); let true_height = element.true_height(); - let bookmark_start = bookmarks.binary_search(&Bookmark::new(true_line_number, 0)).map_or_else(identity, |x| x + 1); - let bookmark_end = bookmarks.binary_search(&Bookmark::new(true_line_number + element.true_height() - 1, 0)).map_or_else(identity, |x| x + 1); - let bookmark_slice = if bookmark_end > bookmark_start || bookmark_end > bookmarks.len() { &mut [] } else { &mut bookmarks[bookmark_start..bookmark_end] }; + let bookmark_slice = &mut bookmarks[true_line_number..true_line_number + element.true_height()]; let reordering_indices = if let Some(compound) = element.as_compound_mut() { compound.entries.sort_by(Self::by_name, line_number, true_line_number, true_height, open, bookmark_slice) } else if let Some(chunk) = element.as_chunk_mut() { @@ -287,9 +283,7 @@ impl ElementAction { Self::SortCompoundByType => { let open = element.open(); let true_height = element.true_height(); - let bookmark_start = bookmarks.binary_search(&Bookmark::new(true_line_number, 0)).map_or_else(identity, |x| x + 1); - let bookmark_end = bookmarks.binary_search(&Bookmark::new(true_line_number + element.true_height() - 1, 0)).map_or_else(identity, |x| x + 1); - let bookmark_slice = if bookmark_end < bookmark_start || bookmark_end > bookmarks.len() { &mut [] } else { &mut bookmarks[bookmark_start..bookmark_end] }; + let bookmark_slice = &mut bookmarks[true_line_number..true_line_number + element.true_height()]; let reordering_indices = if let Some(compound) = element.as_compound_mut() { compound.entries.sort_by(Self::by_type, line_number, true_line_number, true_height, open, bookmark_slice) } else if let Some(chunk) = element.as_chunk_mut() { diff --git a/src/elements/array.rs b/src/elements/array.rs index c57edb6..b3de4e5 100644 --- a/src/elements/array.rs +++ b/src/elements/array.rs @@ -372,13 +372,27 @@ macro_rules! array { } } - impl Debug for $name { - fn fmt<'b, 'a>(&self, f: &'a mut Formatter<'b>) -> fmt::Result { - let mut debug = unsafe { core::mem::transmute::<_, DebugList<'static, 'static>>(f.debug_list()) }; - f.write_str(concat!($char, ";"))?; - let result = debug.entries(self.children()).finish(); - let _ = unsafe { core::mem::transmute::<_, DebugList<'a, 'b>>(debug) }; - result + impl $name { + pub fn pretty_fmt(&self, f: &mut PrettyFormatter) { + if self.is_empty() { + f.write_str(concat!("[", $char, ";]")) + } else { + let len = self.len(); + f.write_str(concat!("[", $char, ";\n")); + f.increase(); + for (idx, element) in self.children().enumerate() { + f.indent(); + element.pretty_fmt(f); + if idx + 1 < len { + f.write_str(",\n"); + } else { + f.write_str("\n"); + } + } + f.decrease(); + f.indent(); + f.write_str("]"); + } } } }; diff --git a/src/elements/chunk.rs b/src/elements/chunk.rs index 3a7c3c0..19a5945 100644 --- a/src/elements/chunk.rs +++ b/src/elements/chunk.rs @@ -1,5 +1,5 @@ use std::alloc::{alloc, Layout}; -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::{Display, Formatter}; use std::intrinsics::likely; use std::mem::{ManuallyDrop, MaybeUninit}; use std::ops::{Deref, DerefMut}; @@ -18,6 +18,7 @@ use crate::tab::FileFormat; use crate::vertex_buffer_builder::VertexBufferBuilder; use crate::{DropFn, RenderContext, SortAlgorithm, StrExt}; use crate::color::TextColor; +use crate::formatter::PrettyFormatter; #[repr(C)] pub struct NbtRegion { @@ -698,13 +699,27 @@ impl NbtRegion { pub const fn max_depth(&self) -> usize { self.max_depth as usize } } -impl Debug for NbtRegion { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut r#struct = f.debug_struct("Region"); - for chunk in self.children().map(|x| unsafe { x.as_chunk_unchecked() }) { - r#struct.field(&format!("x: {:02}, z: {:02}", chunk.x, chunk.z), &chunk); +impl NbtRegion { + pub fn pretty_fmt(&self, f: &mut PrettyFormatter) { + if self.is_empty() { + f.write_str("Region {}") + } else { + f.write_str("Region {\n"); + f.increase(); + let len = self.len(); + for (idx, chunk) in self.children().map(|x| unsafe { x.as_chunk_unchecked() }).enumerate() { + f.indent(); + chunk.pretty_fmt(f); + if idx + 1 < len { + f.write_str(",\n"); + } else { + f.write_str("\n"); + } + } + f.decrease(); + f.indent(); + f.write_str("}"); } - r#struct.finish() } } @@ -940,21 +955,9 @@ impl Display for NbtChunk { } #[allow(clippy::missing_fields_in_debug)] -impl Debug for NbtChunk { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{} | {}", self.x, self.z)?; - if self.is_empty() { - write!(f, "{{}}") - } else { - let mut debug = f.debug_struct(""); - for (key, element) in self.children() { - if key.needs_escape() { - debug.field(&format!("{key:?}"), element); - } else { - debug.field(key, element); - } - } - debug.finish() - } +impl NbtChunk { + pub fn pretty_fmt(&self, f: &mut PrettyFormatter) { + f.write_str(&format!("{} | {} ", self.x, self.z)); + self.inner.pretty_fmt(f) } } diff --git a/src/elements/compound.rs b/src/elements/compound.rs index 2752ab3..36d7adc 100644 --- a/src/elements/compound.rs +++ b/src/elements/compound.rs @@ -1,7 +1,7 @@ use std::alloc::{alloc, Layout}; use std::cmp::Ordering; use std::convert::identity; -use std::fmt::{Debug, Display, Formatter, Write}; +use std::fmt::{Display, Formatter, Write}; use std::hash::Hasher; use std::intrinsics::likely; use std::ops::Deref; @@ -17,8 +17,10 @@ use crate::decoder::Decoder; use crate::elements::chunk::NbtChunk; use crate::elements::element::NbtElement; use crate::encoder::UncheckedBufWriter; -use crate::{Bookmark, DropFn, OptionExt, RenderContext, SortAlgorithm, StrExt, VertexBufferBuilder}; +use crate::{DropFn, OptionExt, RenderContext, SortAlgorithm, StrExt, VertexBufferBuilder}; use crate::color::TextColor; +use crate::formatter::PrettyFormatter; +use crate::bookmark::{Bookmark, BookmarkSlice}; #[allow(clippy::module_name_repetitions)] #[repr(C)] @@ -380,24 +382,55 @@ impl Display for NbtCompound { } } -impl Debug for NbtCompound { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +impl NbtCompound { + pub fn pretty_fmt(&self, f: &mut PrettyFormatter) { if self.is_empty() { - write!(f, "{{}}") + f.write_str("{}") } else { - let mut debug = f.debug_struct(""); - for (key, element) in self.children() { + let len = self.len(); + f.write_str("{\n"); + f.increase(); + for (idx, (key, element)) in self.children().enumerate() { + f.indent(); if key.needs_escape() { - debug.field(&format!("{key:?}"), element); + f.write_str(&format!("{key:?}")); + f.write_str(": "); + } else { + f.write_str(key); + f.write_str(": "); + } + element.pretty_fmt(f); + if idx + 1 < len { + f.write_str(",\n"); } else { - debug.field(key, element); + f.write_str("\n"); } } - debug.finish() + f.decrease(); + f.indent(); + f.write_str("}"); } } } +// impl Debug for NbtCompound { +// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +// if self.is_empty() { +// write!(f, "{{}}") +// } else { +// let mut debug = f.debug_struct(""); +// for (key, element) in self.children() { +// if key.needs_escape() { +// debug.field(&format!("{key:?}"), element); +// } else { +// debug.field(key, element); +// } +// } +// debug.finish() +// } +// } +// } + impl NbtCompound { #[inline] #[allow(clippy::too_many_lines)] @@ -968,7 +1001,7 @@ impl CompoundMap { Some((entry.key.as_ref(), &mut entry.value)) } - pub fn sort_by Ordering>(&mut self, mut f: F, line_number: usize, true_line_number: usize, true_height: usize, open: bool, bookmarks: &mut [Bookmark]) -> Box<[usize]> { + pub fn sort_by Ordering>(&mut self, mut f: F, line_number: usize, true_line_number: usize, true_height: usize, open: bool, bookmarks: &mut BookmarkSlice) -> Box<[usize]> { let hashes = self.entries.iter().map(|entry| entry.hash).collect::>(); let true_line_numbers = { let mut current_line_number = true_line_number + 1; @@ -978,9 +1011,7 @@ impl CompoundMap { let mut current_line_number = line_number + 1; self.entries.iter().map(|entry| { let new_line_number = current_line_number; current_line_number += entry.value.height(); new_line_number }).collect::>() }; - let bookmarks_start = bookmarks.binary_search(&Bookmark::new(true_line_number, 0)).unwrap_or_else(identity); - let bookmarks_end = bookmarks.binary_search(&Bookmark::new(true_line_number + true_height - 1, 0)).map_or_else(identity, |x| x + 1); - let mut new_bookmarks = Box::<[Bookmark]>::new_uninit_slice(bookmarks_end - bookmarks_start); + let mut new_bookmarks = Box::<[Bookmark]>::new_uninit_slice(bookmarks[true_line_number..true_line_number + true_height].len()); let mut new_bookmarks_len = 0; // yeah, it's hacky but there's not much else I *can* do. plus: it works extremely well. for (idx, entry) in self.entries.iter_mut().enumerate() { @@ -1005,20 +1036,17 @@ impl CompoundMap { let height = entry.value.height(); let true_offset = current_true_line_number as isize - true_line_number as isize; let offset = if open { current_line_number as isize - line_number as isize } else { 0 }; - let bookmark_start = bookmarks.binary_search(&Bookmark::new(true_line_number, 0)).unwrap_or_else(identity); - let bookmark_end = bookmarks.binary_search(&Bookmark::new(true_line_number + true_height - 1, 0)).map_or_else(identity, |x| x + 1); - for bookmark in bookmarks.iter().skip(bookmark_start).take(bookmark_end - bookmark_start) { - let adjusted_bookmark = Bookmark::new(bookmark.true_line_number.wrapping_add(true_offset as usize), bookmark.line_number.wrapping_add(offset as usize)); - new_bookmarks[new_bookmarks_len].write(adjusted_bookmark); + for bookmark in bookmarks[true_line_number..true_line_number + true_height].iter() { + new_bookmarks[new_bookmarks_len].write(bookmark.offset(offset as usize, true_offset as usize)); new_bookmarks_len += 1; } - current_true_line_number += true_height; current_line_number += height; inverted_indices[idx].write(new_idx); } } - unsafe { core::ptr::copy_nonoverlapping(new_bookmarks.as_ptr().cast::(), bookmarks.as_mut_ptr().add(bookmarks_start), bookmarks_end - bookmarks_start); } + let bookmark_slice = &mut bookmarks[true_line_number..true_line_number + true_height]; + unsafe { core::ptr::copy_nonoverlapping(new_bookmarks.as_ptr().cast::(), bookmark_slice.as_mut_ptr(), bookmark_slice.len()); } unsafe { inverted_indices.assume_init() } } diff --git a/src/elements/element.rs b/src/elements/element.rs index cc70a99..c396950 100644 --- a/src/elements/element.rs +++ b/src/elements/element.rs @@ -1,4 +1,3 @@ -use core::fmt::DebugList; use std::alloc::{alloc, dealloc, Layout}; use std::fmt::{Debug, Display, Error, Formatter}; use std::intrinsics::likely; @@ -20,24 +19,15 @@ use crate::elements::list::{NbtList, ValueIterator, ValueMutIterator}; use crate::elements::string::NbtString; use crate::encoder::UncheckedBufWriter; use crate::{panic_unchecked, since_epoch, SortAlgorithm, array, primitive, DropFn, RenderContext, StrExt, VertexBufferBuilder, TextColor, assets::JUST_OVERLAPPING_BASE_TEXT_Z}; +use crate::formatter::PrettyFormatter; use crate::tab::FileFormat; primitive!(BYTE_UV, { Some('b') }, NbtByte, i8, 1); primitive!(SHORT_UV, { Some('s') }, NbtShort, i16, 2); primitive!(INT_UV, { None:: }, NbtInt, i32, 3); primitive!(LONG_UV, { Some('L') }, NbtLong, i64, 4); -primitive!(FLOAT_UV, { Some('f') }, NbtFloat, f32, 5, |x| { - format_compact!("{x:.149}") - .trim_end_matches('0') - .trim_end_matches('.') - .to_compact_string() -}); -primitive!(DOUBLE_UV, { Some('d') }, NbtDouble, f64, 6, |x| { - format_compact!("{x:.1076}") - .trim_end_matches('0') - .trim_end_matches('.') - .to_compact_string() -}); +primitive!(FLOAT_UV, { Some('f') }, NbtFloat, f32, 5); +primitive!(DOUBLE_UV, { Some('d') }, NbtDouble, f64, 6); array!(byte, NbtByteArray, i8, 7, 1, 'B', BYTE_ARRAY_UV, BYTE_UV); array!(int, NbtIntArray, i32, 11, 3, 'I', INT_ARRAY_UV, INT_UV); array!(long, NbtLongArray, i64, 12, 4, 'L', LONG_ARRAY_UV, LONG_UV); @@ -523,8 +513,8 @@ impl NbtElement { #[inline] #[must_use] - pub fn from_id(id: u8) -> Option { - Some(match id { + pub fn from_id(id: u8) -> Self { + match id { NbtByte::ID => Self::Byte(NbtByte::default()), NbtShort::ID => Self::Short(NbtShort::default()), NbtInt::ID => Self::Int(NbtInt::default()), @@ -543,8 +533,8 @@ impl NbtElement { FileFormat::Zlib, since_epoch().as_secs() as u32, )), - _ => return None, - }) + _ => unsafe { core::mem::zeroed() }, + } } #[inline] @@ -1166,30 +1156,38 @@ impl Display for NbtElement { } } -impl Debug for NbtElement { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +impl NbtElement { + pub fn pretty_fmt(&self, f: &mut PrettyFormatter) { unsafe { match self.id() { - NbtByte::ID => Debug::fmt(&*self.byte, f), - NbtShort::ID => Debug::fmt(&*self.short, f), - NbtInt::ID => Debug::fmt(&*self.int, f), - NbtLong::ID => Debug::fmt(&*self.long, f), - NbtFloat::ID => Debug::fmt(&*self.float, f), - NbtDouble::ID => Debug::fmt(&*self.double, f), - NbtByteArray::ID => Debug::fmt(&*self.byte_array, f), - NbtString::ID => Debug::fmt(&*self.string, f), - NbtList::ID => Debug::fmt(&*self.list, f), - NbtCompound::ID => Debug::fmt(&*self.compound, f), - NbtIntArray::ID => Debug::fmt(&*self.int_array, f), - NbtLongArray::ID => Debug::fmt(&*self.long_array, f), - NbtChunk::ID => Debug::fmt(&*self.chunk, f), - NbtRegion::ID => Err(Error), + NbtByte::ID => self.byte.pretty_fmt(f), + NbtShort::ID => self.short.pretty_fmt(f), + NbtInt::ID => self.int.pretty_fmt(f), + NbtLong::ID => self.long.pretty_fmt(f), + NbtFloat::ID => self.float.pretty_fmt(f), + NbtDouble::ID => self.double.pretty_fmt(f), + NbtByteArray::ID => self.byte_array.pretty_fmt(f), + NbtString::ID => self.string.pretty_fmt(f), + NbtList::ID => self.list.pretty_fmt(f), + NbtCompound::ID => self.compound.pretty_fmt(f), + NbtIntArray::ID => self.int_array.pretty_fmt(f), + NbtLongArray::ID => self.long_array.pretty_fmt(f), + NbtChunk::ID => self.chunk.pretty_fmt(f), + NbtRegion::ID => self.region.pretty_fmt(f), _ => core::hint::unreachable_unchecked(), } } } } +impl Debug for NbtElement { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut formatter = PrettyFormatter::new(); + self.pretty_fmt(&mut formatter); + write!(f, "{}", formatter.finish()) + } +} + impl Drop for NbtElement { fn drop(&mut self) { unsafe { diff --git a/src/elements/list.rs b/src/elements/list.rs index 0b126e2..9cbeb2c 100644 --- a/src/elements/list.rs +++ b/src/elements/list.rs @@ -1,6 +1,6 @@ use compact_str::{format_compact, CompactString}; use std::alloc::{alloc, Layout}; -use std::fmt::{Debug, Display, Formatter, Write}; +use std::fmt::{Display, Formatter, Write}; use std::intrinsics::likely; use std::slice::{Iter, IterMut}; #[cfg(not(target_arch = "wasm32"))] @@ -13,6 +13,7 @@ use crate::elements::element::{id_to_string_name, NbtElement}; use crate::encoder::UncheckedBufWriter; use crate::{DropFn, OptionExt, RenderContext, SortAlgorithm, StrExt, VertexBufferBuilder}; use crate::color::TextColor; +use crate::formatter::PrettyFormatter; #[allow(clippy::module_name_repetitions)] #[repr(C)] @@ -27,7 +28,11 @@ pub struct NbtList { impl PartialEq for NbtList { fn eq(&self, other: &Self) -> bool { - self.elements == other.elements + if self.is_empty() { + other.is_empty() + } else { + self.elements.iter().all(|a| other.elements.iter().any(|b| a == b)) + } } } @@ -232,8 +237,28 @@ impl Display for NbtList { } } -impl Debug for NbtList { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_list().entries(self.children()).finish() } +impl NbtList { + pub fn pretty_fmt(&self, f: &mut PrettyFormatter) { + if self.is_empty() { + f.write_str("[]") + } else { + let len = self.len(); + f.write_str("[\n"); + f.increase(); + for (idx, element) in self.children().enumerate() { + f.indent(); + element.pretty_fmt(f); + if idx + 1 < len { + f.write_str(",\n"); + } else { + f.write_str("\n"); + } + } + f.decrease(); + f.indent(); + f.write_str("]"); + } + } } impl NbtList { diff --git a/src/elements/primitive.rs b/src/elements/primitive.rs index 62361d6..d9da66f 100644 --- a/src/elements/primitive.rs +++ b/src/elements/primitive.rs @@ -64,8 +64,8 @@ macro_rules! primitive { } } - impl Debug for $name { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) } + impl $name { + pub fn pretty_fmt(&self, f: &mut PrettyFormatter) { f.write_str(&self.to_string()) } } }; } diff --git a/src/elements/string.rs b/src/elements/string.rs index 1f8a91d..d8d8538 100644 --- a/src/elements/string.rs +++ b/src/elements/string.rs @@ -1,5 +1,5 @@ use std::alloc::{alloc, dealloc, Layout}; -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::{Display, Formatter}; use std::mem::{ManuallyDrop, MaybeUninit}; use std::ops::Deref; use std::ptr::NonNull; @@ -11,6 +11,7 @@ use crate::decoder::Decoder; use crate::encoder::UncheckedBufWriter; use crate::{RenderContext, StrExt, VertexBufferBuilder}; use crate::color::TextColor; +use crate::formatter::PrettyFormatter; #[repr(transparent)] #[allow(clippy::module_name_repetitions)] @@ -68,8 +69,8 @@ impl Display for NbtString { } } -impl Debug for NbtString { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) } +impl NbtString { + pub fn pretty_fmt(&self, f: &mut PrettyFormatter) { f.write_str(&self.to_string()) } } impl NbtString { diff --git a/src/formatter.rs b/src/formatter.rs new file mode 100644 index 0000000..a872f8e --- /dev/null +++ b/src/formatter.rs @@ -0,0 +1,42 @@ +use std::fmt::Write; + +pub struct PrettyFormatter { + buf: String, + current_depth: usize, +} + +impl PrettyFormatter { + const INDENT: u8 = b' '; + const INDENT_QUANTITY: usize = 4; + + pub const fn new() -> Self { + Self { + buf: String::new(), + current_depth: 0, + } + } + + pub fn write_str(&mut self, s: &str) { + let _ = self.buf.write_str(s); + } + + pub fn increase(&mut self) { + self.current_depth += 1; + } + + pub fn indent(&mut self) { + unsafe { + let vec = self.buf.as_mut_vec(); + let len = vec.len(); + vec.reserve(self.current_depth * Self::INDENT_QUANTITY); + vec.spare_capacity_mut().as_mut_ptr().write_bytes(Self::INDENT, self.current_depth * Self::INDENT_QUANTITY); + vec.set_len(len + self.current_depth * Self::INDENT_QUANTITY); + } + } + + pub fn decrease(&mut self) { + self.current_depth = self.current_depth.saturating_sub(1); + } + + pub fn finish(self) -> String { self.buf } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 9dd8e6b..f3c97c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,7 @@ #![feature(box_patterns)] #![feature(const_black_box)] #![feature(const_collections_with_hasher)] +#![feature(const_mut_refs)] #![feature(core_intrinsics)] #![feature(iter_array_chunks)] #![feature(iter_next_chunk)] @@ -68,6 +69,7 @@ use elements::element::NbtElement; use vertex_buffer_builder::VertexBufferBuilder; use crate::assets::{BASE_TEXT_Z, BASE_Z, BOOKMARK_UV, BOOKMARK_Z, END_LINE_NUMBER_SEPARATOR_UV, HEADER_SIZE, HIDDEN_BOOKMARK_UV, HOVERED_WIDGET_UV, INSERTION_UV, INVALID_STRIPE_UV, LINE_NUMBER_SEPARATOR_UV, LINE_NUMBER_Z, SCROLLBAR_BOOKMARK_Z, SELECTED_TOGGLE_OFF_UV, SELECTED_TOGGLE_ON_UV, SORT_COMPOUND_BY_NAME, SORT_COMPOUND_BY_NOTHING, SORT_COMPOUND_BY_TYPE, TEXT_UNDERLINE_UV, TOGGLE_Z, UNSELECTED_TOGGLE_OFF_UV, UNSELECTED_TOGGLE_ON_UV, UNSELECTED_WIDGET_UV}; +use crate::bookmark::{Bookmark, BookmarkSlice}; use crate::color::TextColor; use crate::elements::compound::{CompoundMap}; use crate::elements::element::{NbtByteArray, NbtIntArray, NbtLongArray}; @@ -94,6 +96,8 @@ mod search_box; mod text; #[cfg(not(target_arch = "wasm32"))] mod cli; +mod formatter; +mod bookmark; #[macro_export] macro_rules! flags { @@ -272,7 +276,6 @@ pub fn main() -> ! { /// # Minor Features /// * open icon for exe ver /// * gear icon to swap toolbar with settings panel -/// * make floats either exact or "exact enough" /// * __ctrl + h__, open a playground `nbt` file to help with user interaction (bonus points if I have some way to tell if you haven't used this editor before) /// * [`last_modified`](NbtChunk) field actually gets some impl /// * autosave @@ -482,31 +485,19 @@ pub fn recache_along_indices(indices: &[usize], root: &mut NbtElement) { #[inline] #[must_use] -pub fn encompasses_or_equal(outer: &[usize], inner: &[usize]) -> bool { - let outer_len = outer.len(); - let inner_len = inner.len(); - if outer_len <= inner_len { - outer == &inner[..outer_len] - } else { - false - } +pub fn encompasses_or_equal(outer: &[T], inner: &[T]) -> bool { + outer.len() <= inner.len() && outer == &inner[..outer.len()] } #[inline] #[must_use] -pub fn encompasses(outer: &[usize], inner: &[usize]) -> bool { - let outer_len = outer.len(); - let inner_len = inner.len(); - if outer_len < inner_len { - outer == &inner[..outer_len] - } else { - false - } +pub fn encompasses(outer: &[T], inner: &[T]) -> bool { + outer.len() < inner.len() && outer == &inner[..outer.len()] } #[inline] #[must_use] -pub fn either_encompass(a: &[usize], b: &[usize]) -> bool { +pub fn either_encompass(a: &[T], b: &[T]) -> bool { let min = usize::min(a.len(), b.len()); a[..min] == b[..min] } @@ -583,14 +574,14 @@ impl SortAlgorithm { Self::Type => SORT_COMPOUND_BY_TYPE, }; - let widget_uv = if (264..280).contains(&ctx.mouse_x) && (26..42).contains(&ctx.mouse_y) { + let widget_uv = if (280..296).contains(&ctx.mouse_x) && (26..42).contains(&ctx.mouse_y) { builder.draw_tooltip(&[&format!("Compound Sorting Algorithm ({self})")], (ctx.mouse_x, ctx.mouse_y), false); HOVERED_WIDGET_UV } else { UNSELECTED_WIDGET_UV }; - builder.draw_texture((264, 26), widget_uv, (16, 16)); - builder.draw_texture((267, 29), uv, (10, 10)); + builder.draw_texture((280, 26), widget_uv, (16, 16)); + builder.draw_texture((283, 29), uv, (10, 10)); } @@ -802,10 +793,10 @@ impl RenderContext { } #[inline] - pub fn render_line_numbers(&self, builder: &mut VertexBufferBuilder, mut bookmarks: &[Bookmark]) { + pub fn render_line_numbers(&self, builder: &mut VertexBufferBuilder, mut bookmarks: &BookmarkSlice) { let start = self.line_numbers.first(); - while let Some((&first, rest)) = bookmarks.split_first() { - if start.is_some_and(|&start| start > first.true_line_number) { + while let Some((head, rest)) = bookmarks.split_first() { + if start.is_some_and(|&start| start > head.true_line_number()) { bookmarks = rest; } else { break; @@ -840,7 +831,7 @@ impl RenderContext { let _ = write!(builder, "{render_line_number}"); builder.color = color; - if let Some((&first, rest)) = bookmarks.split_first() && render_line_number == first.true_line_number { + if let Some((first, rest)) = bookmarks.split_first() && render_line_number == first.true_line_number() { bookmarks = rest; builder.draw_texture_region_z( (2, y + 2), @@ -851,7 +842,7 @@ impl RenderContext { ); } let mut hidden_bookmarks = 0_usize; - while let Some((&first, rest)) = bookmarks.split_first() && next_line_number.is_none_or(|next_line_number| render_line_number <= first.true_line_number && first.true_line_number < next_line_number) { + while let Some((first, rest)) = bookmarks.split_first() && next_line_number.is_none_or(|next_line_number| render_line_number <= first.true_line_number() && first.true_line_number() < next_line_number) { bookmarks = rest; if hidden_bookmarks < 5 { builder.draw_texture_region_z( @@ -898,15 +889,15 @@ impl RenderContext { } #[inline] - pub fn render_scrollbar_bookmarks(&self, builder: &mut VertexBufferBuilder, bookmarks: &[Bookmark], root: &NbtElement) { + pub fn render_scrollbar_bookmarks(&self, builder: &mut VertexBufferBuilder, bookmarks: &BookmarkSlice, root: &NbtElement) { let height = root.height(); let mut hidden_bookmarks_at_y = 0_usize; let mut hidden_bookmark_y = 0; let mut bookmarks_at_y = 0_usize; let mut bookmark_y = 0; - for bookmark in bookmarks { - let y = HEADER_SIZE + (bookmark.line_number * (builder.window_height() - HEADER_SIZE)) / height; - if bookmark.uv == BOOKMARK_UV { + for bookmark in bookmarks.iter() { + let y = HEADER_SIZE + (bookmark.line_number() * (builder.window_height() - HEADER_SIZE)) / height; + if bookmark.uv() == BOOKMARK_UV { if bookmarks_at_y < 5 { builder.draw_texture_z( (builder.window_width() - 8, y), @@ -1046,36 +1037,6 @@ pub struct SinglyLinkedNode { prev: Option>>, } -#[derive(Copy, Clone, Debug)] -pub struct Bookmark { - true_line_number: usize, - line_number: usize, - uv: Vec2u, -} - -impl Bookmark { - pub fn new(true_line_number: usize, line_number: usize) -> Self { - Self { - true_line_number, - line_number, - uv: BOOKMARK_UV, - } - } -} - -impl PartialEq for Bookmark { - fn eq(&self, other: &Self) -> bool { self.true_line_number.eq(&other.true_line_number) } -} - -impl Eq for Bookmark {} - -impl PartialOrd for Bookmark { - fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } -} -impl Ord for Bookmark { - fn cmp(&self, other: &Self) -> Ordering { self.true_line_number.cmp(&other.true_line_number) } -} - pub fn smoothstep64(x: f64) -> f64 { let x = x.clamp(0.0, 1.0); 3.0 * x * x - 2.0 * x * x * x diff --git a/src/search_box.rs b/src/search_box.rs index 0b23d2c..8fca03a 100644 --- a/src/search_box.rs +++ b/src/search_box.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use std::ops::{Deref, DerefMut}; use std::time::Duration; use compact_str::CompactString; @@ -5,26 +6,77 @@ use regex::Regex; use winit::event::MouseButton; use winit::keyboard::KeyCode; -use crate::assets::{ADD_SEARCH_BOOKMARKS, BASE_Z, BOOKMARK_UV, DARK_STRIPE_UV, HIDDEN_BOOKMARK_UV, HOVERED_WIDGET_UV, REGEX_SEARCH_MODE, REMOVE_SEARCH_BOOKMARKS, SEARCH_KEYS, SEARCH_KEYS_AND_VALUES, SEARCH_VALUES, SNBT_SEARCH_MODE, STRING_SEARCH_MODE, UNSELECTED_WIDGET_UV}; +use crate::assets::{ADD_SEARCH_BOOKMARKS_UV, BASE_Z, BOOKMARK_UV, DARK_STRIPE_UV, HIDDEN_BOOKMARK_UV, HOVERED_WIDGET_UV, REGEX_SEARCH_MODE_UV, REMOVE_SEARCH_BOOKMARKS_UV, SEARCH_KEYS_UV, SEARCH_KEYS_AND_VALUES_UV, SEARCH_VALUES_UV, SNBT_SEARCH_MODE_UV, STRING_SEARCH_MODE_UV, UNSELECTED_WIDGET_UV}; use crate::color::TextColor; -use crate::{Bookmark, combined_two_sorted, create_regex, flags, since_epoch, SortAlgorithm, StrExt}; +use crate::{combined_two_sorted, create_regex, flags, since_epoch, SortAlgorithm, StrExt}; use crate::elements::element::NbtElement; use crate::text::{Cachelike, SearchBoxKeyResult, Text}; use crate::vertex_buffer_builder::{Vec2u, VertexBufferBuilder}; +use crate::bookmark::{Bookmark, Bookmarks}; pub struct SearchPredicate { pub search_flags: u8, pub inner: SearchPredicateInner, } -#[derive(Debug)] pub enum SearchPredicateInner { String(String), Regex(Regex), Snbt(Option, NbtElement), } +#[derive(Copy, Clone)] +pub enum SearchMode { + String, + Regex, + Snbt, +} + +impl Display for SearchMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + Self::String => "String", + Self::Regex => "Regex", + Self::Snbt => "SNBT", + }) + } +} + +impl SearchMode { + pub fn cycle(self) -> Self { + match self { + Self::String => Self::Regex, + Self::Regex => Self::Snbt, + Self::Snbt => Self::String, + } + } + + pub fn rev_cycle(self) -> Self { + match self { + Self::String => Self::Snbt, + Self::Regex => Self::String, + Self::Snbt => Self::Regex, + } + } + + pub fn uv(self) -> Vec2u { + match self { + Self::String => STRING_SEARCH_MODE_UV, + Self::Regex => REGEX_SEARCH_MODE_UV, + Self::Snbt => SNBT_SEARCH_MODE_UV + } + } + + pub fn into_predicate(self, value: String, search_flags: u8) -> Option { + Some(match self { + Self::String => SearchPredicate { inner: SearchPredicateInner::String(value), search_flags }, + Self::Regex => if let Some(regex) = create_regex(value) { SearchPredicate { inner: SearchPredicateInner::Regex(regex), search_flags } } else { return None }, + Self::Snbt => if let Some((key, value)) = NbtElement::from_str(&value, SortAlgorithm::None) { SearchPredicate { inner: SearchPredicateInner::Snbt(key.map(CompactString::into_string), value), search_flags } } else { return None }, + }) + } +} + impl SearchPredicate { fn matches(&self, key: Option<&str>, value: &NbtElement) -> bool { match &self.inner { @@ -82,9 +134,9 @@ impl Cachelike for SearchBoxCache { pub struct SearchBoxAdditional { selected: bool, horizontal_scroll: usize, - hits: Option<(usize, Duration)>, - flags: u8, - mode: u8, + pub hits: Option<(usize, Duration)>, + pub flags: u8, + pub mode: SearchMode, } pub struct SearchBox(Text); @@ -105,7 +157,7 @@ impl DerefMut for SearchBox { impl SearchBox { pub fn new() -> Self { - Self(Text::new(String::new(), 0, true, SearchBoxAdditional { selected: false, horizontal_scroll: 0, hits: None, flags: 0b01, mode: 0 })) + Self(Text::new(String::new(), 0, true, SearchBoxAdditional { selected: false, horizontal_scroll: 0, hits: None, flags: 0b01, mode: SearchMode::String })) } pub const fn uninit() -> Self { @@ -115,7 +167,7 @@ impl SearchBox { pub fn render(&self, builder: &mut VertexBufferBuilder, shift: bool, mouse: (usize, usize)) { use std::fmt::Write; - let pos = Vec2u::new(284, 23); + let pos = Vec2u::new(316, 23); let (mouse_x, mouse_y) = mouse; builder.draw_texture_region_z( @@ -126,14 +178,18 @@ impl SearchBox { (16, 16), ); - let hover = (pos.x..builder.window_width() - 215 - 17 - 16 - 16).contains(&mouse_x) && (23..45).contains(&mouse_y); + let hover = (pos.x..builder.window_width() - 215 - 17 - 16 - 16).contains(&mouse_x) && (23..46).contains(&mouse_y); builder.horizontal_scroll = self.horizontal_scroll; if self.value.is_empty() { builder.settings(pos + (0, 3), false, 0); builder.color = TextColor::Gray.to_raw(); - let _ = write!(builder, "Search..."); + let _ = write!(builder, "{}", match self.mode { + SearchMode::String => r#"Search..."#, + SearchMode::Regex => r#"/[Ss]earch\.*/g"#, + SearchMode::Snbt => r#"{dialog: "search", ...}"#, + }); } if self.is_selected() { self.0.render(builder, TextColor::White, pos + (0, 3), 0); @@ -144,13 +200,13 @@ impl SearchBox { } if let Some((hits, stat)) = self.hits && (self.is_selected() || hover) { - builder.draw_tooltip(&[&format!("{hits} hit{s} for \"{arg}\" ({ms}ms)", s = if hits == 1 { "" } else { "s" }, arg = self.value, ms = stat.as_millis())], if !self.is_selected() && hover { mouse } else { (284, 30) }, true); + builder.draw_tooltip(&[&format!("{hits} hit{s} for \"{arg}\" ({ms}ms)", s = if hits == 1 { "" } else { "s" }, arg = self.value, ms = stat.as_millis())], if !self.is_selected() && hover { mouse } else { (316, 30) }, true); } builder.horizontal_scroll = 0; { - let bookmark_uv = if shift { REMOVE_SEARCH_BOOKMARKS } else { ADD_SEARCH_BOOKMARKS }; + let bookmark_uv = if shift { REMOVE_SEARCH_BOOKMARKS_UV } else { ADD_SEARCH_BOOKMARKS_UV }; let widget_uv = if (builder.window_width() - 215 - 17 - 16 - 16..builder.window_width() - 215 - 1 - 16 - 16).contains(&mouse_x) && (26..42).contains(&mouse_y) { builder.draw_tooltip(&[if shift { "Remove all bookmarks (Shift + Enter)" } else { "Add search bookmarks (Enter)" }], mouse, false); HOVERED_WIDGET_UV @@ -163,7 +219,7 @@ impl SearchBox { } { - let search_uv = match self.flags { 0b01 => SEARCH_VALUES, 0b10 => SEARCH_KEYS, _ => SEARCH_KEYS_AND_VALUES }; + let search_uv = match self.flags { 0b01 => SEARCH_VALUES_UV, 0b10 => SEARCH_KEYS_UV, _ => SEARCH_KEYS_AND_VALUES_UV }; let widget_uv = if (builder.window_width() - 215 - 17 - 16..builder.window_width() - 215 - 1 - 16).contains(&mouse_x) && (26..42).contains(&mouse_y) { builder.draw_tooltip(&[match self.flags { 0b01 => "Values only", 0b10 => "Keys only", _ => "Keys + Values" }], mouse, false); HOVERED_WIDGET_UV @@ -176,9 +232,9 @@ impl SearchBox { } { - let mode_uv = match self.mode { 0 => STRING_SEARCH_MODE, 1 => REGEX_SEARCH_MODE, _ => SNBT_SEARCH_MODE }; + let mode_uv = self.mode.uv(); let widget_uv = if (builder.window_width() - 215 - 17..builder.window_width() - 215 - 1).contains(&mouse_x) && (26..42).contains(&mouse_y) { - builder.draw_tooltip(&[match self.mode { 0 => "String Mode", 1 => "Regex Mode", _ => "SNBT Mode" }], mouse, false); + builder.draw_tooltip(&[&format!("{mode} Mode", mode = self.mode)], mouse, false); HOVERED_WIDGET_UV } else { UNSELECTED_WIDGET_UV @@ -224,7 +280,7 @@ impl SearchBox { } #[inline] - pub fn on_bookmark_widget(&mut self, shift: bool, bookmarks: &mut Vec, root: &mut NbtElement) { + pub fn on_bookmark_widget(&mut self, shift: bool, bookmarks: &mut Bookmarks, root: &mut NbtElement) { if shift { bookmarks.clear(); } else { @@ -239,30 +295,26 @@ impl SearchBox { #[inline] pub fn on_mode_widget(&mut self, shift: bool) { - self.mode = (self.mode as i8).wrapping_add((!shift) as i8 * 2 - 1).rem_euclid(3) as u8; + self.mode = if shift { self.mode.rev_cycle() } else { self.mode.cycle() }; } #[inline] - pub fn search(&mut self, bookmarks: &mut Vec, root: &NbtElement, count_only: bool) { + pub fn search(&mut self, bookmarks: &mut Bookmarks, root: &NbtElement, count_only: bool) { if self.value.is_empty() { return; } - let predicate = match self.mode { - 0 => SearchPredicate { inner: SearchPredicateInner::String(self.value.clone()), search_flags: self.flags }, - 1 => if let Some(regex) = create_regex(self.value.clone()) { SearchPredicate { inner: SearchPredicateInner::Regex(regex), search_flags: self.flags } } else { return }, - _ => if let Some((key, value)) = NbtElement::from_str(&self.value, SortAlgorithm::None) { SearchPredicate { inner: SearchPredicateInner::Snbt(key.map(CompactString::into_string), value), search_flags: self.flags } } else { return }, - }; + let Some(predicate) = self.mode.into_predicate(self.value.clone(), self.flags) else { return }; let start = since_epoch(); let new_bookmarks = Self::search0(root, &predicate); self.hits = Some((new_bookmarks.len(), since_epoch() - start)); if !count_only { - let old_bookmarks = core::mem::replace(bookmarks, vec![]); - *bookmarks = combined_two_sorted(new_bookmarks.into_boxed_slice(), old_bookmarks.into_boxed_slice()); + let old_bookmarks = core::mem::replace(bookmarks, Bookmarks::new()); + *bookmarks = unsafe { Bookmarks::from_raw(combined_two_sorted(new_bookmarks.into_raw(), old_bookmarks.into_raw())) }; } } - pub fn search0(root: &NbtElement, predicate: &SearchPredicate) -> Vec { + pub fn search0(root: &NbtElement, predicate: &SearchPredicate) -> Bookmarks { let mut new_bookmarks = Vec::new(); let mut queue = Vec::new(); queue.push((None, &*root, true)); @@ -270,9 +322,7 @@ impl SearchBox { let mut line_number = 0; while let Some((key, element, parent_open)) = queue.pop() { if predicate.matches(key, element) { - let mut bookmark = Bookmark::new(true_line_number, line_number); - bookmark.uv = if parent_open { BOOKMARK_UV } else { HIDDEN_BOOKMARK_UV }; - new_bookmarks.push(bookmark); + new_bookmarks.push(Bookmark::with_uv(true_line_number, line_number, if parent_open { BOOKMARK_UV } else { HIDDEN_BOOKMARK_UV })); } match element.children() { @@ -290,7 +340,7 @@ impl SearchBox { line_number += 1; } } - new_bookmarks + unsafe { Bookmarks::from_raw(new_bookmarks) } } #[inline] @@ -303,7 +353,7 @@ impl SearchBox { pub fn post_input(&mut self, window_dims: (usize, usize)) { let (window_width, _) = window_dims; self.0.post_input(); - let field_width = window_width - 215 - 284 - 17 - 16 - 16; + let field_width = window_width - 215 - 316 - 17 - 16 - 16; let precursor_width = self.value.split_at(self.cursor).0.width(); // 8px space just to look cleaner let horizontal_scroll = (precursor_width + 8).saturating_sub(field_width); diff --git a/src/selected_text.rs b/src/selected_text.rs index b49cd6e..a2bddcc 100644 --- a/src/selected_text.rs +++ b/src/selected_text.rs @@ -4,7 +4,7 @@ use std::ops::{Deref, DerefMut}; use winit::keyboard::KeyCode; use crate::{flags, OptionExt, StrExt}; -use crate::assets::{BASE_TEXT_Z, ELEMENT_HIGHLIGHT_Z, HEADER_SIZE, SELECTED_TEXT_Z, SELECTION_UV}; +use crate::assets::{BASE_TEXT_Z, HEADER_SIZE, SELECTED_TEXT_Z, SELECTION_UV}; use crate::color::TextColor; use crate::selected_text::SelectedTextKeyResult::{Down, ForceClose, ForceOpen, Keyfix, ShiftDown, ShiftUp, Up, Valuefix}; use crate::text::{Cachelike, SelectedTextKeyResult, Text}; @@ -315,7 +315,7 @@ impl SelectedText { let prefix_width = self.prefix.0.as_str().width() + self.keyfix.as_ref().map_or(0, |x| x.0.width()); self.0.render(builder, self.value_color, (x + prefix_width, y).into(), SELECTED_TEXT_Z); - builder.draw_texture_z((x - 4 - 16, y), ELEMENT_HIGHLIGHT_Z, SELECTION_UV, (16, 16)); + builder.draw_texture_z((x - 4 - 16, y), SELECTED_TEXT_Z, SELECTION_UV, (16, 16)); builder.settings((x, y), false, BASE_TEXT_Z); if let Some((keyfix, keyfix_color)) = self.keyfix.as_ref() { builder.color = keyfix_color.to_raw(); diff --git a/src/tab.rs b/src/tab.rs index 9454a88..e632124 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -8,15 +8,17 @@ use anyhow::{anyhow, Context, Result}; use compact_str::{CompactString, ToCompactString}; use flate2::Compression; use uuid::Uuid; +use zune_inflate::DeflateDecoder; -use crate::{Bookmark, LinkedQueue, OptionExt, panic_unchecked, RenderContext, StrExt, WindowProperties}; -use crate::assets::{BASE_Z, BYTE_ARRAY_GHOST_UV, BYTE_ARRAY_UV, BYTE_GRAYSCALE_UV, BYTE_UV, CHUNK_GHOST_UV, CHUNK_UV, COMPOUND_GHOST_UV, COMPOUND_ROOT_UV, COMPOUND_UV, DOUBLE_GRAYSCALE_UV, DOUBLE_UV, ENABLED_FREEHAND_MODE_UV, FLOAT_GRAYSCALE_UV, FLOAT_UV, FREEHAND_MODE_UV, GZIP_FILE_TYPE_UV, HEADER_SIZE, HELD_SCROLLBAR_UV, INT_ARRAY_GHOST_UV, INT_ARRAY_UV, INT_GRAYSCALE_UV, INT_UV, JUST_OVERLAPPING_BASE_Z, LINE_NUMBER_SEPARATOR_UV, LIST_GHOST_UV, LIST_UV, LONG_ARRAY_GHOST_UV, LONG_ARRAY_UV, LONG_GRAYSCALE_UV, LONG_UV, MCA_FILE_TYPE_UV, NBT_FILE_TYPE_UV, REDO_UV, REGION_UV, SCROLLBAR_Z, SHORT_GRAYSCALE_UV, SHORT_UV, SNBT_FILE_TYPE_UV, STEAL_ANIMATION_OVERLAY_UV, STRING_GHOST_UV, STRING_UV, UNDO_UV, UNHELD_SCROLLBAR_UV, UNKNOWN_NBT_GHOST_UV, UNKNOWN_NBT_UV, ZLIB_FILE_TYPE_UV}; +use crate::{LinkedQueue, OptionExt, panic_unchecked, RenderContext, SortAlgorithm, StrExt, WindowProperties}; +use crate::assets::{BASE_Z, BYTE_ARRAY_GHOST_UV, BYTE_ARRAY_UV, BYTE_GRAYSCALE_UV, BYTE_UV, CHUNK_GHOST_UV, CHUNK_UV, COMPOUND_GHOST_UV, COMPOUND_ROOT_UV, COMPOUND_UV, DISABLED_REFRESH_UV, DOUBLE_GRAYSCALE_UV, DOUBLE_UV, ENABLED_FREEHAND_MODE_UV, FLOAT_GRAYSCALE_UV, FLOAT_UV, FREEHAND_MODE_UV, GZIP_FILE_TYPE_UV, HEADER_SIZE, HELD_SCROLLBAR_UV, HOVERED_WIDGET_UV, INT_ARRAY_GHOST_UV, INT_ARRAY_UV, INT_GRAYSCALE_UV, INT_UV, JUST_OVERLAPPING_BASE_Z, LINE_NUMBER_SEPARATOR_UV, LIST_GHOST_UV, LIST_UV, LONG_ARRAY_GHOST_UV, LONG_ARRAY_UV, LONG_GRAYSCALE_UV, LONG_UV, MCA_FILE_TYPE_UV, NBT_FILE_TYPE_UV, REDO_UV, REFRESH_UV, REGION_UV, SCROLLBAR_Z, SHORT_GRAYSCALE_UV, SHORT_UV, SNBT_FILE_TYPE_UV, STEAL_ANIMATION_OVERLAY_UV, STRING_GHOST_UV, STRING_UV, UNDO_UV, UNHELD_SCROLLBAR_UV, UNKNOWN_NBT_GHOST_UV, UNKNOWN_NBT_UV, UNSELECTED_WIDGET_UV, ZLIB_FILE_TYPE_UV}; use crate::color::TextColor; use crate::elements::chunk::NbtRegion; use crate::elements::compound::NbtCompound; use crate::elements::element::NbtElement; use crate::selected_text::{SelectedText, SelectedTextAdditional}; use crate::text::Text; +use crate::bookmark::Bookmarks; use crate::tree_travel::Navigate; use crate::vertex_buffer_builder::{Vec2u, VertexBufferBuilder}; use crate::workbench_action::WorkbenchAction; @@ -33,11 +35,12 @@ pub struct Tab { pub horizontal_scroll: usize, pub window_height: usize, pub window_width: usize, - pub bookmarks: Vec, // must be ordered least to greatest + pub bookmarks: Bookmarks, pub uuid: Uuid, pub freehand_mode: bool, pub selected_text: Option, pub last_close_attempt: Duration, + pub last_selected_text_interaction: (usize, usize, Duration), } impl Tab { @@ -57,11 +60,12 @@ impl Tab { horizontal_scroll: 0, window_height, window_width, - bookmarks: vec![], + bookmarks: Bookmarks::new(), uuid: Uuid::new_v4(), freehand_mode: false, selected_text: None, last_close_attempt: Duration::ZERO, + last_selected_text_interaction: (0, 0, Duration::ZERO) }) } @@ -200,14 +204,14 @@ impl Tab { { // shifted one left to center between clipboard and freehand builder.draw_texture_region_z( - (244, 22), + (260, 22), BASE_Z, LINE_NUMBER_SEPARATOR_UV, (2, 23), (2, 16), ); let freehand_uv = { - let hovering = (248..264).contains(&mouse_x) && (26..42).contains(&mouse_y); + let hovering = (264..280).contains(&mouse_x) && (26..42).contains(&mouse_y); if hovering { builder.draw_tooltip(&["Freehand Mode (Ctrl + Shift + F)"], (mouse_x, mouse_y), false); } @@ -222,28 +226,41 @@ impl Tab { } } }; - builder.draw_texture((248, 26), freehand_uv, (16, 16)); + builder.draw_texture((264, 26), freehand_uv, (16, 16)); } { - let mx = if (24..46).contains(&mouse_y) && mouse_x >= 16 + 4 { - Some((mouse_x - (16 + 4)) & !15) + let enabled = self.path.is_some(); + let widget_uv = if enabled && (296..312).contains(&ctx.mouse_x) && (26..42).contains(&ctx.mouse_y) { + builder.draw_tooltip(&["Refresh Tab (Ctrl + R)"], (ctx.mouse_x, ctx.mouse_y), false); + HOVERED_WIDGET_UV + } else { + UNSELECTED_WIDGET_UV + }; + + builder.draw_texture((296, 26), widget_uv, (16, 16)); + builder.draw_texture((296, 26), if enabled { REFRESH_UV } else { DISABLED_REFRESH_UV }, (16, 16)); + } + + { + let mx = if (24..46).contains(&mouse_y) && mouse_x >= 16 + 16 + 4 { + Some((mouse_x - (16 + 16 + 4)) & !15) } else { None }; for (idx, (selected, unselected, name)) in [ - (BYTE_UV, BYTE_GRAYSCALE_UV, "Byte (Alt + 1)"), - (SHORT_UV, SHORT_GRAYSCALE_UV, "Short (Alt + 2)"), - (INT_UV, INT_GRAYSCALE_UV, "Int (Alt + 3)"), - (LONG_UV, LONG_GRAYSCALE_UV, "Long (Alt + 4)"), - (FLOAT_UV, FLOAT_GRAYSCALE_UV, "Float (Alt + 5)"), - (DOUBLE_UV, DOUBLE_GRAYSCALE_UV, "Double (Alt + 6)"), - (BYTE_ARRAY_UV, BYTE_ARRAY_GHOST_UV, "Byte Array (Alt + 7)"), - (INT_ARRAY_UV, INT_ARRAY_GHOST_UV, "Int Array (Alt + 8)"), - (LONG_ARRAY_UV, LONG_ARRAY_GHOST_UV, "Long Array (Alt + 9)"), - (STRING_UV, STRING_GHOST_UV, "String (Alt + 0)"), - (LIST_UV, LIST_GHOST_UV, "List (Alt + -)"), - (COMPOUND_UV, COMPOUND_GHOST_UV, "Compound (Alt + +)"), + (BYTE_UV, BYTE_GRAYSCALE_UV, "Byte (1)"), + (SHORT_UV, SHORT_GRAYSCALE_UV, "Short (2)"), + (INT_UV, INT_GRAYSCALE_UV, "Int (3)"), + (LONG_UV, LONG_GRAYSCALE_UV, "Long (4)"), + (FLOAT_UV, FLOAT_GRAYSCALE_UV, "Float (5)"), + (DOUBLE_UV, DOUBLE_GRAYSCALE_UV, "Double (6)"), + (BYTE_ARRAY_UV, BYTE_ARRAY_GHOST_UV, "Byte Array (7)"), + (INT_ARRAY_UV, INT_ARRAY_GHOST_UV, "Int Array (8)"), + (LONG_ARRAY_UV, LONG_ARRAY_GHOST_UV, "Long Array (9)"), + (STRING_UV, STRING_GHOST_UV, "String (0)"), + (LIST_UV, LIST_GHOST_UV, "List (-)"), + (COMPOUND_UV, COMPOUND_GHOST_UV, "Compound (=)"), ] .into_iter() .enumerate() @@ -255,27 +272,27 @@ impl Tab { unselected }; - builder.draw_texture((idx * 16 + 16 + 4, 26), uv, (16, 16)); + builder.draw_texture((idx * 16 + 16 + 16 + 4, 26), uv, (16, 16)); } { let uv = if mx == Some(192) && self.value.id() == NbtRegion::ID && !skip_tooltips { - builder.draw_tooltip(&["Chunk"], (mouse_x, mouse_y), false); + builder.draw_tooltip(&["Chunk (`)"], (mouse_x, mouse_y), false); CHUNK_UV } else { CHUNK_GHOST_UV }; - builder.draw_texture((192 + 16 + 4, 26), uv, (16, 16)); + builder.draw_texture((192 + 16 + 16 + 4, 26), uv, (16, 16)); } { let uv = if mx == Some(208) && !skip_tooltips { - builder.draw_tooltip(&["Clipboard"], (mouse_x, mouse_y), false); + builder.draw_tooltip(&["Clipboard (C)"], (mouse_x, mouse_y), false); UNKNOWN_NBT_UV } else { UNKNOWN_NBT_GHOST_UV }; - builder.draw_texture((208 + 16 + 4, 26), uv, (16, 16)); + builder.draw_texture((208 + 16 + 16 + 4, 26), uv, (16, 16)); } } @@ -508,6 +525,9 @@ impl Tab { }); self.selected_text = None; } else { + if self.path.as_ref().map(|path| path.as_os_str().to_string_lossy()).as_deref().unwrap_or(&self.name) == value { + return true; + } let buf = PathBuf::from(value); return if let Some(name) = buf .file_name() @@ -539,6 +559,79 @@ impl Tab { } true } + + #[inline] + pub fn parse_raw(path: &Path, buf: Vec, sort_algorithm: SortAlgorithm) -> Result<(NbtElement, FileFormat)> { + Ok(if path.extension().and_then(OsStr::to_str) == Some("mca") { + ( + NbtElement::from_mca(buf.as_slice(), sort_algorithm).context("Failed to parse MCA file")?, + FileFormat::Mca, + ) + } else if let Some(0x1F8B) = buf.first_chunk::<2>().copied().map(u16::from_be_bytes) { + ( + NbtElement::from_file( + &DeflateDecoder::new(buf.as_slice()) + .decode_gzip() + .context("Failed to decode gzip compressed NBT")?, + sort_algorithm, + ) + .context("Failed to parse NBT")?, + FileFormat::Gzip, + ) + } else if let Some(0x7801 | 0x789C | 0x78DA) = buf.first_chunk::<2>().copied().map(u16::from_be_bytes) { + ( + NbtElement::from_file( + &DeflateDecoder::new(buf.as_slice()) + .decode_zlib() + .context("Failed to decode zlib compressed NBT")?, + sort_algorithm, + ) + .context("Failed to parse NBT")?, + FileFormat::Zlib, + ) + } else if let Some(nbt) = NbtElement::from_file(buf.as_slice(), sort_algorithm) { + (nbt, FileFormat::Nbt) + } else { + ( + core::str::from_utf8(&buf) + .ok() + .and_then(|s| NbtElement::from_str(s, sort_algorithm)) + .context(anyhow!( + "Failed to find file type for file {}", + path.file_name() + .unwrap_or(&OsStr::new("")) + .to_string_lossy() + ))? + .1, + FileFormat::Snbt, + ) + }) + } + + #[cfg(not(target_os = "wasm32"))] + pub fn refresh(&mut self, sort_algorithm: SortAlgorithm) -> Result<()> { + let Some(path) = self.path.as_deref() else { return Err(anyhow!("File path was not present in tab")) }; + let bytes = std::fs::read(path)?; + let (value, format) = Tab::parse_raw(path, bytes, sort_algorithm)?; + + self.bookmarks.clear(); + self.scroll = 0; + self.compression = format; + self.history_changed = false; + self.undos.clear(); + self.redos.clear(); + self.uuid = Uuid::new_v4(); + self.selected_text = None; + let old = core::mem::replace(&mut self.value, Box::new(value)); + std::thread::Builder::new().stack_size(50_331_648 /*48MiB*/).spawn(move || drop(old)).expect("Failed to spawn thread"); + + Ok(()) + } + + #[cfg(target_os = "wasm32")] + pub fn refresh(&mut self) -> Result<()> { + Err(anyhow!("File refresh not supported on web")) + } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/src/window.rs b/src/window.rs index bdc5982..26286a3 100644 --- a/src/window.rs +++ b/src/window.rs @@ -29,7 +29,7 @@ use crate::{assets, WORKBENCH, WINDOW_PROPERTIES, error, OptionExt, since_epoch, pub const WINDOW_HEIGHT: usize = 420; pub const WINDOW_WIDTH: usize = 720; pub const MIN_WINDOW_HEIGHT: usize = HEADER_SIZE + 16; -pub const MIN_WINDOW_WIDTH: usize = 620; +pub const MIN_WINDOW_WIDTH: usize = 720; pub async fn run() -> ! { let event_loop = EventLoop::new().expect("Event loop was unconstructable"); @@ -49,9 +49,7 @@ pub async fn run() -> ! { .expect("valid format"), )); #[cfg(target_os = "windows")] { - builder = builder - .with_drag_and_drop(true) - .with_transparent(std::env::args().any(|x| x.eq("--transparent"))); + builder = builder.with_drag_and_drop(true) } let window = Rc::new(builder.build(&event_loop).expect("Window was constructable")); #[cfg(target_arch = "wasm32")] @@ -124,7 +122,6 @@ pub struct State<'window> { diffuse_bind_group: BindGroup, text_render_pipeline: RenderPipeline, unicode_bind_group: BindGroup, - load_op: LoadOp, last_tick: Duration, } @@ -418,16 +415,6 @@ impl<'window> State<'window> { }, multiview: None, }); - let load_op = if std::env::args().any(|x| x.eq("--transparent")) { - LoadOp::Load - } else { - LoadOp::Clear(Color { - r: 0.11774103726, - g: 0.11774103726, - b: 0.11774103726, - a: 1.0, - }) - }; Self { surface, @@ -439,7 +426,6 @@ impl<'window> State<'window> { diffuse_bind_group, text_render_pipeline, unicode_bind_group, - load_op, last_tick: Duration::ZERO, } } @@ -558,7 +544,12 @@ impl<'window> State<'window> { view: &view, resolve_target: None, ops: Operations { - load: self.load_op, + load: LoadOp::Clear(Color { + r: 0.11774103726, + g: 0.11774103726, + b: 0.11774103726, + a: 1.0, + }), store: StoreOp::Store, }, })], diff --git a/src/workbench.rs b/src/workbench.rs index 3d2bbd8..a23b701 100644 --- a/src/workbench.rs +++ b/src/workbench.rs @@ -1,5 +1,3 @@ -use std::convert::identity; -use std::ffi::OsStr; use std::fmt::Write; use std::fs::read; use std::path::{Path, PathBuf}; @@ -7,18 +5,18 @@ use std::str::FromStr; use std::sync::mpsc::TryRecvError; use std::time::Duration; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use compact_str::{CompactString, format_compact, ToCompactString}; use fxhash::{FxBuildHasher, FxHashSet}; use uuid::Uuid; use winit::dpi::PhysicalPosition; use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta}; use winit::keyboard::{KeyCode, PhysicalKey}; -use zune_inflate::DeflateDecoder; use crate::{Bookmark, DropFn, encompasses, encompasses_or_equal, FileUpdateSubscription, FileUpdateSubscriptionType, flags, get_clipboard, HeldEntry, LinkedQueue, OptionExt, panic_unchecked, Position, recache_along_indices, RenderContext, set_clipboard, since_epoch, SortAlgorithm, StrExt, sum_indices, tab, tab_mut, WindowProperties}; use crate::alert::Alert; -use crate::assets::{ACTION_WHEEL_Z, BACKDROP_UV, BASE_TEXT_Z, BASE_Z, BOOKMARK_UV, CLOSED_WIDGET_UV, DARK_STRIPE_UV, EDITED_UV, HEADER_SIZE, HELD_ENTRY_Z, HIDDEN_BOOKMARK_UV, HORIZONTAL_SEPARATOR_UV, HOVERED_STRIPE_UV, HOVERED_WIDGET_UV, JUST_OVERLAPPING_BASE_TEXT_Z, LIGHT_STRIPE_UV, LINE_NUMBER_SEPARATOR_UV, OPEN_FOLDER_UV, SELECTED_ACTION_WHEEL, SELECTED_WIDGET_UV, SELECTION_UV, TRAY_UV, UNEDITED_UV, UNSELECTED_ACTION_WHEEL, UNSELECTED_WIDGET_UV}; +use crate::assets::{ACTION_WHEEL_Z, BACKDROP_UV, BASE_TEXT_Z, BASE_Z, BOOKMARK_UV, CLOSED_WIDGET_UV, DARK_STRIPE_UV, EDITED_UV, HEADER_SIZE, HELD_ENTRY_Z, HIDDEN_BOOKMARK_UV, HORIZONTAL_SEPARATOR_UV, HOVERED_STRIPE_UV, HOVERED_WIDGET_UV, JUST_OVERLAPPING_BASE_TEXT_Z, LIGHT_STRIPE_UV, LINE_NUMBER_SEPARATOR_UV, NEW_FILE_UV, OPEN_FOLDER_UV, SELECTED_ACTION_WHEEL, SELECTED_WIDGET_UV, SELECTION_UV, TRAY_UV, UNEDITED_UV, UNSELECTED_ACTION_WHEEL, UNSELECTED_WIDGET_UV}; +use crate::bookmark::Bookmarks; use crate::color::TextColor; use crate::elements::chunk::{NbtChunk, NbtRegion}; use crate::elements::compound::NbtCompound; @@ -153,11 +151,12 @@ impl Workbench { horizontal_scroll: 0, window_height: WINDOW_HEIGHT, window_width: WINDOW_WIDTH, - bookmarks: vec![], + bookmarks: Bookmarks::new(), uuid: Uuid::new_v4(), freehand_mode: false, selected_text: None, last_close_attempt: Duration::ZERO, + last_selected_text_interaction: (0, 0, Duration::ZERO) }); } workbench @@ -169,52 +168,7 @@ impl Workbench { #[inline] #[allow(clippy::equatable_if_let)] pub fn on_open_file(&mut self, path: &Path, buf: Vec, window_properties: &mut WindowProperties) -> Result<()> { - let (nbt, compressed) = { - if path.extension().and_then(OsStr::to_str) == Some("mca") { - ( - NbtElement::from_mca(buf.as_slice(), self.sort_algorithm).context("Failed to parse MCA file")?, - FileFormat::Mca, - ) - } else if let Some(0x1F8B) = buf.first_chunk::<2>().copied().map(u16::from_be_bytes) { - ( - NbtElement::from_file( - &DeflateDecoder::new(buf.as_slice()) - .decode_gzip() - .context("Failed to decode gzip compressed NBT")?, - self.sort_algorithm, - ) - .context("Failed to parse NBT")?, - FileFormat::Gzip, - ) - } else if let Some(0x7801 | 0x789C | 0x78DA) = buf.first_chunk::<2>().copied().map(u16::from_be_bytes) { - ( - NbtElement::from_file( - &DeflateDecoder::new(buf.as_slice()) - .decode_zlib() - .context("Failed to decode zlib compressed NBT")?, - self.sort_algorithm, - ) - .context("Failed to parse NBT")?, - FileFormat::Zlib, - ) - } else if let Some(nbt) = NbtElement::from_file(buf.as_slice(), self.sort_algorithm) { - (nbt, FileFormat::Nbt) - } else { - ( - core::str::from_utf8(&buf) - .ok() - .and_then(|s| NbtElement::from_str(s, self.sort_algorithm)) - .context(anyhow!( - "Failed to find file type for file {}", - path.file_name() - .unwrap_or(&OsStr::new("")) - .to_string_lossy() - ))? - .1, - FileFormat::Snbt, - ) - } - }; + let (nbt, compressed) = Tab::parse_raw(path, buf, self.sort_algorithm)?; let mut tab = Tab::new(nbt, path, compressed, self.window_height, self.window_width)?; if !tab.close_selected_text(false, window_properties) { tab.selected_text = None; @@ -273,149 +227,160 @@ impl Workbench { let shift = self.held_keys.contains(&KeyCode::ShiftLeft) | self.held_keys.contains(&KeyCode::ShiftRight); let x = self.mouse_x; let y = self.mouse_y; - if state == ElementState::Released { - if self.process_action_wheel() { return true } - self.scrollbar_offset = None; - if button == MouseButton::Left { - self.steal_animation_data = None; - } - if let MouseButton::Left | MouseButton::Right = button { - if self.try_select_search_box(button) { - return true; - } else { - self.search_box.deselect(); - } - } - if let MouseButton::Left | MouseButton::Right = button { - let shift = (self.held_keys.contains(&KeyCode::ShiftLeft) || self.held_keys.contains(&KeyCode::ShiftRight)) ^ (button == MouseButton::Right); + match state { + ElementState::Pressed => { + { + self.held_mouse_keys.insert(button); - if (self.window_width - 215 - 17 - 16 - 16..self.window_width - 215 - 1 - 16 - 16).contains(&self.mouse_x) && (26..42).contains(&self.mouse_y) { let tab = tab_mut!(self); - self.search_box.on_bookmark_widget(shift, &mut tab.bookmarks, &mut tab.value); - return true; - } - - if (self.window_width - 215 - 17 - 16..self.window_width - 215 - 1 - 16).contains(&self.mouse_x) && (26..42).contains(&self.mouse_y) { - self.search_box.on_search_widget(shift); - return true; - } + let _ = tab.close_selected_text(false, window_properties); + tab.selected_text = None; - if (self.window_width - 215 - 17..self.window_width - 215 - 1).contains(&self.mouse_x) && (26..42).contains(&self.mouse_y) { - self.search_box.on_mode_widget(shift); - return true; + if let MouseButton::Left | MouseButton::Right = button && self.try_select_search_box(button) { + return true; + } else { + self.search_box.deselect(); + } } - } - self.held_mouse_keys.remove(&button); - if y < 19 && x > 2 && y > 3 { - self.click_tab(button, window_properties); - } else if y < 42 && y > 26 && x < 16 { - self.open_file(window_properties); - } else if y >= HEADER_SIZE { - let left_margin = self.left_margin(); 'a: { - if MouseButton::Left == button { - if self.toggle(shift, tab!(self).freehand_mode) { - break 'a; + if x >= left_margin && y >= HEADER_SIZE { + match self.action_wheel.take() { + Some(_) => {} + None => { + if button == MouseButton::Right { + let tab = tab_mut!(self); + let depth = Traverse::new(tab.scroll() / 16 + (y - HEADER_SIZE) / 16, &mut tab.value).enumerate().last().0; + self.action_wheel = Some((left_margin + depth * 16 + 16 + 6, ((y - HEADER_SIZE) & !15) + HEADER_SIZE + 7)); + break 'a; + } + } + } + + if button == MouseButton::Left { + if self.try_steal(true) { + if self.steal_animation_data.as_ref().is_some_and(|x| (since_epoch() - x.0) >= Duration::from_millis(500)) && self.steal() { + break 'a; + } + } else { + self.steal_animation_data = None; + } } - } - match core::mem::replace(&mut self.held_entry, HeldEntry::Empty) { - HeldEntry::Empty => {}, - HeldEntry::FromAether(x) => { - self.drop(x, None, left_margin); - break 'a + if ((self.window_width - 7)..self.window_width).contains(&x) { + let tab = tab_mut!(self); + let height = tab.value.height() * 16 + 48; + let total = self.window_height - HEADER_SIZE; + if height - 48 > total { + let start = total * self.scroll() / height + HEADER_SIZE; + let end = start + total * total / height; + if (start..=end).contains(&y) { + self.scrollbar_offset = Some(y - start); + break 'a; + } + } } - HeldEntry::FromKnown(x, indices) => { - self.drop(x, Some(indices), left_margin); - break 'a + } else { + if !tab!(self).freehand_mode && self.held_entry.is_empty() && (24..46).contains(&y) && button == MouseButton::Left { + match self.hold_entry(button) { + Ok(true) => break 'a, + Err(e) => self.alert(Alert::new("Error!", TextColor::Red, e.to_string())), + _ => {} + } } - } - if (y - HEADER_SIZE) < 16 && x > 32 + left_margin { - if self.rename(x + horizontal_scroll) { - break 'a; + if let MouseButton::Left | MouseButton::Right = button { + if (264..280).contains(&x) && (26..42).contains(&y) { + let tab = tab_mut!(self); + tab.freehand_mode = !tab.freehand_mode; + break 'a; + } + if (280..296).contains(&x) && (26..42).contains(&y) { + self.sort_algorithm = if (button == MouseButton::Right) ^ shift { self.sort_algorithm.rev_cycle() } else { self.sort_algorithm.cycle() }; + break 'a; + } } } + } + } + ElementState::Released => { + if self.process_action_wheel() { return true; } + self.scrollbar_offset = None; + if button == MouseButton::Left { + self.steal_animation_data = None; + } + if let MouseButton::Left | MouseButton::Right = button { + let shift = (self.held_keys.contains(&KeyCode::ShiftLeft) || self.held_keys.contains(&KeyCode::ShiftRight)) ^ (button == MouseButton::Right); - if button == MouseButton::Middle { - if self.delete(shift) { - break 'a; - } + if (self.window_width - 215 - 17 - 16 - 16..self.window_width - 215 - 1 - 16 - 16).contains(&self.mouse_x) & &(26..42).contains(&self.mouse_y) { + let tab = tab_mut!( self ); + self.search_box.on_bookmark_widget(shift, &mut tab.bookmarks, &mut tab.value); + return true; } - if button == MouseButton::Left { - if self.try_select_text() { - break 'a; - } + if (self.window_width - 215 - 17 - 16..self.window_width - 215 - 1 - 16).contains(&self.mouse_x) & &(26..42).contains(&self.mouse_y) { + self.search_box.on_search_widget(shift); + return true; } - if button == MouseButton::Left { - if self.bookmark_line() { - break 'a; - } + if (self.window_width - 215 - 17..self.window_width - 215 - 1).contains(&self.mouse_x) & &(26..42).contains(&self.mouse_y) { + self.search_box.on_mode_widget(shift); + return true; } } - } - } else { - { - let tab = tab_mut!(self); - let _ = tab.close_selected_text(false, window_properties); - tab.selected_text = None; - } + self.held_mouse_keys.remove(&button); + if y < 19 && x > 2 && y > 3 { + self.click_tab(button, window_properties); + } else if y < 42 && y > 26 && x < 16 { + self.open_file(window_properties); + } else if y < 42 && y > 26 && x < 32 { + self.new_tab(window_properties, shift); + } else if y < 42 && y > 26 && x >= 296 && x < 312 { + if let Err(e) = tab_mut!(self).refresh(self.sort_algorithm) { + self.alert(Alert::new("Error!", TextColor::Red, e.to_string())) + } + } else if y >= HEADER_SIZE { + let left_margin = self.left_margin(); + 'a: { + if MouseButton::Left == button { + if self.toggle(shift, tab!(self).freehand_mode) { + break 'a; + } + } - self.held_mouse_keys.insert(button); - 'a: { - let freehand_mode = tab!(self).freehand_mode; + match core::mem::replace(&mut self.held_entry, HeldEntry::Empty) { + HeldEntry::Empty => {} + HeldEntry::FromAether(x) => { + self.drop(x, None, left_margin); + break 'a; + } + HeldEntry::FromKnown(x, indices) => { + self.drop(x, Some(indices), left_margin); + break 'a; + } + } - if x >= left_margin && y >= HEADER_SIZE { - match self.action_wheel.take() { - Some(_) => {}, - None => { - if button == MouseButton::Right { - let tab = tab_mut!(self); - let depth = Traverse::new(tab.scroll() / 16 + (y - HEADER_SIZE) / 16, &mut tab.value).enumerate().last().0; - self.action_wheel = Some((left_margin + depth * 16 + 16 + 6, ((y - HEADER_SIZE) & !15) + HEADER_SIZE + 7)); + if (y - HEADER_SIZE) < 16 && x > 32 + left_margin { + if self.rename(x + horizontal_scroll) { break 'a; } } - } - } - if !freehand_mode && self.held_entry.is_empty() && (24..46).contains(&y) && button == MouseButton::Left { - match self.hold_entry(button) { - Ok(true) => break 'a, - Err(e) => self.alert(Alert::new("Error!", TextColor::Red, e.to_string())), - _ => {} - } - } - if button == MouseButton::Left { - if self.try_steal(true) { - if self.steal_animation_data.as_ref().is_some_and(|x| (since_epoch() - x.0) >= Duration::from_millis(500)) && self.steal() { - break 'a; + if button == MouseButton::Middle { + if self.delete(shift) { + break 'a; + } } - } else { - self.steal_animation_data = None; - } - } - if let MouseButton::Left | MouseButton::Right = button && (248..264).contains(&x) && (26..42).contains(&y) { - let tab = tab_mut!(self); - tab.freehand_mode = !tab.freehand_mode; - break 'a; - } - if let MouseButton::Left | MouseButton::Right = button && (264..280).contains(&x) && (26..42).contains(&y) { - self.sort_algorithm = if button == MouseButton::Left { self.sort_algorithm.cycle() } else { self.sort_algorithm.rev_cycle() }; - break 'a; - } - if ((self.window_width - 7)..self.window_width).contains(&x) { - let tab = tab_mut!(self); - let height = tab.value.height() * 16 + 48; - let total = self.window_height - HEADER_SIZE; - if height - 48 > total { - let start = total * self.scroll() / height + HEADER_SIZE; - let end = start + total * total / height; - if (start..=end).contains(&y) { - self.scrollbar_offset = Some(y - start); - break 'a; + + if button == MouseButton::Left { + if self.try_select_text() { + break 'a; + } + } + + if button == MouseButton::Left { + if self.bookmark_line() { + break 'a; + } } } } @@ -486,10 +451,10 @@ impl Workbench { #[inline] #[allow(clippy::too_many_lines)] pub fn try_subscription(&mut self) -> Result<()> { - fn write_snbt(subscription: &FileUpdateSubscription, data: &[u8], tab: &mut Tab, sort: SortAlgorithm) -> Result<()> { + fn write_snbt(subscription: &FileUpdateSubscription, data: &[u8], tab: &mut Tab) -> Result<()> { let Some((key, value)) = core::str::from_utf8(data) .ok() - .and_then(|s| NbtElement::from_str(s, sort)) + .and_then(|s| NbtElement::from_str(s, SortAlgorithm::None)) else { return Err(anyhow!("SNBT failed to parse.")); }; @@ -549,19 +514,7 @@ impl Workbench { line_number += unsafe { parent.get_mut(idx).panic_unchecked("always valid idx") }.true_height(); } line_number += 1; - let idx = tab - .bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); - for bookmark in tab - .bookmarks - .iter_mut() - .skip(idx) - .take_while(|bookmark| bookmark.true_line_number < line_number + old_true_height) - { - bookmark.true_line_number = bookmark.true_line_number.wrapping_add(true_diff); - bookmark.line_number = bookmark.line_number.wrapping_add(diff); - } + tab.bookmarks[line_number..line_number + old_true_height].increment(diff, true_diff); let mut iter = Navigate::new(rest.iter().copied(), &mut tab.value); while let Some((_, _, _, parent, _)) = iter.next() { parent.increment(diff, true_diff); @@ -637,19 +590,7 @@ impl Workbench { line_number += unsafe { parent.get_mut(idx).panic_unchecked("valid idx") }.true_height(); } line_number += 1; - let idx = tab - .bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); - for bookmark in tab - .bookmarks - .iter_mut() - .skip(idx) - .take_while(|bookmark| bookmark.true_line_number < line_number + old_true_height) - { - bookmark.true_line_number = bookmark.true_line_number.wrapping_add(true_diff); - bookmark.line_number = bookmark.line_number.wrapping_add(diff); - } + tab.bookmarks[line_number..line_number + old_true_height].increment(diff, true_diff); let mut iter = Navigate::new(rest.iter().copied(), &mut tab.value); while let Some((_, _, _, parent, _)) = iter.next() { parent.increment(diff, true_diff); @@ -674,7 +615,7 @@ impl Workbench { }; match subscription.rx.try_recv() { Ok(data) => match subscription.subscription_type { - FileUpdateSubscriptionType::Snbt => write_snbt(subscription, &data, tab, self.sort_algorithm)?, + FileUpdateSubscriptionType::Snbt => write_snbt(subscription, &data, tab)?, FileUpdateSubscriptionType::ByteArray => write_array(subscription, tab, { let mut array = NbtByteArray::new(); for (idx, byte) in data.into_iter().enumerate() { @@ -787,20 +728,9 @@ impl Workbench { recache_along_indices(&indices[..indices.len() - 1], &mut tab.value); value.1.shut(); - let mut idx = tab - .bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); - while let Some(bookmark) = tab.bookmarks.get_mut(idx) { - if bookmark.true_line_number - line_number < true_height { - let _ = tab.bookmarks.remove(idx); - } else { - bookmark.true_line_number -= true_height; - bookmark.line_number -= height; - idx += 1; - } - } - // no need for encompass or equal since that's handled by `drop` + let _ = tab.bookmarks.remove(line_number..line_number + true_height); + tab.bookmarks[line_number..].decrement(height, true_height); + // no need for encompass_or_equal since that's handled by `drop` self.held_entry = HeldEntry::FromKnown(value, indices.into_boxed_slice()); true } else { @@ -877,18 +807,7 @@ impl Workbench { } } recache_along_indices(&indices[..indices.len() - 1], &mut tab.value); - let start = tab - .bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); - for bookmark in tab.bookmarks.iter_mut().skip(start) { - if bookmark.true_line_number - line_number < true_height { - // do nothing, since the bookmark is within the tail node - } else { - bookmark.true_line_number += true_height; - bookmark.line_number += height; - } - } + tab.bookmarks[line_number + true_height..].increment(height, true_height); if let Some(subscription) = &mut self.subscription && encompasses(&indices[..indices.len() - 1], &subscription.indices) { @@ -1003,19 +922,8 @@ impl Workbench { unsafe { panic_unchecked("parents were dodged") } }; recache_along_indices(&indices[..indices.len() - 1], &mut tab.value); - let mut idx = tab - .bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); - while let Some(bookmark) = tab.bookmarks.get_mut(idx) { - if bookmark.true_line_number - line_number < true_height { - let _ = tab.bookmarks.remove(idx); - } else { - bookmark.true_line_number -= true_height; - bookmark.line_number -= height; - idx += 1; - } - } + let _ = tab.bookmarks.remove(line_number..line_number + true_height); + tab.bookmarks[line_number..].decrement(height, true_height); if let Some(subscription) = &mut self.subscription { if *indices == *subscription.indices { self.subscription = None; @@ -1103,14 +1011,7 @@ impl Workbench { }); } recache_along_indices(&indices[..indices.len() - 1], &mut tab.value); - let start = tab - .bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); - for bookmark in tab.bookmarks.iter_mut().skip(start) { - bookmark.true_line_number += true_height; - bookmark.line_number += height; - } + tab.bookmarks[line_number..].increment(height, true_height); self.subscription = None; } } @@ -1119,9 +1020,9 @@ impl Workbench { #[inline] fn hold_entry(&mut self, button: MouseButton) -> Result { - if button == MouseButton::Left && self.mouse_x >= 16 + 4 { + if button == MouseButton::Left && self.mouse_x >= 16 + 16 + 4 { let tab = tab!(self); - let x = self.mouse_x - (16 + 4); + let x = self.mouse_x - (16 + 16 + 4); if x / 16 == 13 { match NbtElement::from_str(&get_clipboard().ok_or_else(|| anyhow!("Failed to get clipboard"))?, self.sort_algorithm) { Some((key, element)) => { @@ -1134,28 +1035,23 @@ impl Workbench { None => return Err(anyhow!("Could not parse clipboard as SNBT")), } } else { - self.held_entry = unsafe { - HeldEntry::FromAether(( - None, - NbtElement::from_id(match x / 16 { - 0 => NbtByte::ID, - 1 => NbtShort::ID, - 2 => NbtInt::ID, - 3 => NbtLong::ID, - 4 => NbtFloat::ID, - 5 => NbtDouble::ID, - 6 => NbtByteArray::ID, - 7 => NbtIntArray::ID, - 8 => NbtLongArray::ID, - 9 => NbtString::ID, - 10 => NbtList::ID, - 11 => NbtCompound::ID, - 12 if tab.value.id() == NbtRegion::ID => NbtChunk::ID, - _ => return Ok(false), - }) - .panic_unchecked("Type was invalid somehow, even though we map each one"), - )) - }; + self.held_entry = HeldEntry::FromAether((None, NbtElement::from_id(match x / 16 { + 0 => NbtByte::ID, + 1 => NbtShort::ID, + 2 => NbtInt::ID, + 3 => NbtLong::ID, + 4 => NbtFloat::ID, + 5 => NbtDouble::ID, + 6 => NbtByteArray::ID, + 7 => NbtIntArray::ID, + 8 => NbtLongArray::ID, + 9 => NbtString::ID, + 10 => NbtList::ID, + 11 => NbtCompound::ID, + 12 if tab.value.id() == NbtRegion::ID => NbtChunk::ID, + _ => return Ok(false), + }), + )); } } Ok(true) @@ -1201,15 +1097,15 @@ impl Workbench { } if button == MouseButton::Middle { - self.new_tab(window_properties); + self.new_tab(window_properties, self.held_keys.contains(&KeyCode::ShiftLeft) | self.held_keys.contains(&KeyCode::ShiftRight)); } } } #[inline] - pub fn new_tab(&mut self, window_properties: &mut WindowProperties) { + pub fn new_tab(&mut self, window_properties: &mut WindowProperties, region: bool) { self.new_custom_tab(window_properties, Tab { - value: Box::new(NbtElement::Compound(NbtCompound::new())), + value: Box::new(if region { NbtElement::Region(NbtRegion::new()) } else { NbtElement::Compound(NbtCompound::new()) }), name: "new.nbt".into(), path: None, compression: FileFormat::Nbt, @@ -1220,11 +1116,12 @@ impl Workbench { horizontal_scroll: 0, window_height: self.window_height, window_width: self.window_width, - bookmarks: vec![], + bookmarks: Bookmarks::new(), uuid: Uuid::new_v4(), freehand_mode: false, selected_text: None, last_close_attempt: Duration::ZERO, + last_selected_text_interaction: (0, 0, Duration::ZERO) }); } @@ -1328,16 +1225,12 @@ impl Workbench { .panic_unchecked("nothing is wrong i didn't do .next") } .2; - let start = tab - .bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); let mut next_line_number = line_number; let mut next_line_number_idx = 0; - for bookmark in tab.bookmarks.iter_mut().skip(start) { - if bookmark.true_line_number - line_number < true_height { + for bookmark in tab.bookmarks[line_number..].iter_mut() { + *bookmark = if bookmark.true_line_number() - line_number < true_height { if open { - while next_line_number < bookmark.true_line_number { + while next_line_number < bookmark.true_line_number() { next_line_number += unsafe { element .get(next_line_number_idx) @@ -1346,28 +1239,21 @@ impl Workbench { .true_height(); next_line_number_idx += 1; } - let new_line_number = y + next_line_number_idx + 1; - bookmark.line_number = new_line_number; - if bookmark.true_line_number == next_line_number { - bookmark.uv = BOOKMARK_UV; - } else { - bookmark.uv = HIDDEN_BOOKMARK_UV; - } + Bookmark::with_uv(bookmark.true_line_number(), y + next_line_number_idx + 1, if bookmark.true_line_number() == next_line_number { BOOKMARK_UV } else { HIDDEN_BOOKMARK_UV }) } else { - bookmark.line_number = y; - bookmark.uv = HIDDEN_BOOKMARK_UV; + Bookmark::with_uv(bookmark.true_line_number(), y, HIDDEN_BOOKMARK_UV) } } else { - bookmark.line_number = bookmark.line_number.wrapping_add(increment); - } + bookmark.offset(increment, 0) + }; } true } #[inline] fn try_select_search_box(&mut self, button: MouseButton) -> bool { - if (283..self.window_width - 215 - 17 - 16 - 16).contains(&self.mouse_x) && (23..45).contains(&self.mouse_y) { - self.search_box.select(self.mouse_x - 283, button); + if (316..self.window_width - 215 - 17 - 16 - 16).contains(&self.mouse_x) && (23..45).contains(&self.mouse_y) { + self.search_box.select(self.mouse_x - 316, button); true } else { false @@ -1399,6 +1285,15 @@ impl Workbench { child.id() == NbtChunk::ID, indices, ); + if let Some(selected_text) = tab.selected_text.as_mut() { + let now = since_epoch(); + let (old_y, old_cursor, timestamp) = core::mem::replace(&mut tab.last_selected_text_interaction, (y, selected_text.cursor, now)); + if now - timestamp < Duration::from_millis(500) && old_y == y && old_cursor == selected_text.cursor && !selected_text.value.is_empty() { + tab.last_selected_text_interaction = (0, 0, Duration::ZERO); + selected_text.cursor = selected_text.value.len(); + selected_text.selection = Some(0); + } + } return tab.selected_text.is_some(); } } @@ -1420,13 +1315,8 @@ impl Workbench { .panic_unchecked("Traverse always has something") } .3; - let line_number = Bookmark::new(true_height, (self.mouse_y + scroll - HEADER_SIZE) / 16); - match tab.bookmarks.binary_search(&line_number) { - Ok(idx) => { - let _ = tab.bookmarks.remove(idx); - } - Err(idx) => tab.bookmarks.insert(idx, line_number), - } + let bookmark = Bookmark::new(true_height, (self.mouse_y + scroll - HEADER_SIZE) / 16); + let _ = tab.bookmarks.toggle(bookmark); true } @@ -1850,13 +1740,8 @@ impl Workbench { } } recache_along_indices(&indices, &mut tab.value); - let start = tab - .bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); - for bookmark in tab.bookmarks.iter_mut().skip(start) { - bookmark.line_number = (*y - HEADER_SIZE) / 16; - bookmark.uv = HIDDEN_BOOKMARK_UV; + for bookmark in tab.bookmarks[line_number..].iter_mut() { + *bookmark = Bookmark::with_uv(bookmark.true_line_number(), (*y - HEADER_SIZE) / 16, HIDDEN_BOOKMARK_UV); } } } @@ -1888,19 +1773,14 @@ impl Workbench { } } recache_along_indices(&indices, &mut tab.value); - let start = tab - .bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); if shift { let parent_line_number = (*y - HEADER_SIZE) / 16; - for bookmark in tab.bookmarks.iter_mut().skip(start) { - if bookmark.true_line_number - line_number < true_height { - bookmark.line_number = parent_line_number + bookmark.true_line_number - line_number; - bookmark.uv = BOOKMARK_UV; + for bookmark in tab.bookmarks[line_number..].iter_mut() { + *bookmark = if bookmark.true_line_number() - line_number < true_height { + bookmark.open(parent_line_number + bookmark.true_line_number() - line_number) } else { - bookmark.line_number += increment; - } + Bookmark::with_uv(bookmark.true_line_number(), bookmark.line_number() + increment, bookmark.uv()) + }; } } else { let element = Navigate::new(indices.iter().copied(), &mut tab.value) @@ -1908,9 +1788,9 @@ impl Workbench { .2; let mut next_line_number = line_number; let mut next_line_number_idx = 0; - for bookmark in tab.bookmarks.iter_mut().skip(start) { - if bookmark.true_line_number - line_number < true_height { - while next_line_number < bookmark.true_line_number { + for bookmark in tab.bookmarks[line_number..].iter_mut() { + *bookmark = if bookmark.true_line_number() - line_number < true_height { + while next_line_number < bookmark.true_line_number() { next_line_number += unsafe { element .get(next_line_number_idx) @@ -1920,15 +1800,14 @@ impl Workbench { next_line_number_idx += 1; } let new_line_number = y + next_line_number_idx + 1; - bookmark.line_number = new_line_number; - if bookmark.true_line_number == next_line_number { - bookmark.uv = BOOKMARK_UV; + if bookmark.true_line_number() == next_line_number { + bookmark.open(new_line_number) } else { - bookmark.uv = HIDDEN_BOOKMARK_UV; + bookmark.hidden(new_line_number) } } else { - bookmark.line_number += increment; - } + Bookmark::with_uv(bookmark.true_line_number(), bookmark.line_number() + increment, bookmark.uv()) + }; } } } @@ -1980,6 +1859,7 @@ impl Workbench { } SearchBoxKeyResult::Escape => { self.search_box.post_input((self.window_width, self.window_height)); + self.search_box.deselect(); return true; } SearchBoxKeyResult::ClearAllBookmarks => { @@ -2152,106 +2032,19 @@ impl Workbench { return true; } } - if flags == flags!(Alt) { - let id = if key == KeyCode::Digit1 { - NbtByte::ID - } else if key == KeyCode::Digit2 { - NbtShort::ID - } else if key == KeyCode::Digit3 { - NbtInt::ID - } else if key == KeyCode::Digit4 { - NbtLong::ID - } else if key == KeyCode::Digit5 { - NbtFloat::ID - } else if key == KeyCode::Digit6 { - NbtDouble::ID - } else if key == KeyCode::Digit7 { - NbtByteArray::ID - } else if key == KeyCode::Digit8 { - NbtIntArray::ID - } else if key == KeyCode::Digit9 { - NbtLongArray::ID - } else if key == KeyCode::Digit0 { - NbtString::ID - } else if key == KeyCode::Minus { - NbtList::ID - } else if key == KeyCode::Equal { - NbtCompound::ID - } else { - 0 - }; - if let Some(element) = NbtElement::from_id(id) { - if let HeldEntry::FromKnown(element, indices) = core::mem::replace(&mut self.held_entry, HeldEntry::FromAether((None, element))) { - tab.append_to_history(WorkbenchAction::Remove { - indices, - element, - }); - } - return true; - } - } - 'a: { - if key == KeyCode::KeyR && flags == flags!(Ctrl) { - if tab.history_changed { - break 'a; - }; - let Some(path) = &tab.path else { break 'a }; - let Ok(buf) = read(path) else { break 'a }; - let (nbt, compression) = { - if path.extension().and_then(OsStr::to_str) == Some("mca") { - (NbtElement::from_mca(buf.as_slice(), self.sort_algorithm), FileFormat::Mca) - } else if buf.first_chunk::<2>().copied().map(u16::from_be_bytes) == Some(0x1F8B) { - ( - DeflateDecoder::new(buf.as_slice()) - .decode_gzip() - .ok() - .and_then(|x| NbtElement::from_file(&x, self.sort_algorithm)), - FileFormat::Zlib, - ) - } else if let Some(0x7801 | 0x789C | 0x78DA) = buf.first_chunk::<2>().copied().map(u16::from_be_bytes) { - ( - DeflateDecoder::new(buf.as_slice()) - .decode_zlib() - .ok() - .and_then(|x| NbtElement::from_file(&x, self.sort_algorithm)), - FileFormat::Zlib, - ) - } else if let Some(nbt) = NbtElement::from_file(&buf, self.sort_algorithm) { - (Some(nbt), FileFormat::Nbt) - } else { - let nbt = core::str::from_utf8(&buf) - .ok() - .and_then(|s| NbtElement::from_str(s, self.sort_algorithm)) - .map(|x| x.1); - if let Some(NbtCompound::ID | NbtRegion::ID) = nbt.as_ref().map(NbtElement::id) { - (nbt, FileFormat::Snbt) - } else { - break 'a; - } - } - }; - let Some(nbt) = nbt else { break 'a }; - let old = core::mem::replace(&mut tab.value, Box::new(nbt)); - #[cfg(not(target_arch = "wasm32"))] - std::thread::spawn(move || drop(old)); - #[cfg(target_arch = "wasm32")] - drop(old); - tab.compression = compression; - tab.bookmarks = vec![]; - tab.history_changed = false; - tab.undos = LinkedQueue::new(); - tab.redos = LinkedQueue::new(); - tab.scroll = tab.scroll(); - tab.horizontal_scroll = tab.horizontal_scroll(self.held_entry.element()); + if key == KeyCode::KeyR && flags == flags!(Ctrl) { + if let Err(e) = tab.refresh(self.sort_algorithm) { + self.alert(Alert::new("Error!", TextColor::Red, e.to_string())) } + return true; } if key == KeyCode::KeyF && flags == flags!(Ctrl + Shift) { tab.freehand_mode = !tab.freehand_mode; return true; } - if key == KeyCode::KeyN && flags == flags!(Ctrl) { + if key == KeyCode::KeyN && flags & (!flags!(Shift)) == flags!(Ctrl) { tab.selected_text = None; - self.new_tab(window_properties); + self.new_tab(window_properties, (flags & flags!(Shift)) > 0); return true; } if key == KeyCode::KeyO && flags == flags!(Ctrl) { @@ -2322,6 +2115,56 @@ impl Workbench { return true; } } + if flags == flags!() { + let tab = tab_mut!(self); + let x = if key == KeyCode::Digit1 { + (None, NbtElement::from_id(NbtByte::ID)) + } else if key == KeyCode::Digit2 { + (None, NbtElement::from_id(NbtShort::ID)) + } else if key == KeyCode::Digit3 { + (None, NbtElement::from_id(NbtInt::ID)) + } else if key == KeyCode::Digit4 { + (None, NbtElement::from_id(NbtLong::ID)) + } else if key == KeyCode::Digit5 { + (None, NbtElement::from_id(NbtFloat::ID)) + } else if key == KeyCode::Digit6 { + (None, NbtElement::from_id(NbtDouble::ID)) + } else if key == KeyCode::Digit7 { + (None, NbtElement::from_id(NbtByteArray::ID)) + } else if key == KeyCode::Digit8 { + (None, NbtElement::from_id(NbtIntArray::ID)) + } else if key == KeyCode::Digit9 { + (None, NbtElement::from_id(NbtLongArray::ID)) + } else if key == KeyCode::Digit0 { + (None, NbtElement::from_id(NbtString::ID)) + } else if key == KeyCode::Minus { + (None, NbtElement::from_id(NbtList::ID)) + } else if key == KeyCode::Equal { + (None, NbtElement::from_id(NbtCompound::ID)) + } else if key == KeyCode::Backquote && tab.value.id() == NbtRegion::ID { + (None, NbtElement::from_id(NbtChunk::ID)) + } else if key == KeyCode::KeyC { + let Some(clipboard) = get_clipboard() else { + self.alert(Alert::new("Error!", TextColor::Red, "Failed to get clipboard")); + return true; + }; + if let Some((key, value)) = NbtElement::from_str(&clipboard, self.sort_algorithm) { + (key, value) + } else { + self.alert(Alert::new("Error!", TextColor::Red, "Could not parse clipboard as SNBT")); + return true; + } + } else { + return true; + }; + if let HeldEntry::FromKnown(element, indices) = core::mem::replace(&mut self.held_entry, HeldEntry::FromAether(x)) { + tab.append_to_history(WorkbenchAction::Remove { + indices, + element, + }); + } + return true; + } } } else if key.state == ElementState::Released { if let PhysicalKey::Code(x) = key.physical_key { @@ -2402,9 +2245,11 @@ impl Workbench { pub fn render(&mut self, builder: &mut VertexBufferBuilder) { if self.raw_window_width < MIN_WINDOW_WIDTH || self.raw_window_height < MIN_WINDOW_HEIGHT { return; } + let shift = self.held_keys.contains(&KeyCode::ShiftLeft) || self.held_keys.contains(&KeyCode::ShiftRight); + { builder.draw_texture_region_z( - (281, 22), + (313, 22), BASE_Z, LINE_NUMBER_SEPARATOR_UV, (2, 23), @@ -2488,12 +2333,21 @@ impl Workbench { } { builder.draw_texture((0, 26), OPEN_FOLDER_UV, (16, 16)); + builder.draw_texture((16, 26), NEW_FILE_UV, (16, 16)); if (0..16).contains(&ctx.mouse_x) && (26..42).contains(&ctx.mouse_y) { builder.draw_texture((0, 26), SELECTION_UV, (16, 16)); builder.draw_tooltip(&["Open File (Ctrl + O)"], (self.mouse_x, self.mouse_y), false); } + if (16..32).contains(&ctx.mouse_x) && (26..42).contains(&ctx.mouse_y) { + builder.draw_texture((16, 26), SELECTION_UV, (16, 16)); + if shift { + builder.draw_tooltip(&["Create New Region File (Ctrl + Shift + N)"], (self.mouse_x, self.mouse_y), false); + } else { + builder.draw_tooltip(&["Create New NBT File (Ctrl + N)"], (self.mouse_x, self.mouse_y), false); + } + } builder.draw_texture_region_z( - (17, 22), + (33, 22), BASE_Z, LINE_NUMBER_SEPARATOR_UV, (2, 23), diff --git a/src/workbench_action.rs b/src/workbench_action.rs index 1cf9df5..1073e95 100644 --- a/src/workbench_action.rs +++ b/src/workbench_action.rs @@ -7,9 +7,10 @@ use crate::assets::{ADD_TAIL_UV, ADD_UV, MOVE_TAIL_UV, MOVE_UV, REMOVE_TAIL_UV, use crate::elements::element::NbtElement; use crate::vertex_buffer_builder::VertexBufferBuilder; use crate::{encompasses, encompasses_or_equal, FileUpdateSubscription}; -use crate::{panic_unchecked, Bookmark, Position, sum_indices}; +use crate::{panic_unchecked, Position, sum_indices}; use crate::{Navigate, OptionExt}; use crate::elements::compound::{CompoundMap, Entry}; +use crate::bookmark::{Bookmark, Bookmarks}; pub enum WorkbenchAction { Remove { @@ -41,7 +42,7 @@ pub enum WorkbenchAction { impl WorkbenchAction { #[cfg_attr(debug_assertions, inline(never))] - pub fn undo(self, root: &mut NbtElement, bookmarks: &mut Vec, subscription: &mut Option, path: &mut Option, name: &mut Box) -> Self { + pub fn undo(self, root: &mut NbtElement, bookmarks: &mut Bookmarks, subscription: &mut Option, path: &mut Option, name: &mut Box) -> Self { unsafe { self.undo0(root, bookmarks, subscription, path, name) .panic_unchecked("Failed to undo action") @@ -55,7 +56,7 @@ impl WorkbenchAction { clippy::too_many_lines, clippy::cognitive_complexity )] - unsafe fn undo0(self, root: &mut NbtElement, bookmarks: &mut Vec, subscription: &mut Option, path: &mut Option, name: &mut Box) -> Option { + unsafe fn undo0(self, root: &mut NbtElement, bookmarks: &mut Bookmarks, subscription: &mut Option, path: &mut Option, name: &mut Box) -> Option { Some(match self { Self::Remove { element: (key, value), @@ -82,13 +83,8 @@ impl WorkbenchAction { line_number += element.get(n)?.true_height(); } line_number += 1; - let start = bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .map_or_else(|x| x + 1, identity); - for bookmark in bookmarks.iter_mut().skip(start) { - bookmark.true_line_number += true_height; - bookmark.line_number += height; - } + + bookmarks[line_number..].increment(height, true_height); break; } } @@ -119,18 +115,7 @@ impl WorkbenchAction { element.decrement(height, true_height); } - let mut idx = bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); - while let Some(bookmark) = bookmarks.get_mut(idx) { - if bookmark.true_line_number - line_number < true_height { - let _ = bookmarks.remove(idx); - } else { - bookmark.true_line_number -= true_height; - bookmark.line_number -= height; - idx += 1; - } - } + bookmarks[line_number..].decrement(height, true_height); if let Some(inner_subscription) = subscription { if *rem == *inner_subscription.indices { @@ -231,18 +216,8 @@ impl WorkbenchAction { while let Some((_, _, _, element, _)) = iter.next() { element.decrement(height, true_height); } - let mut idx = bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); - while let Some(bookmark) = bookmarks.get_mut(idx) { - if bookmark.true_line_number - line_number < true_height { - let _ = bookmarks.remove(idx); - } else { - bookmark.true_line_number -= true_height; - bookmark.line_number -= height; - idx += 1; - } - } + let _ = bookmarks.remove(line_number..line_number + true_height); + bookmarks[line_number..].decrement(height, true_height); if let Some(subscription) = subscription { if to == subscription.indices { subscription.indices = from.clone(); @@ -291,13 +266,7 @@ impl WorkbenchAction { } } } - let start = bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .map_or_else(|x| x + 1, std::convert::identity); - for bookmark in bookmarks.iter_mut().skip(start) { - bookmark.true_line_number += true_height; - bookmark.line_number += height; - } + bookmarks[line_number..].increment(height, true_height); if let Some(subscription) = subscription && !changed_subscription_indices && encompasses_or_equal(rem, &subscription.indices) @@ -342,19 +311,8 @@ impl WorkbenchAction { panic_unchecked("oh crap"); } } - - let mut idx = bookmarks - .binary_search(&Bookmark::new(line_number, 0)) - .unwrap_or_else(identity); - while let Some(bookmark) = bookmarks.get_mut(idx) { - if bookmark.line_number < old_true_height + line_number { - bookmarks.remove(idx); - } else { - bookmark.true_line_number = bookmark.true_line_number.wrapping_add(true_diff); - bookmark.line_number = bookmark.line_number.wrapping_add(diff); - idx += 1; - } - } + let _ = bookmarks.remove(line_number..line_number + old_true_height); + bookmarks[line_number..].increment(true_diff, diff); crate::recache_along_indices(rest, root); if let Some(inner_subscription) = subscription @@ -401,9 +359,7 @@ impl WorkbenchAction { let mut current_line_number = true_line_number + 1; entries.iter().map(|entry| { let new_line_number = current_line_number; current_line_number += entry.value.true_height(); new_line_number }).collect::>() }; - let bookmarks_start = bookmarks.binary_search(&Bookmark::new(true_line_number, 0)).unwrap_or_else(identity); - let bookmarks_end = bookmarks.binary_search(&Bookmark::new(true_line_number + true_height - 1, 0)).map_or_else(identity, |x| x + 1); - let mut new_bookmarks = Box::<[Bookmark]>::new_uninit_slice(bookmarks_end - bookmarks_start); + let mut new_bookmarks = Box::<[Bookmark]>::new_uninit_slice(bookmarks[true_line_number..true_line_number + true_height].len()); let mut new_bookmarks_len = 0; let mut previous_entries = core::mem::transmute::<_, Box<[MaybeUninit]>>(core::mem::take(entries).into_boxed_slice()); let mut new_entries = Box::<[Entry]>::new_uninit_slice(previous_entries.len()); @@ -419,11 +375,8 @@ impl WorkbenchAction { let true_height = entry.value.true_height(); let offset = if open { current_line_number as isize - line_number as isize } else { 0 }; let true_offset = current_true_line_number as isize - true_line_number as isize; - let bookmark_start = bookmarks.binary_search(&Bookmark::new(true_line_number, 0)).unwrap_or_else(identity); - let bookmark_end = bookmarks.binary_search(&Bookmark::new(true_line_number + true_height - 1, 0)).map_or_else(identity, |x| x + 1); - for bookmark in bookmarks.iter().skip(bookmark_start).take(bookmark_end - bookmark_start) { - let adjusted_bookmark = Bookmark::new(bookmark.true_line_number.wrapping_add(true_offset as usize), bookmark.line_number.wrapping_add(offset as usize)); - new_bookmarks[new_bookmarks_len].write(adjusted_bookmark); + for bookmark in bookmarks[true_line_number..true_line_number + true_height].iter() { + new_bookmarks[new_bookmarks_len].write(bookmark.offset(offset as usize, true_offset as usize)); new_bookmarks_len += 1; } @@ -433,7 +386,8 @@ impl WorkbenchAction { current_true_line_number += true_height; current_line_number += height; } - unsafe { core::ptr::copy_nonoverlapping(new_bookmarks.as_ptr().cast::(), bookmarks.as_mut_ptr().add(bookmarks_start), bookmarks_end - bookmarks_start); } + let bookmark_slice = &mut bookmarks[true_line_number..true_line_number + true_height]; + unsafe { core::ptr::copy_nonoverlapping(new_bookmarks.as_ptr().cast::(), bookmark_slice.as_mut_ptr(), bookmark_slice.len()); } *entries = new_entries.assume_init().into_vec(); return Some(Self::ReorderCompound { indices: traversal_indices,