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:
Jason Lee 2025-07-23 13:33:09 +08:00 committed by GitHub
parent dfbda3999a
commit 271116926c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 126 additions and 18 deletions

View file

@ -1,11 +1,11 @@
use gpui::*;
use gpui::{prelude::FluentBuilder, *};
use gpui_component::{
button::{Button, ButtonVariants as _},
dropdown::{Dropdown, DropdownEvent, DropdownState},
h_flex,
highlighter::{Language, LanguageConfig, LanguageRegistry},
input::{InputEvent, InputState, Marker, TabSize, TextInput},
v_flex, ActiveTheme, Selectable, Sizable,
v_flex, ActiveTheme, ContextModal, IconName, Sizable,
};
use story::Assets;
@ -24,7 +24,8 @@ fn init(cx: &mut App) {
}
pub struct Example {
input_state: Entity<InputState>,
editor: Entity<InputState>,
go_to_line_state: Entity<InputState>,
language_state: Entity<DropdownState<Vec<SharedString>>>,
language: Lang,
line_number: bool,
@ -90,7 +91,7 @@ const LANGUAGES: [(Lang, &'static str); 10] = [
impl Example {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let default_language = LANGUAGES[0].clone();
let input_state = cx.new(|cx| {
let editor = cx.new(|cx| {
InputState::new(window, cx)
.code_editor(default_language.0.name().to_string())
.line_number(true)
@ -101,6 +102,7 @@ impl Example {
.default_value(default_language.1)
.placeholder("Enter your code here...")
});
let go_to_line_state = cx.new(|cx| InputState::new(window, cx));
let language_state = cx.new(|cx| {
DropdownState::new(
LANGUAGES.iter().map(|s| s.0.name().into()).collect(),
@ -111,7 +113,7 @@ impl Example {
});
let _subscribes = vec![
cx.subscribe(&input_state, |_, _, _: &InputEvent, cx| {
cx.subscribe(&editor, |_, _, _: &InputEvent, cx| {
cx.notify();
}),
cx.subscribe(
@ -132,7 +134,8 @@ impl Example {
];
Self {
input_state,
editor,
go_to_line_state,
language_state,
language: default_language.0,
line_number: true,
@ -150,7 +153,7 @@ impl Example {
return;
}
self.input_state.update(cx, |state, cx| {
self.editor.update(cx, |state, cx| {
state.set_markers(
vec![
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 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_highlighter(language, cx);
});
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 {
@ -190,10 +232,10 @@ impl Render for Example {
.id("source")
.w_full()
.flex_1()
.p_4()
.gap_2()
.child(
TextInput::new(&self.input_state)
TextInput::new(&self.editor)
.bordered(false)
.h_full()
.font_family("Monaco")
.text_size(px(12.))
@ -203,6 +245,11 @@ impl Render for Example {
h_flex()
.justify_between()
.text_sm()
.bg(cx.theme().secondary)
.py_1p5()
.px_4()
.border_t_1()
.border_color(cx.theme().border)
.text_color(cx.theme().muted_foreground)
.child(
h_flex()
@ -210,17 +257,17 @@ impl Render for Example {
.child(
Dropdown::new(&self.language_state)
.menu_width(px(160.))
.small(),
.xsmall(),
)
.child(
Button::new("line-number")
.ghost()
.when(self.line_number, |this| this.icon(IconName::Check))
.label("Line Number")
.small()
.selected(self.line_number)
.xsmall()
.on_click(cx.listener(|this, _, window, cx| {
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);
});
cx.notify();
@ -228,9 +275,14 @@ impl Render for Example {
),
)
.child({
let loc = self.input_state.read(cx).line_column();
let cursor = self.input_state.read(cx).cursor();
format!("{} ({} c)", loc, cursor.offset())
let loc = self.editor.read(cx).line_column();
let cursor = self.editor.read(cx).cursor();
Button::new("line-column")
.ghost()
.xsmall()
.label(format!("{} ({} c)", loc, cursor.offset()))
.on_click(cx.listener(Self::go_to_line))
}),
),
)

View file

@ -804,11 +804,36 @@ impl InputState {
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 {
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.
pub fn focus(&self, window: &mut Window, _: &mut Context<Self>) {
self.focus_handle.focus(window);
@ -1319,6 +1344,9 @@ impl InputState {
// Add newline and indent
let new_line_text = format!("\n{}", indent);
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 {

View file

@ -115,4 +115,32 @@ impl TextWrapper {
(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)
}
}