Skip to content

Commit

Permalink
Add file explorer and tree helper
Browse files Browse the repository at this point in the history
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: JJ <[email protected]>
Co-authored-by: Quan Tong <[email protected]>
  • Loading branch information
4 people committed May 1, 2024
1 parent 2cadec0 commit 0657679
Show file tree
Hide file tree
Showing 13 changed files with 4,405 additions and 28 deletions.
9 changes: 9 additions & 0 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,3 +402,12 @@ S-tab = "move_parent_node_start"
tab = "extend_parent_node_end"
S-tab = "extend_parent_node_start"
```

### `[editor.explorer]` Section

Sets explorer side width and style.

| Key | Description | Default |
| -------------- | ------------------------------------------- | ------- |
| `column-width` | explorer side width | 30 |
| `position` | explorer widget position, `left` or `right` | `left` |
5 changes: 5 additions & 0 deletions book/src/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ This layer is a kludge of mappings, mostly pickers.
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` |
| `?` | Open command palette | `command_palette` |
| `e` | Reveal current file in explorer | `reveal_current_file` |

> 💡 Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file.
Expand Down Expand Up @@ -457,3 +458,7 @@ Keys to use within prompt, Remapping currently not supported.
| `Tab` | Select next completion item |
| `BackTab` | Select previous completion item |
| `Enter` | Open selected |

## File explorer

Press `?` to see keymaps. Remapping currently not supported.
45 changes: 45 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,8 @@ impl MappableCommand {
command_palette, "Open command palette",
goto_word, "Jump to a two-character label",
extend_to_word, "Extend to a two-character label",
open_or_focus_explorer, "Open or focus explorer",
reveal_current_file, "Reveal current file in explorer",
);
}

Expand Down Expand Up @@ -2834,6 +2836,49 @@ fn file_picker_in_current_directory(cx: &mut Context) {
cx.push_layer(Box::new(overlaid(picker)));
}

fn open_or_focus_explorer(cx: &mut Context) {
cx.callback.push(Box::new(
|compositor: &mut Compositor, cx: &mut compositor::Context| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
match editor.explorer.as_mut() {
Some(explore) => explore.focus(),
None => match ui::Explorer::new(cx) {
Ok(explore) => editor.explorer = Some(explore),
Err(err) => cx.editor.set_error(format!("{}", err)),
},
}
}
},
));
}

fn reveal_file_in_explorer(cx: &mut Context, path: Option<PathBuf>) {
cx.callback.push(Box::new(
|compositor: &mut Compositor, cx: &mut compositor::Context| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
(|| match editor.explorer.as_mut() {
Some(explorer) => match path {
Some(path) => explorer.reveal_file(path),
None => explorer.reveal_current_file(cx),
},
None => {
editor.explorer = Some(ui::Explorer::new(cx)?);
if let Some(explorer) = editor.explorer.as_mut() {
explorer.reveal_current_file(cx)?;
}
Ok(())
}
})()
.unwrap_or_else(|err| cx.editor.set_error(err.to_string()))
}
},
));
}

fn reveal_current_file(cx: &mut Context) {
reveal_file_in_explorer(cx, None)
}

fn buffer_picker(cx: &mut Context) {
let current = view!(cx.editor).doc;

Expand Down
63 changes: 63 additions & 0 deletions helix-term/src/compositor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,56 @@ impl<'a> Context<'a> {
tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?;
Ok(())
}

/// Purpose: to test `handle_event` without escalating the test case to integration test
/// Usage:
/// ```
/// let mut editor = Context::dummy_editor();
/// let mut jobs = Context::dummy_jobs();
/// let mut cx = Context::dummy(&mut jobs, &mut editor);
/// ```
#[cfg(test)]
pub fn dummy(jobs: &'a mut Jobs, editor: &'a mut helix_view::Editor) -> Context<'a> {
Context {
jobs,
scroll: None,
editor,
}
}

#[cfg(test)]
pub fn dummy_jobs() -> Jobs {
Jobs::new()
}

#[cfg(test)]
pub fn dummy_editor() -> Editor {
use crate::config::Config;
use crate::handlers;
use arc_swap::{access::Map, ArcSwap};
use helix_core::syntax::{self, Configuration};
use helix_view::theme;
use std::{collections::HashMap, sync::Arc};

let config = Arc::new(ArcSwap::from_pointee(Config::default()));
let handlers = handlers::setup(config.clone());
Editor::new(
Rect::new(0, 0, 60, 120),
Arc::new(theme::Loader::new(&[])),
Arc::new(ArcSwap::from_pointee(syntax::Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
}).unwrap())),
Arc::new(Arc::new(Map::new(
Arc::clone(&config),
|config: &Config| &config.editor,
))),
handlers,
)
}
}


pub trait Component: Any + AnyComponent {
/// Process input events, return true if handled.
fn handle_event(&mut self, _event: &Event, _ctx: &mut Context) -> EventResult {
Expand Down Expand Up @@ -73,6 +121,21 @@ pub trait Component: Any + AnyComponent {
fn id(&self) -> Option<&'static str> {
None
}

#[cfg(test)]
/// Utility method for testing `handle_event` without using integration test.
/// Especially useful for testing helper components such as `Prompt`, `TreeView` etc
fn handle_events(&mut self, events: &str) -> anyhow::Result<()> {
use helix_view::input::parse_macro;

let mut editor = Context::dummy_editor();
let mut jobs = Context::dummy_jobs();
let mut cx = Context::dummy(&mut jobs, &mut editor);
for event in parse_macro(events)? {
self.handle_event(&Event::Key(event), &mut cx);
}
Ok(())
}
}

pub struct Compositor {
Expand Down
1 change: 1 addition & 0 deletions helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"C" => toggle_block_comments,
"A-c" => toggle_line_comments,
"?" => command_palette,
"e" => reveal_current_file,
},
"z" => { "View"
"z" | "c" => align_view_center,
Expand Down
68 changes: 60 additions & 8 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
keymap::{KeymapResult, Keymaps},
ui::{
document::{render_document, LinePos, TextRenderer, TranslatedPosition},
Completion, ProgressSpinners,
Completion, Explorer, ProgressSpinners,
},
};

Expand All @@ -23,7 +23,7 @@ use helix_core::{
};
use helix_view::{
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig},
editor::{CompleteAction, CursorShapeConfig, ExplorerPosition},
graphics::{Color, CursorKind, Modifier, Rect, Style},
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
keyboard::{KeyCode, KeyModifiers},
Expand All @@ -42,6 +42,7 @@ pub struct EditorView {
pseudo_pending: Vec<KeyEvent>,
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>,
pub(crate) explorer: Option<Explorer>,
spinners: ProgressSpinners,
/// Tracks if the terminal window is focused by reaction to terminal focus events
terminal_focused: bool,
Expand Down Expand Up @@ -72,6 +73,7 @@ impl EditorView {
pseudo_pending: Vec::new(),
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
explorer: None,
spinners: ProgressSpinners::default(),
terminal_focused: true,
}
Expand Down Expand Up @@ -1288,6 +1290,11 @@ impl Component for EditorView {
event: &Event,
context: &mut crate::compositor::Context,
) -> EventResult {
if let Some(explore) = self.explorer.as_mut() {
if let EventResult::Consumed(callback) = explore.handle_event(event, context) {
return EventResult::Consumed(callback);
}
}
let mut cx = commands::Context {
editor: context.editor,
count: None,
Expand Down Expand Up @@ -1445,6 +1452,8 @@ impl Component for EditorView {
surface.set_style(area, cx.editor.theme.get("ui.background"));
let config = cx.editor.config();

let editor_area = area.clip_bottom(1);

// check if bufferline should be rendered
use helix_view::editor::BufferLine;
let use_bufferline = match config.bufferline {
Expand All @@ -1453,17 +1462,46 @@ impl Component for EditorView {
_ => false,
};

// -1 for commandline and -1 for bufferline
let mut editor_area = area.clip_bottom(1);
if use_bufferline {
editor_area = editor_area.clip_top(1);
}
let editor_area = if use_bufferline {
editor_area.clip_top(1)
} else {
editor_area
};

let bufferline_area = area.with_height(1);
let (editor_area, bufferline_area) = if let Some(explorer) = &self.explorer { let explorer_column_width = if explorer.is_opened() {
explorer.column_width().saturating_add(2)
} else {
0
};
// For future developer:
// We should have a Dock trait that allows a component to dock to the top/left/bottom/right
// of another component.
match config.explorer.position {
ExplorerPosition::Left => (
editor_area.clip_left(explorer_column_width),
bufferline_area.clip_left(explorer_column_width),
),
ExplorerPosition::Right => (
editor_area.clip_right(explorer_column_width),
bufferline_area.clip_left(explorer_column_width),
),
}
} else {
(editor_area, bufferline_area)
};

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

if let Some(explorer) = self.explorer.as_mut() {
if !explorer.is_focus() {
explorer.render(area, surface, cx);
}
}

if use_bufferline {
Self::render_bufferline(cx.editor, area.with_height(1), surface);
Self::render_bufferline(cx.editor, bufferline_area, surface);
}

for (view, is_focused) in cx.editor.tree.views() {
Expand Down Expand Up @@ -1540,9 +1578,23 @@ impl Component for EditorView {
if let Some(completion) = self.completion.as_mut() {
completion.render(area, surface, cx);
}

if let Some(explore) = self.explorer.as_mut() {
if explore.is_focus() {
explore.render(area, surface, cx);
}
}
}

fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
if let Some(explore) = &self.explorer {
if explore.is_focus() {
let cursor = explore.cursor(_area, editor);
if cursor.0.is_some() {
return cursor;
}
}
}
match editor.cursor() {
// all block cursors are drawn manually
(pos, CursorKind::Block) => {
Expand Down
Loading

0 comments on commit 0657679

Please sign in to comment.