diff options
Diffstat (limited to 'helix-term/src')
-rw-r--r-- | helix-term/src/application.rs | 4 | ||||
-rw-r--r-- | helix-term/src/commands.rs | 328 | ||||
-rw-r--r-- | helix-term/src/keymap.rs | 11 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 289 | ||||
-rw-r--r-- | helix-term/src/ui/markdown.rs | 7 | ||||
-rw-r--r-- | helix-term/src/ui/mod.rs | 11 |
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}", |