Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,10 @@ See [`gruvbox.toml`](gruvbox.toml) for a complete example with all available col
| `Shift+Sel` | Select text |
| `Shift+T` | Open theme picker |
| `Shift+E` | Open editor picker |
| `Shift+L` | Toggle line numbers |
| `Shift+P` | Open file browser |
| `Ctrl+E` | Open in editor |
| `Ctrl+L` | Go to line |
| `Ctrl+P` | Open fuzzy picker |
| `Ctrl+F` / `/` | Find |
| `Ctrl+Click` | Open link |
Expand Down
3 changes: 3 additions & 0 deletions src/app/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ impl App {
self.theme_picker.open = false;
self.search.mode = false;
self.reset_search_state();
self.clear_active_goto_line();
self.invalidate_theme_preview_cache();
self.store_current_theme_preview_from(&lines, &toc);
self.replace_content(lines, toc, link_spans, block_starts);
Expand Down Expand Up @@ -148,6 +149,8 @@ impl App {
self.invalidate_theme_preview_cache();
self.store_current_theme_preview_from(&new_lines, &new_toc);
self.replace_content(new_lines, new_toc, link_spans, block_starts);
self.goto_line.target = None;
self.goto_line.error = false;
if !self.search.query.is_empty() && !self.search.mode {
self.run_search();
}
Expand Down
92 changes: 92 additions & 0 deletions src/app/goto_line.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use super::App;
use crate::markdown::hash_str;

const GOTO_LINE_CONTEXT_OFFSET: usize = 5;

pub(crate) struct GotoLineState {
pub(super) mode: bool,
pub(super) draft: String,
pub(super) target: Option<usize>,
pub(super) error: bool,
pub(super) draft_hash: u64,
}

impl App {
pub(crate) fn is_goto_line_mode(&self) -> bool {
self.goto_line.mode
}

pub(crate) fn goto_line_draft(&self) -> &str {
&self.goto_line.draft
}

pub(crate) fn has_active_goto_line(&self) -> bool {
self.goto_line.target.is_some() || self.goto_line.error
}

pub(crate) fn goto_line_error(&self) -> bool {
self.goto_line.error
}

pub(crate) fn goto_line_target(&self) -> Option<usize> {
self.goto_line.target
}

pub(crate) fn begin_goto_line(&mut self) {
self.reset_numkey_state();
self.clear_active_search();
self.goto_line.mode = true;
self.goto_line.draft.clear();
self.goto_line.draft_hash = 0;
self.goto_line.error = false;
self.goto_line.target = None;
self.line_number_visible = true;
}

pub(crate) fn push_goto_draft(&mut self, ch: char) {
if ch.is_ascii_digit() {
self.goto_line.draft.push(ch);
self.goto_line.draft_hash = hash_str(&self.goto_line.draft);
}
}

pub(crate) fn pop_goto_draft(&mut self) {
self.goto_line.draft.pop();
self.goto_line.draft_hash = hash_str(&self.goto_line.draft);
}

pub(crate) fn confirm_goto_line(&mut self) {
self.goto_line.mode = false;
if self.goto_line.draft.is_empty() {
self.clear_active_goto_line();
return;
}
let logical = match self.goto_line.draft.parse::<usize>() {
Ok(n) if n >= 1 => n,
_ => {
self.goto_line.error = true;
return;
}
};
if let Some(render_index) = self.find_render_index_for_logical(logical) {
self.goto_line.target = Some(render_index);
self.goto_line.error = false;
let scroll_pos = render_index.saturating_sub(GOTO_LINE_CONTEXT_OFFSET);
self.scroll = scroll_pos.min(self.max_scroll());
} else {
self.goto_line.error = true;
}
}

pub(crate) fn clear_active_goto_line(&mut self) {
self.goto_line.mode = false;
self.goto_line.draft.clear();
self.goto_line.target = None;
self.goto_line.error = false;
self.goto_line.draft_hash = 0;
}

fn find_render_index_for_logical(&self, logical: usize) -> Option<usize> {
self.line_number_map.iter().position(|&n| n == logical)
}
}
19 changes: 19 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use std::{
mod search;
pub(crate) use search::SearchState;

mod goto_line;
pub(crate) use goto_line::GotoLineState;

mod file_picker;
mod fuzzy;
pub(crate) use file_picker::{FilePickerMode, FilePickerState, PickerIndexTruncation};
Expand Down Expand Up @@ -51,6 +54,10 @@ pub(crate) struct StatusCacheKey {
search_query_len: usize,
search_match_count: usize,
search_idx: usize,
goto_line_mode: bool,
goto_line_draft_hash: u64,
goto_line_target: Option<usize>,
goto_line_error: bool,
watch: bool,
flash_active: bool,
editor_flash_active: bool,
Expand Down Expand Up @@ -81,6 +88,7 @@ pub(crate) struct App {
line_number_map: Vec<usize>,
line_number_visible: bool,
pub(super) search: SearchState,
pub(super) goto_line: GotoLineState,
pub(super) debug_input: bool,
pub(super) filename: String,
pub(super) source: String,
Expand Down Expand Up @@ -198,6 +206,13 @@ impl App {
draft_hash: 0,
query_hash: 0,
},
goto_line: GotoLineState {
mode: false,
draft: String::new(),
target: None,
error: false,
draft_hash: 0,
},
debug_input,
filename,
source,
Expand Down Expand Up @@ -488,6 +503,10 @@ impl App {
search_query_len: self.search.query.len(),
search_match_count: self.search.matches.len(),
search_idx: self.search.idx,
goto_line_mode: self.goto_line.mode,
goto_line_draft_hash: self.goto_line.draft_hash,
goto_line_target: self.goto_line.target,
goto_line_error: self.goto_line.error,
watch: self.watch,
flash_active: self
.reload_flash
Expand Down
23 changes: 18 additions & 5 deletions src/render/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,32 @@ pub(super) fn render_content_panel(f: &mut Frame, app: &mut App, area: Rect) {

if app.is_line_number_visible() {
let digit_width = app.line_number_total().max(1).to_string().len();
let blank_gutter = format!("{:>w$}│ ", "", w = digit_width);
let gutter_style = Style::default().fg(theme.markdown.code_gutter);
let goto_target = app.goto_line_target();
let highlight_bg = theme.markdown.search_highlight_bg;
for (i, line) in visible_lines.iter_mut().enumerate() {
let idx = scroll + i;
let logical = app.line_number_at(idx);
let is_first =
logical > 0 && (idx == 0 || app.line_number_at(idx.saturating_sub(1)) != logical);
let gutter = if is_first {
format!("{:>w$}│ ", logical, w = digit_width)
let is_goto_target = goto_target == Some(idx);
let num_part = if is_first {
format!("{:>w$}", logical, w = digit_width)
} else {
blank_gutter.clone()
format!("{:>w$}", "", w = digit_width)
};
line.spans.insert(0, Span::styled(gutter, gutter_style));
if is_goto_target {
let hl = gutter_style.bg(highlight_bg);
line.spans.insert(0, Span::styled("│ ", hl));
line.spans
.insert(0, Span::styled(num_part, hl.fg(Color::White)));
for span in &mut line.spans[2..] {
span.style = span.style.bg(highlight_bg);
}
} else {
let gutter = format!("{num_part}│ ");
line.spans.insert(0, Span::styled(gutter, gutter_style));
}
}
}

Expand Down
36 changes: 21 additions & 15 deletions src/render/popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,54 +97,53 @@ pub(super) fn render_help_popup(f: &mut Frame, _app: &App) {
)]),
Line::from(""),
Line::from(vec![Span::styled(
"Navigation Search",
"Navigation Mouse",
section_style,
)]),
Line::from(vec![
Span::styled("j/k, ↑/↓ ", key_style),
Span::styled("scroll", text_style),
Span::raw(" "),
Span::styled("ctrl+f ", key_style),
Span::styled("find", text_style),
Span::styled("dbl-click ", key_style),
Span::styled("copy link", text_style),
]),
Line::from(vec![
Span::styled("u/d ", key_style),
Span::styled("page up/down", text_style),
Span::raw(" "),
Span::styled("n/N ", key_style),
Span::styled("next/prev", text_style),
Span::styled("ctrl+click ", key_style),
Span::styled("open link", text_style),
]),
Line::from(vec![
Span::styled("g/G ", key_style),
Span::styled("top/bottom", text_style),
Span::raw(" "),
Span::styled("shift+slct ", key_style),
Span::styled("select text", text_style),
]),
Line::from(vec![
Span::styled("1-9/0+1-9 ", key_style),
Span::styled("jump/reverse", text_style),
]),
Line::from(""),
Line::from(vec![
Span::styled("Mouse ", section_style),
Span::styled("Search ", section_style),
Span::styled("Watch", section_style),
]),
Line::from(vec![
Span::styled("dbl-click ", key_style),
Span::styled("copy link", text_style),
Span::raw(" "),
Span::styled("ctrl+f ", key_style),
Span::styled("find", text_style),
Span::raw(" "),
Span::styled("ctrl+w, w ", key_style),
Span::styled("toggle", text_style),
]),
Line::from(vec![
Span::styled("ctrl+click ", key_style),
Span::styled("open link", text_style),
Span::styled("n/N ", key_style),
Span::styled("next/prev", text_style),
Span::raw(" "),
Span::styled("ctrl+r, r ", key_style),
Span::styled("reload", text_style),
]),
Line::from(vec![
Span::styled("shift+slct ", key_style),
Span::styled("select text", text_style),
]),
Line::from(""),
Line::from(vec![Span::styled("Actions", section_style)]),
Line::from(vec![
Expand All @@ -154,6 +153,13 @@ pub(super) fn render_help_popup(f: &mut Frame, _app: &App) {
Span::styled("ctrl+e ", key_style),
Span::styled("edit", text_style),
]),
Line::from(vec![
Span::styled("shift+l ", key_style),
Span::styled("line number", text_style),
Span::raw(" "),
Span::styled("ctrl+l ", key_style),
Span::styled("goto", text_style),
]),
Line::from(vec![
Span::styled("shift+p ", key_style),
Span::styled("file browser", text_style),
Expand Down
44 changes: 43 additions & 1 deletion src/render/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,47 @@ pub(crate) fn status_search_section(app: &App) -> Option<Vec<Span<'static>>> {
Some(vec![span])
}

pub(crate) fn status_goto_line_section(app: &App) -> Option<Vec<Span<'static>>> {
let theme = app_theme();
if app.is_goto_line_mode() {
return Some(vec![Span::styled(
format!(" :{} ", app.goto_line_draft()),
Style::default()
.fg(theme.ui.status_search_fg)
.bg(theme.ui.status_search_bg),
)]);
}

if !app.has_active_goto_line() {
return None;
}

let span = if app.goto_line_error() {
Span::styled(
format!(" ✗ :{} ", app.goto_line_draft()),
Style::default()
.fg(theme.ui.status_error_fg)
.bg(theme.ui.status_error_bg),
)
} else if let Some(target) = app.goto_line_target() {
let logical = app.line_number_at(target);
Span::styled(
format!(" :{} ", logical),
Style::default()
.fg(theme.ui.status_success_fg)
.bg(theme.ui.status_success_bg),
)
} else {
return None;
};
Some(vec![span])
}

pub(crate) fn status_hint_segments(app: &App) -> &'static [&'static str] {
if app.is_search_mode() {
if app.is_goto_line_mode() || app.is_search_mode() {
&["enter confirm", "esc cancel"]
} else if app.has_active_goto_line() {
&["esc cancel"]
} else if app.has_active_search() {
&["n/N next/prev", "esc cancel"]
} else {
Expand Down Expand Up @@ -291,6 +329,10 @@ pub(crate) fn build_status_bar(app: &App, pct: u16) -> Vec<Span<'static>> {
left_section.extend(section);
}

if let Some(section) = status_goto_line_section(app) {
left_section.extend(section);
}

let file_open = app.has_content() || (!app.is_file_picker_open() && !app.is_picker_loading());
if file_open {
if let Some(section) = status_watch_section(app) {
Expand Down
Loading
Loading