on master help sign in
Show file preview in split pane in fuzzy finder (#534)
d84f8b5Gokul Soumya
helix-term/src/commands.rs+26−7
@@ -27,7 +27,7 @@ use movement::Movement;
27
27
28use crate::{
28use crate::{
29 compositor::{self, Component, Compositor},
29 compositor::{self, Component, Compositor},
30− ui::{self, Picker, Popup, Prompt, PromptEvent},
30+ ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent},
31};
31};
32
32
33use crate::job::{self, Job, Jobs};
33use crate::job::{self, Job, Jobs};
@@ -2212,7 +2212,7 @@ fn file_picker(cx: &mut Context) {
2212fn buffer_picker(cx: &mut Context) {
2212fn buffer_picker(cx: &mut Context) {
2213 let current = view!(cx.editor).doc;
2213 let current = view!(cx.editor).doc;
2214
2214
2215− let picker = Picker::new(
2215+ let picker = FilePicker::new(
2216 cx.editor
2216 cx.editor
2217 .documents
2217 .documents
2218 .iter()
2218 .iter()
@@ -2234,6 +2234,15 @@ fn buffer_picker(cx: &mut Context) {
2234 |editor: &mut Editor, (id, _path): &(DocumentId, Option<PathBuf>), _action| {
2234 |editor: &mut Editor, (id, _path): &(DocumentId, Option<PathBuf>), _action| {
2235 editor.switch(*id, Action::Replace);
2235 editor.switch(*id, Action::Replace);
2236 },
2236 },
2237+ |editor, (id, path)| {
2238+ let doc = &editor.documents.get(*id)?;
2239+ let &view_id = doc.selections().keys().next()?;
2240+ let line = doc
2241+ .selection(view_id)
2242+ .primary()
2243+ .cursor_line(doc.text().slice(..));
2244+ Some((path.clone()?, Some(line)))
2245+ },
2237 );
2246 );
2238 cx.push_layer(Box::new(picker));
2247 cx.push_layer(Box::new(picker));
2239}
2248}
@@ -2287,7 +2296,7 @@ fn symbol_picker(cx: &mut Context) {
2287 }
2296 }
2288 };
2297 };
2289
2298
2290− let picker = Picker::new(
2299+ let picker = FilePicker::new(
2291 symbols,
2300 symbols,
2292 |symbol| (&symbol.name).into(),
2301 |symbol| (&symbol.name).into(),
2293 move |editor: &mut Editor, symbol, _action| {
2302 move |editor: &mut Editor, symbol, _action| {
@@ -2297,10 +2306,15 @@ fn symbol_picker(cx: &mut Context) {
2297 if let Some(range) =
2306 if let Some(range) =
2298 lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
2307 lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
2299 {
2308 {
2300− doc.set_selection(view.id, Selection::single(range.to(), range.from()));
2309+ doc.set_selection(view.id, Selection::single(range.anchor, range.head));
2301 align_view(doc, view, Align::Center);
2310 align_view(doc, view, Align::Center);
2302 }
2311 }
2303 },
2312 },
2313+ move |_editor, symbol| {
2314+ let path = symbol.location.uri.to_file_path().unwrap();
2315+ let line = Some(symbol.location.range.start.line as usize);
2316+ Some((path, line))
2317+ },
2304 );
2318 );
2305 compositor.push(Box::new(picker))
2319 compositor.push(Box::new(picker))
2306 }
2320 }
@@ -2332,6 +2346,7 @@ pub fn code_action(cx: &mut Context) {
2332 response: Option<lsp::CodeActionResponse>| {
2346 response: Option<lsp::CodeActionResponse>| {
2333 if let Some(actions) = response {
2347 if let Some(actions) = response {
2334 let picker = Picker::new(
2348 let picker = Picker::new(
2349+ true,
2335 actions,
2350 actions,
2336 |action| match action {
2351 |action| match action {
2337 lsp::CodeActionOrCommand::CodeAction(action) => {
2352 lsp::CodeActionOrCommand::CodeAction(action) => {
@@ -2703,7 +2718,7 @@ fn goto_impl(
2703 editor.set_error("No definition found.".to_string());
2718 editor.set_error("No definition found.".to_string());
2704 }
2719 }
2705 _locations => {
2720 _locations => {
2706− let picker = ui::Picker::new(
2721+ let picker = FilePicker::new(
2707 locations,
2722 locations,
2708 move |location| {
2723 move |location| {
2709 let file: Cow<'_, str> = (location.uri.scheme() == "file")
2724 let file: Cow<'_, str> = (location.uri.scheme() == "file")
@@ -2728,6 +2743,11 @@ fn goto_impl(
2728 move |editor: &mut Editor, location, action| {
2743 move |editor: &mut Editor, location, action| {
2729 jump_to(editor, location, offset_encoding, action)
2744 jump_to(editor, location, offset_encoding, action)
2730 },
2745 },
2746+ |_editor, location| {
2747+ let path = location.uri.to_file_path().unwrap();
2748+ let line = Some(location.range.start.line as usize);
2749+ Some((path, line))
2750+ },
2731 );
2751 );
2732 compositor.push(Box::new(picker));
2752 compositor.push(Box::new(picker));
2733 }
2753 }
@@ -3729,8 +3749,7 @@ fn keep_primary_selection(cx: &mut Context) {
3729 let (view, doc) = current!(cx.editor);
3749 let (view, doc) = current!(cx.editor);
3730
3750
3731 let range = doc.selection(view.id).primary();
3751 let range = doc.selection(view.id).primary();
3732− let selection = Selection::single(range.anchor, range.head);
3752+ doc.set_selection(view.id, Selection::single(range.anchor, range.head));
3733− doc.set_selection(view.id, selection);
3734}
3753}
3735
3754
3736fn completion(cx: &mut Context) {
3755fn completion(cx: &mut Context) {
helix-term/src/compositor.rs+3−3
@@ -46,7 +46,7 @@ pub trait Component: Any + AnyComponent {
46 }
46 }
47
47
48 /// Render the component onto the provided surface.
48 /// Render the component onto the provided surface.
49− fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
49+ fn render(&mut self, area: Rect, frame: &mut Surface, ctx: &mut Context);
50
50
51 /// Get cursor position and cursor kind.
51 /// Get cursor position and cursor kind.
52 fn cursor(&self, _area: Rect, _ctx: &Editor) -> (Option<Position>, CursorKind) {
52 fn cursor(&self, _area: Rect, _ctx: &Editor) -> (Option<Position>, CursorKind) {
@@ -152,8 +152,8 @@ impl Compositor {
152
152
153 let area = *surface.area();
153 let area = *surface.area();
154
154
155− for layer in &self.layers {
155+ for layer in &mut self.layers {
156− layer.render(area, surface, cx)
156+ layer.render(area, surface, cx);
157 }
157 }
158
158
159 let (pos, kind) = self.cursor(area, cx.editor);
159 let (pos, kind) = self.cursor(area, cx.editor);
helix-term/src/ui/completion.rs+2−2
@@ -241,7 +241,7 @@ impl Component for Completion {
241 self.popup.required_size(viewport)
241 self.popup.required_size(viewport)
242 }
242 }
243
243
244− fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
244+ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
245 self.popup.render(area, surface, cx);
245 self.popup.render(area, surface, cx);
246
246
247 // if we have a selection, render a markdown popup on top/below with info
247 // if we have a selection, render a markdown popup on top/below with info
@@ -263,7 +263,7 @@ impl Component for Completion {
263 let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
263 let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
264 - view.first_line) as u16;
264 - view.first_line) as u16;
265
265
266− let doc = match &option.documentation {
266+ let mut doc = match &option.documentation {
267 Some(lsp::Documentation::String(contents))
267 Some(lsp::Documentation::String(contents))
268 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
268 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
269 kind: lsp::MarkupKind::PlainText,
269 kind: lsp::MarkupKind::PlainText,
helix-term/src/ui/editor.rs+235−173
@@ -34,7 +34,7 @@ pub struct EditorView {
34 last_insert: (commands::Command, Vec<KeyEvent>),
34 last_insert: (commands::Command, Vec<KeyEvent>),
35 completion: Option<Completion>,
35 completion: Option<Completion>,
36 spinners: ProgressSpinners,
36 spinners: ProgressSpinners,
37− pub autoinfo: Option<Info>,
37+ autoinfo: Option<Info>,
38}
38}
39
39
40pub const GUTTER_OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
40pub const GUTTER_OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
@@ -78,8 +78,26 @@ impl EditorView {
78 view.area.width - GUTTER_OFFSET,
78 view.area.width - GUTTER_OFFSET,
79 view.area.height.saturating_sub(1),
79 view.area.height.saturating_sub(1),
80 ); // - 1 for statusline
80 ); // - 1 for statusline
81+ let offset = Position::new(view.first_line, view.first_col);
82+ let height = view.area.height.saturating_sub(1); // - 1 for statusline
81
83
82− self.render_buffer(doc, view, area, surface, theme, is_focused, loader);
84+ let highlights = Self::doc_syntax_highlights(doc, offset, height, theme, loader);
85+ let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
86+ let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
87+ Box::new(syntax::merge(
88+ highlights,
89+ Self::doc_selection_highlights(doc, view, theme),
90+ ))
91+ } else {
92+ Box::new(highlights)
93+ };
94+
95+ Self::render_text_highlights(doc, offset, area, surface, theme, highlights);
96+ Self::render_gutter(doc, view, area, surface, theme);
97+
98+ if is_focused {
99+ Self::render_focused_view_elements(view, doc, area, theme, surface);
100+ }
83
101
84 // if we're not at the edge of the screen, draw a right border
102 // if we're not at the edge of the screen, draw a right border
85 if viewport.right() != view.area.right() {
103 if viewport.right() != view.area.right() {
@@ -94,7 +112,7 @@ impl EditorView {
94 }
112 }
95 }
113 }
96
114
97− self.render_diagnostics(doc, view, area, surface, theme, is_focused);
115+ self.render_diagnostics(doc, view, area, surface, theme);
98
116
99 let area = Rect::new(
117 let area = Rect::new(
100 view.area.x,
118 view.area.x,
@@ -105,31 +123,34 @@ impl EditorView {
105 self.render_statusline(doc, view, area, surface, theme, is_focused);
123 self.render_statusline(doc, view, area, surface, theme, is_focused);
106 }
124 }
107
125
126+ /// Get syntax highlights for a document in a view represented by the first line
127+ /// and column (`offset`) and the last line. This is done instead of using a view
128+ /// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview)
108 #[allow(clippy::too_many_arguments)]
129 #[allow(clippy::too_many_arguments)]
109− pub fn render_buffer(
130+ pub fn doc_syntax_highlights<'doc>(
110− &self,
111− doc: &Document,
131+ doc: &'doc Document,
112− view: &View,
132+ offset: Position,
113− viewport: Rect,
133+ height: u16,
114− surface: &mut Surface,
115 theme: &Theme,
134 theme: &Theme,
116− is_focused: bool,
117 loader: &syntax::Loader,
135 loader: &syntax::Loader,
118− ) {
136+ ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
119 let text = doc.text().slice(..);
137 let text = doc.text().slice(..);
120−
121− let last_line = view.last_line(doc);
138+ let last_line = std::cmp::min(
139+ // Saturating subs to make it inclusive zero indexing.
140+ (offset.row + height as usize).saturating_sub(1),
141+ doc.text().len_lines().saturating_sub(1),
142+ );
122
143
123 let range = {
144 let range = {
124 // calculate viewport byte ranges
145 // calculate viewport byte ranges
125− let start = text.line_to_byte(view.first_line);
146+ let start = text.line_to_byte(offset.row);
126 let end = text.line_to_byte(last_line + 1);
147 let end = text.line_to_byte(last_line + 1);
127
148
128 start..end
149 start..end
129 };
150 };
130
151
131 // TODO: range doesn't actually restrict source, just highlight range
152 // TODO: range doesn't actually restrict source, just highlight range
132− let highlights: Vec<_> = match doc.syntax() {
153+ let highlights = match doc.syntax() {
133 Some(syntax) => {
154 Some(syntax) => {
134 let scopes = theme.scopes();
155 let scopes = theme.scopes();
135 syntax
156 syntax
@@ -151,20 +172,16 @@ impl EditorView {
151 Some(config_ref)
172 Some(config_ref)
152 })
173 })
153 })
174 })
175+ .map(|event| event.unwrap())
154 .collect() // TODO: we collect here to avoid holding the lock, fix later
176 .collect() // TODO: we collect here to avoid holding the lock, fix later
155 }
177 }
156− None => vec![Ok(HighlightEvent::Source {
178+ None => vec![HighlightEvent::Source {
157 start: range.start,
179 start: range.start,
158 end: range.end,
180 end: range.end,
159− })],
181+ }],
160− };
182+ }
161− let mut spans = Vec::new();
183+ .into_iter()
162− let mut visual_x = 0u16;
163− let mut line = 0u16;
164− let tab_width = doc.tab_width();
165− let tab = " ".repeat(tab_width);
166−
167− let highlights = highlights.into_iter().map(|event| match event.unwrap() {
184+ .map(move |event| match event {
168 // convert byte offsets to char offset
185 // convert byte offsets to char offset
169 HighlightEvent::Source { start, end } => {
186 HighlightEvent::Source { start, end } => {
170 let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start));
187 let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start));
@@ -174,13 +191,44 @@ impl EditorView {
174 event => event,
191 event => event,
175 });
192 });
176
193
194+ Box::new(highlights)
195+ }
196+
197+ /// Get highlight spans for document diagnostics
198+ pub fn doc_diagnostics_highlights(
199+ doc: &Document,
200+ theme: &Theme,
201+ ) -> Vec<(usize, std::ops::Range<usize>)> {
202+ let diagnostic_scope = theme
203+ .find_scope_index("diagnostic")
204+ .or_else(|| theme.find_scope_index("ui.cursor"))
205+ .or_else(|| theme.find_scope_index("ui.selection"))
206+ .expect("no selection scope found!");
207+
208+ doc.diagnostics()
209+ .iter()
210+ .map(|diagnostic| {
211+ (
212+ diagnostic_scope,
213+ diagnostic.range.start..diagnostic.range.end,
214+ )
215+ })
216+ .collect()
217+ }
218+
219+ /// Get highlight spans for selections in a document view.
220+ pub fn doc_selection_highlights(
221+ doc: &Document,
222+ view: &View,
223+ theme: &Theme,
224+ ) -> Vec<(usize, std::ops::Range<usize>)> {
225+ let text = doc.text().slice(..);
177− let selections = doc.selection(view.id);
226+ let selection = doc.selection(view.id);
178− let primary_idx = selections.primary_index();
227+ let primary_idx = selection.primary_index();
179
228
180 let selection_scope = theme
229 let selection_scope = theme
181 .find_scope_index("ui.selection")
230 .find_scope_index("ui.selection")
182 .expect("no selection scope found!");
231 .expect("no selection scope found!");
183−
184 let base_cursor_scope = theme
232 let base_cursor_scope = theme
185 .find_scope_index("ui.cursor")
233 .find_scope_index("ui.cursor")
186 .unwrap_or(selection_scope);
234 .unwrap_or(selection_scope);
@@ -192,64 +240,59 @@ impl EditorView {
192 }
240 }
193 .unwrap_or(base_cursor_scope);
241 .unwrap_or(base_cursor_scope);
194
242
195− let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
196− // TODO: primary + insert mode patching:
197− // (ui.cursor.primary).patch(mode).unwrap_or(cursor)
198− let primary_cursor_scope = theme
243+ let primary_cursor_scope = theme
199− .find_scope_index("ui.cursor.primary")
244+ .find_scope_index("ui.cursor.primary")
200− .unwrap_or(cursor_scope);
245+ .unwrap_or(cursor_scope);
201− let primary_selection_scope = theme
246+ let primary_selection_scope = theme
202− .find_scope_index("ui.selection.primary")
247+ .find_scope_index("ui.selection.primary")
203− .unwrap_or(selection_scope);
204−
205− // inject selections as highlight scopes
206− let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
207− for (i, range) in selections.iter().enumerate() {
208− let (cursor_scope, selection_scope) = if i == primary_idx {
209− (primary_cursor_scope, primary_selection_scope)
210− } else {
211− (cursor_scope, selection_scope)
248+ .unwrap_or(selection_scope);
212− };
213
249
214− // Special-case: cursor at end of the rope.
250+ let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
215− if range.head == range.anchor && range.head == text.len_chars() {
251+ for (i, range) in selection.iter().enumerate() {
216− spans.push((cursor_scope, range.head..range.head + 1));
252+ let (cursor_scope, selection_scope) = if i == primary_idx {
217− continue;
253+ (primary_cursor_scope, primary_selection_scope)
254+ } else {
255+ (cursor_scope, selection_scope)
218− }
256+ };
219
257
220− let range = range.min_width_1(text);
258+ // Special-case: cursor at end of the rope.
221− if range.head > range.anchor {
259+ if range.head == range.anchor && range.head == text.len_chars() {
222− // Standard case.
223− let cursor_start = prev_grapheme_boundary(text, range.head);
224− spans.push((selection_scope, range.anchor..cursor_start));
225− spans.push((cursor_scope, cursor_start..range.head));
226− } else {
227− // Reverse case.
228− let cursor_end = next_grapheme_boundary(text, range.head);
229− spans.push((cursor_scope, range.head..cursor_end));
260+ spans.push((cursor_scope, range.head..range.head + 1));
230− spans.push((selection_scope, cursor_end..range.anchor));
261+ continue;
231− }
232 }
262 }
233
263
234− Box::new(syntax::merge(highlights, spans))
264+ let range = range.min_width_1(text);
265+ if range.head > range.anchor {
266+ // Standard case.
267+ let cursor_start = prev_grapheme_boundary(text, range.head);
268+ spans.push((selection_scope, range.anchor..cursor_start));
269+ spans.push((cursor_scope, cursor_start..range.head));
235− } else {
270+ } else {
271+ // Reverse case.
272+ let cursor_end = next_grapheme_boundary(text, range.head);
273+ spans.push((cursor_scope, range.head..cursor_end));
274+ spans.push((selection_scope, cursor_end..range.anchor));
275+ }
276+ }
277+
278+ spans
279+ }
280+
281+ pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
282+ doc: &Document,
283+ offset: Position,
284+ viewport: Rect,
285+ surface: &mut Surface,
286+ theme: &Theme,
236− Box::new(highlights)
287+ highlights: H,
237− };
288+ ) {
289+ let text = doc.text().slice(..);
238
290
239− // diagnostic injection
291+ let mut spans = Vec::new();
240− let diagnostic_scope = theme.find_scope_index("diagnostic").unwrap_or(cursor_scope);
292+ let mut visual_x = 0u16;
241− let highlights = Box::new(syntax::merge(
293+ let mut line = 0u16;
242− highlights,
294+ let tab_width = doc.tab_width();
243− doc.diagnostics()
295+ let tab = " ".repeat(tab_width);
244− .iter()
245− .map(|diagnostic| {
246− (
247− diagnostic_scope,
248− diagnostic.range.start..diagnostic.range.end,
249− )
250− })
251− .collect(),
252− ));
253
296
254 'outer: for event in highlights {
297 'outer: for event in highlights {
255 match event {
298 match event {
@@ -273,14 +316,14 @@ impl EditorView {
273 });
316 });
274
317
275 for grapheme in RopeGraphemes::new(text) {
318 for grapheme in RopeGraphemes::new(text) {
276− let out_of_bounds = visual_x < view.first_col as u16
319+ let out_of_bounds = visual_x < offset.col as u16
277− || visual_x >= viewport.width + view.first_col as u16;
320+ || visual_x >= viewport.width + offset.col as u16;
278
321
279 if LineEnding::from_rope_slice(&grapheme).is_some() {
322 if LineEnding::from_rope_slice(&grapheme).is_some() {
280 if !out_of_bounds {
323 if !out_of_bounds {
281 // we still want to render an empty cell with the style
324 // we still want to render an empty cell with the style
282 surface.set_string(
325 surface.set_string(
283− viewport.x + visual_x - view.first_col as u16,
326+ viewport.x + visual_x - offset.col as u16,
284 viewport.y + line,
327 viewport.y + line,
285 " ",
328 " ",
286 style,
329 style,
@@ -310,7 +353,7 @@ impl EditorView {
310 if !out_of_bounds {
353 if !out_of_bounds {
311 // if we're offscreen just keep going until we hit a new line
354 // if we're offscreen just keep going until we hit a new line
312 surface.set_string(
355 surface.set_string(
313− viewport.x + visual_x - view.first_col as u16,
356+ viewport.x + visual_x - offset.col as u16,
314 viewport.y + line,
357 viewport.y + line,
315 grapheme,
358 grapheme,
316 style,
359 style,
@@ -323,14 +366,108 @@ impl EditorView {
323 }
366 }
324 }
367 }
325 }
368 }
369+ }
370+
371+ /// Render brace match, selected line numbers, etc (meant for the focused view only)
372+ pub fn render_focused_view_elements(
373+ view: &View,
374+ doc: &Document,
375+ viewport: Rect,
376+ theme: &Theme,
377+ surface: &mut Surface,
378+ ) {
379+ let text = doc.text().slice(..);
380+ let selection = doc.selection(view.id);
381+ let last_line = view.last_line(doc);
382+ let screen = {
383+ let start = text.line_to_char(view.first_line);
384+ let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text.
385+ Range::new(start, end)
386+ };
387+
388+ // render selected linenr(s)
389+ let linenr_select: Style = theme
390+ .try_get("ui.linenr.selected")
391+ .unwrap_or_else(|| theme.get("ui.linenr"));
392+
393+ // Whether to draw the line number for the last line of the
394+ // document or not. We only draw it if it's not an empty line.
395+ let draw_last = text.line_to_byte(last_line) < text.len_bytes();
396+
397+ for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
398+ let head = view.screen_coords_at_pos(
399+ doc,
400+ text,
401+ if selection.head > selection.anchor {
402+ selection.head - 1
403+ } else {
404+ selection.head
405+ },
406+ );
407+ if let Some(head) = head {
408+ // Highlight line number for selected lines.
409+ let line_number = view.first_line + head.row;
410+ let line_number_text = if line_number == last_line && !draw_last {
411+ " ~".into()
412+ } else {
413+ format!("{:>5}", line_number + 1)
414+ };
415+ surface.set_stringn(
416+ viewport.x - GUTTER_OFFSET + 1,
417+ viewport.y + head.row as u16,
418+ line_number_text,
419+ 5,
420+ linenr_select,
421+ );
422+
423+ // Highlight matching braces
424+ // TODO: set cursor position for IME
425+ if let Some(syntax) = doc.syntax() {
426+ use helix_core::match_brackets;
427+ let pos = doc
428+ .selection(view.id)
429+ .primary()
430+ .cursor(doc.text().slice(..));
431+ let pos = match_brackets::find(syntax, doc.text(), pos)
432+ .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
433+
434+ if let Some(pos) = pos {
435+ // ensure col is on screen
436+ if (pos.col as u16) < viewport.width + view.first_col as u16
437+ && pos.col >= view.first_col
438+ {
439+ let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
440+ Style::default()
441+ .add_modifier(Modifier::REVERSED)
442+ .add_modifier(Modifier::DIM)
443+ });
444+
445+ surface
446+ .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16)
447+ .set_style(style);
448+ }
449+ }
450+ }
451+ }
452+ }
453+ }
326
454
327− // render gutters
455+ #[allow(clippy::too_many_arguments)]
456+ pub fn render_gutter(
457+ doc: &Document,
458+ view: &View,
459+ viewport: Rect,
460+ surface: &mut Surface,
461+ theme: &Theme,
462+ ) {
463+ let text = doc.text().slice(..);
464+ let last_line = view.last_line(doc);
328
465
329− let linenr: Style = theme.get("ui.linenr");
466+ let linenr = theme.get("ui.linenr");
330− let warning: Style = theme.get("warning");
467+ let warning = theme.get("warning");
331− let error: Style = theme.get("error");
468+ let error = theme.get("error");
332− let info: Style = theme.get("info");
469+ let info = theme.get("info");
333− let hint: Style = theme.get("hint");
470+ let hint = theme.get("hint");
334
471
335 // Whether to draw the line number for the last line of the
472 // Whether to draw the line number for the last line of the
336 // document or not. We only draw it if it's not an empty line.
473 // document or not. We only draw it if it's not an empty line.
@@ -368,80 +505,6 @@ impl EditorView {
368 linenr,
505 linenr,
369 );
506 );
370 }
507 }
371−
372− // render selections and selected linenr(s)
373− let linenr_select: Style = theme
374− .try_get("ui.linenr.selected")
375− .unwrap_or_else(|| theme.get("ui.linenr"));
376−
377− if is_focused {
378− let screen = {
379− let start = text.line_to_char(view.first_line);
380− let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text.
381− Range::new(start, end)
382− };
383−
384− let selection = doc.selection(view.id);
385−
386− for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
387− let head = view.screen_coords_at_pos(
388− doc,
389− text,
390− if selection.head > selection.anchor {
391− selection.head - 1
392− } else {
393− selection.head
394− },
395− );
396− if let Some(head) = head {
397− // Draw line number for selected lines.
398− let line_number = view.first_line + head.row;
399− let line_number_text = if line_number == last_line && !draw_last {
400− " ~".into()
401− } else {
402− format!("{:>5}", line_number + 1)
403− };
404− surface.set_stringn(
405− viewport.x + 1 - GUTTER_OFFSET,
406− viewport.y + head.row as u16,
407− line_number_text,
408− 5,
409− linenr_select,
410− );
411−
412− // TODO: set cursor position for IME
413− if let Some(syntax) = doc.syntax() {
414− use helix_core::match_brackets;
415− let pos = doc
416− .selection(view.id)
417− .primary()
418− .cursor(doc.text().slice(..));
419− let pos = match_brackets::find(syntax, doc.text(), pos)
420− .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
421−
422− if let Some(pos) = pos {
423− // ensure col is on screen
424− if (pos.col as u16) < viewport.width + view.first_col as u16
425− && pos.col >= view.first_col
426− {
427− let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
428− Style::default()
429− .add_modifier(Modifier::REVERSED)
430− .add_modifier(Modifier::DIM)
431− });
432−
433− surface
434− .get_mut(
435− viewport.x + pos.col as u16,
436− viewport.y + pos.row as u16,
437− )
438− .set_style(style);
439− }
440− }
441− }
442− }
443− }
444− }
445 }
508 }
446
509
447 pub fn render_diagnostics(
510 pub fn render_diagnostics(
@@ -451,7 +514,6 @@ impl EditorView {
451 viewport: Rect,
514 viewport: Rect,
452 surface: &mut Surface,
515 surface: &mut Surface,
453 theme: &Theme,
516 theme: &Theme,
454− _is_focused: bool,
455 ) {
517 ) {
456 use helix_core::diagnostic::Severity;
518 use helix_core::diagnostic::Severity;
457 use tui::{
519 use tui::{
@@ -469,10 +531,10 @@ impl EditorView {
469 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
531 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
470 });
532 });
471
533
472− let warning: Style = theme.get("warning");
534+ let warning = theme.get("warning");
473− let error: Style = theme.get("error");
535+ let error = theme.get("error");
474− let info: Style = theme.get("info");
536+ let info = theme.get("info");
475− let hint: Style = theme.get("hint");
537+ let hint = theme.get("hint");
476
538
477 // Vec::with_capacity(diagnostics.len()); // rough estimate
539 // Vec::with_capacity(diagnostics.len()); // rough estimate
478 let mut lines = Vec::new();
540 let mut lines = Vec::new();
@@ -961,7 +1023,7 @@ impl Component for EditorView {
961 }
1023 }
962 }
1024 }
963
1025
964− fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
1026+ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
965 // clear with background color
1027 // clear with background color
966 surface.set_style(area, cx.editor.theme.get("ui.background"));
1028 surface.set_style(area, cx.editor.theme.get("ui.background"));
967
1029
@@ -983,7 +1045,7 @@ impl Component for EditorView {
983 );
1045 );
984 }
1046 }
985
1047
986− if let Some(ref info) = self.autoinfo {
1048+ if let Some(ref mut info) = self.autoinfo {
987 info.render(area, surface, cx);
1049 info.render(area, surface, cx);
988 }
1050 }
989
1051
@@ -1030,7 +1092,7 @@ impl Component for EditorView {
1030 );
1092 );
1031 }
1093 }
1032
1094
1033− if let Some(completion) = &self.completion {
1095+ if let Some(completion) = self.completion.as_mut() {
1034 completion.render(area, surface, cx);
1096 completion.render(area, surface, cx);
1035 }
1097 }
1036 }
1098 }
helix-term/src/ui/info.rs+1−1
@@ -5,7 +5,7 @@ use tui::buffer::Buffer as Surface;
5use tui::widgets::{Block, Borders, Widget};
5use tui::widgets::{Block, Borders, Widget};
6
6
7impl Component for Info {
7impl Component for Info {
8− fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
8+ fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
9 let style = cx.editor.theme.get("ui.popup");
9 let style = cx.editor.theme.get("ui.popup");
10
10
11 // Calculate the area of the terminal to modify. Because we want to
11 // Calculate the area of the terminal to modify. Because we want to
helix-term/src/ui/markdown.rs+1−1
@@ -198,7 +198,7 @@ fn parse<'a>(
198 Text::from(lines)
198 Text::from(lines)
199}
199}
200impl Component for Markdown {
200impl Component for Markdown {
201− fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
201+ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
202 use tui::widgets::{Paragraph, Widget, Wrap};
202 use tui::widgets::{Paragraph, Widget, Wrap};
203
203
204 let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
204 let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
helix-term/src/ui/menu.rs+1−1
@@ -258,7 +258,7 @@ impl<T: Item + 'static> Component for Menu<T> {
258
258
259 // TODO: required size should re-trigger when we filter items so we can draw a smaller menu
259 // TODO: required size should re-trigger when we filter items so we can draw a smaller menu
260
260
261− fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
261+ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
262 let style = cx.editor.theme.get("ui.text");
262 let style = cx.editor.theme.get("ui.text");
263 let selected = cx.editor.theme.get("ui.menu.selected");
263 let selected = cx.editor.theme.get("ui.menu.selected");
264
264
helix-term/src/ui/mod.rs+20−22
@@ -13,7 +13,7 @@ pub use completion::Completion;
13pub use editor::EditorView;
13pub use editor::EditorView;
14pub use markdown::Markdown;
14pub use markdown::Markdown;
15pub use menu::Menu;
15pub use menu::Menu;
16−pub use picker::Picker;
16+pub use picker::{FilePicker, Picker};
17pub use popup::Popup;
17pub use popup::Popup;
18pub use prompt::{Prompt, PromptEvent};
18pub use prompt::{Prompt, PromptEvent};
19pub use spinner::{ProgressSpinners, Spinner};
19pub use spinner::{ProgressSpinners, Spinner};
@@ -73,29 +73,26 @@ pub fn regex_prompt(
73 )
73 )
74}
74}
75
75
76−pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
76+pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
77 use ignore::Walk;
77 use ignore::Walk;
78 use std::time;
78 use std::time;
79− let files = Walk::new(root.clone()).filter_map(|entry| match entry {
79+ let files = Walk::new(root.clone()).filter_map(|entry| {
80− Ok(entry) => {
80+ let entry = entry.ok()?;
81− // filter dirs, but we might need special handling for symlinks!
81+ // Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
82− if !entry.file_type().map_or(false, |entry| entry.is_dir()) {
82+ if entry.path().is_dir() {
83− let time = if let Ok(metadata) = entry.metadata() {
83+ // Will give a false positive if metadata cannot be read (eg. permission error)
84− metadata
85− .accessed()
86− .or_else(|_| metadata.modified())
87− .or_else(|_| metadata.created())
88− .unwrap_or(time::UNIX_EPOCH)
89− } else {
90− time::UNIX_EPOCH
91− };
92−
93− Some((entry.into_path(), time))
94− } else {
95− None
84+ return None;
96− }
97 }
85 }
98− Err(_err) => None,
86+
87+ let time = entry.metadata().map_or(time::UNIX_EPOCH, |metadata| {
88+ metadata
89+ .accessed()
90+ .or_else(|_| metadata.modified())
91+ .or_else(|_| metadata.created())
92+ .unwrap_or(time::UNIX_EPOCH)
93+ });
94+
95+ Some((entry.into_path(), time))
99 });
96 });
100
97
101 let mut files: Vec<_> = if root.join(".git").is_dir() {
98 let mut files: Vec<_> = if root.join(".git").is_dir() {
@@ -109,7 +106,7 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
109
106
110 let files = files.into_iter().map(|(path, _)| path).collect();
107 let files = files.into_iter().map(|(path, _)| path).collect();
111
108
112− Picker::new(
109+ FilePicker::new(
113 files,
110 files,
114 move |path: &PathBuf| {
111 move |path: &PathBuf| {
115 // format_fn
112 // format_fn
@@ -124,6 +121,7 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
124 .open(path.into(), action)
121 .open(path.into(), action)
125 .expect("editor.open failed");
122 .expect("editor.open failed");
126 },
123 },
124+ |_editor, path| Some((path.clone(), None)),
127 )
125 )
128}
126}
129
127
helix-term/src/ui/picker.rs+171−20
@@ -1,4 +1,7 @@
1+use crate::{
1−use crate::compositor::{Component, Compositor, Context, EventResult};
2+ compositor::{Component, Compositor, Context, EventResult},
3+ ui::EditorView,
4+};
2use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
5use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
3use tui::{
6use tui::{
4 buffer::Buffer as Surface,
7 buffer::Buffer as Surface,
@@ -7,17 +10,153 @@ use tui::{
7
10
8use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
11use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
9use fuzzy_matcher::FuzzyMatcher;
12use fuzzy_matcher::FuzzyMatcher;
13+use tui::widgets::Widget;
10
14
11−use std::borrow::Cow;
15+use std::{borrow::Cow, collections::HashMap, path::PathBuf};
12
16
13use crate::ui::{Prompt, PromptEvent};
17use crate::ui::{Prompt, PromptEvent};
14use helix_core::Position;
18use helix_core::Position;
15use helix_view::{
19use helix_view::{
20+ document::canonicalize_path,
16 editor::Action,
21 editor::Action,
17 graphics::{Color, CursorKind, Rect, Style},
22 graphics::{Color, CursorKind, Rect, Style},
18− Editor,
23+ Document, Editor,
19};
24};
20
25
26+pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
27+
28+/// File path and line number (used to align and highlight a line)
29+type FileLocation = (PathBuf, Option<usize>);
30+
31+pub struct FilePicker<T> {
32+ picker: Picker<T>,
33+ /// Caches paths to documents
34+ preview_cache: HashMap<PathBuf, Document>,
35+ /// Given an item in the picker, return the file path and line number to display.
36+ file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
37+}
38+
39+impl<T> FilePicker<T> {
40+ pub fn new(
41+ options: Vec<T>,
42+ format_fn: impl Fn(&T) -> Cow<str> + 'static,
43+ callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
44+ preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
45+ ) -> Self {
46+ Self {
47+ picker: Picker::new(false, options, format_fn, callback_fn),
48+ preview_cache: HashMap::new(),
49+ file_fn: Box::new(preview_fn),
50+ }
51+ }
52+
53+ fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
54+ self.picker
55+ .selection()
56+ .and_then(|current| (self.file_fn)(editor, current))
57+ .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line)))
58+ }
59+
60+ fn calculate_preview(&mut self, editor: &Editor) {
61+ if let Some((path, _line)) = self.current_file(editor) {
62+ if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
63+ // TODO: enable syntax highlighting; blocked by async rendering
64+ let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
65+ self.preview_cache.insert(path, doc);
66+ }
67+ }
68+ }
69+}
70+
71+impl<T: 'static> Component for FilePicker<T> {
72+ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
73+ // +---------+ +---------+
74+ // |prompt | |preview |
75+ // +---------+ | |
76+ // |picker | | |
77+ // | | | |
78+ // +---------+ +---------+
79+ self.calculate_preview(cx.editor);
80+ let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
81+ let area = inner_rect(area);
82+ // -- Render the frame:
83+ // clear area
84+ let background = cx.editor.theme.get("ui.background");
85+ surface.clear_with(area, background);
86+
87+ let picker_width = if render_preview {
88+ area.width / 2
89+ } else {
90+ area.width
91+ };
92+
93+ let picker_area = Rect::new(area.x, area.y, picker_width, area.height);
94+ self.picker.render(picker_area, surface, cx);
95+
96+ if !render_preview {
97+ return;
98+ }
99+
100+ let preview_area = Rect::new(area.x + picker_width, area.y, area.width / 2, area.height);
101+
102+ // don't like this but the lifetime sucks
103+ let block = Block::default().borders(Borders::ALL);
104+
105+ // calculate the inner area inside the box
106+ let mut inner = block.inner(preview_area);
107+ // 1 column gap on either side
108+ inner.x += 1;
109+ inner.width = inner.width.saturating_sub(2);
110+
111+ block.render(preview_area, surface);
112+
113+ if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, line)| {
114+ cx.editor
115+ .document_by_path(&path)
116+ .or_else(|| self.preview_cache.get(&path))
117+ .zip(Some(line))
118+ }) {
119+ // align to middle
120+ let first_line = line.unwrap_or(0).saturating_sub(inner.height as usize / 2);
121+ let offset = Position::new(first_line, 0);
122+
123+ let highlights = EditorView::doc_syntax_highlights(
124+ doc,
125+ offset,
126+ area.height,
127+ &cx.editor.theme,
128+ &cx.editor.syn_loader,
129+ );
130+ EditorView::render_text_highlights(
131+ doc,
132+ offset,
133+ inner,
134+ surface,
135+ &cx.editor.theme,
136+ highlights,
137+ );
138+
139+ // highlight the line
140+ if let Some(line) = line {
141+ for x in inner.left()..inner.right() {
142+ surface
143+ .get_mut(x, inner.y + line.saturating_sub(first_line) as u16)
144+ .set_style(cx.editor.theme.get("ui.selection.primary"));
145+ }
146+ }
147+ }
148+ }
149+
150+ fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult {
151+ // TODO: keybinds for scrolling preview
152+ self.picker.handle_event(event, ctx)
153+ }
154+
155+ fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
156+ self.picker.cursor(area, ctx)
157+ }
158+}
159+
21pub struct Picker<T> {
160pub struct Picker<T> {
22 options: Vec<T>,
161 options: Vec<T>,
23 // filter: String,
162 // filter: String,
@@ -30,6 +169,8 @@ pub struct Picker<T> {
30 cursor: usize,
169 cursor: usize,
31 // pattern: String,
170 // pattern: String,
32 prompt: Prompt,
171 prompt: Prompt,
172+ /// Whether to render in the middle of the area
173+ render_centered: bool,
33
174
34 format_fn: Box<dyn Fn(&T) -> Cow<str>>,
175 format_fn: Box<dyn Fn(&T) -> Cow<str>>,
35 callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>,
176 callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>,
@@ -37,6 +178,7 @@ pub struct Picker<T> {
37
178
38impl<T> Picker<T> {
179impl<T> Picker<T> {
39 pub fn new(
180 pub fn new(
181+ render_centered: bool,
40 options: Vec<T>,
182 options: Vec<T>,
41 format_fn: impl Fn(&T) -> Cow<str> + 'static,
183 format_fn: impl Fn(&T) -> Cow<str> + 'static,
42 callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
184 callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
@@ -57,6 +199,7 @@ impl<T> Picker<T> {
57 filters: Vec::new(),
199 filters: Vec::new(),
58 cursor: 0,
200 cursor: 0,
59 prompt,
201 prompt,
202+ render_centered,
60 format_fn: Box::new(format_fn),
203 format_fn: Box::new(format_fn),
61 callback_fn: Box::new(callback_fn),
204 callback_fn: Box::new(callback_fn),
62 };
205 };
@@ -139,8 +282,8 @@ impl<T> Picker<T> {
139// - score all the names in relation to input
282// - score all the names in relation to input
140
283
141fn inner_rect(area: Rect) -> Rect {
284fn inner_rect(area: Rect) -> Rect {
142− let padding_vertical = area.height * 20 / 100;
285+ let padding_vertical = area.height * 10 / 100;
143− let padding_horizontal = area.width * 20 / 100;
286+ let padding_horizontal = area.width * 10 / 100;
144
287
145 Rect::new(
288 Rect::new(
146 area.x + padding_horizontal,
289 area.x + padding_horizontal,
@@ -174,7 +317,9 @@ impl<T: 'static> Component for Picker<T> {
174 | KeyEvent {
317 | KeyEvent {
175 code: KeyCode::Char('p'),
318 code: KeyCode::Char('p'),
176 modifiers: KeyModifiers::CONTROL,
319 modifiers: KeyModifiers::CONTROL,
320+ } => {
177− } => self.move_up(),
321+ self.move_up();
322+ }
178 KeyEvent {
323 KeyEvent {
179 code: KeyCode::Down,
324 code: KeyCode::Down,
180 ..
325 ..
@@ -185,7 +330,9 @@ impl<T: 'static> Component for Picker<T> {
185 | KeyEvent {
330 | KeyEvent {
186 code: KeyCode::Char('n'),
331 code: KeyCode::Char('n'),
187 modifiers: KeyModifiers::CONTROL,
332 modifiers: KeyModifiers::CONTROL,
333+ } => {
188− } => self.move_down(),
334+ self.move_down();
335+ }
189 KeyEvent {
336 KeyEvent {
190 code: KeyCode::Esc, ..
337 code: KeyCode::Esc, ..
191 }
338 }
@@ -239,16 +386,18 @@ impl<T: 'static> Component for Picker<T> {
239 EventResult::Consumed(None)
386 EventResult::Consumed(None)
240 }
387 }
241
388
242− fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
389+ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
390+ let area = if self.render_centered {
243− let area = inner_rect(area);
391+ inner_rect(area)
392+ } else {
393+ area
394+ };
244
395
245 // -- Render the frame:
396 // -- Render the frame:
246−
247 // clear area
397 // clear area
248 let background = cx.editor.theme.get("ui.background");
398 let background = cx.editor.theme.get("ui.background");
249 surface.clear_with(area, background);
399 surface.clear_with(area, background);
250
400
251− use tui::widgets::Widget;
252 // don't like this but the lifetime sucks
401 // don't like this but the lifetime sucks
253 let block = Block::default().borders(Borders::ALL);
402 let block = Block::default().borders(Borders::ALL);
254
403
@@ -263,21 +412,23 @@ impl<T: 'static> Component for Picker<T> {
263 self.prompt.render(area, surface, cx);
412 self.prompt.render(area, surface, cx);
264
413
265 // -- Separator
414 // -- Separator
266− let style = Style::default().fg(Color::Rgb(90, 89, 119));
415+ let sep_style = Style::default().fg(Color::Rgb(90, 89, 119));
267− let symbols = BorderType::line_symbols(BorderType::Plain);
416+ let borders = BorderType::line_symbols(BorderType::Plain);
268 for x in inner.left()..inner.right() {
417 for x in inner.left()..inner.right() {
269 surface
418 surface
270 .get_mut(x, inner.y + 1)
419 .get_mut(x, inner.y + 1)
271− .set_symbol(symbols.horizontal)
420+ .set_symbol(borders.horizontal)
272− .set_style(style);
421+ .set_style(sep_style);
273 }
422 }
274
423
275 // -- Render the contents:
424 // -- Render the contents:
425+ // subtract the area of the prompt (-2) and current item marker " > " (-3)
426+ let inner = Rect::new(inner.x + 3, inner.y + 2, inner.width - 3, inner.height - 2);
276
427
277 let style = cx.editor.theme.get("ui.text");
428 let style = cx.editor.theme.get("ui.text");
278 let selected = Style::default().fg(Color::Rgb(255, 255, 255));
429 let selected = Style::default().fg(Color::Rgb(255, 255, 255));
279
430
280− let rows = inner.height - 2; // -1 for search bar
431+ let rows = inner.height;
281 let offset = self.cursor / (rows as usize) * (rows as usize);
432 let offset = self.cursor / (rows as usize) * (rows as usize);
282
433
283 let files = self.matches.iter().skip(offset).map(|(index, _score)| {
434 let files = self.matches.iter().skip(offset).map(|(index, _score)| {
@@ -286,14 +437,14 @@ impl<T: 'static> Component for Picker<T> {
286
437
287 for (i, (_index, option)) in files.take(rows as usize).enumerate() {
438 for (i, (_index, option)) in files.take(rows as usize).enumerate() {
288 if i == (self.cursor - offset) {
439 if i == (self.cursor - offset) {
289− surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
440+ surface.set_string(inner.x - 2, inner.y + i as u16, ">", selected);
290 }
441 }
291
442
292 surface.set_string_truncated(
443 surface.set_string_truncated(
293− inner.x + 3,
444+ inner.x,
294− inner.y + 2 + i as u16,
445+ inner.y + i as u16,
295 (self.format_fn)(option),
446 (self.format_fn)(option),
296− (inner.width as usize).saturating_sub(3), // account for the " > "
447+ inner.width as usize,
297 if i == (self.cursor - offset) {
448 if i == (self.cursor - offset) {
298 selected
449 selected
299 } else {
450 } else {
helix-term/src/ui/popup.rs+1−1
@@ -105,7 +105,7 @@ impl<T: Component> Component for Popup<T> {
105 Some(self.size)
105 Some(self.size)
106 }
106 }
107
107
108− fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
108+ fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
109 cx.scroll = Some(self.scroll);
109 cx.scroll = Some(self.scroll);
110
110
111 let position = self
111 let position = self
helix-term/src/ui/prompt.rs+2−2
@@ -352,7 +352,7 @@ impl Prompt {
352 }
352 }
353
353
354 if let Some(doc) = (self.doc_fn)(&self.line) {
354 if let Some(doc) = (self.doc_fn)(&self.line) {
355− let text = ui::Text::new(doc.to_string());
355+ let mut text = ui::Text::new(doc.to_string());
356
356
357 let viewport = area;
357 let viewport = area;
358 let area = viewport.intersection(Rect::new(
358 let area = viewport.intersection(Rect::new(
@@ -546,7 +546,7 @@ impl Component for Prompt {
546 EventResult::Consumed(None)
546 EventResult::Consumed(None)
547 }
547 }
548
548
549− fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
549+ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
550 self.render_prompt(area, surface, cx)
550 self.render_prompt(area, surface, cx)
551 }
551 }
552
552
helix-term/src/ui/text.rs+1−1
@@ -13,7 +13,7 @@ impl Text {
13 }
13 }
14}
14}
15impl Component for Text {
15impl Component for Text {
16− fn render(&self, area: Rect, surface: &mut Surface, _cx: &mut Context) {
16+ fn render(&mut self, area: Rect, surface: &mut Surface, _cx: &mut Context) {
17 use tui::widgets::{Paragraph, Widget, Wrap};
17 use tui::widgets::{Paragraph, Widget, Wrap};
18 let contents = tui::text::Text::from(self.contents.clone());
18 let contents = tui::text::Text::from(self.contents.clone());
19
19
helix-view/src/document.rs+7−3
@@ -432,14 +432,14 @@ impl Document {
432 /// Create a new document from `path`. Encoding is auto-detected, but it can be manually
432 /// Create a new document from `path`. Encoding is auto-detected, but it can be manually
433 /// overwritten with the `encoding` parameter.
433 /// overwritten with the `encoding` parameter.
434 pub fn open(
434 pub fn open(
435− path: PathBuf,
435+ path: &Path,
436 encoding: Option<&'static encoding_rs::Encoding>,
436 encoding: Option<&'static encoding_rs::Encoding>,
437 theme: Option<&Theme>,
437 theme: Option<&Theme>,
438 config_loader: Option<&syntax::Loader>,
438 config_loader: Option<&syntax::Loader>,
439 ) -> Result<Self, Error> {
439 ) -> Result<Self, Error> {
440 let (rope, encoding) = if path.exists() {
440 let (rope, encoding) = if path.exists() {
441 let mut file =
441 let mut file =
442− std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
442+ std::fs::File::open(path).context(format!("unable to open {:?}", path))?;
443 from_reader(&mut file, encoding)?
443 from_reader(&mut file, encoding)?
444 } else {
444 } else {
445 let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
445 let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
@@ -449,7 +449,7 @@ impl Document {
449 let mut doc = Self::from(rope, Some(encoding));
449 let mut doc = Self::from(rope, Some(encoding));
450
450
451 // set the path and try detecting the language
451 // set the path and try detecting the language
452− doc.set_path(&path)?;
452+ doc.set_path(path)?;
453 if let Some(loader) = config_loader {
453 if let Some(loader) = config_loader {
454 doc.detect_language(theme, loader);
454 doc.detect_language(theme, loader);
455 }
455 }
@@ -904,6 +904,10 @@ impl Document {
904 &self.selections[&view_id]
904 &self.selections[&view_id]
905 }
905 }
906
906
907+ pub fn selections(&self) -> &HashMap<ViewId, Selection> {
908+ &self.selections
909+ }
910+
907 pub fn relative_path(&self) -> Option<PathBuf> {
911 pub fn relative_path(&self) -> Option<PathBuf> {
908 let cwdir = std::env::current_dir().expect("couldn't determine current directory");
912 let cwdir = std::env::current_dir().expect("couldn't determine current directory");
909
913
helix-view/src/editor.rs+11−2
@@ -7,7 +7,11 @@ use crate::{
7};
7};
8
8
9use futures_util::future;
9use futures_util::future;
10+use std::{
11+ path::{Path, PathBuf},
12+ sync::Arc,
10−use std::{path::PathBuf, sync::Arc, time::Duration};
13+ time::Duration,
14+};
11
15
12use slotmap::SlotMap;
16use slotmap::SlotMap;
13
17
@@ -222,7 +226,7 @@ impl Editor {
222 let id = if let Some(id) = id {
226 let id = if let Some(id) = id {
223 id
227 id
224 } else {
228 } else {
225− let mut doc = Document::open(path, None, Some(&self.theme), Some(&self.syn_loader))?;
229+ let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
226
230
227 // try to find a language server based on the language name
231 // try to find a language server based on the language name
228 let language_server = doc
232 let language_server = doc
@@ -316,6 +320,11 @@ impl Editor {
316 self.documents.iter_mut().map(|(_id, doc)| doc)
320 self.documents.iter_mut().map(|(_id, doc)| doc)
317 }
321 }
318
322
323+ pub fn document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> {
324+ self.documents()
325+ .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
326+ }
327+
319 // pub fn current_document(&self) -> Document {
328 // pub fn current_document(&self) -> Document {
320 // let id = self.view().doc;
329 // let id = self.view().doc;
321 // let doc = &mut editor.documents[id];
330 // let doc = &mut editor.documents[id];
helix-view/src/view.rs+1−1
@@ -10,7 +10,7 @@ use helix_core::{
10
10
11type Jump = (DocumentId, Selection);
11type Jump = (DocumentId, Selection);
12
12
13−#[derive(Debug)]
13+#[derive(Debug, Clone)]
14pub struct JumpList {
14pub struct JumpList {
15 jumps: Vec<Jump>,
15 jumps: Vec<Jump>,
16 current: usize,
16 current: usize,