Compare commits

...

2 Commits

@ -357,3 +357,11 @@ wrap-indicator = "" # set wrap-indicator to "" to hide it
|------------|-------------|---------|
| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` |
| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` |
### `[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` |

@ -296,6 +296,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 file explorer | `reveal_curren_file`
> 💡 Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file.
@ -452,3 +453,8 @@ 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.

@ -25,11 +25,11 @@ use tui::backend::Backend;
use crate::{
args::Args,
commands::apply_workspace_edit,
compositor::{Compositor, Event},
compositor::{self, Compositor, Event},
config::Config,
job::Jobs,
keymap::Keymaps,
ui::{self, overlay::overlaid},
ui::{self, overlay::overlaid, Explorer},
};
use log::{debug, error, warn};
@ -153,7 +153,21 @@ impl Application {
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.keys
}));
let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys)));
let mut editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys)));
let mut jobs = Jobs::new();
if args.show_explorer {
let mut context = compositor::Context {
editor: &mut editor,
scroll: None,
jobs: &mut jobs,
};
let mut explorer = Explorer::new(&mut context)?;
explorer.unfocus();
editor_view.explorer = Some(explorer);
}
compositor.push(editor_view);
if args.load_tutor {
@ -243,7 +257,7 @@ impl Application {
syn_loader,
signals,
jobs: Jobs::new(),
jobs,
lsp_progress: LspProgressMap::new(),
};

@ -17,6 +17,7 @@ pub struct Args {
pub log_file: Option<PathBuf>,
pub config_file: Option<PathBuf>,
pub files: Vec<(PathBuf, Position)>,
pub show_explorer: bool,
}
impl Args {
@ -32,6 +33,7 @@ impl Args {
"--version" => args.display_version = true,
"--help" => args.display_help = true,
"--tutor" => args.load_tutor = true,
"--show-explorer" => args.show_explorer = true,
"--vsplit" => match args.split {
Some(_) => anyhow::bail!("can only set a split once of a specific type"),
None => args.split = Some(Layout::Vertical),

@ -490,6 +490,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",
);
}
@ -5743,3 +5745,46 @@ fn replay_macro(cx: &mut Context) {
cx.editor.macro_replaying.pop();
}));
}
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)
}

@ -35,6 +35,50 @@ 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::{collections::HashMap, 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![],
language_server: HashMap::new(),
})),
Arc::new(Arc::new(Map::new(
Arc::clone(&config),
|config: &Config| &config.editor,
))),
)
}
}
pub trait Component: Any + AnyComponent {
@ -73,6 +117,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 {

@ -224,6 +224,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"D" => workspace_diagnostics_picker,
"a" => code_action,
"'" => last_picker,
"e" => reveal_current_file,
"g" => { "Debug (experimental)" sticky=true
"l" => dap_launch,
"r" => dap_restart,

@ -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},
@ -45,6 +45,7 @@ pub struct EditorView {
spinners: ProgressSpinners,
/// Tracks if the terminal window is focused by reaction to terminal focus events
terminal_focused: bool,
pub(crate) explorer: Option<Explorer>,
}
#[derive(Debug, Clone)]
@ -74,6 +75,7 @@ impl EditorView {
completion: None,
spinners: ProgressSpinners::default(),
terminal_focused: true,
explorer: None,
}
}
@ -1235,6 +1237,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,
@ -1414,10 +1421,37 @@ impl Component for EditorView {
if use_bufferline {
editor_area = editor_area.clip_top(1);
}
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);
}
@ -1496,9 +1530,48 @@ 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 needs_update = explore.is_focus() || {
if let Some(current_document_path) = doc!(cx.editor).path().cloned() {
if let Some(current_explore_path) = explore.current_file() {
if *current_explore_path != current_document_path {
let _ = explore.reveal_file(current_document_path);
true
} else {
false
}
} else {
let _ = explore.reveal_file(current_document_path);
true
}
} else {
false
}
};
if needs_update {
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 info;
pub mod lsp;
mod markdown;
@ -12,6 +13,7 @@ mod prompt;
mod spinner;
mod statusline;
mod text;
mod tree;
use crate::compositor::{Component, Compositor};
use crate::filter_picker_entry;
@ -26,6 +28,9 @@ pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text;
pub use explorer::Explorer;
pub use tree::{TreeOp, TreeView, TreeViewItem};
use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder;
use helix_view::Editor;

@ -112,6 +112,11 @@ impl Prompt {
self.completion = (self.completion_fn)(editor, &self.line);
}
#[cfg(test)]
pub fn prompt(&self) -> &Cow<str> {
&self.prompt
}
/// Compute the cursor position after applying movement
/// Taken from: <https://github.com/wez/wezterm/blob/e0b62d07ca9bf8ce69a61e30a3c20e7abc48ce7e/termwiz/src/lineedit/mod.rs#L516-L611>
fn eval_movement(&self, movement: Movement) -> usize {

File diff suppressed because it is too large Load Diff

@ -210,6 +210,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 {
@ -291,6 +315,8 @@ pub struct Config {
pub insert_final_newline: bool,
/// Enables smart tab
pub smart_tab: Option<SmartTabConfig>,
/// File Explorer config
pub explorer: ExplorerConfig,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
@ -846,6 +872,7 @@ impl Default for Config {
default_line_ending: LineEndingConfig::default(),
insert_final_newline: true,
smart_tab: Some(SmartTabConfig::default()),
explorer: ExplorerConfig::default(),
}
}
}
@ -1011,6 +1038,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 overlaid(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