organize input element types and implementation into dedicated files
This commit is contained in:
@@ -1,5 +1,15 @@
|
||||
mod element;
|
||||
pub use element::*;
|
||||
|
||||
pub mod actions;
|
||||
mod colors;
|
||||
mod cursor;
|
||||
pub use cursor::*;
|
||||
mod element;
|
||||
mod history;
|
||||
mod paint;
|
||||
mod state;
|
||||
mod state_input_handler;
|
||||
pub(self) mod unicode;
|
||||
|
||||
pub use colors::*;
|
||||
pub(self) use cursor::*;
|
||||
pub use element::*;
|
||||
pub(self) use history::*;
|
||||
pub use state::*;
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/// The key context used for input element keybindings.
|
||||
pub const DEFAULT_INPUT_CONTEXT: &str = "Input";
|
||||
|
||||
gpui::actions!(
|
||||
actions,
|
||||
[
|
||||
/// Delete the character before the cursor.
|
||||
Backspace,
|
||||
/// Delete the character after the cursor.
|
||||
Delete,
|
||||
/// Blur focus from the input.
|
||||
Escape,
|
||||
/// Delete the word before the cursor.
|
||||
DeleteWordLeft,
|
||||
/// Delete the word after the cursor.
|
||||
DeleteWordRight,
|
||||
/// Delete from the cursor to the beginning of the line.
|
||||
DeleteToBeginningOfLine,
|
||||
/// Delete from the cursor to the end of the line.
|
||||
DeleteToEndOfLine,
|
||||
/// Insert a tab character at the cursor position.
|
||||
Tab,
|
||||
/// Move the cursor one character to the left.
|
||||
Left,
|
||||
/// Move the cursor one character to the right.
|
||||
Right,
|
||||
/// Move the cursor up one visual line.
|
||||
Up,
|
||||
/// Move the cursor down one visual line.
|
||||
Down,
|
||||
/// Extend selection one character to the left.
|
||||
SelectLeft,
|
||||
/// Extend selection one character to the right.
|
||||
SelectRight,
|
||||
/// Extend selection up one visual line.
|
||||
SelectUp,
|
||||
/// Extend selection down one visual line.
|
||||
SelectDown,
|
||||
/// Select all text content.
|
||||
SelectAll,
|
||||
/// Move cursor to the start of the current line.
|
||||
Home,
|
||||
/// Move cursor to the end of the current line.
|
||||
End,
|
||||
/// Extend selection to the beginning of the content.
|
||||
SelectToBeginning,
|
||||
/// Extend selection to the end of the content.
|
||||
SelectToEnd,
|
||||
/// Move cursor to the beginning of the content.
|
||||
MoveToBeginning,
|
||||
/// Move cursor to the end of the content.
|
||||
MoveToEnd,
|
||||
/// Paste from clipboard at the cursor position.
|
||||
Paste,
|
||||
/// Cut selected text to clipboard.
|
||||
Cut,
|
||||
/// Copy selected text to clipboard.
|
||||
Copy,
|
||||
/// Insert a newline at the cursor position.
|
||||
Enter,
|
||||
/// Move cursor one word to the left.
|
||||
WordLeft,
|
||||
/// Move cursor one word to the right.
|
||||
WordRight,
|
||||
/// Extend selection one word to the left.
|
||||
SelectWordLeft,
|
||||
/// Extend selection one word to the right.
|
||||
SelectWordRight,
|
||||
/// Undo the last edit.
|
||||
Undo,
|
||||
/// Redo the last undone edit.
|
||||
Redo,
|
||||
]
|
||||
);
|
||||
|
||||
pub fn default_bindings() -> gpui::ActionBindingCollection {
|
||||
let mut bindings = gpui::ActionBindingCollection::default();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
bindings = bindings
|
||||
.with::<Backspace>("backspace")
|
||||
.with::<Delete>("delete")
|
||||
.with::<DeleteWordLeft>("alt-backspace")
|
||||
.with::<DeleteWordRight>("alt-delete")
|
||||
.with::<DeleteToBeginningOfLine>("cmd-backspace")
|
||||
.with::<DeleteToEndOfLine>("ctrl-k")
|
||||
.with::<Tab>("tab")
|
||||
.with::<Enter>("enter")
|
||||
.with::<Left>("left")
|
||||
.with::<Right>("right")
|
||||
.with::<Up>("up")
|
||||
.with::<Down>("down")
|
||||
.with::<SelectLeft>("shift-left")
|
||||
.with::<SelectRight>("shift-right")
|
||||
.with::<SelectUp>("shift-up")
|
||||
.with::<SelectDown>("shift-down")
|
||||
.with::<SelectAll>("cmd-a")
|
||||
// Mac keyboards don't have Home/End keys, so cmd-left/right are standard
|
||||
.with::<Home>("cmd-left")
|
||||
.with::<End>("cmd-right")
|
||||
.with::<MoveToBeginning>("cmd-up")
|
||||
.with::<MoveToEnd>("cmd-down")
|
||||
.with::<SelectToBeginning>("cmd-shift-up")
|
||||
.with::<SelectToEnd>("cmd-shift-down")
|
||||
.with::<WordLeft>("alt-left")
|
||||
.with::<WordRight>("alt-right")
|
||||
.with::<SelectWordLeft>("alt-shift-left")
|
||||
.with::<SelectWordRight>("alt-shift-right")
|
||||
.with::<Copy>("cmd-c")
|
||||
.with::<Cut>("cmd-x")
|
||||
.with::<Paste>("cmd-v")
|
||||
.with::<Undo>("cmd-z")
|
||||
.with::<Redo>("cmd-shift-z")
|
||||
.with::<Escape>("escape");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
bindings = bindings
|
||||
.with::<Backspace>("backspace")
|
||||
.with::<Delete>("delete")
|
||||
.with::<DeleteWordLeft>("ctrl-backspace")
|
||||
.with::<DeleteWordRight>("ctrl-delete")
|
||||
.with::<DeleteToBeginningOfLine>("ctrl-shift-backspace")
|
||||
.with::<DeleteToEndOfLine>("ctrl-shift-delete")
|
||||
.with::<Tab>("tab")
|
||||
.with::<Enter>("enter")
|
||||
.with::<Left>("left")
|
||||
.with::<Right>("right")
|
||||
.with::<Up>("up")
|
||||
.with::<Down>("down")
|
||||
.with::<SelectLeft>("shift-left")
|
||||
.with::<SelectRight>("shift-right")
|
||||
.with::<SelectUp>("shift-up")
|
||||
.with::<SelectDown>("shift-down")
|
||||
.with::<SelectAll>("ctrl-a")
|
||||
.with::<Home>("home")
|
||||
.with::<End>("end")
|
||||
.with::<MoveToBeginning>("ctrl-home")
|
||||
.with::<MoveToEnd>("ctrl-end")
|
||||
.with::<SelectToBeginning>("ctrl-shift-home")
|
||||
.with::<SelectToEnd>("ctrl-shift-end")
|
||||
.with::<WordLeft>("ctrl-left")
|
||||
.with::<WordRight>("ctrl-right")
|
||||
.with::<SelectWordLeft>("ctrl-shift-left")
|
||||
.with::<SelectWordRight>("ctrl-shift-right")
|
||||
.with::<Copy>("ctrl-c")
|
||||
.with::<Cut>("ctrl-x")
|
||||
.with::<Paste>("ctrl-v")
|
||||
.with::<Undo>("ctrl-z")
|
||||
.with::<Redo>("ctrl-shift-z")
|
||||
.with::<Escape>("escape");
|
||||
}
|
||||
|
||||
bindings
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use gpui::Hsla;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PaintColors {
|
||||
pub selection: Hsla,
|
||||
pub cursor: Hsla,
|
||||
pub placeholder: Hsla,
|
||||
}
|
||||
|
||||
impl Default for PaintColors {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
selection: Hsla::blue().opacity(0.2),
|
||||
cursor: Hsla::white().opacity(0.8),
|
||||
placeholder: gpui::hsla(0.6, 0.6, 0.6, 1.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
use gpui::Context;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Default interval for cursor blinking.
|
||||
pub const DEFAULT_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
/// Manages the blinking state of a text cursor.
|
||||
///
|
||||
/// The cursor blinks at a configurable interval when enabled. Blinking can be
|
||||
@@ -32,11 +35,6 @@ impl CursorBlink {
|
||||
self.visible
|
||||
}
|
||||
|
||||
/// Returns whether blinking is currently active.
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
/// Activates cursor blinking.
|
||||
///
|
||||
/// When activated, the cursor will alternate between visible and hidden
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
use std::{
|
||||
ops::Range,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Maximum number of history entries to keep.
|
||||
pub const MAX_HISTORY_LEN: usize = 1000;
|
||||
|
||||
/// Default interval for grouping consecutive edits into a single undo entry.
|
||||
pub const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300);
|
||||
|
||||
/// A patch-based history entry for memory-efficient undo/redo operations.
|
||||
/// Instead of storing the full content, we store only the change needed to reverse the edit.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HistoryEntry {
|
||||
/// The byte range that was modified (after the edit, for undo; before the edit, for redo).
|
||||
pub range: Range<usize>,
|
||||
/// The text that was replaced (to restore on undo).
|
||||
pub old_text: String,
|
||||
/// The length of the new text that replaced old_text (to know how much to remove on undo).
|
||||
pub new_text_len: usize,
|
||||
/// The selection range before the edit.
|
||||
pub selected_range: Range<usize>,
|
||||
/// Whether the selection was reversed before the edit.
|
||||
pub selection_reversed: bool,
|
||||
/// Timestamp for grouping consecutive edits.
|
||||
pub timestamp: Instant,
|
||||
}
|
||||
|
||||
impl HistoryEntry {
|
||||
/// Apply this patch to undo an edit, returning the reverse patch for redo.
|
||||
pub fn apply_undo(&self, content: &mut String) -> HistoryEntry {
|
||||
let undo_start = self.range.start;
|
||||
let undo_end = (self.range.start + self.new_text_len).min(content.len());
|
||||
|
||||
// Capture what we're about to remove (the "new" text that was inserted)
|
||||
let removed_text = content[undo_start..undo_end].to_string();
|
||||
|
||||
// Replace with the old text
|
||||
content.replace_range(undo_start..undo_end, &self.old_text);
|
||||
|
||||
// Return reverse patch for redo
|
||||
HistoryEntry {
|
||||
range: undo_start..undo_start + self.old_text.len(),
|
||||
old_text: removed_text,
|
||||
new_text_len: self.old_text.len(),
|
||||
selected_range: self.selected_range.clone(),
|
||||
selection_reversed: self.selection_reversed,
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply this patch to redo an edit, returning the reverse patch for undo.
|
||||
pub fn apply_redo(&self, content: &mut String) -> HistoryEntry {
|
||||
// Redo is the same operation as undo - we're reversing the undo
|
||||
self.apply_undo(content)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,229 @@
|
||||
use super::unicode::UnicodeString;
|
||||
use crate::input::InputStateEvent;
|
||||
use gpui::{Bounds, Context, EntityInputHandler, Pixels, Point, UTF16Selection, Window, point, px};
|
||||
use std::ops::Range;
|
||||
|
||||
impl EntityInputHandler for super::InputState {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
adjusted_range: &mut Option<Range<usize>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<String> {
|
||||
let range = self.utf_range_16to8(&range_utf16);
|
||||
let clamped_range =
|
||||
range.start.min(self.content().len())..range.end.min(self.content().len());
|
||||
adjusted_range.replace(self.utf_range_8to16(&clamped_range));
|
||||
Some(self.content()[clamped_range].to_string())
|
||||
}
|
||||
|
||||
fn selected_text_range(
|
||||
&mut self,
|
||||
_ignore_disabled_input: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<UTF16Selection> {
|
||||
Some(UTF16Selection {
|
||||
range: self.utf_range_8to16(&self.selected_range),
|
||||
reversed: self.selection_reversed,
|
||||
})
|
||||
}
|
||||
|
||||
fn marked_text_range(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Range<usize>> {
|
||||
self.marked_range
|
||||
.as_ref()
|
||||
.map(|range| self.utf_range_8to16(range))
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
||||
self.marked_range = None;
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.utf_range_16to8(range_utf16))
|
||||
.or(self.marked_range.clone())
|
||||
.unwrap_or(self.selected_range.clone());
|
||||
|
||||
let range = range.start.min(self.content().len())..range.end.min(self.content().len());
|
||||
|
||||
// Strip newlines for single-line input
|
||||
let sanitized_text;
|
||||
let text_to_insert = if self.multiline {
|
||||
new_text
|
||||
} else {
|
||||
sanitized_text = new_text.replace('\n', " ").replace('\r', "");
|
||||
&sanitized_text
|
||||
};
|
||||
|
||||
// Record patch for undo before modifying content
|
||||
self.push_undo_patch(range.clone(), text_to_insert.len());
|
||||
|
||||
// Update cached UTF-16 length incrementally if available
|
||||
if let Some(cached_len) = self.cached_utf16_len {
|
||||
let removed_utf16_len: usize = self.content()[range.clone()]
|
||||
.chars()
|
||||
.map(|c| c.len_utf16())
|
||||
.sum();
|
||||
let added_utf16_len: usize = text_to_insert.chars().map(|c| c.len_utf16()).sum();
|
||||
self.cached_utf16_len = Some(cached_len - removed_utf16_len + added_utf16_len);
|
||||
}
|
||||
|
||||
self.content_mut()
|
||||
.replace_range(range.clone(), text_to_insert);
|
||||
self.selected_range =
|
||||
range.start + text_to_insert.len()..range.start + text_to_insert.len();
|
||||
self.marked_range.take();
|
||||
self.needs_layout = true;
|
||||
self.pause_cursor_blink(cx);
|
||||
cx.emit(InputStateEvent::TextChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range_utf16: Option<Range<usize>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.utf_range_16to8(range_utf16))
|
||||
.or(self.marked_range.clone())
|
||||
.unwrap_or(self.selected_range.clone());
|
||||
|
||||
let range = range.start.min(self.content().len())..range.end.min(self.content().len());
|
||||
|
||||
// Strip newlines for single-line input
|
||||
let sanitized_text;
|
||||
let text_to_insert = if self.multiline {
|
||||
new_text
|
||||
} else {
|
||||
sanitized_text = new_text.replace('\n', " ").replace('\r', "");
|
||||
&sanitized_text
|
||||
};
|
||||
|
||||
// Update cached UTF-16 length incrementally if available
|
||||
if let Some(cached_len) = self.cached_utf16_len {
|
||||
let removed_utf16_len: usize = self.content()[range.clone()]
|
||||
.chars()
|
||||
.map(|c| c.len_utf16())
|
||||
.sum();
|
||||
let added_utf16_len: usize = text_to_insert.chars().map(|c| c.len_utf16()).sum();
|
||||
self.cached_utf16_len = Some(cached_len - removed_utf16_len + added_utf16_len);
|
||||
}
|
||||
|
||||
self.content_mut()
|
||||
.replace_range(range.clone(), text_to_insert);
|
||||
|
||||
if !text_to_insert.is_empty() {
|
||||
self.marked_range = Some(range.start..range.start + text_to_insert.len());
|
||||
} else {
|
||||
self.marked_range = None;
|
||||
}
|
||||
|
||||
self.selected_range = new_selected_range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.utf_range_16to8(range_utf16))
|
||||
.map(|new_range| new_range.start + range.start..new_range.end + range.start)
|
||||
.unwrap_or_else(|| {
|
||||
range.start + text_to_insert.len()..range.start + text_to_insert.len()
|
||||
});
|
||||
|
||||
self.needs_layout = true;
|
||||
cx.emit(InputStateEvent::TextChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
let range = self.utf_range_16to8(&range_utf16);
|
||||
|
||||
for line in &self.line_layouts {
|
||||
if line.text_range.is_empty() {
|
||||
if range.start == line.text_range.start {
|
||||
return Some(Bounds::from_corners(
|
||||
point(bounds.left(), bounds.top() + line.y_offset),
|
||||
point(
|
||||
bounds.left() + px(4.),
|
||||
bounds.top() + line.y_offset + self.line_height,
|
||||
),
|
||||
));
|
||||
}
|
||||
} else if line.text_range.contains(&range.start) {
|
||||
if let Some(wrapped) = &line.wrapped_line {
|
||||
let local_start = range.start - line.text_range.start;
|
||||
let local_end = (range.end - line.text_range.start).min(wrapped.text.len());
|
||||
|
||||
let start_pos = wrapped
|
||||
.position_for_index(local_start, self.line_height)
|
||||
.unwrap_or(point(px(0.), px(0.)));
|
||||
let end_pos = wrapped
|
||||
.position_for_index(local_end, self.line_height)
|
||||
.unwrap_or_else(|| {
|
||||
let last_line_y =
|
||||
self.line_height * (line.visual_line_count - 1) as f32;
|
||||
point(wrapped.width(), last_line_y)
|
||||
});
|
||||
|
||||
let start_visual_line = (start_pos.y / self.line_height).floor() as usize;
|
||||
let end_visual_line = (end_pos.y / self.line_height).floor() as usize;
|
||||
|
||||
if start_visual_line == end_visual_line {
|
||||
return Some(Bounds::from_corners(
|
||||
point(
|
||||
bounds.left() + start_pos.x,
|
||||
bounds.top() + line.y_offset + start_pos.y,
|
||||
),
|
||||
point(
|
||||
bounds.left() + end_pos.x,
|
||||
bounds.top() + line.y_offset + start_pos.y + self.line_height,
|
||||
),
|
||||
));
|
||||
} else {
|
||||
return Some(Bounds::from_corners(
|
||||
point(
|
||||
bounds.left() + start_pos.x,
|
||||
bounds.top() + line.y_offset + start_pos.y,
|
||||
),
|
||||
point(
|
||||
bounds.left() + wrapped.width(),
|
||||
bounds.top() + line.y_offset + start_pos.y + self.line_height,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn character_index_for_point(
|
||||
&mut self,
|
||||
point: Point<Pixels>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
let index = self.index_for_position(point);
|
||||
Some(self.utf_offset_8to16(index))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use std::ops::Range;
|
||||
|
||||
pub trait UnicodeString {
|
||||
fn len_utf16_cached(&self) -> Option<usize>;
|
||||
/// Returns a reference to the utf8 string.
|
||||
fn content_utf8(&self) -> &str;
|
||||
/// Returns the UTF-16 length of the content.
|
||||
fn len_utf16(&self) -> usize;
|
||||
|
||||
fn utf_offset_8to16(&self, pos_uft8: usize) -> usize {
|
||||
// Fast path: if offset is 0, return 0
|
||||
if pos_uft8 == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fast path: if offset is at or past end, return cached length
|
||||
if pos_uft8 >= self.content_utf8().len() {
|
||||
return self.len_utf16();
|
||||
}
|
||||
|
||||
let mut pos_utf16 = 0;
|
||||
let mut counter_utf8 = 0;
|
||||
|
||||
for character in self.content_utf8().chars() {
|
||||
if counter_utf8 >= pos_uft8 {
|
||||
break;
|
||||
}
|
||||
counter_utf8 += character.len_utf8();
|
||||
pos_utf16 += character.len_utf16();
|
||||
}
|
||||
|
||||
pos_utf16
|
||||
}
|
||||
|
||||
fn utf_offset_16to8(&self, pos_utf16: usize) -> usize {
|
||||
// Fast path: if offset is 0, return 0
|
||||
if pos_utf16 == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fast path: if we have cached length and offset is at or past end
|
||||
if let Some(utf16_len) = self.len_utf16_cached() {
|
||||
if pos_utf16 >= utf16_len {
|
||||
return self.content_utf8().len();
|
||||
}
|
||||
}
|
||||
|
||||
let mut pos_utf8 = 0;
|
||||
let mut counter_utf16 = 0;
|
||||
|
||||
for character in self.content_utf8().chars() {
|
||||
if counter_utf16 >= pos_utf16 {
|
||||
break;
|
||||
}
|
||||
counter_utf16 += character.len_utf16();
|
||||
pos_utf8 += character.len_utf8();
|
||||
}
|
||||
|
||||
pos_utf8.min(self.content_utf8().len())
|
||||
}
|
||||
|
||||
fn utf_range_8to16(&self, range_utf8: &Range<usize>) -> Range<usize> {
|
||||
self.utf_offset_8to16(range_utf8.start)..self.utf_offset_8to16(range_utf8.end)
|
||||
}
|
||||
|
||||
fn utf_range_16to8(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
||||
self.utf_offset_16to8(range_utf16.start)..self.utf_offset_16to8(range_utf16.end)
|
||||
}
|
||||
}
|
||||
impl UnicodeString for super::InputState {
|
||||
fn len_utf16_cached(&self) -> Option<usize> {
|
||||
self.cached_utf16_len
|
||||
}
|
||||
|
||||
fn content_utf8(&self) -> &str {
|
||||
self.content()
|
||||
}
|
||||
|
||||
fn len_utf16(&self) -> usize {
|
||||
if let Some(len) = self.cached_utf16_len {
|
||||
return len;
|
||||
}
|
||||
self.content_utf8().chars().map(|c| c.len_utf16()).sum()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user