Show file preview in split pane in fuzzy finder (#534)
* Add preview pane for fuzzy finder * Fix picker preview lag by caching * Add picker preview for document symbols * Cache picker preview per document instead of view * Use line instead of range for preview doc * Add picker preview for buffer picker * Fix render bug and refactor picker * Refactor picker preview rendering * Split picker and preview and compose The current selected item is cloned on every event, which is undesirable * Refactor out clones in previewed picker * Retrieve doc from editor if possible in filepicker * Disable syntax highlight for picker preview Files already loaded in memory have syntax highlighting enabled * Ignore directory symlinks in file picker * Cleanup unnecessary pubs and derives * Remove unnecessary highlight from file picker * Reorganize buffer rendering * Use normal picker for code actions * Remove unnecessary generics and trait impls * Remove prepare_for_render and make render mutable * Skip picker preview if screen small, less padding
This commit is contained in:
parent
7d51805e94
commit
d84f8b5fde
15 changed files with 483 additions and 240 deletions
|
@ -27,7 +27,7 @@ use movement::Movement;
|
|||
|
||||
use crate::{
|
||||
compositor::{self, Component, Compositor},
|
||||
ui::{self, Picker, Popup, Prompt, PromptEvent},
|
||||
ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent},
|
||||
};
|
||||
|
||||
use crate::job::{self, Job, Jobs};
|
||||
|
@ -2212,7 +2212,7 @@ fn file_picker(cx: &mut Context) {
|
|||
fn buffer_picker(cx: &mut Context) {
|
||||
let current = view!(cx.editor).doc;
|
||||
|
||||
let picker = Picker::new(
|
||||
let picker = FilePicker::new(
|
||||
cx.editor
|
||||
.documents
|
||||
.iter()
|
||||
|
@ -2234,6 +2234,15 @@ fn buffer_picker(cx: &mut Context) {
|
|||
|editor: &mut Editor, (id, _path): &(DocumentId, Option<PathBuf>), _action| {
|
||||
editor.switch(*id, Action::Replace);
|
||||
},
|
||||
|editor, (id, path)| {
|
||||
let doc = &editor.documents.get(*id)?;
|
||||
let &view_id = doc.selections().keys().next()?;
|
||||
let line = doc
|
||||
.selection(view_id)
|
||||
.primary()
|
||||
.cursor_line(doc.text().slice(..));
|
||||
Some((path.clone()?, Some(line)))
|
||||
},
|
||||
);
|
||||
cx.push_layer(Box::new(picker));
|
||||
}
|
||||
|
@ -2287,7 +2296,7 @@ fn symbol_picker(cx: &mut Context) {
|
|||
}
|
||||
};
|
||||
|
||||
let picker = Picker::new(
|
||||
let picker = FilePicker::new(
|
||||
symbols,
|
||||
|symbol| (&symbol.name).into(),
|
||||
move |editor: &mut Editor, symbol, _action| {
|
||||
|
@ -2297,10 +2306,15 @@ fn symbol_picker(cx: &mut Context) {
|
|||
if let Some(range) =
|
||||
lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
|
||||
{
|
||||
doc.set_selection(view.id, Selection::single(range.to(), range.from()));
|
||||
doc.set_selection(view.id, Selection::single(range.anchor, range.head));
|
||||
align_view(doc, view, Align::Center);
|
||||
}
|
||||
},
|
||||
move |_editor, symbol| {
|
||||
let path = symbol.location.uri.to_file_path().unwrap();
|
||||
let line = Some(symbol.location.range.start.line as usize);
|
||||
Some((path, line))
|
||||
},
|
||||
);
|
||||
compositor.push(Box::new(picker))
|
||||
}
|
||||
|
@ -2332,6 +2346,7 @@ pub fn code_action(cx: &mut Context) {
|
|||
response: Option<lsp::CodeActionResponse>| {
|
||||
if let Some(actions) = response {
|
||||
let picker = Picker::new(
|
||||
true,
|
||||
actions,
|
||||
|action| match action {
|
||||
lsp::CodeActionOrCommand::CodeAction(action) => {
|
||||
|
@ -2703,7 +2718,7 @@ fn goto_impl(
|
|||
editor.set_error("No definition found.".to_string());
|
||||
}
|
||||
_locations => {
|
||||
let picker = ui::Picker::new(
|
||||
let picker = FilePicker::new(
|
||||
locations,
|
||||
move |location| {
|
||||
let file: Cow<'_, str> = (location.uri.scheme() == "file")
|
||||
|
@ -2728,6 +2743,11 @@ fn goto_impl(
|
|||
move |editor: &mut Editor, location, action| {
|
||||
jump_to(editor, location, offset_encoding, action)
|
||||
},
|
||||
|_editor, location| {
|
||||
let path = location.uri.to_file_path().unwrap();
|
||||
let line = Some(location.range.start.line as usize);
|
||||
Some((path, line))
|
||||
},
|
||||
);
|
||||
compositor.push(Box::new(picker));
|
||||
}
|
||||
|
@ -3729,8 +3749,7 @@ fn keep_primary_selection(cx: &mut Context) {
|
|||
let (view, doc) = current!(cx.editor);
|
||||
|
||||
let range = doc.selection(view.id).primary();
|
||||
let selection = Selection::single(range.anchor, range.head);
|
||||
doc.set_selection(view.id, selection);
|
||||
doc.set_selection(view.id, Selection::single(range.anchor, range.head));
|
||||
}
|
||||
|
||||
fn completion(cx: &mut Context) {
|
||||
|
|
|
@ -46,7 +46,7 @@ pub trait Component: Any + AnyComponent {
|
|||
}
|
||||
|
||||
/// Render the component onto the provided surface.
|
||||
fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
|
||||
fn render(&mut self, area: Rect, frame: &mut Surface, ctx: &mut Context);
|
||||
|
||||
/// Get cursor position and cursor kind.
|
||||
fn cursor(&self, _area: Rect, _ctx: &Editor) -> (Option<Position>, CursorKind) {
|
||||
|
@ -152,8 +152,8 @@ impl Compositor {
|
|||
|
||||
let area = *surface.area();
|
||||
|
||||
for layer in &self.layers {
|
||||
layer.render(area, surface, cx)
|
||||
for layer in &mut self.layers {
|
||||
layer.render(area, surface, cx);
|
||||
}
|
||||
|
||||
let (pos, kind) = self.cursor(area, cx.editor);
|
||||
|
|
|
@ -241,7 +241,7 @@ impl Component for Completion {
|
|||
self.popup.required_size(viewport)
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
self.popup.render(area, surface, cx);
|
||||
|
||||
// if we have a selection, render a markdown popup on top/below with info
|
||||
|
@ -263,7 +263,7 @@ impl Component for Completion {
|
|||
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
|
||||
- view.first_line) as u16;
|
||||
|
||||
let doc = match &option.documentation {
|
||||
let mut doc = match &option.documentation {
|
||||
Some(lsp::Documentation::String(contents))
|
||||
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::PlainText,
|
||||
|
|
|
@ -34,7 +34,7 @@ pub struct EditorView {
|
|||
last_insert: (commands::Command, Vec<KeyEvent>),
|
||||
completion: Option<Completion>,
|
||||
spinners: ProgressSpinners,
|
||||
pub autoinfo: Option<Info>,
|
||||
autoinfo: Option<Info>,
|
||||
}
|
||||
|
||||
pub const GUTTER_OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
||||
|
@ -78,8 +78,26 @@ impl EditorView {
|
|||
view.area.width - GUTTER_OFFSET,
|
||||
view.area.height.saturating_sub(1),
|
||||
); // - 1 for statusline
|
||||
let offset = Position::new(view.first_line, view.first_col);
|
||||
let height = view.area.height.saturating_sub(1); // - 1 for statusline
|
||||
|
||||
self.render_buffer(doc, view, area, surface, theme, is_focused, loader);
|
||||
let highlights = Self::doc_syntax_highlights(doc, offset, height, theme, loader);
|
||||
let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
|
||||
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
|
||||
Box::new(syntax::merge(
|
||||
highlights,
|
||||
Self::doc_selection_highlights(doc, view, theme),
|
||||
))
|
||||
} else {
|
||||
Box::new(highlights)
|
||||
};
|
||||
|
||||
Self::render_text_highlights(doc, offset, area, surface, theme, highlights);
|
||||
Self::render_gutter(doc, view, area, surface, theme);
|
||||
|
||||
if is_focused {
|
||||
Self::render_focused_view_elements(view, doc, area, theme, surface);
|
||||
}
|
||||
|
||||
// if we're not at the edge of the screen, draw a right border
|
||||
if viewport.right() != view.area.right() {
|
||||
|
@ -94,7 +112,7 @@ impl EditorView {
|
|||
}
|
||||
}
|
||||
|
||||
self.render_diagnostics(doc, view, area, surface, theme, is_focused);
|
||||
self.render_diagnostics(doc, view, area, surface, theme);
|
||||
|
||||
let area = Rect::new(
|
||||
view.area.x,
|
||||
|
@ -105,31 +123,34 @@ impl EditorView {
|
|||
self.render_statusline(doc, view, area, surface, theme, is_focused);
|
||||
}
|
||||
|
||||
/// Get syntax highlights for a document in a view represented by the first line
|
||||
/// and column (`offset`) and the last line. This is done instead of using a view
|
||||
/// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_buffer(
|
||||
&self,
|
||||
doc: &Document,
|
||||
view: &View,
|
||||
viewport: Rect,
|
||||
surface: &mut Surface,
|
||||
pub fn doc_syntax_highlights<'doc>(
|
||||
doc: &'doc Document,
|
||||
offset: Position,
|
||||
height: u16,
|
||||
theme: &Theme,
|
||||
is_focused: bool,
|
||||
loader: &syntax::Loader,
|
||||
) {
|
||||
) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
|
||||
let text = doc.text().slice(..);
|
||||
|
||||
let last_line = view.last_line(doc);
|
||||
let last_line = std::cmp::min(
|
||||
// Saturating subs to make it inclusive zero indexing.
|
||||
(offset.row + height as usize).saturating_sub(1),
|
||||
doc.text().len_lines().saturating_sub(1),
|
||||
);
|
||||
|
||||
let range = {
|
||||
// calculate viewport byte ranges
|
||||
let start = text.line_to_byte(view.first_line);
|
||||
let start = text.line_to_byte(offset.row);
|
||||
let end = text.line_to_byte(last_line + 1);
|
||||
|
||||
start..end
|
||||
};
|
||||
|
||||
// TODO: range doesn't actually restrict source, just highlight range
|
||||
let highlights: Vec<_> = match doc.syntax() {
|
||||
let highlights = match doc.syntax() {
|
||||
Some(syntax) => {
|
||||
let scopes = theme.scopes();
|
||||
syntax
|
||||
|
@ -151,20 +172,16 @@ impl EditorView {
|
|||
Some(config_ref)
|
||||
})
|
||||
})
|
||||
.map(|event| event.unwrap())
|
||||
.collect() // TODO: we collect here to avoid holding the lock, fix later
|
||||
}
|
||||
None => vec![Ok(HighlightEvent::Source {
|
||||
None => vec![HighlightEvent::Source {
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
})],
|
||||
};
|
||||
let mut spans = Vec::new();
|
||||
let mut visual_x = 0u16;
|
||||
let mut line = 0u16;
|
||||
let tab_width = doc.tab_width();
|
||||
let tab = " ".repeat(tab_width);
|
||||
|
||||
let highlights = highlights.into_iter().map(|event| match event.unwrap() {
|
||||
}],
|
||||
}
|
||||
.into_iter()
|
||||
.map(move |event| match event {
|
||||
// convert byte offsets to char offset
|
||||
HighlightEvent::Source { start, end } => {
|
||||
let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start));
|
||||
|
@ -174,13 +191,44 @@ impl EditorView {
|
|||
event => event,
|
||||
});
|
||||
|
||||
let selections = doc.selection(view.id);
|
||||
let primary_idx = selections.primary_index();
|
||||
Box::new(highlights)
|
||||
}
|
||||
|
||||
/// Get highlight spans for document diagnostics
|
||||
pub fn doc_diagnostics_highlights(
|
||||
doc: &Document,
|
||||
theme: &Theme,
|
||||
) -> Vec<(usize, std::ops::Range<usize>)> {
|
||||
let diagnostic_scope = theme
|
||||
.find_scope_index("diagnostic")
|
||||
.or_else(|| theme.find_scope_index("ui.cursor"))
|
||||
.or_else(|| theme.find_scope_index("ui.selection"))
|
||||
.expect("no selection scope found!");
|
||||
|
||||
doc.diagnostics()
|
||||
.iter()
|
||||
.map(|diagnostic| {
|
||||
(
|
||||
diagnostic_scope,
|
||||
diagnostic.range.start..diagnostic.range.end,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get highlight spans for selections in a document view.
|
||||
pub fn doc_selection_highlights(
|
||||
doc: &Document,
|
||||
view: &View,
|
||||
theme: &Theme,
|
||||
) -> Vec<(usize, std::ops::Range<usize>)> {
|
||||
let text = doc.text().slice(..);
|
||||
let selection = doc.selection(view.id);
|
||||
let primary_idx = selection.primary_index();
|
||||
|
||||
let selection_scope = theme
|
||||
.find_scope_index("ui.selection")
|
||||
.expect("no selection scope found!");
|
||||
|
||||
let base_cursor_scope = theme
|
||||
.find_scope_index("ui.cursor")
|
||||
.unwrap_or(selection_scope);
|
||||
|
@ -192,64 +240,59 @@ impl EditorView {
|
|||
}
|
||||
.unwrap_or(base_cursor_scope);
|
||||
|
||||
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
|
||||
// TODO: primary + insert mode patching:
|
||||
// (ui.cursor.primary).patch(mode).unwrap_or(cursor)
|
||||
let primary_cursor_scope = theme
|
||||
.find_scope_index("ui.cursor.primary")
|
||||
.unwrap_or(cursor_scope);
|
||||
let primary_selection_scope = theme
|
||||
.find_scope_index("ui.selection.primary")
|
||||
.unwrap_or(selection_scope);
|
||||
let primary_cursor_scope = theme
|
||||
.find_scope_index("ui.cursor.primary")
|
||||
.unwrap_or(cursor_scope);
|
||||
let primary_selection_scope = theme
|
||||
.find_scope_index("ui.selection.primary")
|
||||
.unwrap_or(selection_scope);
|
||||
|
||||
// inject selections as highlight scopes
|
||||
let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
|
||||
for (i, range) in selections.iter().enumerate() {
|
||||
let (cursor_scope, selection_scope) = if i == primary_idx {
|
||||
(primary_cursor_scope, primary_selection_scope)
|
||||
} else {
|
||||
(cursor_scope, selection_scope)
|
||||
};
|
||||
let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
|
||||
for (i, range) in selection.iter().enumerate() {
|
||||
let (cursor_scope, selection_scope) = if i == primary_idx {
|
||||
(primary_cursor_scope, primary_selection_scope)
|
||||
} else {
|
||||
(cursor_scope, selection_scope)
|
||||
};
|
||||
|
||||
// Special-case: cursor at end of the rope.
|
||||
if range.head == range.anchor && range.head == text.len_chars() {
|
||||
spans.push((cursor_scope, range.head..range.head + 1));
|
||||
continue;
|
||||
}
|
||||
|
||||
let range = range.min_width_1(text);
|
||||
if range.head > range.anchor {
|
||||
// Standard case.
|
||||
let cursor_start = prev_grapheme_boundary(text, range.head);
|
||||
spans.push((selection_scope, range.anchor..cursor_start));
|
||||
spans.push((cursor_scope, cursor_start..range.head));
|
||||
} else {
|
||||
// Reverse case.
|
||||
let cursor_end = next_grapheme_boundary(text, range.head);
|
||||
spans.push((cursor_scope, range.head..cursor_end));
|
||||
spans.push((selection_scope, cursor_end..range.anchor));
|
||||
}
|
||||
// Special-case: cursor at end of the rope.
|
||||
if range.head == range.anchor && range.head == text.len_chars() {
|
||||
spans.push((cursor_scope, range.head..range.head + 1));
|
||||
continue;
|
||||
}
|
||||
|
||||
Box::new(syntax::merge(highlights, spans))
|
||||
} else {
|
||||
Box::new(highlights)
|
||||
};
|
||||
let range = range.min_width_1(text);
|
||||
if range.head > range.anchor {
|
||||
// Standard case.
|
||||
let cursor_start = prev_grapheme_boundary(text, range.head);
|
||||
spans.push((selection_scope, range.anchor..cursor_start));
|
||||
spans.push((cursor_scope, cursor_start..range.head));
|
||||
} else {
|
||||
// Reverse case.
|
||||
let cursor_end = next_grapheme_boundary(text, range.head);
|
||||
spans.push((cursor_scope, range.head..cursor_end));
|
||||
spans.push((selection_scope, cursor_end..range.anchor));
|
||||
}
|
||||
}
|
||||
|
||||
// diagnostic injection
|
||||
let diagnostic_scope = theme.find_scope_index("diagnostic").unwrap_or(cursor_scope);
|
||||
let highlights = Box::new(syntax::merge(
|
||||
highlights,
|
||||
doc.diagnostics()
|
||||
.iter()
|
||||
.map(|diagnostic| {
|
||||
(
|
||||
diagnostic_scope,
|
||||
diagnostic.range.start..diagnostic.range.end,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
));
|
||||
spans
|
||||
}
|
||||
|
||||
pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
|
||||
doc: &Document,
|
||||
offset: Position,
|
||||
viewport: Rect,
|
||||
surface: &mut Surface,
|
||||
theme: &Theme,
|
||||
highlights: H,
|
||||
) {
|
||||
let text = doc.text().slice(..);
|
||||
|
||||
let mut spans = Vec::new();
|
||||
let mut visual_x = 0u16;
|
||||
let mut line = 0u16;
|
||||
let tab_width = doc.tab_width();
|
||||
let tab = " ".repeat(tab_width);
|
||||
|
||||
'outer: for event in highlights {
|
||||
match event {
|
||||
|
@ -273,14 +316,14 @@ impl EditorView {
|
|||
});
|
||||
|
||||
for grapheme in RopeGraphemes::new(text) {
|
||||
let out_of_bounds = visual_x < view.first_col as u16
|
||||
|| visual_x >= viewport.width + view.first_col as u16;
|
||||
let out_of_bounds = visual_x < offset.col as u16
|
||||
|| visual_x >= viewport.width + offset.col as u16;
|
||||
|
||||
if LineEnding::from_rope_slice(&grapheme).is_some() {
|
||||
if !out_of_bounds {
|
||||
// we still want to render an empty cell with the style
|
||||
surface.set_string(
|
||||
viewport.x + visual_x - view.first_col as u16,
|
||||
viewport.x + visual_x - offset.col as u16,
|
||||
viewport.y + line,
|
||||
" ",
|
||||
style,
|
||||
|
@ -310,7 +353,7 @@ impl EditorView {
|
|||
if !out_of_bounds {
|
||||
// if we're offscreen just keep going until we hit a new line
|
||||
surface.set_string(
|
||||
viewport.x + visual_x - view.first_col as u16,
|
||||
viewport.x + visual_x - offset.col as u16,
|
||||
viewport.y + line,
|
||||
grapheme,
|
||||
style,
|
||||
|
@ -323,14 +366,108 @@ impl EditorView {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// render gutters
|
||||
/// Render brace match, selected line numbers, etc (meant for the focused view only)
|
||||
pub fn render_focused_view_elements(
|
||||
view: &View,
|
||||
doc: &Document,
|
||||
viewport: Rect,
|
||||
theme: &Theme,
|
||||
surface: &mut Surface,
|
||||
) {
|
||||
let text = doc.text().slice(..);
|
||||
let selection = doc.selection(view.id);
|
||||
let last_line = view.last_line(doc);
|
||||
let screen = {
|
||||
let start = text.line_to_char(view.first_line);
|
||||
let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text.
|
||||
Range::new(start, end)
|
||||
};
|
||||
|
||||
let linenr: Style = theme.get("ui.linenr");
|
||||
let warning: Style = theme.get("warning");
|
||||
let error: Style = theme.get("error");
|
||||
let info: Style = theme.get("info");
|
||||
let hint: Style = theme.get("hint");
|
||||
// render selected linenr(s)
|
||||
let linenr_select: Style = theme
|
||||
.try_get("ui.linenr.selected")
|
||||
.unwrap_or_else(|| theme.get("ui.linenr"));
|
||||
|
||||
// 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();
|
||||
|
||||
for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
|
||||
let head = view.screen_coords_at_pos(
|
||||
doc,
|
||||
text,
|
||||
if selection.head > selection.anchor {
|
||||
selection.head - 1
|
||||
} else {
|
||||
selection.head
|
||||
},
|
||||
);
|
||||
if let Some(head) = head {
|
||||
// Highlight line number for selected lines.
|
||||
let line_number = view.first_line + head.row;
|
||||
let line_number_text = if line_number == last_line && !draw_last {
|
||||
" ~".into()
|
||||
} else {
|
||||
format!("{:>5}", line_number + 1)
|
||||
};
|
||||
surface.set_stringn(
|
||||
viewport.x - GUTTER_OFFSET + 1,
|
||||
viewport.y + head.row as u16,
|
||||
line_number_text,
|
||||
5,
|
||||
linenr_select,
|
||||
);
|
||||
|
||||
// Highlight matching braces
|
||||
// TODO: set cursor position for IME
|
||||
if let Some(syntax) = doc.syntax() {
|
||||
use helix_core::match_brackets;
|
||||
let pos = doc
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
let pos = match_brackets::find(syntax, doc.text(), pos)
|
||||
.and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
|
||||
|
||||
if let Some(pos) = pos {
|
||||
// ensure col is on screen
|
||||
if (pos.col as u16) < viewport.width + view.first_col as u16
|
||||
&& pos.col >= view.first_col
|
||||
{
|
||||
let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
|
||||
Style::default()
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.add_modifier(Modifier::DIM)
|
||||
});
|
||||
|
||||
surface
|
||||
.get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16)
|
||||
.set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_gutter(
|
||||
doc: &Document,
|
||||
view: &View,
|
||||
viewport: Rect,
|
||||
surface: &mut Surface,
|
||||
theme: &Theme,
|
||||
) {
|
||||
let text = doc.text().slice(..);
|
||||
let last_line = view.last_line(doc);
|
||||
|
||||
let linenr = theme.get("ui.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.
|
||||
|
@ -368,80 +505,6 @@ impl EditorView {
|
|||
linenr,
|
||||
);
|
||||
}
|
||||
|
||||
// render selections and selected linenr(s)
|
||||
let linenr_select: Style = theme
|
||||
.try_get("ui.linenr.selected")
|
||||
.unwrap_or_else(|| theme.get("ui.linenr"));
|
||||
|
||||
if is_focused {
|
||||
let screen = {
|
||||
let start = text.line_to_char(view.first_line);
|
||||
let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text.
|
||||
Range::new(start, end)
|
||||
};
|
||||
|
||||
let selection = doc.selection(view.id);
|
||||
|
||||
for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
|
||||
let head = view.screen_coords_at_pos(
|
||||
doc,
|
||||
text,
|
||||
if selection.head > selection.anchor {
|
||||
selection.head - 1
|
||||
} else {
|
||||
selection.head
|
||||
},
|
||||
);
|
||||
if let Some(head) = head {
|
||||
// Draw line number for selected lines.
|
||||
let line_number = view.first_line + head.row;
|
||||
let line_number_text = if line_number == last_line && !draw_last {
|
||||
" ~".into()
|
||||
} else {
|
||||
format!("{:>5}", line_number + 1)
|
||||
};
|
||||
surface.set_stringn(
|
||||
viewport.x + 1 - GUTTER_OFFSET,
|
||||
viewport.y + head.row as u16,
|
||||
line_number_text,
|
||||
5,
|
||||
linenr_select,
|
||||
);
|
||||
|
||||
// TODO: set cursor position for IME
|
||||
if let Some(syntax) = doc.syntax() {
|
||||
use helix_core::match_brackets;
|
||||
let pos = doc
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
let pos = match_brackets::find(syntax, doc.text(), pos)
|
||||
.and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
|
||||
|
||||
if let Some(pos) = pos {
|
||||
// ensure col is on screen
|
||||
if (pos.col as u16) < viewport.width + view.first_col as u16
|
||||
&& pos.col >= view.first_col
|
||||
{
|
||||
let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
|
||||
Style::default()
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.add_modifier(Modifier::DIM)
|
||||
});
|
||||
|
||||
surface
|
||||
.get_mut(
|
||||
viewport.x + pos.col as u16,
|
||||
viewport.y + pos.row as u16,
|
||||
)
|
||||
.set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_diagnostics(
|
||||
|
@ -451,7 +514,6 @@ impl EditorView {
|
|||
viewport: Rect,
|
||||
surface: &mut Surface,
|
||||
theme: &Theme,
|
||||
_is_focused: bool,
|
||||
) {
|
||||
use helix_core::diagnostic::Severity;
|
||||
use tui::{
|
||||
|
@ -469,10 +531,10 @@ impl EditorView {
|
|||
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
|
||||
});
|
||||
|
||||
let warning: Style = theme.get("warning");
|
||||
let error: Style = theme.get("error");
|
||||
let info: Style = theme.get("info");
|
||||
let hint: Style = theme.get("hint");
|
||||
let warning = theme.get("warning");
|
||||
let error = theme.get("error");
|
||||
let info = theme.get("info");
|
||||
let hint = theme.get("hint");
|
||||
|
||||
// Vec::with_capacity(diagnostics.len()); // rough estimate
|
||||
let mut lines = Vec::new();
|
||||
|
@ -961,7 +1023,7 @@ impl Component for EditorView {
|
|||
}
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
// clear with background color
|
||||
surface.set_style(area, cx.editor.theme.get("ui.background"));
|
||||
|
||||
|
@ -983,7 +1045,7 @@ impl Component for EditorView {
|
|||
);
|
||||
}
|
||||
|
||||
if let Some(ref info) = self.autoinfo {
|
||||
if let Some(ref mut info) = self.autoinfo {
|
||||
info.render(area, surface, cx);
|
||||
}
|
||||
|
||||
|
@ -1030,7 +1092,7 @@ impl Component for EditorView {
|
|||
);
|
||||
}
|
||||
|
||||
if let Some(completion) = &self.completion {
|
||||
if let Some(completion) = self.completion.as_mut() {
|
||||
completion.render(area, surface, cx);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ use tui::buffer::Buffer as Surface;
|
|||
use tui::widgets::{Block, Borders, Widget};
|
||||
|
||||
impl Component for Info {
|
||||
fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
let style = cx.editor.theme.get("ui.popup");
|
||||
|
||||
// Calculate the area of the terminal to modify. Because we want to
|
||||
|
|
|
@ -198,7 +198,7 @@ fn parse<'a>(
|
|||
Text::from(lines)
|
||||
}
|
||||
impl Component for Markdown {
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
use tui::widgets::{Paragraph, Widget, Wrap};
|
||||
|
||||
let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
|
||||
|
|
|
@ -258,7 +258,7 @@ impl<T: Item + 'static> Component for Menu<T> {
|
|||
|
||||
// TODO: required size should re-trigger when we filter items so we can draw a smaller menu
|
||||
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
let style = cx.editor.theme.get("ui.text");
|
||||
let selected = cx.editor.theme.get("ui.menu.selected");
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ pub use completion::Completion;
|
|||
pub use editor::EditorView;
|
||||
pub use markdown::Markdown;
|
||||
pub use menu::Menu;
|
||||
pub use picker::Picker;
|
||||
pub use picker::{FilePicker, Picker};
|
||||
pub use popup::Popup;
|
||||
pub use prompt::{Prompt, PromptEvent};
|
||||
pub use spinner::{ProgressSpinners, Spinner};
|
||||
|
@ -73,29 +73,26 @@ pub fn regex_prompt(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
|
||||
pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
|
||||
use ignore::Walk;
|
||||
use std::time;
|
||||
let files = Walk::new(root.clone()).filter_map(|entry| match entry {
|
||||
Ok(entry) => {
|
||||
// filter dirs, but we might need special handling for symlinks!
|
||||
if !entry.file_type().map_or(false, |entry| entry.is_dir()) {
|
||||
let time = if let Ok(metadata) = entry.metadata() {
|
||||
metadata
|
||||
.accessed()
|
||||
.or_else(|_| metadata.modified())
|
||||
.or_else(|_| metadata.created())
|
||||
.unwrap_or(time::UNIX_EPOCH)
|
||||
} else {
|
||||
time::UNIX_EPOCH
|
||||
};
|
||||
|
||||
Some((entry.into_path(), time))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let files = Walk::new(root.clone()).filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
// Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
|
||||
if entry.path().is_dir() {
|
||||
// Will give a false positive if metadata cannot be read (eg. permission error)
|
||||
return None;
|
||||
}
|
||||
Err(_err) => None,
|
||||
|
||||
let time = entry.metadata().map_or(time::UNIX_EPOCH, |metadata| {
|
||||
metadata
|
||||
.accessed()
|
||||
.or_else(|_| metadata.modified())
|
||||
.or_else(|_| metadata.created())
|
||||
.unwrap_or(time::UNIX_EPOCH)
|
||||
});
|
||||
|
||||
Some((entry.into_path(), time))
|
||||
});
|
||||
|
||||
let mut files: Vec<_> = if root.join(".git").is_dir() {
|
||||
|
@ -109,7 +106,7 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
|
|||
|
||||
let files = files.into_iter().map(|(path, _)| path).collect();
|
||||
|
||||
Picker::new(
|
||||
FilePicker::new(
|
||||
files,
|
||||
move |path: &PathBuf| {
|
||||
// format_fn
|
||||
|
@ -124,6 +121,7 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
|
|||
.open(path.into(), action)
|
||||
.expect("editor.open failed");
|
||||
},
|
||||
|_editor, path| Some((path.clone(), None)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use crate::compositor::{Component, Compositor, Context, EventResult};
|
||||
use crate::{
|
||||
compositor::{Component, Compositor, Context, EventResult},
|
||||
ui::EditorView,
|
||||
};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
|
@ -7,17 +10,153 @@ use tui::{
|
|||
|
||||
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use tui::widgets::Widget;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::ui::{Prompt, PromptEvent};
|
||||
use helix_core::Position;
|
||||
use helix_view::{
|
||||
document::canonicalize_path,
|
||||
editor::Action,
|
||||
graphics::{Color, CursorKind, Rect, Style},
|
||||
Editor,
|
||||
Document, Editor,
|
||||
};
|
||||
|
||||
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
|
||||
|
||||
/// File path and line number (used to align and highlight a line)
|
||||
type FileLocation = (PathBuf, Option<usize>);
|
||||
|
||||
pub struct FilePicker<T> {
|
||||
picker: Picker<T>,
|
||||
/// Caches paths to documents
|
||||
preview_cache: HashMap<PathBuf, Document>,
|
||||
/// Given an item in the picker, return the file path and line number to display.
|
||||
file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
|
||||
}
|
||||
|
||||
impl<T> FilePicker<T> {
|
||||
pub fn new(
|
||||
options: Vec<T>,
|
||||
format_fn: impl Fn(&T) -> Cow<str> + 'static,
|
||||
callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
|
||||
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
picker: Picker::new(false, options, format_fn, callback_fn),
|
||||
preview_cache: HashMap::new(),
|
||||
file_fn: Box::new(preview_fn),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
|
||||
self.picker
|
||||
.selection()
|
||||
.and_then(|current| (self.file_fn)(editor, current))
|
||||
.and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line)))
|
||||
}
|
||||
|
||||
fn calculate_preview(&mut self, editor: &Editor) {
|
||||
if let Some((path, _line)) = self.current_file(editor) {
|
||||
if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
|
||||
// TODO: enable syntax highlighting; blocked by async rendering
|
||||
let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
|
||||
self.preview_cache.insert(path, doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Component for FilePicker<T> {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
// +---------+ +---------+
|
||||
// |prompt | |preview |
|
||||
// +---------+ | |
|
||||
// |picker | | |
|
||||
// | | | |
|
||||
// +---------+ +---------+
|
||||
self.calculate_preview(cx.editor);
|
||||
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
|
||||
let area = inner_rect(area);
|
||||
// -- Render the frame:
|
||||
// clear area
|
||||
let background = cx.editor.theme.get("ui.background");
|
||||
surface.clear_with(area, background);
|
||||
|
||||
let picker_width = if render_preview {
|
||||
area.width / 2
|
||||
} else {
|
||||
area.width
|
||||
};
|
||||
|
||||
let picker_area = Rect::new(area.x, area.y, picker_width, area.height);
|
||||
self.picker.render(picker_area, surface, cx);
|
||||
|
||||
if !render_preview {
|
||||
return;
|
||||
}
|
||||
|
||||
let preview_area = Rect::new(area.x + picker_width, area.y, area.width / 2, area.height);
|
||||
|
||||
// don't like this but the lifetime sucks
|
||||
let block = Block::default().borders(Borders::ALL);
|
||||
|
||||
// calculate the inner area inside the box
|
||||
let mut inner = block.inner(preview_area);
|
||||
// 1 column gap on either side
|
||||
inner.x += 1;
|
||||
inner.width = inner.width.saturating_sub(2);
|
||||
|
||||
block.render(preview_area, surface);
|
||||
|
||||
if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, line)| {
|
||||
cx.editor
|
||||
.document_by_path(&path)
|
||||
.or_else(|| self.preview_cache.get(&path))
|
||||
.zip(Some(line))
|
||||
}) {
|
||||
// align to middle
|
||||
let first_line = line.unwrap_or(0).saturating_sub(inner.height as usize / 2);
|
||||
let offset = Position::new(first_line, 0);
|
||||
|
||||
let highlights = EditorView::doc_syntax_highlights(
|
||||
doc,
|
||||
offset,
|
||||
area.height,
|
||||
&cx.editor.theme,
|
||||
&cx.editor.syn_loader,
|
||||
);
|
||||
EditorView::render_text_highlights(
|
||||
doc,
|
||||
offset,
|
||||
inner,
|
||||
surface,
|
||||
&cx.editor.theme,
|
||||
highlights,
|
||||
);
|
||||
|
||||
// highlight the line
|
||||
if let Some(line) = line {
|
||||
for x in inner.left()..inner.right() {
|
||||
surface
|
||||
.get_mut(x, inner.y + line.saturating_sub(first_line) as u16)
|
||||
.set_style(cx.editor.theme.get("ui.selection.primary"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult {
|
||||
// TODO: keybinds for scrolling preview
|
||||
self.picker.handle_event(event, ctx)
|
||||
}
|
||||
|
||||
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
|
||||
self.picker.cursor(area, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Picker<T> {
|
||||
options: Vec<T>,
|
||||
// filter: String,
|
||||
|
@ -30,6 +169,8 @@ pub struct Picker<T> {
|
|||
cursor: usize,
|
||||
// pattern: String,
|
||||
prompt: Prompt,
|
||||
/// Whether to render in the middle of the area
|
||||
render_centered: bool,
|
||||
|
||||
format_fn: Box<dyn Fn(&T) -> Cow<str>>,
|
||||
callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>,
|
||||
|
@ -37,6 +178,7 @@ pub struct Picker<T> {
|
|||
|
||||
impl<T> Picker<T> {
|
||||
pub fn new(
|
||||
render_centered: bool,
|
||||
options: Vec<T>,
|
||||
format_fn: impl Fn(&T) -> Cow<str> + 'static,
|
||||
callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
|
||||
|
@ -57,6 +199,7 @@ impl<T> Picker<T> {
|
|||
filters: Vec::new(),
|
||||
cursor: 0,
|
||||
prompt,
|
||||
render_centered,
|
||||
format_fn: Box::new(format_fn),
|
||||
callback_fn: Box::new(callback_fn),
|
||||
};
|
||||
|
@ -139,8 +282,8 @@ impl<T> Picker<T> {
|
|||
// - score all the names in relation to input
|
||||
|
||||
fn inner_rect(area: Rect) -> Rect {
|
||||
let padding_vertical = area.height * 20 / 100;
|
||||
let padding_horizontal = area.width * 20 / 100;
|
||||
let padding_vertical = area.height * 10 / 100;
|
||||
let padding_horizontal = area.width * 10 / 100;
|
||||
|
||||
Rect::new(
|
||||
area.x + padding_horizontal,
|
||||
|
@ -174,7 +317,9 @@ impl<T: 'static> Component for Picker<T> {
|
|||
| KeyEvent {
|
||||
code: KeyCode::Char('p'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.move_up(),
|
||||
} => {
|
||||
self.move_up();
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Down,
|
||||
..
|
||||
|
@ -185,7 +330,9 @@ impl<T: 'static> Component for Picker<T> {
|
|||
| KeyEvent {
|
||||
code: KeyCode::Char('n'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => self.move_down(),
|
||||
} => {
|
||||
self.move_down();
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
}
|
||||
|
@ -239,16 +386,18 @@ impl<T: 'static> Component for Picker<T> {
|
|||
EventResult::Consumed(None)
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
let area = inner_rect(area);
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
let area = if self.render_centered {
|
||||
inner_rect(area)
|
||||
} else {
|
||||
area
|
||||
};
|
||||
|
||||
// -- Render the frame:
|
||||
|
||||
// clear area
|
||||
let background = cx.editor.theme.get("ui.background");
|
||||
surface.clear_with(area, background);
|
||||
|
||||
use tui::widgets::Widget;
|
||||
// don't like this but the lifetime sucks
|
||||
let block = Block::default().borders(Borders::ALL);
|
||||
|
||||
|
@ -263,21 +412,23 @@ impl<T: 'static> Component for Picker<T> {
|
|||
self.prompt.render(area, surface, cx);
|
||||
|
||||
// -- Separator
|
||||
let style = Style::default().fg(Color::Rgb(90, 89, 119));
|
||||
let symbols = BorderType::line_symbols(BorderType::Plain);
|
||||
let sep_style = Style::default().fg(Color::Rgb(90, 89, 119));
|
||||
let borders = BorderType::line_symbols(BorderType::Plain);
|
||||
for x in inner.left()..inner.right() {
|
||||
surface
|
||||
.get_mut(x, inner.y + 1)
|
||||
.set_symbol(symbols.horizontal)
|
||||
.set_style(style);
|
||||
.set_symbol(borders.horizontal)
|
||||
.set_style(sep_style);
|
||||
}
|
||||
|
||||
// -- Render the contents:
|
||||
// subtract the area of the prompt (-2) and current item marker " > " (-3)
|
||||
let inner = Rect::new(inner.x + 3, inner.y + 2, inner.width - 3, inner.height - 2);
|
||||
|
||||
let style = cx.editor.theme.get("ui.text");
|
||||
let selected = Style::default().fg(Color::Rgb(255, 255, 255));
|
||||
|
||||
let rows = inner.height - 2; // -1 for search bar
|
||||
let rows = inner.height;
|
||||
let offset = self.cursor / (rows as usize) * (rows as usize);
|
||||
|
||||
let files = self.matches.iter().skip(offset).map(|(index, _score)| {
|
||||
|
@ -286,14 +437,14 @@ impl<T: 'static> Component for Picker<T> {
|
|||
|
||||
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
|
||||
if i == (self.cursor - offset) {
|
||||
surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
|
||||
surface.set_string(inner.x - 2, inner.y + i as u16, ">", selected);
|
||||
}
|
||||
|
||||
surface.set_string_truncated(
|
||||
inner.x + 3,
|
||||
inner.y + 2 + i as u16,
|
||||
inner.x,
|
||||
inner.y + i as u16,
|
||||
(self.format_fn)(option),
|
||||
(inner.width as usize).saturating_sub(3), // account for the " > "
|
||||
inner.width as usize,
|
||||
if i == (self.cursor - offset) {
|
||||
selected
|
||||
} else {
|
||||
|
|
|
@ -105,7 +105,7 @@ impl<T: Component> Component for Popup<T> {
|
|||
Some(self.size)
|
||||
}
|
||||
|
||||
fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
cx.scroll = Some(self.scroll);
|
||||
|
||||
let position = self
|
||||
|
|
|
@ -352,7 +352,7 @@ impl Prompt {
|
|||
}
|
||||
|
||||
if let Some(doc) = (self.doc_fn)(&self.line) {
|
||||
let text = ui::Text::new(doc.to_string());
|
||||
let mut text = ui::Text::new(doc.to_string());
|
||||
|
||||
let viewport = area;
|
||||
let area = viewport.intersection(Rect::new(
|
||||
|
@ -546,7 +546,7 @@ impl Component for Prompt {
|
|||
EventResult::Consumed(None)
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
self.render_prompt(area, surface, cx)
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ impl Text {
|
|||
}
|
||||
}
|
||||
impl Component for Text {
|
||||
fn render(&self, area: Rect, surface: &mut Surface, _cx: &mut Context) {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, _cx: &mut Context) {
|
||||
use tui::widgets::{Paragraph, Widget, Wrap};
|
||||
let contents = tui::text::Text::from(self.contents.clone());
|
||||
|
||||
|
|
|
@ -432,14 +432,14 @@ impl Document {
|
|||
/// Create a new document from `path`. Encoding is auto-detected, but it can be manually
|
||||
/// overwritten with the `encoding` parameter.
|
||||
pub fn open(
|
||||
path: PathBuf,
|
||||
path: &Path,
|
||||
encoding: Option<&'static encoding_rs::Encoding>,
|
||||
theme: Option<&Theme>,
|
||||
config_loader: Option<&syntax::Loader>,
|
||||
) -> Result<Self, Error> {
|
||||
let (rope, encoding) = if path.exists() {
|
||||
let mut file =
|
||||
std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
|
||||
std::fs::File::open(path).context(format!("unable to open {:?}", path))?;
|
||||
from_reader(&mut file, encoding)?
|
||||
} else {
|
||||
let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
|
||||
|
@ -449,7 +449,7 @@ impl Document {
|
|||
let mut doc = Self::from(rope, Some(encoding));
|
||||
|
||||
// set the path and try detecting the language
|
||||
doc.set_path(&path)?;
|
||||
doc.set_path(path)?;
|
||||
if let Some(loader) = config_loader {
|
||||
doc.detect_language(theme, loader);
|
||||
}
|
||||
|
@ -904,6 +904,10 @@ impl Document {
|
|||
&self.selections[&view_id]
|
||||
}
|
||||
|
||||
pub fn selections(&self) -> &HashMap<ViewId, Selection> {
|
||||
&self.selections
|
||||
}
|
||||
|
||||
pub fn relative_path(&self) -> Option<PathBuf> {
|
||||
let cwdir = std::env::current_dir().expect("couldn't determine current directory");
|
||||
|
||||
|
|
|
@ -7,7 +7,11 @@ use crate::{
|
|||
};
|
||||
|
||||
use futures_util::future;
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use slotmap::SlotMap;
|
||||
|
||||
|
@ -222,7 +226,7 @@ impl Editor {
|
|||
let id = if let Some(id) = id {
|
||||
id
|
||||
} else {
|
||||
let mut doc = Document::open(path, None, Some(&self.theme), Some(&self.syn_loader))?;
|
||||
let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
|
||||
|
||||
// try to find a language server based on the language name
|
||||
let language_server = doc
|
||||
|
@ -316,6 +320,11 @@ impl Editor {
|
|||
self.documents.iter_mut().map(|(_id, doc)| doc)
|
||||
}
|
||||
|
||||
pub fn document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> {
|
||||
self.documents()
|
||||
.find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
|
||||
}
|
||||
|
||||
// pub fn current_document(&self) -> Document {
|
||||
// let id = self.view().doc;
|
||||
// let doc = &mut editor.documents[id];
|
||||
|
|
|
@ -10,7 +10,7 @@ use helix_core::{
|
|||
|
||||
type Jump = (DocumentId, Selection);
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JumpList {
|
||||
jumps: Vec<Jump>,
|
||||
current: usize,
|
||||
|
|
Loading…
Reference in a new issue