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::{ 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))
}), }),
), ),
) )

View file

@ -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 {

View file

@ -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)
}
} }