diff --git a/book/src/editor.md b/book/src/editor.md index 88541a7bc..52b6ab51c 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -76,7 +76,7 @@ The `[editor.statusline]` key takes the following sub-keys: | Key | Description | Default | | --- | --- | --- | -| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` | +| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "zoom", "file-modification-indicator"]` | | `center` | A list of elements aligned to the middle of the statusline | `[]` | | `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` | | `separator` | The character used to separate elements in the statusline | `"│"` | @@ -109,6 +109,7 @@ The following statusline elements can be configured: | `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) | | `version-control` | The current branch name or detached commit hash of the opened workspace | | `register` | The current selected register | +| `zoom` | The current window zoom/zen state | ### `[editor.lsp]` Section diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index cf1b550dc..2de3687dc 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -88,3 +88,4 @@ | `:move` | Move the current buffer and its corresponding file to a different path | | `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default | | `:read`, `:r` | Load a file into buffer | +| `:set-max-width` | Set the maximum width of the editor. If set to 0 it will take up the entire width. | diff --git a/book/src/keymap.md b/book/src/keymap.md index 27c5a6b60..a7ce94e35 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -270,6 +270,7 @@ This layer is similar to Vim keybindings as Kakoune does not support windows. | `J` | Swap window downwards | `swap_view_down` | | `K` | Swap window upwards | `swap_view_up` | | `L` | Swap window to the right | `swap_view_right` | +| `z` | Toggle zoom for the focused view | `toggle_zoom` | #### Space mode diff --git a/book/src/remapping.md b/book/src/remapping.md index d762c6add..3a2e1b191 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -46,6 +46,10 @@ k = "scroll_down" m = ":run-shell-command make" c = ":run-shell-command cargo build" t = ":run-shell-command cargo test" + +# Creates a basic 'zen-mode' similar to VSCode's +z = ["toggle_zoom", ":set-max-width 120", ":set gutters.layout []"] +Z = ["toggle_zoom", ":set-max-width 0", ':set gutters.layout ["diagnostics","spacer","line-numbers","spacer","diff"]'] ``` ## Special keys and modifiers diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7be2ea095..68d8f6254 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -462,6 +462,7 @@ impl MappableCommand { vsplit_new, "Vertical right split scratch buffer", wclose, "Close window", wonly, "Close windows except current", + toggle_zoom, "Toggle zoom for current window", select_register, "Select register", insert_register, "Insert register", align_view_middle, "Align view middle", @@ -5187,6 +5188,11 @@ fn wonly(cx: &mut Context) { } } +fn toggle_zoom(cx: &mut Context) { + cx.editor.tree.zoom = !cx.editor.tree.zoom; + cx.editor.tree.recalculate(); +} + fn select_register(cx: &mut Context) { cx.editor.autoinfo = Some(Info::from_registers(&cx.editor.registers)); cx.on_next_key(move |cx, event| { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index f38ae6bba..2d6b80597 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2488,6 +2488,30 @@ fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> Ok(()) } +fn set_max_width( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let Some(width) = args.first() else { bail!(":set-max-width takes one argument") }; + let width = width.parse()?; + cx.editor.tree.max_width = width; + cx.editor.tree.recalculate(); + + if width == 0 { + cx.editor.set_status("Unset maximum width"); + } else { + cx.editor + .set_status(format!("Set maximum width to {}", width)); + } + + Ok(()) +} + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -3109,6 +3133,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: read, signature: CommandSignature::positional(&[completers::filename]), }, + TypableCommand { + name: "set-max-width", + aliases: &[], + doc: "Set the maximum width of the editor. If set to 0 it will take up the entire width.", + fun: set_max_width, + signature: CommandSignature::positional(&[completers::none]), + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> = diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 5a3e8eed4..e67ffb474 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -207,6 +207,7 @@ pub fn default() -> HashMap { "C-s" | "s" => hsplit_new, "C-v" | "v" => vsplit_new, }, + "z" => toggle_zoom, }, // move under c @@ -272,6 +273,7 @@ pub fn default() -> HashMap { "C-s" | "s" => hsplit_new, "C-v" | "v" => vsplit_new, }, + "z" => toggle_zoom, }, "y" => yank_to_clipboard, "Y" => yank_main_selection_to_clipboard, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 97f90f625..5c2ba1944 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -206,8 +206,8 @@ impl EditorView { ); Self::render_rulers(editor, doc, view, inner, surface, theme); - // if we're not at the edge of the screen, draw a right border - if viewport.right() != view.area.right() { + // if we're not at the edge of the screen or zoomed, draw a right border + if viewport.right() != view.area.right() && !editor.tree.zoom { let x = area.right(); let border_style = theme.get("ui.window"); for y in area.top()..area.bottom() { @@ -1489,8 +1489,10 @@ impl Component for EditorView { } for (view, is_focused) in cx.editor.tree.views() { - let doc = cx.editor.document(view.doc).unwrap(); - self.render_view(cx.editor, doc, view, area, surface, is_focused); + if !cx.editor.tree.zoom || is_focused { + let doc = cx.editor.document(view.doc).unwrap(); + self.render_view(cx.editor, doc, view, area, surface, is_focused); + } } if config.auto_info { diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 7437cbd07..76177a741 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -163,6 +163,7 @@ where helix_view::editor::StatusLineElement::Spacer => render_spacer, helix_view::editor::StatusLineElement::VersionControl => render_version_control, helix_view::editor::StatusLineElement::Register => render_register, + helix_view::editor::StatusLineElement::Zoom => render_zoom, } } @@ -531,3 +532,11 @@ where write(context, format!(" reg={} ", reg), None) } } + +fn render_zoom<'a>(context: &RenderContext) -> Spans<'a> { + if context.editor.tree.zoom { + "[zoom]".into() + } else { + Spans::default() + } +} diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 5540c5182..17795e872 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -469,6 +469,7 @@ impl Default for StatusLineConfig { E::Spinner, E::FileName, E::ReadOnlyIndicator, + E::Zoom, E::FileModificationIndicator, ], center: vec![], @@ -568,6 +569,9 @@ pub enum StatusLineElement { /// Indicator for selected register Register, + + /// Current zoom/zen state + Zoom, } // Cursor shape is read and used on every rendered frame and so needs diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index be8bd4e5b..bb0a25782 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -8,8 +8,13 @@ pub struct Tree { root: ViewId, // (container, index inside the container) pub focus: ViewId, - // fullscreen: bool, area: Rect, + // Maximum width the views will take up. If 0 then they will take up the + // entire width regardless of state. + pub max_width: u16, + // If true, the focused view gets all the available space and the rest are + // not rendered. + pub zoom: bool, nodes: HopSlotMap, @@ -96,8 +101,9 @@ impl Tree { Self { root, focus: root, - // fullscreen: false, area, + max_width: 0, + zoom: false, nodes, stack: Vec::new(), } @@ -360,7 +366,27 @@ impl Tree { return; } - self.stack.push((self.root, self.area)); + let area = if self.max_width > 0 { + let width = std::cmp::min(self.max_width, self.area.width); + Rect::new( + self.area.width / 2 - width / 2, + self.area.y, + width, + self.area.height, + ) + } else { + self.area + }; + + if self.zoom { + for (view, _focused) in self.views_mut() { + view.area = area; + } + + return; + } + + self.stack.push((self.root, area)); // take the area // fetch the node