code-editor: Add go_to_line method to CodeEditor. (#1081)
- Fix to emit `enter` press key in Input single-line mode.
This commit is contained in:
parent
dfbda3999a
commit
271116926c
3 changed files with 126 additions and 18 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
use gpui::*;
|
use gpui::{prelude::FluentBuilder, *};
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
button::{Button, ButtonVariants as _},
|
button::{Button, ButtonVariants as _},
|
||||||
dropdown::{Dropdown, DropdownEvent, DropdownState},
|
dropdown::{Dropdown, DropdownEvent, DropdownState},
|
||||||
h_flex,
|
h_flex,
|
||||||
highlighter::{Language, LanguageConfig, LanguageRegistry},
|
highlighter::{Language, LanguageConfig, LanguageRegistry},
|
||||||
input::{InputEvent, InputState, Marker, TabSize, TextInput},
|
input::{InputEvent, InputState, Marker, TabSize, TextInput},
|
||||||
v_flex, ActiveTheme, Selectable, Sizable,
|
v_flex, ActiveTheme, ContextModal, IconName, Sizable,
|
||||||
};
|
};
|
||||||
use story::Assets;
|
use story::Assets;
|
||||||
|
|
||||||
|
|
@ -24,7 +24,8 @@ fn init(cx: &mut App) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Example {
|
pub struct Example {
|
||||||
input_state: Entity<InputState>,
|
editor: Entity<InputState>,
|
||||||
|
go_to_line_state: Entity<InputState>,
|
||||||
language_state: Entity<DropdownState<Vec<SharedString>>>,
|
language_state: Entity<DropdownState<Vec<SharedString>>>,
|
||||||
language: Lang,
|
language: Lang,
|
||||||
line_number: bool,
|
line_number: bool,
|
||||||
|
|
@ -90,7 +91,7 @@ const LANGUAGES: [(Lang, &'static str); 10] = [
|
||||||
impl Example {
|
impl Example {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let default_language = LANGUAGES[0].clone();
|
let default_language = LANGUAGES[0].clone();
|
||||||
let input_state = cx.new(|cx| {
|
let editor = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
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)
|
||||||
|
|
@ -101,6 +102,7 @@ impl Example {
|
||||||
.default_value(default_language.1)
|
.default_value(default_language.1)
|
||||||
.placeholder("Enter your code here...")
|
.placeholder("Enter your code here...")
|
||||||
});
|
});
|
||||||
|
let go_to_line_state = cx.new(|cx| InputState::new(window, cx));
|
||||||
let language_state = cx.new(|cx| {
|
let language_state = cx.new(|cx| {
|
||||||
DropdownState::new(
|
DropdownState::new(
|
||||||
LANGUAGES.iter().map(|s| s.0.name().into()).collect(),
|
LANGUAGES.iter().map(|s| s.0.name().into()).collect(),
|
||||||
|
|
@ -111,7 +113,7 @@ impl Example {
|
||||||
});
|
});
|
||||||
|
|
||||||
let _subscribes = vec![
|
let _subscribes = vec![
|
||||||
cx.subscribe(&input_state, |_, _, _: &InputEvent, cx| {
|
cx.subscribe(&editor, |_, _, _: &InputEvent, cx| {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}),
|
}),
|
||||||
cx.subscribe(
|
cx.subscribe(
|
||||||
|
|
@ -132,7 +134,8 @@ impl Example {
|
||||||
];
|
];
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
input_state,
|
editor,
|
||||||
|
go_to_line_state,
|
||||||
language_state,
|
language_state,
|
||||||
language: default_language.0,
|
language: default_language.0,
|
||||||
line_number: true,
|
line_number: true,
|
||||||
|
|
@ -150,7 +153,7 @@ impl Example {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.input_state.update(cx, |state, cx| {
|
self.editor.update(cx, |state, cx| {
|
||||||
state.set_markers(
|
state.set_markers(
|
||||||
vec![
|
vec![
|
||||||
Marker::new("warning", (2, 1), (2, 31), "Import but not used."),
|
Marker::new("warning", (2, 1), (2, 31), "Import but not used."),
|
||||||
|
|
@ -171,13 +174,52 @@ impl Example {
|
||||||
|
|
||||||
let language = self.language.name().to_string();
|
let language = self.language.name().to_string();
|
||||||
let code = LANGUAGES.iter().find(|s| s.0.name() == language).unwrap().1;
|
let code = LANGUAGES.iter().find(|s| s.0.name() == language).unwrap().1;
|
||||||
self.input_state.update(cx, |state, cx| {
|
self.editor.update(cx, |state, cx| {
|
||||||
state.set_value(code, window, cx);
|
state.set_value(code, window, cx);
|
||||||
state.set_highlighter(language, cx);
|
state.set_highlighter(language, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.need_update = false;
|
self.need_update = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn go_to_line(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
let input_state = self.go_to_line_state.clone();
|
||||||
|
|
||||||
|
window.open_modal(cx, move |modal, window, cx| {
|
||||||
|
input_state.update(cx, |state, cx| {
|
||||||
|
state.set_placeholder(format!("{}", editor.read(cx).line_column()), window, cx);
|
||||||
|
state.focus(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
modal
|
||||||
|
.title("Go to line")
|
||||||
|
.child(TextInput::new(&input_state))
|
||||||
|
.confirm()
|
||||||
|
.on_ok({
|
||||||
|
let editor = editor.clone();
|
||||||
|
let input_state = input_state.clone();
|
||||||
|
move |_, window, cx| {
|
||||||
|
let query = input_state.read(cx).value();
|
||||||
|
let mut parts = query
|
||||||
|
.split(':')
|
||||||
|
.map(|s| s.trim().parse::<usize>().ok())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter();
|
||||||
|
let Some(line) = parts.next().and_then(|l| l) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let column = parts.next().and_then(|c| c);
|
||||||
|
|
||||||
|
editor.update(cx, |state, cx| {
|
||||||
|
state.go_to_line(line, column, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Example {
|
impl Render for Example {
|
||||||
|
|
@ -190,10 +232,10 @@ impl Render for Example {
|
||||||
.id("source")
|
.id("source")
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.p_4()
|
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&self.input_state)
|
TextInput::new(&self.editor)
|
||||||
|
.bordered(false)
|
||||||
.h_full()
|
.h_full()
|
||||||
.font_family("Monaco")
|
.font_family("Monaco")
|
||||||
.text_size(px(12.))
|
.text_size(px(12.))
|
||||||
|
|
@ -203,6 +245,11 @@ impl Render for Example {
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
|
.bg(cx.theme().secondary)
|
||||||
|
.py_1p5()
|
||||||
|
.px_4()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
@ -210,17 +257,17 @@ impl Render for Example {
|
||||||
.child(
|
.child(
|
||||||
Dropdown::new(&self.language_state)
|
Dropdown::new(&self.language_state)
|
||||||
.menu_width(px(160.))
|
.menu_width(px(160.))
|
||||||
.small(),
|
.xsmall(),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("line-number")
|
Button::new("line-number")
|
||||||
.ghost()
|
.ghost()
|
||||||
|
.when(self.line_number, |this| this.icon(IconName::Check))
|
||||||
.label("Line Number")
|
.label("Line Number")
|
||||||
.small()
|
.xsmall()
|
||||||
.selected(self.line_number)
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.line_number = !this.line_number;
|
this.line_number = !this.line_number;
|
||||||
this.input_state.update(cx, |state, cx| {
|
this.editor.update(cx, |state, cx| {
|
||||||
state.set_line_number(this.line_number, window, cx);
|
state.set_line_number(this.line_number, window, cx);
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
@ -228,9 +275,14 @@ impl Render for Example {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child({
|
.child({
|
||||||
let loc = self.input_state.read(cx).line_column();
|
let loc = self.editor.read(cx).line_column();
|
||||||
let cursor = self.input_state.read(cx).cursor();
|
let cursor = self.editor.read(cx).cursor();
|
||||||
format!("{} ({} c)", loc, cursor.offset())
|
|
||||||
|
Button::new("line-column")
|
||||||
|
.ghost()
|
||||||
|
.xsmall()
|
||||||
|
.label(format!("{} ({} c)", loc, cursor.offset()))
|
||||||
|
.on_click(cx.listener(Self::go_to_line))
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -804,11 +804,36 @@ impl InputState {
|
||||||
self.mask_pattern.unmask(&self.text).into()
|
self.mask_pattern.unmask(&self.text).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the line and column (1-based) of the cursor.
|
/// Return the (1-based) line and column of the cursor.
|
||||||
pub fn line_column(&self) -> LineColumn {
|
pub fn line_column(&self) -> LineColumn {
|
||||||
self.text_wrapper.line_column(self.cursor().offset)
|
self.text_wrapper.line_column(self.cursor().offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set (1-based) line and column of the cursor.
|
||||||
|
///
|
||||||
|
/// This will move the cursor to the specified line and column, and update the selection range.
|
||||||
|
///
|
||||||
|
/// - The `column` is optional, if it is `None`, it will return the start of the line.
|
||||||
|
/// - If the `line` is 0, it will return 0.
|
||||||
|
/// - If the `line` is greater than the number of lines, it will return
|
||||||
|
/// the length of the text.
|
||||||
|
///
|
||||||
|
/// Ignore, if the line, column is invalid.
|
||||||
|
pub fn go_to_line(
|
||||||
|
&mut self,
|
||||||
|
line: usize,
|
||||||
|
column: Option<usize>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if let Some(offset) = self
|
||||||
|
.text_wrapper
|
||||||
|
.offset_for_line_column(line, column.unwrap_or(1))
|
||||||
|
{
|
||||||
|
self.move_to(Cursor::new(offset), window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Focus the input field.
|
/// Focus the input field.
|
||||||
pub fn focus(&self, window: &mut Window, _: &mut Context<Self>) {
|
pub fn focus(&self, window: &mut Window, _: &mut Context<Self>) {
|
||||||
self.focus_handle.focus(window);
|
self.focus_handle.focus(window);
|
||||||
|
|
@ -1319,6 +1344,9 @@ impl InputState {
|
||||||
// Add newline and indent
|
// Add newline and indent
|
||||||
let new_line_text = format!("\n{}", indent);
|
let new_line_text = format!("\n{}", indent);
|
||||||
self.replace_text_in_range(None, &new_line_text, window, cx);
|
self.replace_text_in_range(None, &new_line_text, window, cx);
|
||||||
|
} else {
|
||||||
|
// Single line input, just emit the event (e.g.: In a modal dialog to confirm).
|
||||||
|
cx.propagate();
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.emit(InputEvent::PressEnter {
|
cx.emit(InputEvent::PressEnter {
|
||||||
|
|
|
||||||
|
|
@ -115,4 +115,32 @@ impl TextWrapper {
|
||||||
|
|
||||||
(line + 1, column + 1).into()
|
(line + 1, column + 1).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the offset of the given line and column (1-based).
|
||||||
|
///
|
||||||
|
/// - If the `line` is 0, it will return 0.
|
||||||
|
/// - If the `line` is greater than the number of lines, it will return
|
||||||
|
/// the length of the text.
|
||||||
|
pub(super) fn offset_for_line_column(&self, line: usize, column: usize) -> Option<usize> {
|
||||||
|
if line == 0 || self.lines.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = line.saturating_sub(1);
|
||||||
|
if line >= self.lines.len() {
|
||||||
|
return Some(self.text.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(line_wrap) = &self.lines.get(line) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset = line_wrap.range.start;
|
||||||
|
if column == 0 {
|
||||||
|
return Some(offset);
|
||||||
|
}
|
||||||
|
let offset = offset + column.saturating_sub(1).min(line_wrap.range.len());
|
||||||
|
|
||||||
|
Some(offset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue