aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/application.rs107
-rw-r--r--helix-term/src/commands.rs662
-rw-r--r--helix-term/src/job.rs4
-rw-r--r--helix-term/src/keymap.rs147
-rw-r--r--helix-term/src/ui/completion.rs45
-rw-r--r--helix-term/src/ui/editor.rs45
-rw-r--r--helix-term/src/ui/markdown.rs30
-rw-r--r--helix-term/src/ui/menu.rs76
-rw-r--r--helix-term/src/ui/mod.rs29
-rw-r--r--helix-term/src/ui/picker.rs25
-rw-r--r--helix-term/src/ui/popup.rs61
-rw-r--r--helix-term/src/ui/text.rs20
12 files changed, 811 insertions, 440 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 7a573adc..b99fccdf 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -434,16 +434,42 @@ impl Application {
};
match notification {
- Notification::PublishDiagnostics(params) => {
- let path = Some(params.uri.to_file_path().unwrap());
+ Notification::Initialized => {
+ let language_server =
+ match self.editor.language_servers.get_by_id(server_id) {
+ Some(language_server) => language_server,
+ None => {
+ warn!("can't find language server with id `{}`", server_id);
+ return;
+ }
+ };
- let doc = self
- .editor
- .documents
- .iter_mut()
- .find(|(_, doc)| doc.path() == path.as_ref());
+ let docs = self.editor.documents().filter(|doc| {
+ doc.language_server().map(|server| server.id()) == Some(server_id)
+ });
- if let Some((_, doc)) = doc {
+ // trigger textDocument/didOpen for docs that are already open
+ for doc in docs {
+ // TODO: extract and share with editor.open
+ let language_id = doc
+ .language()
+ .and_then(|s| s.split('.').last()) // source.rust
+ .map(ToOwned::to_owned)
+ .unwrap_or_default();
+
+ tokio::spawn(language_server.text_document_did_open(
+ doc.url().unwrap(),
+ doc.version(),
+ doc.text(),
+ language_id,
+ ));
+ }
+ }
+ Notification::PublishDiagnostics(params) => {
+ let path = params.uri.to_file_path().unwrap();
+ let doc = self.editor.document_by_path_mut(&path);
+
+ if let Some(doc) = doc {
let text = doc.text();
let diagnostics = params
@@ -506,7 +532,7 @@ impl Application {
log::warn!("unhandled window/showMessage: {:?}", params);
}
Notification::LogMessage(params) => {
- log::warn!("unhandled window/logMessage: {:?}", params);
+ log::info!("window/logMessage: {:?}", params);
}
Notification::ProgressMessage(params) => {
let lsp::ProgressParams { token, value } = params;
@@ -588,10 +614,27 @@ impl Application {
Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
method, params, id, ..
}) => {
+ let language_server = match self.editor.language_servers.get_by_id(server_id) {
+ Some(language_server) => language_server,
+ None => {
+ warn!("can't find language server with id `{}`", server_id);
+ return;
+ }
+ };
+
let call = match MethodCall::parse(&method, params) {
Some(call) => call,
None => {
error!("Method not found {}", method);
+ // language_server.reply(
+ // call.id,
+ // // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
+ // Err(helix_lsp::jsonrpc::Error {
+ // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound,
+ // message: "Method not found".to_string(),
+ // data: None,
+ // }),
+ // );
return;
}
};
@@ -604,53 +647,9 @@ impl Application {
if spinner.is_stopped() {
spinner.start();
}
-
- let doc = self.editor.documents().find(|doc| {
- doc.language_server()
- .map(|server| server.id() == server_id)
- .unwrap_or_default()
- });
- match doc {
- Some(doc) => {
- // it's ok to unwrap, we check for the language server before
- let server = doc.language_server().unwrap();
- tokio::spawn(server.reply(id, Ok(serde_json::Value::Null)));
- }
- None => {
- if let Some(server) =
- self.editor.language_servers.get_by_id(server_id)
- {
- log::warn!(
- "missing document with language server id `{}`",
- server_id
- );
- tokio::spawn(server.reply(
- id,
- Err(helix_lsp::jsonrpc::Error {
- code: helix_lsp::jsonrpc::ErrorCode::InternalError,
- message: "document missing".to_string(),
- data: None,
- }),
- ));
- } else {
- log::warn!(
- "can't find language server with id `{}`",
- server_id
- );
- }
- }
- }
+ tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
}
}
- // self.language_server.reply(
- // call.id,
- // // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
- // Err(helix_lsp::jsonrpc::Error {
- // code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound,
- // message: "Method not found".to_string(),
- // data: None,
- // }),
- // );
}
e => unreachable!("{:?}", e),
}
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index a4dd080b..acaba6d6 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -9,7 +9,7 @@ use helix_core::{
match_brackets,
movement::{self, Direction},
object, pos_at_coords,
- regex::{self, Regex},
+ regex::{self, Regex, RegexBuilder},
register::Register,
search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
RopeSlice, Selection, SmallVec, Tendril, Transaction,
@@ -35,7 +35,7 @@ use crate::{
};
use crate::job::{self, Job, Jobs};
-use futures_util::FutureExt;
+use futures_util::{FutureExt, StreamExt};
use std::num::NonZeroUsize;
use std::{collections::HashMap, fmt, future::Future};
@@ -47,8 +47,13 @@ use std::{
use once_cell::sync::Lazy;
use serde::de::{self, Deserialize, Deserializer};
+use grep_regex::RegexMatcherBuilder;
+use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
+use ignore::{DirEntry, WalkBuilder, WalkState};
+use tokio_stream::wrappers::UnboundedReceiverStream;
+
pub struct Context<'a> {
- pub selected_register: helix_view::RegisterSelection,
+ pub register: Option<char>,
pub count: Option<NonZeroUsize>,
pub editor: &'a mut Editor,
@@ -166,6 +171,7 @@ impl Command {
#[rustfmt::skip]
commands!(
+ no_op, "Do nothing",
move_char_left, "Move left",
move_char_right, "Move right",
move_line_up, "Move up",
@@ -184,6 +190,9 @@ impl Command {
move_next_long_word_end, "Move to end of next long word",
extend_next_word_start, "Extend to beginning of next word",
extend_prev_word_start, "Extend to beginning of previous word",
+ extend_next_long_word_start, "Extend to beginning of next long word",
+ extend_prev_long_word_start, "Extend to beginning of previous long word",
+ extend_next_long_word_end, "Extend to end of next long word",
extend_next_word_end, "Extend to end of next word",
find_till_char, "Move till next occurance of char",
find_next_char, "Move to next occurance of char",
@@ -209,6 +218,7 @@ impl Command {
search_next, "Select next search match",
extend_search_next, "Add next search match to selection",
search_selection, "Use current selection as search pattern",
+ global_search, "Global Search in workspace folder",
extend_line, "Select current line, if already selected, extend to next line",
extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
delete_selection, "Delete selection",
@@ -253,6 +263,9 @@ impl Command {
// TODO: different description ?
goto_line_end_newline, "Goto line end",
goto_first_nonwhitespace, "Goto first non-blank in line",
+ extend_to_line_start, "Extend to line start",
+ extend_to_line_end, "Extend to line end",
+ extend_to_line_end_newline, "Extend to line end",
signature_help, "Show signature help",
insert_tab, "Insert tab char",
insert_newline, "Insert newline char",
@@ -281,6 +294,7 @@ impl Command {
join_selections, "Join lines inside selection",
keep_selections, "Keep selections matching regex",
keep_primary_selection, "Keep primary selection",
+ remove_primary_selection, "Remove primary selection",
completion, "Invoke completion popup",
hover, "Show docs for item under cursor",
toggle_comments, "Comment/uncomment selections",
@@ -372,52 +386,58 @@ impl PartialEq for Command {
}
}
-fn move_char_left(cx: &mut Context) {
+fn no_op(_cx: &mut Context) {}
+
+fn move_impl<F>(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement)
+where
+ F: Fn(RopeSlice, Range, Direction, usize, Movement) -> Range,
+{
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).clone().transform(|range| {
- movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move)
- });
+ let selection = doc
+ .selection(view.id)
+ .clone()
+ .transform(|range| move_fn(text, range, dir, count, behaviour));
doc.set_selection(view.id, selection);
}
-fn move_char_right(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
+use helix_core::movement::{move_horizontally, move_vertically};
- let selection = doc.selection(view.id).clone().transform(|range| {
- movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move)
- });
- doc.set_selection(view.id, selection);
+fn move_char_left(cx: &mut Context) {
+ move_impl(cx, move_horizontally, Direction::Backward, Movement::Move)
}
-fn move_line_up(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
+fn move_char_right(cx: &mut Context) {
+ move_impl(cx, move_horizontally, Direction::Forward, Movement::Move)
+}
- let selection = doc.selection(view.id).clone().transform(|range| {
- movement::move_vertically(text, range, Direction::Backward, count, Movement::Move)
- });
- doc.set_selection(view.id, selection);
+fn move_line_up(cx: &mut Context) {
+ move_impl(cx, move_vertically, Direction::Backward, Movement::Move)
}
fn move_line_down(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
+ move_impl(cx, move_vertically, Direction::Forward, Movement::Move)
+}
- let selection = doc.selection(view.id).clone().transform(|range| {
- movement::move_vertically(text, range, Direction::Forward, count, Movement::Move)
- });
- doc.set_selection(view.id, selection);
+fn extend_char_left(cx: &mut Context) {
+ move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend)
}
-fn goto_line_end(cx: &mut Context) {
- let (view, doc) = current!(cx.editor);
+fn extend_char_right(cx: &mut Context) {
+ move_impl(cx, move_horizontally, Direction::Forward, Movement::Extend)
+}
+
+fn extend_line_up(cx: &mut Context) {
+ move_impl(cx, move_vertically, Direction::Backward, Movement::Extend)
+}
+
+fn extend_line_down(cx: &mut Context) {
+ move_impl(cx, move_vertically, Direction::Forward, Movement::Extend)
+}
+
+fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) {
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone().transform(|range| {
@@ -427,26 +447,60 @@ fn goto_line_end(cx: &mut Context) {
let pos = graphemes::prev_grapheme_boundary(text, line_end_char_index(&text, line))
.max(line_start);
- range.put_cursor(text, pos, doc.mode == Mode::Select)
+ range.put_cursor(text, pos, movement == Movement::Extend)
});
doc.set_selection(view.id, selection);
}
-fn goto_line_end_newline(cx: &mut Context) {
+fn goto_line_end(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+ goto_line_end_impl(
+ view,
+ doc,
+ if doc.mode == Mode::Select {
+ Movement::Extend
+ } else {
+ Movement::Move
+ },
+ )
+}
+
+fn extend_to_line_end(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ goto_line_end_impl(view, doc, Movement::Extend)
+}
+
+fn goto_line_end_newline_impl(view: &mut View, doc: &mut Document, movement: Movement) {
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone().transform(|range| {
let line = range.cursor_line(text);
let pos = line_end_char_index(&text, line);
- range.put_cursor(text, pos, doc.mode == Mode::Select)
+ range.put_cursor(text, pos, movement == Movement::Extend)
});
doc.set_selection(view.id, selection);
}
-fn goto_line_start(cx: &mut Context) {
+fn goto_line_end_newline(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ goto_line_end_newline_impl(
+ view,
+ doc,
+ if doc.mode == Mode::Select {
+ Movement::Extend
+ } else {
+ Movement::Move
+ },
+ )
+}
+
+fn extend_to_line_end_newline(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+ goto_line_end_newline_impl(view, doc, Movement::Extend)
+}
+
+fn goto_line_start_impl(view: &mut View, doc: &mut Document, movement: Movement) {
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone().transform(|range| {
@@ -454,11 +508,29 @@ fn goto_line_start(cx: &mut Context) {
// adjust to start of the line
let pos = text.line_to_char(line);
- range.put_cursor(text, pos, doc.mode == Mode::Select)
+ range.put_cursor(text, pos, movement == Movement::Extend)
});
doc.set_selection(view.id, selection);
}
+fn goto_line_start(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ goto_line_start_impl(
+ view,
+ doc,
+ if doc.mode == Mode::Select {
+ Movement::Extend
+ } else {
+ Movement::Move
+ },
+ )
+}
+
+fn extend_to_line_start(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ goto_line_start_impl(view, doc, Movement::Extend)
+}
+
fn goto_first_nonwhitespace(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -512,7 +584,10 @@ fn goto_window_bottom(cx: &mut Context) {
goto_window(cx, Align::Bottom)
}
-fn move_next_word_start(cx: &mut Context) {
+fn move_word_impl<F>(cx: &mut Context, move_fn: F)
+where
+ F: Fn(RopeSlice, Range, usize) -> Range,
+{
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -520,68 +595,32 @@ fn move_next_word_start(cx: &mut Context) {
let selection = doc
.selection(view.id)
.clone()
- .transform(|range| movement::move_next_word_start(text, range, count));
+ .transform(|range| move_fn(text, range, count));
doc.set_selection(view.id, selection);
}
-fn move_prev_word_start(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
+fn move_next_word_start(cx: &mut Context) {
+ move_word_impl(cx, movement::move_next_word_start)
+}
- let selection = doc
- .selection(view.id)
- .clone()
- .transform(|range| movement::move_prev_word_start(text, range, count));
- doc.set_selection(view.id, selection);
+fn move_prev_word_start(cx: &mut Context) {
+ move_word_impl(cx, movement::move_prev_word_start)
}
fn move_next_word_end(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
-
- let selection = doc
- .selection(view.id)
- .clone()
- .transform(|range| movement::move_next_word_end(text, range, count));
- doc.set_selection(view.id, selection);
+ move_word_impl(cx, movement::move_next_word_end)
}
fn move_next_long_word_start(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
-
- let selection = doc
- .selection(view.id)
- .clone()
- .transform(|range| movement::move_next_long_word_start(text, range, count));
- doc.set_selection(view.id, selection);
+ move_word_impl(cx, movement::move_next_long_word_start)
}
fn move_prev_long_word_start(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
-
- let selection = doc
- .selection(view.id)
- .clone()
- .transform(|range| movement::move_prev_long_word_start(text, range, count));
- doc.set_selection(view.id, selection);
+ move_word_impl(cx, movement::move_prev_long_word_start)
}
fn move_next_long_word_end(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
-
- let selection = doc
- .selection(view.id)
- .clone()
- .transform(|range| movement::move_next_long_word_end(text, range, count));
- doc.set_selection(view.id, selection);
+ move_word_impl(cx, movement::move_next_long_word_end)
}
fn goto_file_start(cx: &mut Context) {
@@ -600,43 +639,44 @@ fn goto_file_end(cx: &mut Context) {
doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
}
-fn extend_next_word_start(cx: &mut Context) {
+fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
+where
+ F: Fn(RopeSlice, Range, usize) -> Range,
+{
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone().transform(|range| {
- let word = movement::move_next_word_start(text, range, count);
+ let word = extend_fn(text, range, count);
let pos = word.cursor(text);
range.put_cursor(text, pos, true)
});
doc.set_selection(view.id, selection);
}
-fn extend_prev_word_start(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
+fn extend_next_word_start(cx: &mut Context) {
+ extend_word_impl(cx, movement::move_next_word_start)
+}
- let selection = doc.selection(view.id).clone().transform(|range| {
- let word = movement::move_prev_word_start(text, range, count);
- let pos = word.cursor(text);
- range.put_cursor(text, pos, true)
- });
- doc.set_selection(view.id, selection);
+fn extend_prev_word_start(cx: &mut Context) {
+ extend_word_impl(cx, movement::move_prev_word_start)
}
fn extend_next_word_end(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
+ extend_word_impl(cx, movement::move_next_word_end)
+}
- let selection = doc.selection(view.id).clone().transform(|range| {
- let word = movement::move_next_word_end(text, range, count);
- let pos = word.cursor(text);
- range.put_cursor(text, pos, true)
- });
- doc.set_selection(view.id, selection);
+fn extend_next_long_word_start(cx: &mut Context) {
+ extend_word_impl(cx, movement::move_next_long_word_start)
+}
+
+fn extend_prev_long_word_start(cx: &mut Context) {
+ extend_word_impl(cx, movement::move_prev_long_word_start)
+}
+
+fn extend_next_long_word_end(cx: &mut Context) {
+ extend_word_impl(cx, movement::move_next_long_word_end)
}
#[inline]
@@ -852,12 +892,25 @@ fn replace(cx: &mut Context) {
})
}
-fn switch_case(cx: &mut Context) {
+fn switch_case_impl<F>(cx: &mut Context, change_fn: F)
+where
+ F: Fn(Cow<str>) -> Tendril,
+{
let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
- let text: Tendril = range
- .fragment(doc.text().slice(..))
+ let text: Tendril = change_fn(range.fragment(doc.text().slice(..)));
+
+ (range.from(), range.to(), Some(text))
+ });
+
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+}
+
+fn switch_case(cx: &mut Context) {
+ switch_case_impl(cx, |string| {
+ string
.chars()
.flat_map(|ch| {
if ch.is_lowercase() {
@@ -868,39 +921,16 @@ fn switch_case(cx: &mut Context) {
vec![ch]
}
})
- .collect();
-
- (range.from(), range.to(), Some(text))
+ .collect()
});
-
- doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn switch_to_uppercase(cx: &mut Context) {
- let (view, doc) = current!(cx.editor);
- let selection = doc.selection(view.id);
- let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
- let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into();
-
- (range.from(), range.to(), Some(text))
- });
-
- doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
+ switch_case_impl(cx, |string| string.to_uppercase().into());
}
fn switch_to_lowercase(cx: &mut Context) {
- let (view, doc) = current!(cx.editor);
- let selection = doc.selection(view.id);
- let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
- let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into();
-
- (range.from(), range.to(), Some(text))
- });
-
- doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
+ switch_case_impl(cx, |string| string.to_lowercase().into());
}
pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
@@ -976,28 +1006,6 @@ fn half_page_down(cx: &mut Context) {
scroll(cx, offset, Direction::Forward);
}
-fn extend_char_left(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
-
- let selection = doc.selection(view.id).clone().transform(|range| {
- movement::move_horizontally(text, range, Direction::Backward, count, Movement::Extend)
- });
- doc.set_selection(view.id, selection);
-}
-
-fn extend_char_right(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
-
- let selection = doc.selection(view.id).clone().transform(|range| {
- movement::move_horizontally(text, range, Direction::Forward, count, Movement::Extend)
- });
- doc.set_selection(view.id, selection);
-}
-
fn copy_selection_on_line(cx: &mut Context, direction: Direction) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
@@ -1068,28 +1076,6 @@ fn copy_selection_on_next_line(cx: &mut Context) {
copy_selection_on_line(cx, Direction::Forward)
}
-fn extend_line_up(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
-
- let selection = doc.selection(view.id).clone().transform(|range| {
- movement::move_vertically(text, range, Direction::Backward, count, Movement::Extend)
- });
- doc.set_selection(view.id, selection);
-}
-
-fn extend_line_down(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
-
- let selection = doc.selection(view.id).clone().transform(|range| {
- movement::move_vertically(text, range, Direction::Forward, count, Movement::Extend)
- });
- doc.set_selection(view.id, selection);
-}
-
fn select_all(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
@@ -1098,23 +1084,42 @@ fn select_all(cx: &mut Context) {
}
fn select_regex(cx: &mut Context) {
- let prompt = ui::regex_prompt(cx, "select:".into(), move |view, doc, _, regex| {
- let text = doc.text().slice(..);
- if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), &regex)
- {
- doc.set_selection(view.id, selection);
- }
- });
+ let reg = cx.register.unwrap_or('/');
+ let prompt = ui::regex_prompt(
+ cx,
+ "select:".into(),
+ Some(reg),
+ move |view, doc, regex, event| {
+ if event != PromptEvent::Update {
+ return;
+ }
+ let text = doc.text().slice(..);
+ if let Some(selection) =
+ selection::select_on_matches(text, doc.selection(view.id), &regex)
+ {
+ doc.set_selection(view.id, selection);
+ }
+ },
+ );
cx.push_layer(Box::new(prompt));
}
fn split_selection(cx: &mut Context) {
- let prompt = ui::regex_prompt(cx, "split:".into(), move |view, doc, _, regex| {
- let text = doc.text().slice(..);
- let selection = selection::split_on_matches(text, doc.selection(view.id), &regex);
- doc.set_selection(view.id, selection);
- });
+ let reg = cx.register.unwrap_or('/');
+ let prompt = ui::regex_prompt(
+ cx,
+ "split:".into(),
+ Some(reg),
+ move |view, doc, regex, event| {
+ if event != PromptEvent::Update {
+ return;
+ }
+ let text = doc.text().slice(..);
+ let selection = selection::split_on_matches(text, doc.selection(view.id), &regex);
+ doc.set_selection(view.id, selection);
+ },
+ );
cx.push_layer(Box::new(prompt));
}
@@ -1168,6 +1173,7 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
// TODO: use one function for search vs extend
fn search(cx: &mut Context) {
+ let reg = cx.register.unwrap_or('/');
let (_, doc) = current!(cx.editor);
// TODO: could probably share with select_on_matches?
@@ -1176,23 +1182,44 @@ fn search(cx: &mut Context) {
// feed chunks into the regex yet
let contents = doc.text().slice(..).to_string();
- let prompt = ui::regex_prompt(cx, "search:".into(), move |view, doc, registers, regex| {
- search_impl(doc, view, &contents, &regex, false);
- // TODO: only store on enter (accept), not update
- registers.write('/', vec![regex.as_str().to_string()]);
- });
+ let prompt = ui::regex_prompt(
+ cx,
+ "search:".into(),
+ Some(reg),
+ move |view, doc, regex, event| {
+ if event != PromptEvent::Update {
+ return;
+ }
+ search_impl(doc, view, &contents, &regex, false);
+ },
+ );
cx.push_layer(Box::new(prompt));
}
fn search_next_impl(cx: &mut Context, extend: bool) {
let (view, doc) = current!(cx.editor);
- let registers = &mut cx.editor.registers;
+ let registers = &cx.editor.registers;
if let Some(query) = registers.read('/') {
- let query = query.first().unwrap();
+ let query = query.last().unwrap();
let contents = doc.text().slice(..).to_string();
- let regex = Regex::new(query).unwrap();
- search_impl(doc, view, &contents, &regex, extend);
+ let case_insensitive = if cx.editor.config.smart_case {
+ !query.chars().any(char::is_uppercase)
+ } else {
+ false
+ };
+ if let Ok(regex) = RegexBuilder::new(query)
+ .case_insensitive(case_insensitive)
+ .build()
+ {
+ search_impl(doc, view, &contents, &regex, extend);
+ } else {
+ // get around warning `mutable_borrow_reservation_conflict`
+ // which will be a hard error in the future
+ // see: https://github.com/rust-lang/rust/issues/59159
+ let query = query.clone();
+ cx.editor.set_error(format!("Invalid regex: {}", query));
+ }
}
}
@@ -1209,8 +1236,119 @@ fn search_selection(cx: &mut Context) {
let contents = doc.text().slice(..);
let query = doc.selection(view.id).primary().fragment(contents);
let regex = regex::escape(&query);
- cx.editor.registers.write('/', vec![regex]);
- search_next(cx);
+ cx.editor.registers.get_mut('/').push(regex);
+ let msg = format!("register '{}' set to '{}'", '\\', query);
+ cx.editor.set_status(msg);
+}
+
+fn global_search(cx: &mut Context) {
+ let (all_matches_sx, all_matches_rx) =
+ tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
+ let smart_case = cx.editor.config.smart_case;
+ let prompt = ui::regex_prompt(
+ cx,
+ "global search:".into(),
+ None,
+ move |_view, _doc, regex, event| {
+ if event != PromptEvent::Validate {
+ return;
+ }
+
+ if let Ok(matcher) = RegexMatcherBuilder::new()
+ .case_smart(smart_case)
+ .build(regex.as_str())
+ {
+ let searcher = SearcherBuilder::new()
+ .binary_detection(BinaryDetection::quit(b'\x00'))
+ .build();
+
+ let search_root = std::env::current_dir()
+ .expect("Global search error: Failed to get current dir");
+ WalkBuilder::new(search_root).build_parallel().run(|| {
+ let mut searcher_cl = searcher.clone();
+ let matcher_cl = matcher.clone();
+ let all_matches_sx_cl = all_matches_sx.clone();
+ Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState {
+ let dent = match dent {
+ Ok(dent) => dent,
+ Err(_) => return WalkState::Continue,
+ };
+
+ match dent.file_type() {
+ Some(fi) => {
+ if !fi.is_file() {
+ return WalkState::Continue;
+ }
+ }
+ None => return WalkState::Continue,
+ }
+
+ let result_sink = sinks::UTF8(|line_num, _| {
+ match all_matches_sx_cl
+ .send((line_num as usize - 1, dent.path().to_path_buf()))
+ {
+ Ok(_) => Ok(true),
+ Err(_) => Ok(false),
+ }
+ });
+ let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
+
+ if let Err(err) = result {
+ log::error!("Global search error: {}, {}", dent.path().display(), err);
+ }
+ WalkState::Continue
+ })
+ });
+ } else {
+ // Otherwise do nothing
+ // log::warn!("Global Search Invalid Pattern")
+ }
+ },
+ );
+
+ cx.push_layer(Box::new(prompt));
+
+ let show_picker = async move {
+ let all_matches: Vec<(usize, PathBuf)> =
+ UnboundedReceiverStream::new(all_matches_rx).collect().await;
+ let call: job::Callback =
+ Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
+ if all_matches.is_empty() {
+ editor.set_status("No matches found".to_string());
+ return;
+ }
+ let picker = FilePicker::new(
+ all_matches,
+ move |(_line_num, path)| path.to_str().unwrap().into(),
+ move |editor: &mut Editor, (line_num, path), action| {
+ match editor.open(path.into(), action) {
+ Ok(_) => {}
+ Err(e) => {
+ editor.set_error(format!(
+ "Failed to open file '{}': {}",
+ path.display(),
+ e
+ ));
+ return;
+ }
+ }
+
+ let line_num = *line_num;
+ let (view, doc) = current!(editor);
+ let text = doc.text();
+ let start = text.line_to_char(line_num);
+ let end = text.line_to_char((line_num + 1).min(text.len_lines()));
+
+ doc.set_selection(view.id, Selection::single(start, end));
+ align_view(doc, view, Align::Center);
+ },
+ |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))),
+ );
+ compositor.push(Box::new(picker));
+ });
+ Ok(call)
+ };
+ cx.jobs.callback(show_picker);
}
fn extend_line(cx: &mut Context) {
@@ -1268,7 +1406,7 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId
}
fn delete_selection(cx: &mut Context) {
- let reg_name = cx.selected_register.name();
+ let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
let reg = registers.get_mut(reg_name);
@@ -1281,7 +1419,7 @@ fn delete_selection(cx: &mut Context) {
}
fn change_selection(cx: &mut Context) {
- let reg_name = cx.selected_register.name();
+ let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
let reg = registers.get_mut(reg_name);
@@ -1403,8 +1541,11 @@ mod cmd {
args: &[&str],
_event: PromptEvent,
) -> anyhow::Result<()> {
+ use helix_core::path::expand_tilde;
let path = args.get(0).context("wrong argument count")?;
- let _ = cx.editor.open(path.into(), Action::Replace)?;
+ let _ = cx
+ .editor
+ .open(expand_tilde(Path::new(path)), Action::Replace)?;
Ok(())
}
@@ -1604,7 +1745,7 @@ mod cmd {
/// Results an error if there are modified buffers remaining and sets editor error,
/// otherwise returns `Ok(())`
- fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
+ pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
let modified: Vec<_> = editor
.documents()
.filter(|doc| doc.is_modified())
@@ -2577,7 +2718,7 @@ fn apply_workspace_edit(
) {
if let Some(ref changes) = workspace_edit.changes {
log::debug!("workspace changes: {:?}", changes);
- editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
+ editor.set_error(String::from("Handling workspace_edit.changes is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
return;
// Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used
// TODO: find some example that uses workspace changes, and test it
@@ -2595,8 +2736,30 @@ fn apply_workspace_edit(
match document_changes {
lsp::DocumentChanges::Edits(document_edits) => {
for document_edit in document_edits {
- let (view, doc) = current!(editor);
- assert_eq!(doc.url().unwrap(), document_edit.text_document.uri);
+ let path = document_edit
+ .text_document
+ .uri
+ .to_file_path()
+ .expect("unable to convert URI to filepath");
+ let current_view_id = view!(editor).id;
+ let doc = editor
+ .document_by_path_mut(path)
+ .expect("Document for document_changes not found");
+
+ // Need to determine a view for apply/append_changes_to_history
+ let selections = doc.selections();
+ let view_id = if selections.contains_key(&current_view_id) {
+ // use current if possible
+ current_view_id
+ } else {
+ // Hack: we take the first available view_id
+ selections
+ .keys()
+ .next()
+ .copied()
+ .expect("No view_id available")
+ };
+
let edits = document_edit
.edits
.iter()
@@ -2614,8 +2777,8 @@ fn apply_workspace_edit(
edits,
offset_encoding,
);
- doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
+ doc.apply(&transaction, view_id);
+ doc.append_changes_to_history(view_id);
}
}
lsp::DocumentChanges::Operations(operations) => {
@@ -2779,6 +2942,10 @@ fn open_above(cx: &mut Context) {
fn normal_mode(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+ if doc.mode == Mode::Normal {
+ return;
+ }
+
doc.mode = Mode::Normal;
doc.append_changes_to_history(view.id);
@@ -3325,17 +3492,20 @@ pub mod insert {
}
use helix_core::auto_pairs;
- const HOOKS: &[Hook] = &[auto_pairs::hook, insert];
- const POST_HOOKS: &[PostHook] = &[completion, signature_help];
pub fn insert_char(cx: &mut Context, c: char) {
let (view, doc) = current!(cx.editor);
+ let hooks: &[Hook] = match cx.editor.config.auto_pairs {
+ true => &[auto_pairs::hook, insert],
+ false => &[insert],
+ };
+
let text = doc.text();
let selection = doc.selection(view.id).clone().cursors(text.slice(..));
// run through insert hooks, stopping on the first one that returns Some(t)
- for hook in HOOKS {
+ for hook in hooks {
if let Some(transaction) = hook(text, &selection, c) {
doc.apply(&transaction, view.id);
break;
@@ -3345,7 +3515,7 @@ pub mod insert {
// TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
// this could also generically look at Transaction, but it's a bit annoying to look at
// Operation instead of Change.
- for hook in POST_HOOKS {
+ for hook in &[completion, signature_help] {
hook(cx, c);
}
}
@@ -3510,12 +3680,12 @@ fn yank(cx: &mut Context) {
let msg = format!(
"yanked {} selection(s) to register {}",
values.len(),
- cx.selected_register.name()
+ cx.register.unwrap_or('"')
);
cx.editor
.registers
- .write(cx.selected_register.name(), values);
+ .write(cx.register.unwrap_or('"'), values);
cx.editor.set_status(msg);
exit_select_mode(cx);
@@ -3624,7 +3794,14 @@ fn paste_impl(
.iter()
.any(|value| get_line_ending_of_str(value).is_some());
- let mut values = values.iter().cloned().map(Tendril::from).chain(repeat);
+ // Only compiled once.
+ #[allow(clippy::trivial_regex)]
+ static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap());
+ let mut values = values
+ .iter()
+ .map(|value| REGEX.replace_all(value, doc.line_ending.as_str()))
+ .map(|value| Tendril::from(value.as_ref()))
+ .chain(repeat);
let text = doc.text();
let selection = doc.selection(view.id);
@@ -3688,7 +3865,7 @@ fn paste_primary_clipboard_before(cx: &mut Context) {
}
fn replace_with_yanked(cx: &mut Context) {
- let reg_name = cx.selected_register.name();
+ let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
@@ -3739,7 +3916,7 @@ fn replace_selections_with_primary_clipboard(cx: &mut Context) {
}
fn paste_after(cx: &mut Context) {
- let reg_name = cx.selected_register.name();
+ let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
@@ -3753,7 +3930,7 @@ fn paste_after(cx: &mut Context) {
}
fn paste_before(cx: &mut Context) {
- let reg_name = cx.selected_register.name();
+ let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
@@ -3934,24 +4111,49 @@ fn join_selections(cx: &mut Context) {
fn keep_selections(cx: &mut Context) {
// keep selections matching regex
- let prompt = ui::regex_prompt(cx, "keep:".into(), move |view, doc, _, regex| {
- let text = doc.text().slice(..);
+ let reg = cx.register.unwrap_or('/');
+ let prompt = ui::regex_prompt(
+ cx,
+ "keep:".into(),
+ Some(reg),
+ move |view, doc, regex, event| {
+ if event != PromptEvent::Update {
+ return;
+ }
+ let text = doc.text().slice(..);
- if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), &regex) {
- doc.set_selection(view.id, selection);
- }
- });
+ if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), &regex) {
+ doc.set_selection(view.id, selection);
+ }
+ },
+ );
cx.push_layer(Box::new(prompt));
}
fn keep_primary_selection(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+ // TODO: handle count
let range = doc.selection(view.id).primary();
doc.set_selection(view.id, Selection::single(range.anchor, range.head));
}
+fn remove_primary_selection(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ // TODO: handle count
+
+ let selection = doc.selection(view.id);
+ if selection.len() == 1 {
+ cx.editor.set_error("no selections remaining".to_owned());
+ return;
+ }
+ let index = selection.primary_index();
+ let selection = selection.clone().remove(index);
+
+ doc.set_selection(view.id, selection);
+}
+
fn completion(cx: &mut Context) {
// trigger on trigger char, or if user calls it
// (or on word char typing??)
@@ -4259,6 +4461,12 @@ fn vsplit(cx: &mut Context) {
}
fn wclose(cx: &mut Context) {
+ if cx.editor.tree.views().count() == 1 {
+ if let Err(err) = cmd::buffers_remaining_impl(cx.editor) {
+ cx.editor.set_error(err.to_string());
+ return;
+ }
+ }
let view_id = view!(cx.editor).id;
// close current split
cx.editor.close(view_id, /* close_buffer */ false);
@@ -4267,7 +4475,7 @@ fn wclose(cx: &mut Context) {
fn select_register(cx: &mut Context) {
cx.on_next_key(move |cx, event| {
if let Some(ch) = event.char() {
- cx.editor.selected_register.select(ch);
+ cx.editor.selected_register = Some(ch);
}
})
}
diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs
index 2ac41926..4fa38174 100644
--- a/helix-term/src/job.rs
+++ b/helix-term/src/job.rs
@@ -61,7 +61,7 @@ impl Jobs {
}
pub fn handle_callback(
- &mut self,
+ &self,
editor: &mut Editor,
compositor: &mut Compositor,
call: anyhow::Result<Option<Callback>>,
@@ -84,7 +84,7 @@ impl Jobs {
}
}
- pub fn add(&mut self, j: Job) {
+ pub fn add(&self, j: Job) {
if j.wait {
self.wait_futures.push(j.future);
} else {
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index a0f2be94..77bb187c 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -4,6 +4,7 @@ use helix_core::hashmap;
use helix_view::{document::Mode, info::Info, input::KeyEvent};
use serde::Deserialize;
use std::{
+ borrow::Cow,
collections::HashMap,
ops::{Deref, DerefMut},
};
@@ -47,13 +48,13 @@ macro_rules! keymap {
};
(@trie
- { $label:literal $($($key:literal)|+ => $value:tt,)+ }
+ { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
) => {
- keymap!({ $label $($($key)|+ => $value,)+ })
+ keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
};
(
- { $label:literal $($($key:literal)|+ => $value:tt,)+ }
+ { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
) => {
// modified from the hashmap! macro
{
@@ -70,7 +71,9 @@ macro_rules! keymap {
_order.push(_key);
)+
)*
- $crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order))
+ let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order);
+ $( _node.is_sticky = $sticky; )?
+ $crate::keymap::KeyTrie::Node(_node)
}
};
}
@@ -84,6 +87,8 @@ pub struct KeyTrieNode {
map: HashMap<KeyEvent, KeyTrie>,
#[serde(skip)]
order: Vec<KeyEvent>,
+ #[serde(skip)]
+ pub is_sticky: bool,
}
impl KeyTrieNode {
@@ -92,6 +97,7 @@ impl KeyTrieNode {
name: name.to_string(),
map,
order,
+ is_sticky: false,
}
}
@@ -119,12 +125,10 @@ impl KeyTrieNode {
}
}
}
-}
-impl From<KeyTrieNode> for Info {
- fn from(node: KeyTrieNode) -> Self {
- let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(node.len());
- for (&key, trie) in node.iter() {
+ pub fn infobox(&self) -> Info {
+ let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(self.len());
+ for (&key, trie) in self.iter() {
let desc = match trie {
KeyTrie::Leaf(cmd) => cmd.doc(),
KeyTrie::Node(n) => n.name(),
@@ -136,16 +140,16 @@ impl From<KeyTrieNode> for Info {
}
}
body.sort_unstable_by_key(|(_, keys)| {
- node.order.iter().position(|&k| k == keys[0]).unwrap()
+ self.order.iter().position(|&k| k == keys[0]).unwrap()
});
- let prefix = format!("{} ", node.name());
+ let prefix = format!("{} ", self.name());
if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
body = body
.into_iter()
.map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
.collect();
}
- Info::new(node.name(), body)
+ Info::new(self.name(), body)
}
}
@@ -218,7 +222,7 @@ impl KeyTrie {
}
#[derive(Debug, Clone, PartialEq)]
-pub enum KeymapResult {
+pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode),
Matched(Command),
@@ -229,14 +233,31 @@ pub enum KeymapResult {
Cancelled(Vec<KeyEvent>),
}
+/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a
+/// reference to the sticky node if one is currently active.
+pub struct KeymapResult<'a> {
+ pub kind: KeymapResultKind,
+ pub sticky: Option<&'a KeyTrieNode>,
+}
+
+impl<'a> KeymapResult<'a> {
+ pub fn new(kind: KeymapResultKind, sticky: Option<&'a KeyTrieNode>) -> Self {
+ Self { kind, sticky }
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Keymap {
/// Always a Node
#[serde(flatten)]
root: KeyTrie,
- /// Stores pending keys waiting for the next key
+ /// Stores pending keys waiting for the next key. This is relative to a
+ /// sticky node if one is in use.
#[serde(skip)]
state: Vec<KeyEvent>,
+ /// Stores the sticky node if one is activated.
+ #[serde(skip)]
+ sticky: Option<KeyTrieNode>,
}
impl Keymap {
@@ -244,6 +265,7 @@ impl Keymap {
Keymap {
root,
state: Vec::new(),
+ sticky: None,
}
}
@@ -251,27 +273,61 @@ impl Keymap {
&self.root
}
+ pub fn sticky(&self) -> Option<&KeyTrieNode> {
+ self.sticky.as_ref()
+ }
+
/// Returns list of keys waiting to be disambiguated.
pub fn pending(&self) -> &[KeyEvent] {
&self.state
}
- /// Lookup `key` in the keymap to try and find a command to execute
+ /// Lookup `key` in the keymap to try and find a command to execute. Escape
+ /// key cancels pending keystrokes. If there are no pending keystrokes but a
+ /// sticky node is in use, it will be cleared.
pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
- let &first = self.state.get(0).unwrap_or(&key);
- let trie = match self.root.search(&[first]) {
- Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd),
- None => return KeymapResult::NotFound,
+ if let key!(Esc) = key {
+ if !self.state.is_empty() {
+ return KeymapResult::new(
+ // Note that Esc is not included here
+ KeymapResultKind::Cancelled(self.state.drain(..).collect()),
+ self.sticky(),
+ );
+ }
+ self.sticky = None;
+ }
+
+ let first = self.state.get(0).unwrap_or(&key);
+ let trie_node = match self.sticky {
+ Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())),
+ None => Cow::Borrowed(&self.root),
+ };
+
+ let trie = match trie_node.search(&[*first]) {
+ Some(&KeyTrie::Leaf(cmd)) => {
+ return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
+ }
+ None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()),
Some(t) => t,
};
+
self.state.push(key);
match trie.search(&self.state[1..]) {
- Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()),
- Some(&KeyTrie::Leaf(command)) => {
+ Some(&KeyTrie::Node(ref map)) => {
+ if map.is_sticky {
+ self.state.clear();
+ self.sticky = Some(map.clone());
+ }
+ KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
+ }
+ Some(&KeyTrie::Leaf(cmd)) => {
self.state.clear();
- KeymapResult::Matched(command)
+ return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
}
- None => KeymapResult::Cancelled(self.state.drain(..).collect()),
+ None => KeymapResult::new(
+ KeymapResultKind::Cancelled(self.state.drain(..).collect()),
+ self.sticky(),
+ ),
}
}
@@ -380,7 +436,6 @@ impl Default for Keymaps {
"A" => append_to_line,
"o" => open_below,
"O" => open_above,
- // [<space> ]<space> equivalents too (add blank new line, no edit)
"d" => delete_selection,
// TODO: also delete without yanking
@@ -440,12 +495,11 @@ impl Default for Keymaps {
"<" => unindent,
"=" => format_selections,
"J" => join_selections,
- // TODO: conflicts hover/doc
"K" => keep_selections,
// TODO: and another method for inverse
- // TODO: clashes with space mode
- "space" => keep_primary_selection,
+ "," => keep_primary_selection,
+ "A-," => remove_primary_selection,
// "q" => record_macro,
// "Q" => replay_macro,
@@ -473,7 +527,6 @@ impl Default for Keymaps {
// move under <space>c
"C-c" => toggle_comments,
- "K" => hover,
// z family for save/restore/combine from/to sels from register
@@ -516,7 +569,8 @@ impl Default for Keymaps {
"p" => paste_clipboard_after,
"P" => paste_clipboard_before,
"R" => replace_selections_with_clipboard,
- "space" => keep_primary_selection,
+ "/" => global_search,
+ "k" => hover,
},
"z" => { "View"
"z" | "c" => align_view_center,
@@ -525,6 +579,22 @@ impl Default for Keymaps {
"m" => align_view_middle,
"k" => scroll_up,
"j" => scroll_down,
+ "b" => page_up,
+ "f" => page_down,
+ "u" => half_page_up,
+ "d" => half_page_down,
+ },
+ "Z" => { "View" sticky=true
+ "z" | "c" => align_view_center,
+ "t" => align_view_top,
+ "b" => align_view_bottom,
+ "m" => align_view_middle,
+ "k" => scroll_up,
+ "j" => scroll_down,
+ "b" => page_up,
+ "f" => page_down,
+ "u" => half_page_up,
+ "d" => half_page_down,
},
"\"" => select_register,
@@ -545,14 +615,17 @@ impl Default for Keymaps {
"w" => extend_next_word_start,
"b" => extend_prev_word_start,
"e" => extend_next_word_end,
+ "W" => extend_next_long_word_start,
+ "B" => extend_prev_long_word_start,
+ "E" => extend_next_long_word_end,
"t" => extend_till_char,
"f" => extend_next_char,
"T" => extend_till_prev_char,
"F" => extend_prev_char,
- "home" => goto_line_start,
- "end" => goto_line_end,
+ "home" => extend_to_line_start,
+ "end" => extend_to_line_end,
"esc" => exit_select_mode,
"v" => normal_mode,
@@ -617,19 +690,19 @@ fn merge_partial_keys() {
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!(
- keymap.get(key!('i')),
- KeymapResult::Matched(Command::normal_mode),
+ keymap.get(key!('i')).kind,
+ KeymapResultKind::Matched(Command::normal_mode),
"Leaf should replace leaf"
);
assert_eq!(
- keymap.get(key!('无')),
- KeymapResult::Matched(Command::insert_mode),
+ keymap.get(key!('无')).kind,
+ KeymapResultKind::Matched(Command::insert_mode),
"New leaf should be present in merged keymap"
);
// Assumes that z is a node in the default keymap
assert_eq!(
- keymap.get(key!('z')),
- KeymapResult::Matched(Command::jump_backward),
+ keymap.get(key!('z')).kind,
+ KeymapResultKind::Matched(Command::jump_backward),
"Leaf should replace node"
);
// Assumes that `g` is a node in default keymap
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 90657764..6c9e3a80 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -262,8 +262,7 @@ impl Component for Completion {
.cursor(doc.text().slice(..));
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- view.offset.row) as u16;
-
- let mut doc = match &option.documentation {
+ let mut markdown_doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
@@ -311,24 +310,42 @@ impl Component for Completion {
None => return,
};
- let half = area.height / 2;
- let height = 15.min(half);
- // we want to make sure the cursor is visible (not hidden behind the documentation)
- let y = if cursor_pos + area.y
- >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
- {
- 0
+ let (popup_x, popup_y) = self.popup.get_rel_position(area, cx);
+ let (popup_width, _popup_height) = self.popup.get_size();
+ let mut width = area
+ .width
+ .saturating_sub(popup_x)
+ .saturating_sub(popup_width);
+ let area = if width > 30 {
+ let mut height = area.height.saturating_sub(popup_y);
+ let x = popup_x + popup_width;
+ let y = popup_y;
+
+ if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
+ width = rel_width;
+ height = rel_height;
+ }
+ Rect::new(x, y, width, height)
} else {
- // -2 to subtract command line + statusline. a bit of a hack, because of splits.
- area.height.saturating_sub(height).saturating_sub(2)
- };
+ let half = area.height / 2;
+ let height = 15.min(half);
+ // we want to make sure the cursor is visible (not hidden behind the documentation)
+ let y = if cursor_pos + area.y
+ >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
+ {
+ 0
+ } else {
+ // -2 to subtract command line + statusline. a bit of a hack, because of splits.
+ area.height.saturating_sub(height).saturating_sub(2)
+ };
- let area = Rect::new(0, y, area.width, height);
+ Rect::new(0, y, area.width, height)
+ };
// clear area
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
- doc.render(area, surface, cx);
+ markdown_doc.render(area, surface, cx);
}
}
}
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 63694d0b..128fe948 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -3,7 +3,7 @@ use crate::{
compositor::{Component, Compositor, Context, EventResult},
job::Callback,
key,
- keymap::{KeymapResult, Keymaps},
+ keymap::{KeymapResult, KeymapResultKind, Keymaps},
ui::{Completion, ProgressSpinners},
};
@@ -165,8 +165,7 @@ impl EditorView {
let scopes = theme.scopes();
syntax
.highlight_iter(text.slice(..), Some(range), None, |language| {
- loader
- .language_config_for_scope(&format!("source.{}", language))
+ loader.language_configuration_for_injection_string(language)
.and_then(|language_config| {
let config = language_config.highlight_config(scopes)?;
let config_ref = config.as_ref();
@@ -852,7 +851,7 @@ impl EditorView {
/// Handle events by looking them up in `self.keymaps`. Returns None
/// if event was handled (a command was executed or a subkeymap was
- /// activated). Only KeymapResult::{NotFound, Cancelled} is returned
+ /// activated). Only KeymapResultKind::{NotFound, Cancelled} is returned
/// otherwise.
fn handle_keymap_event(
&mut self,
@@ -860,8 +859,6 @@ impl EditorView {
cxt: &mut commands::Context,
event: KeyEvent,
) -> Option<KeymapResult> {
- self.autoinfo = None;
-
if let Some(picker) = cxt.editor.debug_config_picker.clone() {
match event {
KeyEvent {
@@ -912,29 +909,32 @@ impl EditorView {
return None;
}
- match self.keymaps.get_mut(&mode).unwrap().get(event) {
- KeymapResult::Matched(command) => command.execute(cxt),
- KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
- k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k),
+ let key_result = self.keymaps.get_mut(&mode).unwrap().get(event);
+ self.autoinfo = key_result.sticky.map(|node| node.infobox());
+
+ match &key_result.kind {
+ KeymapResultKind::Matched(command) => command.execute(cxt),
+ KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
+ KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
}
None
}
fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
- match keyresult {
- KeymapResult::NotFound => {
+ match keyresult.kind {
+ KeymapResultKind::NotFound => {
if let Some(ch) = event.char() {
commands::insert::insert_char(cx, ch)
}
}
- KeymapResult::Cancelled(pending) => {
+ KeymapResultKind::Cancelled(pending) => {
for ev in pending {
match ev.char() {
Some(ch) => commands::insert::insert_char(cx, ch),
None => {
- if let KeymapResult::Matched(command) =
- self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev)
+ if let KeymapResultKind::Matched(command) =
+ self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind
{
command.execute(cx);
}
@@ -972,7 +972,7 @@ impl EditorView {
// debug_assert!(cxt.count != 0);
// set the register
- cxt.selected_register = cxt.editor.selected_register.take();
+ cxt.register = cxt.editor.selected_register.take();
self.handle_keymap_event(mode, cxt, event);
if self.keymaps.pending().is_empty() {
@@ -1196,9 +1196,9 @@ impl EditorView {
impl Component for EditorView {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let mut cxt = commands::Context {
- selected_register: helix_view::RegisterSelection::default(),
editor: &mut cx.editor,
count: None,
+ register: None,
callback: None,
on_next_key_callback: None,
jobs: cx.jobs,
@@ -1288,11 +1288,12 @@ impl Component for EditorView {
// how we entered insert mode is important, and we should track that so
// we can repeat the side effect.
- self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) {
- KeymapResult::Matched(command) => command,
- // FIXME: insert mode can only be entered through single KeyCodes
- _ => unimplemented!(),
- };
+ self.last_insert.0 =
+ match self.keymaps.get_mut(&mode).unwrap().get(key).kind {
+ KeymapResultKind::Matched(command) => command,
+ // FIXME: insert mode can only be entered through single KeyCodes
+ _ => unimplemented!(),
+ };
self.last_insert.1.clear();
}
(Mode::Insert, Mode::Normal) => {
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index 28542cdc..4144ed3c 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -88,7 +88,7 @@ fn parse<'a>(
if let Some(theme) = theme {
let rope = Rope::from(text.as_ref());
let syntax = loader
- .language_config_for_scope(&format!("source.{}", language))
+ .language_configuration_for_injection_string(language)
.and_then(|config| config.highlight_config(theme.scopes()))
.map(|config| Syntax::new(&rope, config));
@@ -215,10 +215,30 @@ impl Component for Markdown {
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
- let contents = parse(&self.contents, None, &self.config_loader);
let padding = 2;
- let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
- let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
- Some((width, height))
+ if padding >= viewport.1 || padding >= viewport.0 {
+ return None;
+ }
+ let contents = parse(&self.contents, None, &self.config_loader);
+ let max_text_width = (viewport.0 - padding).min(120);
+ let mut text_width = 0;
+ let mut height = padding;
+ for content in contents {
+ height += 1;
+ let content_width = content.width() as u16;
+ if content_width > max_text_width {
+ text_width = max_text_width;
+ height += content_width / max_text_width;
+ } else if content_width > text_width {
+ text_width = content_width;
+ }
+
+ if height >= viewport.1 {
+ height = viewport.1;
+ break;
+ }
+ }
+
+ Some((text_width + padding, height))
}
}
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 24dd3e61..dab0c34f 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -33,6 +33,8 @@ pub struct Menu<T: Item> {
scroll: usize,
size: (u16, u16),
+ viewport: (u16, u16),
+ recalculate: bool,
}
impl<T: Item> Menu<T> {
@@ -51,6 +53,8 @@ impl<T: Item> Menu<T> {
callback_fn: Box::new(callback_fn),
scroll: 0,
size: (0, 0),
+ viewport: (0, 0),
+ recalculate: true,
};
// TODO: scoring on empty input should just use a fastpath
@@ -83,6 +87,7 @@ impl<T: Item> Menu<T> {
// reset cursor position
self.cursor = None;
self.scroll = 0;
+ self.recalculate = true;
}
pub fn move_up(&mut self) {
@@ -99,6 +104,41 @@ impl<T: Item> Menu<T> {
self.adjust_scroll();
}
+ fn recalculate_size(&mut self, viewport: (u16, u16)) {
+ let n = self
+ .options
+ .first()
+ .map(|option| option.row().cells.len())
+ .unwrap_or_default();
+ let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
+ let row = option.row();
+ // maintain max for each column
+ for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
+ let width = cell.content.width();
+ if width > *acc {
+ *acc = width;
+ }
+ }
+
+ acc
+ });
+ let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
+ let width = len.min(viewport.0 as usize);
+
+ self.widths = max_lens
+ .into_iter()
+ .map(|len| Constraint::Length(len as u16))
+ .collect();
+
+ let height = self.matches.len().min(10).min(viewport.1 as usize);
+
+ self.size = (width as u16, height as u16);
+
+ // adjust scroll offsets if size changed
+ self.adjust_scroll();
+ self.recalculate = false;
+ }
+
fn adjust_scroll(&mut self) {
let win_height = self.size.1 as usize;
if let Some(cursor) = self.cursor {
@@ -221,43 +261,13 @@ impl<T: Item + 'static> Component for Menu<T> {
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
- let n = self
- .options
- .first()
- .map(|option| option.row().cells.len())
- .unwrap_or_default();
- let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
- let row = option.row();
- // maintain max for each column
- for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
- let width = cell.content.width();
- if width > *acc {
- *acc = width;
- }
- }
-
- acc
- });
- let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
- let width = len.min(viewport.0 as usize);
-
- self.widths = max_lens
- .into_iter()
- .map(|len| Constraint::Length(len as u16))
- .collect();
-
- let height = self.options.len().min(10).min(viewport.1 as usize);
-
- self.size = (width as u16, height as u16);
-
- // adjust scroll offsets if size changed
- self.adjust_scroll();
+ if viewport != self.viewport || self.recalculate {
+ self.recalculate_size(viewport);
+ }
Some(self.size)
}
- // TODO: required size should re-trigger when we filter items so we can draw a smaller menu
-
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let theme = &cx.editor.theme;
let style = theme
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 37148ae2..e66673ca 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -20,7 +20,7 @@ pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text;
use helix_core::regex::Regex;
-use helix_core::register::Registers;
+use helix_core::regex::RegexBuilder;
use helix_view::{Document, Editor, View};
use std::path::PathBuf;
@@ -28,7 +28,8 @@ use std::path::PathBuf;
pub fn regex_prompt(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
- fun: impl Fn(&mut View, &mut Document, &mut Registers, Regex) + 'static,
+ history_register: Option<char>,
+ fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
) -> Prompt {
let (view, doc) = current!(cx.editor);
let view_id = view.id;
@@ -36,7 +37,7 @@ pub fn regex_prompt(
Prompt::new(
prompt,
- None,
+ history_register,
|_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
match event {
@@ -46,6 +47,14 @@ pub fn regex_prompt(
}
PromptEvent::Validate => {
// TODO: push_jump to store selection just before jump
+
+ match Regex::new(input) {
+ Ok(regex) => {
+ let (view, doc) = current!(cx.editor);
+ fun(view, doc, regex, event);
+ }
+ Err(_err) => (), // TODO: mark command line as error
+ }
}
PromptEvent::Update => {
// skip empty input, TODO: trigger default
@@ -53,15 +62,23 @@ pub fn regex_prompt(
return;
}
- match Regex::new(input) {
+ let case_insensitive = if cx.editor.config.smart_case {
+ !input.chars().any(char::is_uppercase)
+ } else {
+ false
+ };
+
+ match RegexBuilder::new(input)
+ .case_insensitive(case_insensitive)
+ .build()
+ {
Ok(regex) => {
let (view, doc) = current!(cx.editor);
- let registers = &mut cx.editor.registers;
// revert state to what it was before the last update
doc.set_selection(view.id, snapshot.clone());
- fun(view, doc, registers, regex);
+ fun(view, doc, regex, event);
view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
}
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 84b8dd72..ee1ec177 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -124,10 +124,13 @@ impl<T: 'static> Component for FilePicker<T> {
}) {
// align to middle
let first_line = line
- .map(|(s, e)| (s.min(doc.text().len_lines()), e.min(doc.text().len_lines())))
- .map(|(start, _)| start)
- .unwrap_or(0)
- .saturating_sub(inner.height as usize / 2);
+ .map(|(start, end)| {
+ let height = end.saturating_sub(start) + 1;
+ let middle = start + (height.saturating_sub(1) / 2);
+ middle.saturating_sub(inner.height as usize / 2).min(start)
+ })
+ .unwrap_or(0);
+
let offset = Position::new(first_line, 0);
let highlights = EditorView::doc_syntax_highlights(
@@ -268,17 +271,15 @@ impl<T> Picker<T> {
}
pub fn move_up(&mut self) {
- self.cursor = self.cursor.saturating_sub(1);
+ let len = self.matches.len();
+ let pos = ((self.cursor + len.saturating_sub(1)) % len) % len;
+ self.cursor = pos;
}
pub fn move_down(&mut self) {
- if self.matches.is_empty() {
- return;
- }
-
- if self.cursor < self.matches.len() - 1 {
- self.cursor += 1;
- }
+ let len = self.matches.len();
+ let pos = (self.cursor + 1) % len;
+ self.cursor = pos;
}
pub fn selection(&self) -> Option<&T> {
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index e126c845..1bab1eae 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -16,8 +16,6 @@ pub struct Popup<T: Component> {
}
impl<T: Component> Popup<T> {
- // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
- // rendering)
pub fn new(contents: T) -> Self {
Self {
contents,
@@ -31,6 +29,39 @@ impl<T: Component> Popup<T> {
self.position = pos;
}
+ pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
+ let position = self
+ .position
+ .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default());
+
+ let (width, height) = self.size;
+
+ // if there's a orientation preference, use that
+ // if we're on the top part of the screen, do below
+ // if we're on the bottom part, do above
+
+ // -- make sure frame doesn't stick out of bounds
+ let mut rel_x = position.col as u16;
+ let mut rel_y = position.row as u16;
+ if viewport.width <= rel_x + width {
+ rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
+ }
+
+ // TODO: be able to specify orientation preference. We want above for most popups, below
+ // for menus/autocomplete.
+ if viewport.height > rel_y + height {
+ rel_y += 1 // position below point
+ } else {
+ rel_y = rel_y.saturating_sub(height) // position above point
+ }
+
+ (rel_x, rel_y)
+ }
+
+ pub fn get_size(&self) -> (u16, u16) {
+ (self.size.0, self.size.1)
+ }
+
pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction {
self.scroll += offset;
@@ -106,31 +137,15 @@ impl<T: Component> Component for Popup<T> {
}
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
- cx.scroll = Some(self.scroll);
+ // trigger required_size so we recalculate if the child changed
+ self.required_size((viewport.width, viewport.height));
- let position = self
- .position
- .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default());
-
- let (width, height) = self.size;
-
- // -- make sure frame doesn't stick out of bounds
- let mut rel_x = position.col as u16;
- let mut rel_y = position.row as u16;
- if viewport.width <= rel_x + width {
- rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
- };
+ cx.scroll = Some(self.scroll);
- // TODO: be able to specify orientation preference. We want above for most popups, below
- // for menus/autocomplete.
- if height <= rel_y {
- rel_y = rel_y.saturating_sub(height) // position above point
- } else {
- rel_y += 1 // position below point
- }
+ let (rel_x, rel_y) = self.get_rel_position(viewport, cx);
// clip to viewport
- let area = viewport.intersection(Rect::new(rel_x, rel_y, width, height));
+ let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1));
// clear area
let background = cx.editor.theme.get("ui.popup");
diff --git a/helix-term/src/ui/text.rs b/helix-term/src/ui/text.rs
index 65a75a4a..4641fae1 100644
--- a/helix-term/src/ui/text.rs
+++ b/helix-term/src/ui/text.rs
@@ -5,11 +5,17 @@ use helix_view::graphics::Rect;
pub struct Text {
contents: String,
+ size: (u16, u16),
+ viewport: (u16, u16),
}
impl Text {
pub fn new(contents: String) -> Self {
- Self { contents }
+ Self {
+ contents,
+ size: (0, 0),
+ viewport: (0, 0),
+ }
}
}
impl Component for Text {
@@ -24,9 +30,13 @@ impl Component for Text {
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
- let contents = tui::text::Text::from(self.contents.clone());
- let width = std::cmp::min(contents.width() as u16, viewport.0);
- let height = std::cmp::min(contents.height() as u16, viewport.1);
- Some((width, height))
+ if viewport != self.viewport {
+ let contents = tui::text::Text::from(self.contents.clone());
+ let width = std::cmp::min(contents.width() as u16, viewport.0);
+ let height = std::cmp::min(contents.height() as u16, viewport.1);
+ self.size = (width, height);
+ self.viewport = viewport;
+ }
+ Some(self.size)
}
}