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::{
|
||||
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))
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue