Merge pull request 'feature/file-explorer' (#9) from feature/file-explorer into main

Reviewed-on: #9
pull/11/head
Trivernis 1 year ago
commit bfb2ce8a7a

@ -185,6 +185,8 @@ auto-pairs = false # defaults to `true`
The default pairs are <code>(){}[]''""``</code>, but these can be customized by
setting `auto-pairs` to a TOML table:
Example
```toml
[editor.auto-pairs]
'(' = ')'
@ -343,3 +345,11 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping
max-indent-retain = 0
wrap-indicator = "" # set wrap-indicator to "" to hide it
```
### `[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` |

@ -291,6 +291,8 @@ 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.
@ -442,3 +444,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.

@ -1 +1,4 @@
/target
# This folder is used by `test_explorer` to create temporary folders needed for testing
test_explorer

@ -472,6 +472,8 @@ impl MappableCommand {
record_macro, "Record macro",
replay_macro, "Replay macro",
command_palette, "Open command palette",
open_or_focus_explorer, "Open or focus explorer",
reveal_current_file, "Reveal current file in explorer",
);
}
@ -2446,6 +2448,49 @@ fn file_picker_in_current_directory(cx: &mut Context) {
cx.push_layer(Box::new(overlayed(picker)));
}
fn open_or_focus_explorer(cx: &mut Context) {
cx.callback = Some(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(cx: &mut Context, path: Option<PathBuf>) {
cx.callback = Some(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(cx, None)
}
fn buffer_picker(cx: &mut Context) {
let current = view!(cx.editor).doc;

@ -34,6 +34,47 @@ 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 arc_swap::{access::Map, ArcSwap};
use helix_core::syntax::{self, Configuration};
use helix_view::theme;
use std::sync::Arc;
let config = Arc::new(ArcSwap::from_pointee(Config::default()));
Editor::new(
Rect::new(0, 0, 60, 120),
Arc::new(theme::Loader::new(&[])),
Arc::new(syntax::Loader::new(Configuration { language: vec![] })),
Arc::new(Arc::new(Map::new(
Arc::clone(&config),
|config: &Config| &config.editor,
))),
)
}
}
pub trait Component: Any + AnyComponent {
@ -72,6 +113,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 {

@ -274,6 +274,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"r" => rename_symbol,
"h" => select_references_to_symbol_under_cursor,
"?" => command_palette,
"e" => reveal_current_file,
},
"z" => { "View"
"z" | "c" => align_view_center,

@ -6,7 +6,7 @@ use crate::{
keymap::{KeymapResult, Keymaps},
ui::{
document::{render_document, LinePos, TextRenderer, TranslatedPosition},
Completion, ProgressSpinners,
Completion, Explorer, ProgressSpinners,
},
};
@ -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},
@ -43,6 +43,7 @@ pub struct EditorView {
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
pub(crate) explorer: Option<Explorer>,
}
#[derive(Debug, Clone)]
@ -68,6 +69,7 @@ impl EditorView {
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
explorer: None,
}
}
@ -1205,6 +1207,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,
@ -1361,6 +1368,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 {
@ -1369,15 +1378,43 @@ 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 editor_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),
ExplorerPosition::Right => editor_area.clip_right(explorer_column_width),
}
} else {
editor_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() {
let area = if use_bufferline {
area.clip_top(1)
} else {
area
};
explorer.render(area, surface, cx);
}
}
if use_bufferline {
Self::render_bufferline(cx.editor, area.with_height(1), surface);
}
@ -1456,9 +1493,28 @@ 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() {
let area = if use_bufferline {
area.clip_top(1)
} else {
area
};
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) => (pos, CursorKind::Hidden),

File diff suppressed because it is too large Load Diff

@ -1,6 +1,7 @@
mod completion;
mod document;
pub(crate) mod editor;
mod explorer;
mod fuzzy_match;
mod info;
pub mod lsp;
@ -13,12 +14,14 @@ mod prompt;
mod spinner;
mod statusline;
mod text;
mod tree;
use crate::compositor::{Component, Compositor};
use crate::filter_picker_entry;
use crate::job::{self, Callback};
pub use completion::Completion;
pub use editor::EditorView;
pub use explorer::Explorer;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker};
@ -26,6 +29,7 @@ pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text;
pub use tree::{TreeOp, TreeView, TreeViewItem};
use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder;

@ -19,26 +19,7 @@ pub struct Overlay<T> {
pub fn overlayed<T>(content: T) -> Overlay<T> {
Overlay {
content,
calc_child_size: Box::new(|rect: Rect| clip_rect_relative(rect.clip_bottom(2), 90, 90)),
}
}
fn clip_rect_relative(rect: Rect, percent_horizontal: u8, percent_vertical: u8) -> Rect {
fn mul_and_cast(size: u16, factor: u8) -> u16 {
((size as u32) * (factor as u32) / 100).try_into().unwrap()
}
let inner_w = mul_and_cast(rect.width, percent_horizontal);
let inner_h = mul_and_cast(rect.height, percent_vertical);
let offset_x = rect.width.saturating_sub(inner_w) / 2;
let offset_y = rect.height.saturating_sub(inner_h) / 2;
Rect {
x: rect.x + offset_x,
y: rect.y + offset_y,
width: inner_w,
height: inner_h,
calc_child_size: Box::new(|rect: Rect| rect.overlayed()),
}
}

@ -94,6 +94,10 @@ impl Prompt {
self
}
pub fn prompt(&self) -> &str {
self.prompt.as_ref()
}
pub fn line(&self) -> &String {
&self.line
}

File diff suppressed because it is too large Load Diff

@ -211,6 +211,30 @@ impl Default for FilePickerConfig {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct ExplorerConfig {
pub position: ExplorerPosition,
/// explorer column width
pub column_width: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExplorerPosition {
Left,
Right,
}
impl Default for ExplorerConfig {
fn default() -> Self {
Self {
position: ExplorerPosition::Left,
column_width: 36,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config {
@ -281,6 +305,8 @@ pub struct Config {
pub indent_guides: IndentGuidesConfig,
/// Whether to color modes with different colors. Defaults to `false`.
pub color_modes: bool,
/// explore config
pub explorer: ExplorerConfig,
pub soft_wrap: SoftWrap,
/// Workspace specific lsp ceiling dirs
pub workspace_lsp_roots: Vec<PathBuf>,
@ -749,6 +775,7 @@ impl Default for Config {
bufferline: BufferLine::default(),
indent_guides: IndentGuidesConfig::default(),
color_modes: false,
explorer: ExplorerConfig::default(),
soft_wrap: SoftWrap::default(),
text_width: 80,
completion_replace: false,
@ -923,6 +950,18 @@ pub enum CloseError {
SaveError(anyhow::Error),
}
impl From<CloseError> for anyhow::Error {
fn from(error: CloseError) -> Self {
match error {
CloseError::DoesNotExist => anyhow::anyhow!("Document doesn't exist"),
CloseError::BufferModified(error) => {
anyhow::anyhow!(format!("Buffer modified: '{error}'"))
}
CloseError::SaveError(error) => anyhow::anyhow!(format!("Save error: {error}")),
}
}
}
impl Editor {
pub fn new(
mut area: Rect,

@ -248,6 +248,34 @@ impl Rect {
&& self.y < other.y + other.height
&& self.y + self.height > other.y
}
/// Returns a smaller `Rect` with a margin of 5% on each side, and an additional 2 rows at the bottom
pub fn overlayed(self) -> Rect {
self.clip_bottom(2).clip_relative(90, 90)
}
/// Returns a smaller `Rect` with width and height clipped to the given `percent_horizontal`
/// and `percent_vertical`.
///
/// Value of `percent_horizontal` and `percent_vertical` is from 0 to 100.
pub fn clip_relative(self, percent_horizontal: u8, percent_vertical: u8) -> Rect {
fn mul_and_cast(size: u16, factor: u8) -> u16 {
((size as u32) * (factor as u32) / 100).try_into().unwrap()
}
let inner_w = mul_and_cast(self.width, percent_horizontal);
let inner_h = mul_and_cast(self.height, percent_vertical);
let offset_x = self.width.saturating_sub(inner_w) / 2;
let offset_y = self.height.saturating_sub(inner_h) / 2;
Rect {
x: self.x + offset_x,
y: self.y + offset_y,
width: inner_w,
height: inner_h,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

Loading…
Cancel
Save