Skip to content

Commit 5c37120

Browse files
omenticcossonleowongjiahau
committed
Add file explorer and tree helper
ref: helix-editor/helix#200 ref: helix-editor/helix#2377 ref: helix-editor/helix#5566 ref: helix-editor/helix#5768 Co-authored-by: cossonleo <[email protected]> Co-authored-by: wongjiahau <[email protected]>
1 parent f6021dd commit 5c37120

File tree

13 files changed

+4404
-27
lines changed

13 files changed

+4404
-27
lines changed

book/src/configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,16 @@ max-indent-retain = 0
350350
wrap-indicator = "" # set wrap-indicator to "" to hide it
351351
```
352352

353+
### `[editor.explorer]` Section
354+
355+
Sets explorer side width and style.
356+
357+
| Key | Description | Default |
358+
| -------------- | ------------------------------------------- | ------- |
359+
| `column-width` | explorer side width | 30 |
360+
| `position` | explorer widget position, `left` or `right` | `left` |
361+
362+
353363
### `[editor.smart-tab]` Section
354364

355365

book/src/keymap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ This layer is a kludge of mappings, mostly pickers.
296296
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
297297
| `/` | Global search in workspace folder | `global_search` |
298298
| `?` | Open command palette | `command_palette` |
299+
| `e` | Reveal current file in explorer | `reveal_current_file` |
299300

300301
> 💡 Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file.
301302
@@ -452,3 +453,7 @@ Keys to use within prompt, Remapping currently not supported.
452453
| `Tab` | Select next completion item |
453454
| `BackTab` | Select previous completion item |
454455
| `Enter` | Open selected |
456+
457+
## File explorer
458+
459+
Press `?` to see keymaps. Remapping currently not supported.

helix-term/src/commands.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,8 @@ impl MappableCommand {
490490
record_macro, "Record macro",
491491
replay_macro, "Replay macro",
492492
command_palette, "Open command palette",
493+
open_or_focus_explorer, "Open or focus explorer",
494+
reveal_current_file, "Reveal current file in explorer",
493495
);
494496
}
495497

@@ -2688,6 +2690,49 @@ fn file_picker_in_current_directory(cx: &mut Context) {
26882690
cx.push_layer(Box::new(overlaid(picker)));
26892691
}
26902692

2693+
fn open_or_focus_explorer(cx: &mut Context) {
2694+
cx.callback = Some(Box::new(
2695+
|compositor: &mut Compositor, cx: &mut compositor::Context| {
2696+
if let Some(editor) = compositor.find::<ui::EditorView>() {
2697+
match editor.explorer.as_mut() {
2698+
Some(explore) => explore.focus(),
2699+
None => match ui::Explorer::new(cx) {
2700+
Ok(explore) => editor.explorer = Some(explore),
2701+
Err(err) => cx.editor.set_error(format!("{}", err)),
2702+
},
2703+
}
2704+
}
2705+
},
2706+
));
2707+
}
2708+
2709+
fn reveal_file_in_explorer(cx: &mut Context, path: Option<PathBuf>) {
2710+
cx.callback = Some(Box::new(
2711+
|compositor: &mut Compositor, cx: &mut compositor::Context| {
2712+
if let Some(editor) = compositor.find::<ui::EditorView>() {
2713+
(|| match editor.explorer.as_mut() {
2714+
Some(explorer) => match path {
2715+
Some(path) => explorer.reveal_file(path),
2716+
None => explorer.reveal_current_file(cx),
2717+
},
2718+
None => {
2719+
editor.explorer = Some(ui::Explorer::new(cx)?);
2720+
if let Some(explorer) = editor.explorer.as_mut() {
2721+
explorer.reveal_current_file(cx)?;
2722+
}
2723+
Ok(())
2724+
}
2725+
})()
2726+
.unwrap_or_else(|err| cx.editor.set_error(err.to_string()))
2727+
}
2728+
},
2729+
));
2730+
}
2731+
2732+
fn reveal_current_file(cx: &mut Context) {
2733+
reveal_file_in_explorer(cx, None)
2734+
}
2735+
26912736
fn buffer_picker(cx: &mut Context) {
26922737
let current = view!(cx.editor).doc;
26932738

helix-term/src/compositor.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,50 @@ impl<'a> Context<'a> {
3535
tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?;
3636
Ok(())
3737
}
38+
39+
/// Purpose: to test `handle_event` without escalating the test case to integration test
40+
/// Usage:
41+
/// ```
42+
/// let mut editor = Context::dummy_editor();
43+
/// let mut jobs = Context::dummy_jobs();
44+
/// let mut cx = Context::dummy(&mut jobs, &mut editor);
45+
/// ```
46+
#[cfg(test)]
47+
pub fn dummy(jobs: &'a mut Jobs, editor: &'a mut helix_view::Editor) -> Context<'a> {
48+
Context {
49+
jobs,
50+
scroll: None,
51+
editor,
52+
}
53+
}
54+
55+
#[cfg(test)]
56+
pub fn dummy_jobs() -> Jobs {
57+
Jobs::new()
58+
}
59+
60+
#[cfg(test)]
61+
pub fn dummy_editor() -> Editor {
62+
use crate::config::Config;
63+
use arc_swap::{access::Map, ArcSwap};
64+
use helix_core::syntax::{self, Configuration};
65+
use helix_view::theme;
66+
use std::{collections::HashMap, sync::Arc};
67+
68+
let config = Arc::new(ArcSwap::from_pointee(Config::default()));
69+
Editor::new(
70+
Rect::new(0, 0, 60, 120),
71+
Arc::new(theme::Loader::new(&[])),
72+
Arc::new(syntax::Loader::new(Configuration {
73+
language: vec![],
74+
language_server: HashMap::new(),
75+
})),
76+
Arc::new(Arc::new(Map::new(
77+
Arc::clone(&config),
78+
|config: &Config| &config.editor,
79+
))),
80+
)
81+
}
3882
}
3983

4084
pub trait Component: Any + AnyComponent {
@@ -73,6 +117,21 @@ pub trait Component: Any + AnyComponent {
73117
fn id(&self) -> Option<&'static str> {
74118
None
75119
}
120+
121+
#[cfg(test)]
122+
/// Utility method for testing `handle_event` without using integration test.
123+
/// Especially useful for testing helper components such as `Prompt`, `TreeView` etc
124+
fn handle_events(&mut self, events: &str) -> anyhow::Result<()> {
125+
use helix_view::input::parse_macro;
126+
127+
let mut editor = Context::dummy_editor();
128+
let mut jobs = Context::dummy_jobs();
129+
let mut cx = Context::dummy(&mut jobs, &mut editor);
130+
for event in parse_macro(events)? {
131+
self.handle_event(&Event::Key(event), &mut cx);
132+
}
133+
Ok(())
134+
}
76135
}
77136

78137
pub struct Compositor {

helix-term/src/keymap/default.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
277277
"r" => rename_symbol,
278278
"h" => select_references_to_symbol_under_cursor,
279279
"?" => command_palette,
280+
"e" => reveal_current_file,
280281
},
281282
"z" => { "View"
282283
"z" | "c" => align_view_center,

helix-term/src/ui/editor.rs

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
keymap::{KeymapResult, Keymaps},
77
ui::{
88
document::{render_document, LinePos, TextRenderer, TranslatedPosition},
9-
Completion, ProgressSpinners,
9+
Completion, Explorer, ProgressSpinners,
1010
},
1111
};
1212

@@ -23,7 +23,7 @@ use helix_core::{
2323
};
2424
use helix_view::{
2525
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
26-
editor::{CompleteAction, CursorShapeConfig},
26+
editor::{CompleteAction, CursorShapeConfig, ExplorerPosition},
2727
graphics::{Color, CursorKind, Modifier, Rect, Style},
2828
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
2929
keyboard::{KeyCode, KeyModifiers},
@@ -42,6 +42,7 @@ pub struct EditorView {
4242
pseudo_pending: Vec<KeyEvent>,
4343
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
4444
pub(crate) completion: Option<Completion>,
45+
pub(crate) explorer: Option<Explorer>,
4546
spinners: ProgressSpinners,
4647
/// Tracks if the terminal window is focused by reaction to terminal focus events
4748
terminal_focused: bool,
@@ -72,6 +73,7 @@ impl EditorView {
7273
pseudo_pending: Vec::new(),
7374
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
7475
completion: None,
76+
explorer: None,
7577
spinners: ProgressSpinners::default(),
7678
terminal_focused: true,
7779
}
@@ -1235,6 +1237,11 @@ impl Component for EditorView {
12351237
event: &Event,
12361238
context: &mut crate::compositor::Context,
12371239
) -> EventResult {
1240+
if let Some(explore) = self.explorer.as_mut() {
1241+
if let EventResult::Consumed(callback) = explore.handle_event(event, context) {
1242+
return EventResult::Consumed(callback);
1243+
}
1244+
}
12381245
let mut cx = commands::Context {
12391246
editor: context.editor,
12401247
count: None,
@@ -1401,6 +1408,8 @@ impl Component for EditorView {
14011408
surface.set_style(area, cx.editor.theme.get("ui.background"));
14021409
let config = cx.editor.config();
14031410

1411+
let editor_area = area.clip_bottom(1);
1412+
14041413
// check if bufferline should be rendered
14051414
use helix_view::editor::BufferLine;
14061415
let use_bufferline = match config.bufferline {
@@ -1409,15 +1418,43 @@ impl Component for EditorView {
14091418
_ => false,
14101419
};
14111420

1412-
// -1 for commandline and -1 for bufferline
1413-
let mut editor_area = area.clip_bottom(1);
1414-
if use_bufferline {
1415-
editor_area = editor_area.clip_top(1);
1416-
}
1421+
let editor_area = if use_bufferline {
1422+
editor_area.clip_top(1)
1423+
} else {
1424+
editor_area
1425+
};
1426+
1427+
let editor_area = if let Some(explorer) = &self.explorer {
1428+
let explorer_column_width = if explorer.is_opened() {
1429+
explorer.column_width().saturating_add(2)
1430+
} else {
1431+
0
1432+
};
1433+
// For future developer:
1434+
// We should have a Dock trait that allows a component to dock to the top/left/bottom/right
1435+
// of another component.
1436+
match config.explorer.position {
1437+
ExplorerPosition::Left => editor_area.clip_left(explorer_column_width),
1438+
ExplorerPosition::Right => editor_area.clip_right(explorer_column_width),
1439+
}
1440+
} else {
1441+
editor_area
1442+
};
14171443

14181444
// if the terminal size suddenly changed, we need to trigger a resize
14191445
cx.editor.resize(editor_area);
14201446

1447+
if let Some(explorer) = self.explorer.as_mut() {
1448+
if !explorer.is_focus() {
1449+
let area = if use_bufferline {
1450+
area.clip_top(1)
1451+
} else {
1452+
area
1453+
};
1454+
explorer.render(area, surface, cx);
1455+
}
1456+
}
1457+
14211458
if use_bufferline {
14221459
Self::render_bufferline(cx.editor, area.with_height(1), surface);
14231460
}
@@ -1496,9 +1533,28 @@ impl Component for EditorView {
14961533
if let Some(completion) = self.completion.as_mut() {
14971534
completion.render(area, surface, cx);
14981535
}
1536+
1537+
if let Some(explore) = self.explorer.as_mut() {
1538+
if explore.is_focus() {
1539+
let area = if use_bufferline {
1540+
area.clip_top(1)
1541+
} else {
1542+
area
1543+
};
1544+
explore.render(area, surface, cx);
1545+
}
1546+
}
14991547
}
15001548

15011549
fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
1550+
if let Some(explore) = &self.explorer {
1551+
if explore.is_focus() {
1552+
let cursor = explore.cursor(_area, editor);
1553+
if cursor.0.is_some() {
1554+
return cursor;
1555+
}
1556+
}
1557+
}
15021558
match editor.cursor() {
15031559
// All block cursors are drawn manually
15041560
(pos, CursorKind::Block) => (pos, CursorKind::Hidden),

0 commit comments

Comments
 (0)