aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/src/application.rs4
-rw-r--r--helix-term/src/commands.rs328
-rw-r--r--helix-term/src/keymap.rs11
-rw-r--r--helix-term/src/ui/editor.rs289
-rw-r--r--helix-term/src/ui/markdown.rs7
-rw-r--r--helix-term/src/ui/mod.rs11
6 files changed, 417 insertions, 233 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index c76a2e28..69a51a21 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -122,7 +122,7 @@ impl Application {
if first.is_dir() {
std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit);
- compositor.push(Box::new(ui::file_picker(".".into())));
+ compositor.push(Box::new(ui::file_picker(".".into(), &config.editor)));
} else {
let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
@@ -270,7 +270,7 @@ impl Application {
use crate::commands::{insert::idle_completion, Context};
use helix_view::document::Mode;
- if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
+ if doc!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
return;
}
let editor_view = self
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 54466c56..084479cc 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -13,7 +13,6 @@ use helix_core::{
numbers::NumberIncrementor,
object, pos_at_coords,
regex::{self, Regex, RegexBuilder},
- register::Register,
search, selection, surround, textobject,
unicode::width::UnicodeWidthChar,
LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
@@ -236,7 +235,9 @@ impl Command {
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",
+ delete_selection_noyank, "Delete selection, without yanking",
change_selection, "Change selection (delete and enter insert mode)",
+ change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)",
collapse_selection, "Collapse selection onto a single cursor",
flip_selections, "Flip selection cursor and anchor",
insert_mode, "Insert before selection",
@@ -262,6 +263,9 @@ impl Command {
goto_implementation, "Goto implementation",
goto_file_start, "Goto file start/line",
goto_file_end, "Goto file end",
+ goto_file, "Goto files in the selection",
+ goto_file_hsplit, "Goto files in the selection in horizontal splits",
+ goto_file_vsplit, "Goto files in the selection in vertical splits",
goto_reference, "Goto references",
goto_window_top, "Goto window top",
goto_window_middle, "Goto window middle",
@@ -318,6 +322,7 @@ impl Command {
join_selections, "Join lines inside selection",
keep_selections, "Keep selections matching regex",
remove_selections, "Remove selections matching regex",
+ align_selections, "Align selections in column",
keep_primary_selection, "Keep primary selection",
remove_primary_selection, "Remove primary selection",
completion, "Invoke completion popup",
@@ -676,11 +681,83 @@ fn trim_selections(cx: &mut Context) {
};
}
+// align text in selection
+fn align_selections(cx: &mut Context) {
+ let align_style = cx.count();
+ if align_style > 3 {
+ cx.editor.set_error(
+ "align only accept 1,2,3 as count to set left/center/right align".to_string(),
+ );
+ return;
+ }
+
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+ let selection = doc.selection(view.id);
+ let mut column_widths = vec![];
+ let mut last_line = text.len_lines();
+ let mut column = 0;
+ // first of all, we need compute all column's width, let use max width of the selections in a column
+ for sel in selection {
+ let (l1, l2) = sel.line_range(text);
+ if l1 != l2 {
+ cx.editor
+ .set_error("align cannot work with multi line selections".to_string());
+ return;
+ }
+ // if the selection is not in the same line with last selection, we set the column to 0
+ column = if l1 != last_line { 0 } else { column + 1 };
+ last_line = l1;
+
+ if column < column_widths.len() {
+ if sel.to() - sel.from() > column_widths[column] {
+ column_widths[column] = sel.to() - sel.from();
+ }
+ } else {
+ // a new column, current selection width is the temp width of the column
+ column_widths.push(sel.to() - sel.from());
+ }
+ }
+ last_line = text.len_lines();
+ // once we get the with of each column, we transform each selection with to it's column width based on the align style
+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
+ let l = range.cursor_line(text);
+ column = if l != last_line { 0 } else { column + 1 };
+ last_line = l;
+
+ (
+ range.from(),
+ range.to(),
+ Some(
+ align_fragment_to_width(&range.fragment(text), column_widths[column], align_style)
+ .into(),
+ ),
+ )
+ });
+
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+}
+
+fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String {
+ let trimed = fragment.trim_matches(|c| c == ' ');
+ let mut s = " ".repeat(width - trimed.chars().count());
+ match align_style {
+ 1 => s.insert_str(0, trimed), // left align
+ 2 => s.insert_str(s.len() / 2, trimed), // center align
+ 3 => s.push_str(trimed), // right align
+ n => unimplemented!("{}", n),
+ }
+ s
+}
+
fn goto_window(cx: &mut Context, align: Align) {
+ let count = cx.count() - 1;
let (view, doc) = current!(cx.editor);
let height = view.inner_area().height as usize;
+ // respect user given count if any
// - 1 so we have at least one gap in the middle.
// a height of 6 with padding of 3 on each side will keep shifting the view back and forth
// as we type
@@ -689,11 +766,12 @@ fn goto_window(cx: &mut Context, align: Align) {
let last_line = view.last_line(doc);
let line = match align {
- Align::Top => (view.offset.row + scrolloff),
- Align::Center => (view.offset.row + (height / 2)),
- Align::Bottom => last_line.saturating_sub(scrolloff),
+ Align::Top => (view.offset.row + scrolloff + count),
+ Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
+ Align::Bottom => last_line.saturating_sub(scrolloff + count),
}
- .min(last_line.saturating_sub(scrolloff));
+ .min(last_line.saturating_sub(scrolloff))
+ .max(view.offset.row + scrolloff);
let pos = doc.text().line_to_char(line);
@@ -782,6 +860,49 @@ fn goto_file_end(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
+fn goto_file(cx: &mut Context) {
+ goto_file_impl(cx, Action::Replace);
+}
+
+fn goto_file_hsplit(cx: &mut Context) {
+ goto_file_impl(cx, Action::HorizontalSplit);
+}
+
+fn goto_file_vsplit(cx: &mut Context) {
+ goto_file_impl(cx, Action::VerticalSplit);
+}
+
+fn goto_file_impl(cx: &mut Context, action: Action) {
+ let (view, doc) = current_ref!(cx.editor);
+ let text = doc.text();
+ let selections = doc.selection(view.id);
+ let mut paths: Vec<_> = selections
+ .iter()
+ .map(|r| text.slice(r.from()..r.to()).to_string())
+ .collect();
+ let primary = selections.primary();
+ if selections.len() == 1 && primary.to() - primary.from() == 1 {
+ let current_word = movement::move_next_long_word_start(
+ text.slice(..),
+ movement::move_prev_long_word_start(text.slice(..), primary, 1),
+ 1,
+ );
+ paths.clear();
+ paths.push(
+ text.slice(current_word.from()..current_word.to())
+ .to_string(),
+ );
+ }
+ for sel in paths {
+ let p = sel.trim();
+ if !p.is_empty() {
+ if let Err(e) = cx.editor.open(PathBuf::from(p), action) {
+ cx.editor.set_error(format!("Open file failed: {:?}", e));
+ }
+ }
+ }
+}
+
fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
where
F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1459,6 +1580,7 @@ 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 file_picker_config = cx.editor.config.file_picker.clone();
let completions = search_completions(cx, None);
let prompt = ui::regex_prompt(
@@ -1487,41 +1609,55 @@ fn global_search(cx: &mut Context) {
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;
+ WalkBuilder::new(search_root)
+ .hidden(file_picker_config.hidden)
+ .parents(file_picker_config.parents)
+ .ignore(file_picker_config.ignore)
+ .git_ignore(file_picker_config.git_ignore)
+ .git_global(file_picker_config.git_global)
+ .git_exclude(file_picker_config.git_exclude)
+ .max_depth(file_picker_config.max_depth)
+ .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,
}
- 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_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
+ );
}
- });
- 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
- })
- });
+ WalkState::Continue
+ })
+ });
} else {
// Otherwise do nothing
// log::warn!("Global Search Invalid Pattern")
@@ -1626,19 +1762,42 @@ fn extend_to_line_bounds(cx: &mut Context) {
);
}
-fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
+enum Operation {
+ Delete,
+ Change,
+}
+
+fn delete_selection_impl(cx: &mut Context, op: Operation) {
+ let (view, doc) = current!(cx.editor);
+
let text = doc.text().slice(..);
- let selection = doc.selection(view_id);
+ let selection = doc.selection(view.id);
- // first yank the selection
- let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
- reg.write(values);
+ if cx.register != Some('_') {
+ // first yank the selection
+ let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
+ let reg_name = cx.register.unwrap_or('"');
+ let registers = &mut cx.editor.registers;
+ let reg = registers.get_mut(reg_name);
+ reg.write(values);
+ };
// then delete
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
(range.from(), range.to(), None)
});
- doc.apply(&transaction, view_id);
+ doc.apply(&transaction, view.id);
+
+ match op {
+ Operation::Delete => {
+ doc.append_changes_to_history(view.id);
+ // exit select mode, if currently in select mode
+ exit_select_mode(cx);
+ }
+ Operation::Change => {
+ enter_insert_mode(doc);
+ }
+ }
}
#[inline]
@@ -1653,25 +1812,21 @@ fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Sel
}
fn delete_selection(cx: &mut Context) {
- 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);
- delete_selection_impl(reg, doc, view.id);
-
- doc.append_changes_to_history(view.id);
+ delete_selection_impl(cx, Operation::Delete);
+}
- // exit select mode, if currently in select mode
- exit_select_mode(cx);
+fn delete_selection_noyank(cx: &mut Context) {
+ cx.register = Some('_');
+ delete_selection_impl(cx, Operation::Delete);
}
fn change_selection(cx: &mut Context) {
- 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);
- delete_selection_impl(reg, doc, view.id);
- enter_insert_mode(doc);
+ delete_selection_impl(cx, Operation::Change);
+}
+
+fn change_selection_noyank(cx: &mut Context) {
+ cx.register = Some('_');
+ delete_selection_impl(cx, Operation::Change);
}
fn collapse_selection(cx: &mut Context) {
@@ -1820,7 +1975,7 @@ mod cmd {
let jobs = &mut cx.jobs;
let (_, doc) = current!(cx.editor);
- if let Some(path) = path {
+ if let Some(ref path) = path {
doc.set_path(Some(path.as_ref()))
.context("invalid filepath")?;
}
@@ -1840,6 +1995,11 @@ mod cmd {
});
let future = doc.format_and_save(fmt);
cx.jobs.add(Job::new(future).wait_before_exiting());
+
+ if path.is_some() {
+ let id = doc.id();
+ let _ = cx.editor.refresh_language_server(id);
+ }
Ok(())
}
@@ -2466,6 +2626,26 @@ mod cmd {
Ok(())
}
+ pub(super) fn goto_line_number(
+ cx: &mut compositor::Context,
+ args: &[&str],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ if args.is_empty() {
+ bail!("Line number required");
+ }
+
+ let line = args[0].parse::<usize>()?;
+
+ goto_line_impl(&mut cx.editor, NonZeroUsize::new(line));
+
+ let (view, doc) = current!(cx.editor);
+
+ view.ensure_cursor_in_view(doc, line);
+
+ Ok(())
+ }
+
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
@@ -2768,6 +2948,13 @@ mod cmd {
fun: tutor,
completer: None,
},
+ TypableCommand {
+ name: "goto",
+ aliases: &["g"],
+ doc: "Go to line number.",
+ fun: goto_line_number,
+ completer: None,
+ }
];
pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2830,6 +3017,15 @@ fn command_mode(cx: &mut Context) {
return;
}
+ // If command is numeric, interpret as line number and go there.
+ if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
+ if let Err(e) = cmd::goto_line_number(cx, &parts[0..], event) {
+ cx.editor.set_error(format!("{}", e));
+ }
+ return;
+ }
+
+ // Handle typable commands
if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
cx.editor.set_error(format!("{}", e));
@@ -2855,7 +3051,7 @@ fn command_mode(cx: &mut Context) {
fn file_picker(cx: &mut Context) {
let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
- let picker = ui::file_picker(root);
+ let picker = ui::file_picker(root, &cx.editor.config);
cx.push_layer(Box::new(picker));
}
@@ -3463,10 +3659,14 @@ fn push_jump(editor: &mut Editor) {
}
fn goto_line(cx: &mut Context) {
- if let Some(count) = cx.count {
- push_jump(cx.editor);
+ goto_line_impl(&mut cx.editor, cx.count)
+}
- let (view, doc) = current!(cx.editor);
+fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
+ if let Some(count) = count {
+ push_jump(editor);
+
+ let (view, doc) = current!(editor);
let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
// If the last line is blank, don't jump to it.
doc.text().len_lines().saturating_sub(2)
@@ -5065,7 +5265,9 @@ fn match_brackets(cx: &mut Context) {
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone().transform(|range| {
- if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
+ if let Some(pos) =
+ match_brackets::find_matching_bracket_fuzzy(syntax, doc.text(), range.anchor)
+ {
range.put_cursor(text, pos, doc.mode == Mode::Select)
} else {
range
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 42a62fc2..b317242d 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -512,6 +512,7 @@ impl Default for Keymaps {
"g" => { "Goto"
"g" => goto_file_start,
"e" => goto_last_line,
+ "f" => goto_file,
"h" => goto_line_start,
"l" => goto_line_end,
"s" => goto_first_nonwhitespace,
@@ -537,9 +538,9 @@ impl Default for Keymaps {
"O" => open_above,
"d" => delete_selection,
- // TODO: also delete without yanking
+ "A-d" => delete_selection_noyank,
"c" => change_selection,
- // TODO: also change delete without yanking
+ "A-c" => change_selection_noyank,
"C" => copy_selection_on_next_line,
"A-C" => copy_selection_on_prev_line,
@@ -604,7 +605,7 @@ impl Default for Keymaps {
// "q" => record_macro,
// "Q" => replay_macro,
- // & align selections
+ "&" => align_selections,
"_" => trim_selections,
"(" => rotate_selections_backward,
@@ -622,6 +623,8 @@ impl Default for Keymaps {
"C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
+ "f" => goto_file_hsplit,
+ "F" => goto_file_vsplit,
"C-q" | "q" => wclose,
"C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left,
@@ -670,6 +673,8 @@ impl Default for Keymaps {
"C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
+ "f" => goto_file_hsplit,
+ "F" => goto_file_vsplit,
"C-q" | "q" => wclose,
"C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left,
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 0e243271..96c5f083 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -18,7 +18,6 @@ use helix_core::{
use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
- editor::LineNumber,
graphics::{Color, CursorKind, Modifier, Rect, Style},
info::Info,
input::KeyEvent,
@@ -392,7 +391,7 @@ impl EditorView {
use helix_core::match_brackets;
let pos = doc.selection(view.id).primary().cursor(text);
- let pos = match_brackets::find(syntax, doc.text(), pos)
+ let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
.and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
if let Some(pos) = pos {
@@ -430,22 +429,6 @@ impl EditorView {
let text = doc.text().slice(..);
let last_line = view.last_line(doc);
- let linenr = theme.get("ui.linenr");
- let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
-
- let warning = theme.get("warning");
- let error = theme.get("error");
- let info = theme.get("info");
- let hint = theme.get("hint");
-
- // Whether to draw the line number for the last line of the
- // document or not. We only draw it if it's not an empty line.
- let draw_last = text.line_to_byte(last_line) < text.len_bytes();
-
- let current_line = doc
- .text()
- .char_to_line(doc.selection(view.id).primary().cursor(text));
-
// it's used inside an iterator so the collect isn't needless:
// https://github.com/rust-lang/rust-clippy/issues/6164
#[allow(clippy::needless_collect)]
@@ -455,146 +438,137 @@ impl EditorView {
.map(|range| range.cursor_line(text))
.collect();
- let mut breakpoints: Option<&Vec<SourceBreakpoint>> = None;
- let mut stack_frame: Option<&StackFrame> = None;
- if let Some(path) = doc.path() {
- breakpoints = all_breakpoints.get(path);
- if let Some(debugger) = debugger {
- // if we have a frame, and the frame path matches document
- if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id)
- {
- let frame = debugger
- .stack_frames
- .get(&thread_id)
- .and_then(|bt| bt.get(frame)); // TODO: drop the clone..
- if let Some(StackFrame {
- source: Some(source),
- ..
- }) = &frame
- {
- if source.path.as_ref() == Some(path) {
- stack_frame = frame;
- }
- };
- };
- }
+ use helix_view::gutter::GutterFn;
+ fn breakpoints<'doc>(
+ doc: &'doc Document,
+ _view: &View,
+ theme: &Theme,
+ _config: &Config,
+ _is_focused: bool,
+ _width: usize,
+ ) -> GutterFn<'doc> {
+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
+ //
+ })
}
-
- for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
- use helix_core::diagnostic::Severity;
- if let Ok(diagnostic) = doc.diagnostics().binary_search_by_key(&line, |d| d.line) {
- let diagnostic = &doc.diagnostics()[diagnostic];
- surface.set_stringn(
- viewport.x,
- viewport.y + i as u16,
- "●",
- 1,
- match diagnostic.severity {
- Some(Severity::Error) => error,
- Some(Severity::Warning) | None => warning,
- Some(Severity::Info) => info,
- Some(Severity::Hint) => hint,
- },
- );
- }
-
- let selected = cursors.contains(&line);
-
- // TODO: debugger should translate received breakpoints to 0-indexing
-
- if let Some(user) = breakpoints.as_ref() {
- let debugger_breakpoint = if let Some(debugger) = dbg_breakpoints.as_ref() {
- debugger.iter().find(|breakpoint| {
- if breakpoint.source.is_some()
- && doc.path().is_some()
- && breakpoint.source.as_ref().unwrap().path == doc.path().cloned()
- {
- match (breakpoint.line, breakpoint.end_line) {
- #[allow(clippy::int_plus_one)]
- (Some(l), Some(el)) => l - 1 <= line && line <= el - 1,
- (Some(l), None) => l - 1 == line,
- _ => false,
- }
- } else {
- false
- }
- })
- } else {
- None
- };
-
- if let Some(breakpoint) = user.iter().find(|breakpoint| breakpoint.line - 1 == line)
- {
- let verified = debugger_breakpoint.map(|b| b.verified).unwrap_or(false);
- let mut style =
- if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
- error.add_modifier(Modifier::UNDERLINED)
- } else if breakpoint.condition.is_some() {
- error
- } else if breakpoint.log_message.is_some() {
- info
- } else {
- warning
- };
- if !verified {
- // Faded colors
- style = if let Some(Color::Rgb(r, g, b)) = style.fg {
- style.fg(Color::Rgb(
- ((r as f32) * 0.4).floor() as u8,
- ((g as f32) * 0.4).floor() as u8,
- ((b as f32) * 0.4).floor() as u8,
- ))
- } else {
- style.fg(Color::Gray)
- }
- };
- surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, style);
- } else if let Some(breakpoint) = debugger_breakpoint {
- let style = if breakpoint.verified {
- info
- } else {
- info.fg(Color::Gray)
- };
- surface.set_stringn(viewport.x, viewport.y + i as u16, "⊚", 1, style);
- }
- }
-
- if let Some(frame) = stack_frame {
- if frame.line - 1 == line {
- surface.set_style(
- Rect::new(viewport.x, viewport.y + i as u16, 6, 1),
- helix_view::graphics::Style::default()
- .bg(helix_view::graphics::Color::LightYellow),
+ // let mut breakpoints: Option<&Vec<SourceBreakpoint>> = None;
+ // let mut stack_frame: Option<&StackFrame> = None;
+ // if let Some(path) = doc.path() {
+ // breakpoints = all_breakpoints.get(path);
+ // if let Some(debugger) = debugger {
+ // // if we have a frame, and the frame path matches document
+ // if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id)
+ // {
+ // let frame = debugger
+ // .stack_frames
+ // .get(&thread_id)
+ // .and_then(|bt| bt.get(frame)); // TODO: drop the clone..
+ // if let Some(StackFrame {
+ // source: Some(source),
+ // ..
+ // }) = &frame
+ // {
+ // if source.path.as_ref() == Some(path) {
+ // stack_frame = frame;
+ // }
+ // };
+ // };
+ // }
+ // }
+
+ // TODO: debugger should translate received breakpoints to 0-indexing
+
+ // if let Some(user) = breakpoints.as_ref() {
+ // let debugger_breakpoint = if let Some(debugger) = dbg_breakpoints.as_ref() {
+ // debugger.iter().find(|breakpoint| {
+ // if breakpoint.source.is_some()
+ // && doc.path().is_some()
+ // && breakpoint.source.as_ref().unwrap().path == doc.path().cloned()
+ // {
+ // match (breakpoint.line, breakpoint.end_line) {
+ // #[allow(clippy::int_plus_one)]
+ // (Some(l), Some(el)) => l - 1 <= line && line <= el - 1,
+ // (Some(l), None) => l - 1 == line,
+ // _ => false,
+ // }
+ // } else {
+ // false
+ // }
+ // })
+ // } else {
+ // None
+ // };
+
+ // if let Some(breakpoint) = user.iter().find(|breakpoint| breakpoint.line - 1 == line)
+ // {
+ // let verified = debugger_breakpoint.map(|b| b.verified).unwrap_or(false);
+ // let mut style =
+ // if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
+ // error.add_modifier(Modifier::UNDERLINED)
+ // } else if breakpoint.condition.is_some() {
+ // error
+ // } else if breakpoint.log_message.is_some() {
+ // info
+ // } else {
+ // warning
+ // };
+ // if !verified {
+ // // Faded colors
+ // style = if let Some(Color::Rgb(r, g, b)) = style.fg {
+ // style.fg(Color::Rgb(
+ // ((r as f32) * 0.4).floor() as u8,
+ // ((g as f32) * 0.4).floor() as u8,
+ // ((b as f32) * 0.4).floor() as u8,
+ // ))
+ // } else {
+ // style.fg(Color::Gray)
+ // }
+ // };
+ // surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, style);
+ // } else if let Some(breakpoint) = debugger_breakpoint {
+ // let style = if breakpoint.verified {
+ // info
+ // } else {
+ // info.fg(Color::Gray)
+ // };
+ // surface.set_stringn(viewport.x, viewport.y + i as u16, "⊚", 1, style);
+ // }
+ // }
+
+ // if let Some(frame) = stack_frame {
+ // if frame.line - 1 == line {
+ // surface.set_style(
+ // Rect::new(viewport.x, viewport.y + i as u16, 6, 1),
+ // helix_view::graphics::Style::default()
+ // .bg(helix_view::graphics::Color::LightYellow),
+ // );
+ // }
+ // }
+
+ let mut offset = 0;
+
+ // avoid lots of small allocations by reusing a text buffer for each line
+ let mut text = String::with_capacity(8);
+
+ for (constructor, width) in view.gutters() {
+ let gutter = constructor(doc, view, theme, config, is_focused, *width);
+ text.reserve(*width); // ensure there's enough space for the gutter
+ for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
+ let selected = cursors.contains(&line);
+
+ if let Some(style) = gutter(line, selected, &mut text) {
+ surface.set_stringn(
+ viewport.x + offset,
+ viewport.y + i as u16,
+ &text,
+ *width,
+ style,
);
}
+ text.clear();
}
- let text = if line == last_line && !draw_last {
- " ~".into()
- } else {
- let line = match config.line_number {
- LineNumber::Absolute => line + 1,
- LineNumber::Relative => {
- if current_line == line {
- line + 1
- } else {
- abs_diff(current_line, line)
- }
- }
- };
- format!("{:>5}", line)
- };
- surface.set_stringn(
- viewport.x + 1,
- viewport.y + i as u16,
- text,
- 5,
- if selected && is_focused {
- linenr_select
- } else {
- linenr
- },
- );
+ offset += *width as u16;
}
}
@@ -1364,12 +1338,3 @@ fn canonicalize_key(key: &mut KeyEvent) {
key.modifiers.remove(KeyModifiers::SHIFT)
}
}
-
-#[inline]
-const fn abs_diff(a: usize, b: usize) -> usize {
- if a > b {
- a - b
- } else {
- b - a
- }
-}
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index 61630d55..ca8303dd 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -55,7 +55,7 @@ fn parse<'a>(
fn to_span(text: pulldown_cmark::CowStr) -> Span {
use std::ops::Deref;
Span::raw::<std::borrow::Cow<_>>(match text {
- CowStr::Borrowed(s) => s.to_string().into(), // could retain borrow
+ CowStr::Borrowed(s) => s.into(),
CowStr::Boxed(s) => s.to_string().into(),
CowStr::Inlined(s) => s.deref().to_owned().into(),
})
@@ -179,7 +179,9 @@ fn parse<'a>(
spans.push(Span::raw(" "));
}
Event::Rule => {
- lines.push(Spans::from("---"));
+ let mut span = Span::raw("---");
+ span.style = code_style;
+ lines.push(Spans::from(span));
lines.push(Spans::default());
}
// TaskListMarker(bool) true if checked
@@ -226,6 +228,7 @@ impl Component for Markdown {
return None;
}
let contents = parse(&self.contents, None, &self.config_loader);
+ // TODO: account for tab width
let max_text_width = (viewport.0 - padding).min(120);
let mut text_width = 0;
let mut height = padding;
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 9d3b0bc5..3c203326 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -93,13 +93,22 @@ pub fn regex_prompt(
)
}
-pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
+pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time;
// We want to exclude files that the editor can't handle yet
let mut type_builder = TypesBuilder::new();
let mut walk_builder = WalkBuilder::new(&root);
+ walk_builder
+ .hidden(config.file_picker.hidden)
+ .parents(config.file_picker.parents)
+ .ignore(config.file_picker.ignore)
+ .git_ignore(config.file_picker.git_ignore)
+ .git_global(config.file_picker.git_global)
+ .git_exclude(config.file_picker.git_exclude)
+ .max_depth(config.file_picker.max_depth);
+
let walk_builder = match type_builder.add(
"compressed",
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",