diff --git a/book/src/editor.md b/book/src/editor.md index 1e5c2a507..f521c393d 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -61,6 +61,7 @@ | `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | "disable" | `clipboard-provider` | Which API to use for clipboard interaction. One of `pasteboard` (MacOS), `wayland`, `x-clip`, `x-sel`, `win-32-yank`, `termux`, `tmux`, `windows`, `termcode`, `none`, or a custom command set. | Platform and environment specific. | | `editor-config` | Whether to read settings from [EditorConfig](https://editorconfig.org) files | `true` | +| `welcome-screen` | Whether to enable the welcome screen | `true` | ### `[editor.clipboard-provider]` Section diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 16a26cb26..c8160ab62 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -215,11 +215,11 @@ impl Application { editor.new_file(Action::VerticalSplit); } } else if stdin().is_tty() || cfg!(feature = "integration") { - editor.new_file(Action::VerticalSplit); + editor.new_file_welcome(Action::VerticalSplit); } else { editor .new_file_from_stdin(Action::VerticalSplit) - .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit)); + .unwrap_or_else(|_| editor.new_file_welcome(Action::VerticalSplit)); } #[cfg(windows)] diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 6be565747..55d289e91 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -22,6 +22,7 @@ use helix_core::{ unicode::width::UnicodeWidthStr, visual_offset_from_block, Change, Position, Range, Selection, Transaction, }; +use helix_loader::VERSION_AND_GIT_HASH; use helix_view::{ annotations::diagnostics::DiagnosticFilter, document::{Mode, SCRATCH_BUFFER_NAME}, @@ -33,7 +34,10 @@ use helix_view::{ }; use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc}; -use tui::{buffer::Buffer as Surface, text::Span}; +use tui::{ + buffer::Buffer as Surface, + text::{Span, Spans}, +}; pub struct EditorView { pub keymaps: Keymaps, @@ -74,6 +78,107 @@ impl EditorView { &mut self.spinners } + pub fn render_welcome(theme: &Theme, view: &View, surface: &mut Surface) { + #[derive(PartialEq, PartialOrd, Eq, Ord)] + enum Align { + Left, + Center, + } + + macro_rules! welcome { + ( + $([$align:ident] $line:expr, $(if $cond:expr;)?)* $(,)? + ) => {{ + let mut lines = vec![]; + let mut longest_left = 0; + let mut longest_center = 0; + $( + let line = Spans::from($line); + let width = line.width(); + lines.push((line, Align::$align)); + match Align::$align { + Align::Left => longest_left = longest_left.max(width), + Align::Center => longest_center = longest_center.max(width), + } + )* + (lines, longest_left, longest_center) + }}; + } + + let (lines, longest_left, longest_center) = welcome! { + [Center] vec!["helix ".into(), Span::styled(VERSION_AND_GIT_HASH, theme.get("comment"))], + [Left] "", + [Center] Span::styled( + "A post-modern modal text editor", + theme.get("ui.text").add_modifier(Modifier::ITALIC), + ), + [Left] "", + [Left] vec![ + Span::styled(":tutor", theme.get("markup.raw")), + Span::styled("", theme.get("comment")), + " learn helix".into(), + ], + [Left] vec![ + Span::styled(":theme", theme.get("markup.raw")), + Span::styled("", theme.get("comment")), + " choose a theme".into(), + ], + [Left] vec![ + Span::styled("e", theme.get("markup.raw")), + " file explorer".into(), + ], + [Left] vec![ + Span::styled("?", theme.get("markup.raw")), + " see all commands".into(), + ], + [Left] vec![ + Span::styled(":config-open", theme.get("markup.raw")), + Span::styled("", theme.get("comment")), + " configure helix".into(), + ], + [Left] vec![ + Span::styled(":quit", theme.get("markup.raw")), + Span::styled("", theme.get("comment")), + " quit helix".into(), + ], + [Left] "", + [Center] vec![ + Span::styled("docs: ", theme.get("ui.text")), + Span::styled("docs.helix-editor.com", theme.get("markup.link.url")), + ], + }; + + // how many total lines there are in the welcome screen + let lines_count = lines.len(); + + // the y-coordinate where we start drawing the welcome screen + let y_start = view.area.y + (view.area.height / 2).saturating_sub(lines_count as u16 / 2); + let y_center = view.area.x + view.area.width / 2; + + let x_start_left = + view.area.x + (view.area.width / 2).saturating_sub(longest_left as u16 / 2) + 2; + + let has_x_left_overflow = (x_start_left + longest_left as u16) > view.area.width; + let has_x_center_overflow = longest_center as u16 > view.area.width; + let has_x_overflow = has_x_left_overflow || has_x_center_overflow; + + // we want lines_count < view.area.height so it does not get drawn + // over the status line + let has_y_overflow = lines_count as u16 >= view.area.height; + + if has_x_overflow || has_y_overflow { + return; + } + + for (lines_drawn, (line, align)) in lines.iter().enumerate() { + let x = match align { + Align::Left => x_start_left, + Align::Center => y_center - line.width() as u16 / 2, + }; + surface.set_spans(x, y_start + lines_drawn as u16, line, line.width() as u16); + } + } + pub fn render_view( &self, editor: &Editor, @@ -178,6 +283,10 @@ impl EditorView { Self::render_rulers(editor, doc, view, inner, surface, theme); + if config.welcome_screen && doc.version() == 0 && doc.is_welcome { + Self::render_welcome(theme, view, surface); + } + let primary_cursor = doc .selection(view.id) .primary() diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 41c9ee1ef..a9a78d4fa 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -207,6 +207,9 @@ pub struct Document { // NOTE: ideally this would live on the handler for color swatches. This is blocked on a // large refactor that would make `&mut Editor` available on the `DocumentDidChange` event. pub color_swatch_controller: TaskController, + + /// Whether to render the welcome screen when opening the document + pub is_welcome: bool, } #[derive(Debug, Clone, Default)] @@ -719,6 +722,7 @@ impl Document { jump_labels: HashMap::new(), color_swatches: None, color_swatch_controller: TaskController::new(), + is_welcome: false, } } @@ -728,6 +732,11 @@ impl Document { Self::from(text, None, config) } + pub fn with_welcome(mut self) -> Self { + self.is_welcome = true; + self + } + // TODO: async fn? /// Create a new document from `path`. Encoding is auto-detected, but it can be manually /// overwritten with the `encoding` parameter. diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index be2218997..2a8833cc6 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -247,6 +247,8 @@ where #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct Config { + /// Whether to enable the welcome screen + pub welcome_screen: bool, /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5. pub scrolloff: usize, /// Number of lines to scroll at once. Defaults to 3 @@ -960,6 +962,7 @@ pub enum PopupBorderConfig { impl Default for Config { fn default() -> Self { Self { + welcome_screen: true, scrolloff: 5, scroll_lines: 3, mouse: true, @@ -1736,6 +1739,14 @@ impl Editor { self.new_file_from_document(action, Document::default(self.config.clone())) } + /// Use when Helix is opened with no arguments passed + pub fn new_file_welcome(&mut self, action: Action) -> DocumentId { + self.new_file_from_document( + action, + Document::default(self.config.clone()).with_welcome(), + ) + } + pub fn new_file_from_stdin(&mut self, action: Action) -> Result { let (stdin, encoding, has_bom) = crate::document::read_to_string(&mut stdin(), None)?; let doc = Document::from(