editor: Add indent guides. (#1426)
<img width="1070" height="1006" alt="image" src="https://github.com/user-attachments/assets/a456b055-b45f-45ae-84b0-452a952cc19c" />
This commit is contained in:
parent
21eff06e11
commit
178269be63
6 changed files with 435 additions and 252 deletions
|
|
@ -46,6 +46,7 @@ pub struct Example {
|
||||||
language_state: Entity<DropdownState<Vec<SharedString>>>,
|
language_state: Entity<DropdownState<Vec<SharedString>>>,
|
||||||
language: Lang,
|
language: Lang,
|
||||||
line_number: bool,
|
line_number: bool,
|
||||||
|
indent_guides: bool,
|
||||||
need_update: bool,
|
need_update: bool,
|
||||||
soft_wrap: bool,
|
soft_wrap: bool,
|
||||||
lsp_store: ExampleLspStore,
|
lsp_store: ExampleLspStore,
|
||||||
|
|
@ -671,6 +672,7 @@ impl Example {
|
||||||
let mut editor = InputState::new(window, cx)
|
let mut editor = InputState::new(window, cx)
|
||||||
.code_editor(default_language.0.name().to_string())
|
.code_editor(default_language.0.name().to_string())
|
||||||
.line_number(true)
|
.line_number(true)
|
||||||
|
.indent_guides(true)
|
||||||
.tab_size(TabSize {
|
.tab_size(TabSize {
|
||||||
tab_size: 4,
|
tab_size: 4,
|
||||||
hard_tabs: false,
|
hard_tabs: false,
|
||||||
|
|
@ -725,6 +727,7 @@ impl Example {
|
||||||
language_state,
|
language_state,
|
||||||
language: default_language.0,
|
language: default_language.0,
|
||||||
line_number: true,
|
line_number: true,
|
||||||
|
indent_guides: true,
|
||||||
need_update: false,
|
need_update: false,
|
||||||
soft_wrap: false,
|
soft_wrap: false,
|
||||||
lsp_store,
|
lsp_store,
|
||||||
|
|
@ -804,6 +807,19 @@ impl Example {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn toggle_indent_guides(
|
||||||
|
&mut self,
|
||||||
|
_: &ClickEvent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.indent_guides = !self.indent_guides;
|
||||||
|
self.editor.update(cx, |state, cx| {
|
||||||
|
state.set_indent_guides(self.indent_guides, window, cx);
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn lint_document(&mut self, cx: &mut Context<Self>) {
|
fn lint_document(&mut self, cx: &mut Context<Self>) {
|
||||||
let language = self.language.name().to_string();
|
let language = self.language.name().to_string();
|
||||||
let lsp_store = self.lsp_store.clone();
|
let lsp_store = self.lsp_store.clone();
|
||||||
|
|
@ -985,6 +1001,14 @@ impl Render for Example {
|
||||||
.label("Soft Wrap")
|
.label("Soft Wrap")
|
||||||
.selected(self.soft_wrap)
|
.selected(self.soft_wrap)
|
||||||
.on_click(cx.listener(Self::toggle_soft_wrap))
|
.on_click(cx.listener(Self::toggle_soft_wrap))
|
||||||
|
})
|
||||||
|
.child({
|
||||||
|
Button::new("indent-guides")
|
||||||
|
.ghost()
|
||||||
|
.xsmall()
|
||||||
|
.label("Indent Guides")
|
||||||
|
.selected(self.indent_guides)
|
||||||
|
.on_click(cx.listener(Self::toggle_indent_guides))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child({
|
.child({
|
||||||
|
|
|
||||||
|
|
@ -712,6 +712,7 @@ pub(super) struct PrepaintState {
|
||||||
search_match_paths: Vec<(Path<Pixels>, bool)>,
|
search_match_paths: Vec<(Path<Pixels>, bool)>,
|
||||||
document_color_paths: Vec<(Path<Pixels>, Hsla)>,
|
document_color_paths: Vec<(Path<Pixels>, Hsla)>,
|
||||||
hover_definition_hitbox: Option<Hitbox>,
|
hover_definition_hitbox: Option<Hitbox>,
|
||||||
|
indent_guides_path: Option<Path<Pixels>>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1083,6 +1084,7 @@ impl Element for TextElement {
|
||||||
};
|
};
|
||||||
|
|
||||||
let hover_definition_hitbox = self.layout_hover_definition_hitbox(state, window, cx);
|
let hover_definition_hitbox = self.layout_hover_definition_hitbox(state, window, cx);
|
||||||
|
let indent_guides_path = self.layout_indent_guides(state, &last_layout);
|
||||||
|
|
||||||
PrepaintState {
|
PrepaintState {
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -1097,6 +1099,7 @@ impl Element for TextElement {
|
||||||
hover_highlight_path,
|
hover_highlight_path,
|
||||||
hover_definition_hitbox,
|
hover_definition_hitbox,
|
||||||
document_color_paths,
|
document_color_paths,
|
||||||
|
indent_guides_path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1189,6 +1192,11 @@ impl Element for TextElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Paint indent guides
|
||||||
|
if let Some(path) = prepaint.indent_guides_path.take() {
|
||||||
|
window.paint_path(path, cx.theme().secondary);
|
||||||
|
}
|
||||||
|
|
||||||
// Paint selections
|
// Paint selections
|
||||||
if window.is_window_active() {
|
if window.is_window_active() {
|
||||||
let secondary_selection = cx.theme().selection.saturation(0.1);
|
let secondary_selection = cx.theme().selection.saturation(0.1);
|
||||||
|
|
|
||||||
391
crates/ui/src/input/indent.rs
Normal file
391
crates/ui/src/input/indent.rs
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
use gpui::{
|
||||||
|
point, px, Context, EntityInputHandler as _, Path, PathBuilder, Pixels, SharedString, Window,
|
||||||
|
};
|
||||||
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
input::{
|
||||||
|
element::TextElement, mode::InputMode, Indent, IndentInline, InputState, LastLayout,
|
||||||
|
Outdent, OutdentInline,
|
||||||
|
},
|
||||||
|
RopeExt,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct TabSize {
|
||||||
|
/// Default is 2
|
||||||
|
pub tab_size: usize,
|
||||||
|
/// Set true to use `\t` as tab indent, default is false
|
||||||
|
pub hard_tabs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TabSize {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
tab_size: 2,
|
||||||
|
hard_tabs: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TabSize {
|
||||||
|
pub(super) fn to_string(&self) -> SharedString {
|
||||||
|
if self.hard_tabs {
|
||||||
|
"\t".into()
|
||||||
|
} else {
|
||||||
|
" ".repeat(self.tab_size).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count the indent size of the line in spaces.
|
||||||
|
pub fn indent_count(&self, line: &RopeSlice) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
for ch in line.chars() {
|
||||||
|
match ch {
|
||||||
|
'\t' => count += self.tab_size,
|
||||||
|
' ' => count += 1,
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputMode {
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn is_indentable(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn has_indent_guides(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
InputMode::CodeEditor { indent_guides, .. } => *indent_guides,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn tab_size(&self) -> TabSize {
|
||||||
|
match self {
|
||||||
|
InputMode::MultiLine { tab, .. } => *tab,
|
||||||
|
InputMode::CodeEditor { tab, .. } => *tab,
|
||||||
|
_ => TabSize::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextElement {
|
||||||
|
pub(super) fn layout_indent_guides(
|
||||||
|
&self,
|
||||||
|
state: &InputState,
|
||||||
|
last_layout: &LastLayout,
|
||||||
|
) -> Option<Path<Pixels>> {
|
||||||
|
if !state.mode.has_indent_guides() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tab_size = state.mode.tab_size();
|
||||||
|
let line_height = last_layout.line_height;
|
||||||
|
let visible_range = last_layout.visible_range.clone();
|
||||||
|
let mut builder = PathBuilder::stroke(px(1.));
|
||||||
|
let mut offset_y =
|
||||||
|
last_layout.visible_top + state.scroll_handle.offset().y + (line_height * 2 - px(3.));
|
||||||
|
let mut last_indents = vec![];
|
||||||
|
for ix in visible_range {
|
||||||
|
let line = state.text.slice_line(ix);
|
||||||
|
let line_layout = last_layout.line(ix).expect("line layout should exist");
|
||||||
|
let mut current_indents = vec![];
|
||||||
|
if line.len() > 0 {
|
||||||
|
let indent_count = tab_size.indent_count(&line);
|
||||||
|
for offset in (0..indent_count).step_by(tab_size.tab_size) {
|
||||||
|
let mut pos = line_layout
|
||||||
|
.position_for_index(offset, line_height)
|
||||||
|
.unwrap_or(point(px(0.), px(0.)));
|
||||||
|
|
||||||
|
pos.x += last_layout.line_number_width;
|
||||||
|
pos.y += offset_y;
|
||||||
|
|
||||||
|
builder.move_to(pos);
|
||||||
|
builder.line_to(point(pos.x, pos.y + line_height));
|
||||||
|
current_indents.push(pos.x);
|
||||||
|
}
|
||||||
|
} else if last_indents.len() > 0 {
|
||||||
|
for x in &last_indents {
|
||||||
|
let pos = point(*x, offset_y);
|
||||||
|
builder.move_to(pos);
|
||||||
|
builder.line_to(point(pos.x, pos.y + line_height));
|
||||||
|
}
|
||||||
|
current_indents = last_indents.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
offset_y += line_layout.wrapped_lines.len() * line_height;
|
||||||
|
last_indents = current_indents;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = builder.build().unwrap();
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
/// Set whether to show indent guides in code editor mode, default is true.
|
||||||
|
///
|
||||||
|
/// Only for [`InputMode::CodeEditor`] mode.
|
||||||
|
pub fn indent_guides(mut self, indent_guides: bool) -> Self {
|
||||||
|
debug_assert!(self.mode.is_code_editor());
|
||||||
|
if let InputMode::CodeEditor {
|
||||||
|
indent_guides: l, ..
|
||||||
|
} = &mut self.mode
|
||||||
|
{
|
||||||
|
*l = indent_guides;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set indent guides in code editor mode.
|
||||||
|
///
|
||||||
|
/// Only for [`InputMode::CodeEditor`] mode.
|
||||||
|
pub fn set_indent_guides(
|
||||||
|
&mut self,
|
||||||
|
indent_guides: bool,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
debug_assert!(self.mode.is_code_editor());
|
||||||
|
if let InputMode::CodeEditor {
|
||||||
|
indent_guides: l, ..
|
||||||
|
} = &mut self.mode
|
||||||
|
{
|
||||||
|
*l = indent_guides;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the tab size for the input.
|
||||||
|
///
|
||||||
|
/// Only for [`InputMode::MultiLine`] and [`InputMode::CodeEditor`] mode.
|
||||||
|
pub fn tab_size(mut self, tab: TabSize) -> Self {
|
||||||
|
debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
|
||||||
|
match &mut self.mode {
|
||||||
|
InputMode::MultiLine { tab: t, .. } => *t = tab,
|
||||||
|
InputMode::CodeEditor { tab: t, .. } => *t = tab,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn indent_inline(
|
||||||
|
&mut self,
|
||||||
|
_: &IndentInline,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.indent(false, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.indent(true, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn outdent_inline(
|
||||||
|
&mut self,
|
||||||
|
_: &OutdentInline,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.outdent(false, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn outdent_block(
|
||||||
|
&mut self,
|
||||||
|
_: &Outdent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.outdent(true, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.mode.is_indentable() {
|
||||||
|
cx.propagate();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let tab_indent = self.mode.tab_size().to_string();
|
||||||
|
let selected_range = self.selected_range;
|
||||||
|
let mut added_len = 0;
|
||||||
|
let is_selected = !self.selected_range.is_empty();
|
||||||
|
|
||||||
|
if is_selected || block {
|
||||||
|
let start_offset = self.start_of_line_of_selection(window, cx);
|
||||||
|
let mut offset = start_offset;
|
||||||
|
|
||||||
|
let selected_text = self
|
||||||
|
.text_for_range(
|
||||||
|
self.range_to_utf16(&(offset..selected_range.end)),
|
||||||
|
&mut None,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap_or("".into());
|
||||||
|
|
||||||
|
for line in selected_text.split('\n') {
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset))),
|
||||||
|
&tab_indent,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
added_len += tab_indent.len();
|
||||||
|
// +1 for "\n", the `\r` is included in the `line`.
|
||||||
|
offset += line.len() + tab_indent.len() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_selected {
|
||||||
|
self.selected_range = (start_offset..selected_range.end + added_len).into();
|
||||||
|
} else {
|
||||||
|
self.selected_range =
|
||||||
|
(selected_range.start + added_len..selected_range.end + added_len).into();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Selected none
|
||||||
|
let offset = self.selected_range.start;
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset))),
|
||||||
|
&tab_indent,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
added_len = tab_indent.len();
|
||||||
|
|
||||||
|
self.selected_range =
|
||||||
|
(selected_range.start + added_len..selected_range.end + added_len).into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.mode.is_indentable() {
|
||||||
|
cx.propagate();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let tab_indent = self.mode.tab_size().to_string();
|
||||||
|
let selected_range = self.selected_range;
|
||||||
|
let mut removed_len = 0;
|
||||||
|
let is_selected = !self.selected_range.is_empty();
|
||||||
|
|
||||||
|
if is_selected || block {
|
||||||
|
let start_offset = self.start_of_line_of_selection(window, cx);
|
||||||
|
let mut offset = start_offset;
|
||||||
|
|
||||||
|
let selected_text = self
|
||||||
|
.text_for_range(
|
||||||
|
self.range_to_utf16(&(offset..selected_range.end)),
|
||||||
|
&mut None,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap_or("".into());
|
||||||
|
|
||||||
|
for line in selected_text.split('\n') {
|
||||||
|
if line.starts_with(tab_indent.as_ref()) {
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
|
||||||
|
"",
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
removed_len += tab_indent.len();
|
||||||
|
|
||||||
|
// +1 for "\n"
|
||||||
|
offset += line.len().saturating_sub(tab_indent.len()) + 1;
|
||||||
|
} else {
|
||||||
|
offset += line.len() + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_selected {
|
||||||
|
self.selected_range =
|
||||||
|
(start_offset..selected_range.end.saturating_sub(removed_len)).into();
|
||||||
|
} else {
|
||||||
|
self.selected_range = (selected_range.start.saturating_sub(removed_len)
|
||||||
|
..selected_range.end.saturating_sub(removed_len))
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Selected none
|
||||||
|
let start_offset = self.selected_range.start;
|
||||||
|
let offset = self.start_of_line_of_selection(window, cx);
|
||||||
|
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
|
||||||
|
// FIXME: To improve performance
|
||||||
|
if self
|
||||||
|
.text
|
||||||
|
.slice(offset..self.text.len())
|
||||||
|
.to_string()
|
||||||
|
.starts_with(tab_indent.as_ref())
|
||||||
|
{
|
||||||
|
self.replace_text_in_range_silent(
|
||||||
|
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
|
||||||
|
"",
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
removed_len = tab_indent.len();
|
||||||
|
let new_offset = start_offset.saturating_sub(removed_len);
|
||||||
|
self.selected_range = (new_offset..new_offset).into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
|
use super::TabSize;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_size() {
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 2,
|
||||||
|
hard_tabs: false,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), " ");
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 4,
|
||||||
|
hard_tabs: false,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), " ");
|
||||||
|
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 2,
|
||||||
|
hard_tabs: true,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), "\t");
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 4,
|
||||||
|
hard_tabs: true,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.to_string(), "\t");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tab_size_indent_count() {
|
||||||
|
let tab = TabSize {
|
||||||
|
tab_size: 4,
|
||||||
|
hard_tabs: false,
|
||||||
|
};
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 2);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 4);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" \tabc")), 6);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc ")), 6);
|
||||||
|
assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ mod change;
|
||||||
mod clear_button;
|
mod clear_button;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
mod element;
|
mod element;
|
||||||
|
mod indent;
|
||||||
mod lsp;
|
mod lsp;
|
||||||
mod mask_pattern;
|
mod mask_pattern;
|
||||||
mod mode;
|
mod mode;
|
||||||
|
|
@ -18,9 +19,9 @@ mod text_wrapper;
|
||||||
|
|
||||||
pub(crate) use clear_button::*;
|
pub(crate) use clear_button::*;
|
||||||
pub use cursor::*;
|
pub use cursor::*;
|
||||||
|
pub use indent::TabSize;
|
||||||
pub use lsp::*;
|
pub use lsp::*;
|
||||||
pub use mask_pattern::MaskPattern;
|
pub use mask_pattern::MaskPattern;
|
||||||
pub use mode::TabSize;
|
|
||||||
pub use number_input::{NumberInput, NumberInputEvent, StepAction};
|
pub use number_input::{NumberInput, NumberInputEvent, StepAction};
|
||||||
pub use otp_input::*;
|
pub use otp_input::*;
|
||||||
pub use state::*;
|
pub use state::*;
|
||||||
|
|
|
||||||
|
|
@ -8,34 +8,7 @@ use tree_sitter::InputEdit;
|
||||||
use super::text_wrapper::TextWrapper;
|
use super::text_wrapper::TextWrapper;
|
||||||
use crate::highlighter::DiagnosticSet;
|
use crate::highlighter::DiagnosticSet;
|
||||||
use crate::highlighter::SyntaxHighlighter;
|
use crate::highlighter::SyntaxHighlighter;
|
||||||
use crate::input::RopeExt as _;
|
use crate::input::{RopeExt as _, TabSize};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub struct TabSize {
|
|
||||||
/// Default is 2
|
|
||||||
pub tab_size: usize,
|
|
||||||
/// Set true to use `\t` as tab indent, default is false
|
|
||||||
pub hard_tabs: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TabSize {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
tab_size: 2,
|
|
||||||
hard_tabs: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TabSize {
|
|
||||||
pub(super) fn to_string(&self) -> SharedString {
|
|
||||||
if self.hard_tabs {
|
|
||||||
"\t".into()
|
|
||||||
} else {
|
|
||||||
" ".repeat(self.tab_size).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub enum InputMode {
|
pub enum InputMode {
|
||||||
|
|
@ -56,6 +29,7 @@ pub enum InputMode {
|
||||||
/// Show line number
|
/// Show line number
|
||||||
line_number: bool,
|
line_number: bool,
|
||||||
language: SharedString,
|
language: SharedString,
|
||||||
|
indent_guides: bool,
|
||||||
highlighter: Rc<RefCell<Option<SyntaxHighlighter>>>,
|
highlighter: Rc<RefCell<Option<SyntaxHighlighter>>>,
|
||||||
diagnostics: DiagnosticSet,
|
diagnostics: DiagnosticSet,
|
||||||
},
|
},
|
||||||
|
|
@ -155,15 +129,6 @@ impl InputMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(super) fn tab_size(&self) -> Option<&TabSize> {
|
|
||||||
match self {
|
|
||||||
InputMode::MultiLine { tab, .. } => Some(tab),
|
|
||||||
InputMode::CodeEditor { tab, .. } => Some(tab),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn update_highlighter(
|
pub(super) fn update_highlighter(
|
||||||
&mut self,
|
&mut self,
|
||||||
selected_range: &Range<usize>,
|
selected_range: &Range<usize>,
|
||||||
|
|
@ -237,33 +202,3 @@ impl InputMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::TabSize;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_tab_size() {
|
|
||||||
let tab = TabSize {
|
|
||||||
tab_size: 2,
|
|
||||||
hard_tabs: false,
|
|
||||||
};
|
|
||||||
assert_eq!(tab.to_string(), " ");
|
|
||||||
let tab = TabSize {
|
|
||||||
tab_size: 4,
|
|
||||||
hard_tabs: false,
|
|
||||||
};
|
|
||||||
assert_eq!(tab.to_string(), " ");
|
|
||||||
|
|
||||||
let tab = TabSize {
|
|
||||||
tab_size: 2,
|
|
||||||
hard_tabs: true,
|
|
||||||
};
|
|
||||||
assert_eq!(tab.to_string(), "\t");
|
|
||||||
let tab = TabSize {
|
|
||||||
tab_size: 4,
|
|
||||||
hard_tabs: true,
|
|
||||||
};
|
|
||||||
assert_eq!(tab.to_string(), "\t");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,8 @@ use sum_tree::Bias;
|
||||||
use unicode_segmentation::*;
|
use unicode_segmentation::*;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
blink_cursor::BlinkCursor,
|
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
|
||||||
change::Change,
|
mode::InputMode, number_input, text_wrapper::TextWrapper, TabSize,
|
||||||
element::TextElement,
|
|
||||||
mask_pattern::MaskPattern,
|
|
||||||
mode::{InputMode, TabSize},
|
|
||||||
number_input,
|
|
||||||
text_wrapper::TextWrapper,
|
|
||||||
};
|
};
|
||||||
use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
|
use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
|
||||||
use crate::input::{
|
use crate::input::{
|
||||||
|
|
@ -468,6 +463,7 @@ impl InputState {
|
||||||
language,
|
language,
|
||||||
highlighter: Rc::new(RefCell::new(None)),
|
highlighter: Rc::new(RefCell::new(None)),
|
||||||
line_number: true,
|
line_number: true,
|
||||||
|
indent_guides: true,
|
||||||
diagnostics: DiagnosticSet::new(&Rope::new()),
|
diagnostics: DiagnosticSet::new(&Rope::new()),
|
||||||
};
|
};
|
||||||
self.searchable = true;
|
self.searchable = true;
|
||||||
|
|
@ -505,19 +501,6 @@ impl InputState {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the tab size for the input.
|
|
||||||
///
|
|
||||||
/// Only for [`InputMode::MultiLine`] and [`InputMode::CodeEditor`] mode.
|
|
||||||
pub fn tab_size(mut self, tab: TabSize) -> Self {
|
|
||||||
debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
|
|
||||||
match &mut self.mode {
|
|
||||||
InputMode::MultiLine { tab: t, .. } => *t = tab,
|
|
||||||
InputMode::CodeEditor { tab: t, .. } => *t = tab,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the number of rows for the multi-line Textarea.
|
/// Set the number of rows for the multi-line Textarea.
|
||||||
///
|
///
|
||||||
/// This is only used when `multi_line` is set to true.
|
/// This is only used when `multi_line` is set to true.
|
||||||
|
|
@ -992,7 +975,11 @@ impl InputState {
|
||||||
/// Get start line of selection start or end (The min value).
|
/// Get start line of selection start or end (The min value).
|
||||||
///
|
///
|
||||||
/// This is means is always get the first line of selection.
|
/// This is means is always get the first line of selection.
|
||||||
fn start_of_line_of_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) -> usize {
|
pub(super) fn start_of_line_of_selection(
|
||||||
|
&mut self,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> usize {
|
||||||
if self.mode.is_single_line() {
|
if self.mode.is_single_line() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -1165,169 +1152,6 @@ impl InputState {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn indent_inline(
|
|
||||||
&mut self,
|
|
||||||
_: &IndentInline,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.indent(false, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.indent(true, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn outdent_inline(
|
|
||||||
&mut self,
|
|
||||||
_: &OutdentInline,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.outdent(false, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn outdent_block(
|
|
||||||
&mut self,
|
|
||||||
_: &Outdent,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.outdent(true, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(tab_size) = self.mode.tab_size() else {
|
|
||||||
cx.propagate();
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let tab_indent = tab_size.to_string();
|
|
||||||
let selected_range = self.selected_range;
|
|
||||||
let mut added_len = 0;
|
|
||||||
let is_selected = !self.selected_range.is_empty();
|
|
||||||
|
|
||||||
if is_selected || block {
|
|
||||||
let start_offset = self.start_of_line_of_selection(window, cx);
|
|
||||||
let mut offset = start_offset;
|
|
||||||
|
|
||||||
let selected_text = self
|
|
||||||
.text_for_range(
|
|
||||||
self.range_to_utf16(&(offset..selected_range.end)),
|
|
||||||
&mut None,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.unwrap_or("".into());
|
|
||||||
|
|
||||||
for line in selected_text.split('\n') {
|
|
||||||
self.replace_text_in_range_silent(
|
|
||||||
Some(self.range_to_utf16(&(offset..offset))),
|
|
||||||
&tab_indent,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
added_len += tab_indent.len();
|
|
||||||
// +1 for "\n", the `\r` is included in the `line`.
|
|
||||||
offset += line.len() + tab_indent.len() + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_selected {
|
|
||||||
self.selected_range = (start_offset..selected_range.end + added_len).into();
|
|
||||||
} else {
|
|
||||||
self.selected_range =
|
|
||||||
(selected_range.start + added_len..selected_range.end + added_len).into();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Selected none
|
|
||||||
let offset = self.selected_range.start;
|
|
||||||
self.replace_text_in_range_silent(
|
|
||||||
Some(self.range_to_utf16(&(offset..offset))),
|
|
||||||
&tab_indent,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
added_len = tab_indent.len();
|
|
||||||
|
|
||||||
self.selected_range =
|
|
||||||
(selected_range.start + added_len..selected_range.end + added_len).into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(tab_size) = self.mode.tab_size() else {
|
|
||||||
cx.propagate();
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let tab_indent = tab_size.to_string();
|
|
||||||
let selected_range = self.selected_range;
|
|
||||||
let mut removed_len = 0;
|
|
||||||
let is_selected = !self.selected_range.is_empty();
|
|
||||||
|
|
||||||
if is_selected || block {
|
|
||||||
let start_offset = self.start_of_line_of_selection(window, cx);
|
|
||||||
let mut offset = start_offset;
|
|
||||||
|
|
||||||
let selected_text = self
|
|
||||||
.text_for_range(
|
|
||||||
self.range_to_utf16(&(offset..selected_range.end)),
|
|
||||||
&mut None,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.unwrap_or("".into());
|
|
||||||
|
|
||||||
for line in selected_text.split('\n') {
|
|
||||||
if line.starts_with(tab_indent.as_ref()) {
|
|
||||||
self.replace_text_in_range_silent(
|
|
||||||
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
|
|
||||||
"",
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
removed_len += tab_indent.len();
|
|
||||||
|
|
||||||
// +1 for "\n"
|
|
||||||
offset += line.len().saturating_sub(tab_indent.len()) + 1;
|
|
||||||
} else {
|
|
||||||
offset += line.len() + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_selected {
|
|
||||||
self.selected_range =
|
|
||||||
(start_offset..selected_range.end.saturating_sub(removed_len)).into();
|
|
||||||
} else {
|
|
||||||
self.selected_range = (selected_range.start.saturating_sub(removed_len)
|
|
||||||
..selected_range.end.saturating_sub(removed_len))
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Selected none
|
|
||||||
let start_offset = self.selected_range.start;
|
|
||||||
let offset = self.start_of_line_of_selection(window, cx);
|
|
||||||
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
|
|
||||||
// FIXME: To improve performance
|
|
||||||
if self
|
|
||||||
.text
|
|
||||||
.slice(offset..self.text.len())
|
|
||||||
.to_string()
|
|
||||||
.starts_with(tab_indent.as_ref())
|
|
||||||
{
|
|
||||||
self.replace_text_in_range_silent(
|
|
||||||
Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
|
|
||||||
"",
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
removed_len = tab_indent.len();
|
|
||||||
let new_offset = start_offset.saturating_sub(removed_len);
|
|
||||||
self.selected_range = (new_offset..new_offset).into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.replace_text("", window, cx);
|
self.replace_text("", window, cx);
|
||||||
self.selected_range = (0..0).into();
|
self.selected_range = (0..0).into();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue