mirror of https://github.com/helix-editor/helix
Compare commits
29 Commits
9fb8895e73
...
d041348312
Author | SHA1 | Date |
---|---|---|
|
d041348312 | |
|
4281228da3 | |
|
c24ee4fe2d | |
|
89da519906 | |
|
99dee0b5ac | |
|
211a8d9e96 | |
|
1b8228a2e8 | |
|
59b3fbbb2c | |
|
fdcbc4e594 | |
|
1288b72b32 | |
|
0b4b8f39bc | |
|
b1c2ca5021 | |
|
1c3efb9160 | |
|
13df887cca | |
|
7c7bd20159 | |
|
761a62df86 | |
|
3c0fcb0679 | |
|
b1c7bd91b9 | |
|
a51334f00b | |
|
55ef555f87 | |
|
603d95f5d3 | |
|
593504bb27 | |
|
3471d82f87 | |
|
1e2f1363bc | |
|
b68aa35bf4 | |
|
871f12b751 | |
|
7e9bf8a3bb | |
|
810ac6023e | |
|
5640161e38 |
|
@ -63,6 +63,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"
|
| `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. |
|
| `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` |
|
| `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
|
### `[editor.clipboard-provider]` Section
|
||||||
|
|
||||||
|
|
|
@ -215,11 +215,11 @@ impl Application {
|
||||||
editor.new_file(Action::VerticalSplit);
|
editor.new_file(Action::VerticalSplit);
|
||||||
}
|
}
|
||||||
} else if stdin().is_tty() || cfg!(feature = "integration") {
|
} else if stdin().is_tty() || cfg!(feature = "integration") {
|
||||||
editor.new_file(Action::VerticalSplit);
|
editor.new_file_welcome();
|
||||||
} else {
|
} else {
|
||||||
editor
|
editor
|
||||||
.new_file_from_stdin(Action::VerticalSplit)
|
.new_file_from_stdin(Action::VerticalSplit)
|
||||||
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
|
.unwrap_or_else(|_| editor.new_file_welcome());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|
|
@ -22,6 +22,7 @@ use helix_core::{
|
||||||
unicode::width::UnicodeWidthStr,
|
unicode::width::UnicodeWidthStr,
|
||||||
visual_offset_from_block, Change, Position, Range, Selection, Transaction,
|
visual_offset_from_block, Change, Position, Range, Selection, Transaction,
|
||||||
};
|
};
|
||||||
|
use helix_loader::VERSION_AND_GIT_HASH;
|
||||||
use helix_view::{
|
use helix_view::{
|
||||||
annotations::diagnostics::DiagnosticFilter,
|
annotations::diagnostics::DiagnosticFilter,
|
||||||
document::{Mode, SCRATCH_BUFFER_NAME},
|
document::{Mode, SCRATCH_BUFFER_NAME},
|
||||||
|
@ -31,9 +32,12 @@ use helix_view::{
|
||||||
keyboard::{KeyCode, KeyModifiers},
|
keyboard::{KeyCode, KeyModifiers},
|
||||||
Document, Editor, Theme, View,
|
Document, Editor, Theme, View,
|
||||||
};
|
};
|
||||||
use std::{mem::take, num::NonZeroUsize, ops, path::PathBuf, rc::Rc};
|
use std::{mem::take, num::NonZeroUsize, ops, path::PathBuf, rc::Rc, sync::LazyLock};
|
||||||
|
|
||||||
use tui::{buffer::Buffer as Surface, text::Span};
|
use tui::{
|
||||||
|
buffer::Buffer as Surface,
|
||||||
|
text::{Span, Spans},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct EditorView {
|
pub struct EditorView {
|
||||||
pub keymaps: Keymaps,
|
pub keymaps: Keymaps,
|
||||||
|
@ -74,6 +78,261 @@ impl EditorView {
|
||||||
&mut self.spinners
|
&mut self.spinners
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_welcome(theme: &Theme, view: &View, surface: &mut Surface, is_colorful: bool) {
|
||||||
|
/// Logo for Helix
|
||||||
|
const LOGO_STR: &str = "\
|
||||||
|
**
|
||||||
|
***** ::
|
||||||
|
******** :::::
|
||||||
|
**:::::::
|
||||||
|
::::::::***=
|
||||||
|
::::::: ====
|
||||||
|
:::: =======
|
||||||
|
:---========
|
||||||
|
=======--
|
||||||
|
===== --------
|
||||||
|
== -----
|
||||||
|
--";
|
||||||
|
|
||||||
|
/// Size of the maximum line of the logo
|
||||||
|
static LOGO_WIDTH: LazyLock<u16> = LazyLock::new(|| {
|
||||||
|
LOGO_STR
|
||||||
|
.lines()
|
||||||
|
.max_by(|line, other| line.len().cmp(&other.len()))
|
||||||
|
.unwrap_or("")
|
||||||
|
.len() as u16
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Use when true color is not supported
|
||||||
|
static LOGO_NO_COLOR: LazyLock<Vec<Spans>> = LazyLock::new(|| {
|
||||||
|
LOGO_STR
|
||||||
|
.lines()
|
||||||
|
.map(|line| Spans(vec![Span::raw(line)]))
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The logo is colored using Helix's colors
|
||||||
|
static LOGO_WITH_COLOR: LazyLock<Vec<Spans>> = LazyLock::new(|| {
|
||||||
|
LOGO_STR
|
||||||
|
.lines()
|
||||||
|
.map(|line| {
|
||||||
|
line.chars()
|
||||||
|
.map(|ch| match ch {
|
||||||
|
'*' | ':' | '=' | '-' => Span::styled(
|
||||||
|
ch.to_string(),
|
||||||
|
Style::new().fg(match ch {
|
||||||
|
// Dark purple
|
||||||
|
'*' => Color::Rgb(112, 107, 200),
|
||||||
|
// Dark blue
|
||||||
|
':' => Color::Rgb(132, 221, 234),
|
||||||
|
// Bright purple
|
||||||
|
'=' => Color::Rgb(153, 123, 200),
|
||||||
|
// Bright blue
|
||||||
|
'-' => Color::Rgb(85, 197, 228),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
' ' => Span::raw(" "),
|
||||||
|
_ => unreachable!("logo should only contain '*', ':', '=', '-' or ' '"),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
/// How much space to put between the help text and the logo
|
||||||
|
const LOGO_LEFT_PADDING: u16 = 6;
|
||||||
|
|
||||||
|
// Shift the help text to the right by this amount, to add space
|
||||||
|
// for the logo
|
||||||
|
static HELP_X_LOGO_OFFSET: LazyLock<u16> =
|
||||||
|
LazyLock::new(|| *LOGO_WIDTH / 2 + LOGO_LEFT_PADDING / 2);
|
||||||
|
|
||||||
|
#[derive(PartialEq, PartialOrd, Eq, Ord)]
|
||||||
|
enum AlignLine {
|
||||||
|
Left,
|
||||||
|
Center,
|
||||||
|
}
|
||||||
|
use AlignLine::*;
|
||||||
|
|
||||||
|
let logo = if is_colorful {
|
||||||
|
&LOGO_WITH_COLOR
|
||||||
|
} else {
|
||||||
|
&LOGO_NO_COLOR
|
||||||
|
};
|
||||||
|
|
||||||
|
let empty_line = || (Spans::from(""), Left);
|
||||||
|
|
||||||
|
let raw_help_lines: [(Spans, AlignLine); 12] = [
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
Span::raw("helix "),
|
||||||
|
Span::styled(VERSION_AND_GIT_HASH, theme.get("comment")),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
Center,
|
||||||
|
),
|
||||||
|
empty_line(),
|
||||||
|
(
|
||||||
|
Span::styled(
|
||||||
|
"A post-modern modal text editor",
|
||||||
|
theme.get("ui.text").add_modifier(Modifier::ITALIC),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
Center,
|
||||||
|
),
|
||||||
|
empty_line(),
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
Span::styled(":tutor", theme.get("markup.raw")),
|
||||||
|
Span::styled("<enter>", theme.get("comment")),
|
||||||
|
Span::raw(" learn helix"),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
Left,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
Span::styled(":theme", theme.get("markup.raw")),
|
||||||
|
Span::styled("<space><tab>", theme.get("comment")),
|
||||||
|
Span::raw(" choose a theme"),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
Left,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
Span::styled("<space>e", theme.get("markup.raw")),
|
||||||
|
Span::raw(" file explorer"),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
Left,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
Span::styled("<space>?", theme.get("markup.raw")),
|
||||||
|
Span::raw(" see all commands"),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
Left,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
Span::styled(":quit", theme.get("markup.raw")),
|
||||||
|
Span::styled("<enter>", theme.get("comment")),
|
||||||
|
Span::raw(" quit helix"),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
Left,
|
||||||
|
),
|
||||||
|
empty_line(),
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
Span::styled("docs: ", theme.get("ui.text")),
|
||||||
|
Span::styled("docs.helix-editor.com", theme.get("markup.link.url")),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
Center,
|
||||||
|
),
|
||||||
|
empty_line(),
|
||||||
|
];
|
||||||
|
|
||||||
|
debug_assert!(
|
||||||
|
raw_help_lines.len() >= LOGO_STR.lines().count(),
|
||||||
|
"help lines get chained with lines of logo. if there are not \
|
||||||
|
enough help lines, logo will be cut off. add `empty_line()`s if necessary"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut help_lines = Vec::with_capacity(raw_help_lines.len());
|
||||||
|
let mut len_of_longest_left_align = 0;
|
||||||
|
let mut len_of_longest_center_align = 0;
|
||||||
|
|
||||||
|
for (spans, align) in raw_help_lines {
|
||||||
|
let width = spans.width();
|
||||||
|
match align {
|
||||||
|
Left => len_of_longest_left_align = len_of_longest_left_align.max(width),
|
||||||
|
Center => len_of_longest_center_align = len_of_longest_center_align.max(width),
|
||||||
|
}
|
||||||
|
help_lines.push((spans, align));
|
||||||
|
}
|
||||||
|
|
||||||
|
let len_of_longest_left_align = len_of_longest_left_align as u16;
|
||||||
|
|
||||||
|
// the y-coordinate where we start drawing the welcome screen
|
||||||
|
let start_drawing_at_y =
|
||||||
|
view.area.y + (view.area.height / 2).saturating_sub(help_lines.len() as u16 / 2);
|
||||||
|
|
||||||
|
// x-coordinate of the center of the viewport
|
||||||
|
let x_view_center = view.area.x + view.area.width / 2;
|
||||||
|
|
||||||
|
// the x-coordinate where we start drawing the `AlignLine::Left` lines
|
||||||
|
// +2 to make the text look like more balanced relative to the center of the help
|
||||||
|
let start_drawing_left_align_at_x =
|
||||||
|
view.area.x + (view.area.width / 2).saturating_sub(len_of_longest_left_align / 2) + 2;
|
||||||
|
|
||||||
|
let are_any_left_aligned_lines_overflowing_x =
|
||||||
|
(start_drawing_left_align_at_x + len_of_longest_left_align) > view.area.width;
|
||||||
|
|
||||||
|
let are_any_center_aligned_lines_overflowing_x =
|
||||||
|
len_of_longest_center_align as u16 > view.area.width;
|
||||||
|
|
||||||
|
let is_help_x_overflowing =
|
||||||
|
are_any_left_aligned_lines_overflowing_x || are_any_center_aligned_lines_overflowing_x;
|
||||||
|
|
||||||
|
// we want `>=` so it does not get drawn over the status line
|
||||||
|
// (essentially, it WON'T be marked as "overflowing" if the help
|
||||||
|
// fully fits vertically in the viewport without touching the status line)
|
||||||
|
let is_help_y_overflowing = (help_lines.len() as u16) >= view.area.height;
|
||||||
|
|
||||||
|
// Not enough space to render the help text even without the logo. Render nothing.
|
||||||
|
if is_help_x_overflowing || is_help_y_overflowing {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point we know that there is enough vertical
|
||||||
|
// and horizontal space to render the help text
|
||||||
|
|
||||||
|
let width_of_help_with_logo = *LOGO_WIDTH + LOGO_LEFT_PADDING + len_of_longest_left_align;
|
||||||
|
|
||||||
|
// If there is not enough space to show LOGO + HELP, then don't show the logo at all
|
||||||
|
//
|
||||||
|
// If we get here we know that there IS enough space to show just the help
|
||||||
|
let show_logo = width_of_help_with_logo <= view.area.width;
|
||||||
|
|
||||||
|
// Each "help" line is effectively "chained" with a line of the logo (if present).
|
||||||
|
for (lines_drawn, (line, align)) in help_lines.iter().enumerate() {
|
||||||
|
// Where to start drawing `AlignLine::Left` rows
|
||||||
|
let x_start_left_help =
|
||||||
|
start_drawing_left_align_at_x + if show_logo { *HELP_X_LOGO_OFFSET } else { 0 };
|
||||||
|
|
||||||
|
// Where to start drawing `AlignLine::Center` rows
|
||||||
|
let x_start_center_help = x_view_center - line.width() as u16 / 2
|
||||||
|
+ if show_logo { *HELP_X_LOGO_OFFSET } else { 0 };
|
||||||
|
|
||||||
|
// Where to start drawing rows for the "help" section
|
||||||
|
// Includes tips about commands. Excludes the logo.
|
||||||
|
let x_start_help = match align {
|
||||||
|
Left => x_start_left_help,
|
||||||
|
Center => x_start_center_help,
|
||||||
|
};
|
||||||
|
|
||||||
|
let y = start_drawing_at_y + lines_drawn as u16;
|
||||||
|
|
||||||
|
// Draw a single line of the help text
|
||||||
|
surface.set_spans(x_start_help, y, line, line.width() as u16);
|
||||||
|
|
||||||
|
if show_logo {
|
||||||
|
// Draw a single line of the logo
|
||||||
|
surface.set_spans(
|
||||||
|
x_start_left_help - LOGO_LEFT_PADDING - *LOGO_WIDTH,
|
||||||
|
y,
|
||||||
|
&logo[lines_drawn],
|
||||||
|
*LOGO_WIDTH,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_view(
|
pub fn render_view(
|
||||||
&self,
|
&self,
|
||||||
editor: &Editor,
|
editor: &Editor,
|
||||||
|
@ -161,6 +420,15 @@ impl EditorView {
|
||||||
|
|
||||||
Self::render_rulers(editor, doc, view, inner, surface, theme);
|
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,
|
||||||
|
config.true_color || crate::true_color(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let primary_cursor = doc
|
let primary_cursor = doc
|
||||||
.selection(view.id)
|
.selection(view.id)
|
||||||
.primary()
|
.primary()
|
||||||
|
|
|
@ -249,6 +249,12 @@ impl<'a> From<Cow<'a, str>> for Spans<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> FromIterator<Span<'a>> for Spans<'a> {
|
||||||
|
fn from_iter<T: IntoIterator<Item = Span<'a>>>(iter: T) -> Self {
|
||||||
|
Spans(iter.into_iter().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
|
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
|
||||||
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
|
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
|
||||||
Spans(spans)
|
Spans(spans)
|
||||||
|
|
|
@ -210,6 +210,8 @@ pub struct Document {
|
||||||
// large refactor that would make `&mut Editor` available on the `DocumentDidChange` event.
|
// large refactor that would make `&mut Editor` available on the `DocumentDidChange` event.
|
||||||
pub color_swatch_controller: TaskController,
|
pub color_swatch_controller: TaskController,
|
||||||
|
|
||||||
|
/// Whether to render the welcome screen when opening the document
|
||||||
|
pub is_welcome: bool,
|
||||||
// NOTE: this field should eventually go away - we should use the Editor's syn_loader instead
|
// NOTE: this field should eventually go away - we should use the Editor's syn_loader instead
|
||||||
// of storing a copy on every doc. Then we can remove the surrounding `Arc` and use the
|
// of storing a copy on every doc. Then we can remove the surrounding `Arc` and use the
|
||||||
// `ArcSwap` directly.
|
// `ArcSwap` directly.
|
||||||
|
@ -727,6 +729,7 @@ impl Document {
|
||||||
jump_labels: HashMap::new(),
|
jump_labels: HashMap::new(),
|
||||||
color_swatches: None,
|
color_swatches: None,
|
||||||
color_swatch_controller: TaskController::new(),
|
color_swatch_controller: TaskController::new(),
|
||||||
|
is_welcome: false,
|
||||||
syn_loader,
|
syn_loader,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -740,6 +743,11 @@ impl Document {
|
||||||
Self::from(text, None, config, syn_loader)
|
Self::from(text, None, config, syn_loader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_welcome(mut self) -> Self {
|
||||||
|
self.is_welcome = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: async fn?
|
// TODO: async fn?
|
||||||
/// Create a new document from `path`. Encoding is auto-detected, but it can be manually
|
/// Create a new document from `path`. Encoding is auto-detected, but it can be manually
|
||||||
/// overwritten with the `encoding` parameter.
|
/// overwritten with the `encoding` parameter.
|
||||||
|
|
|
@ -249,6 +249,8 @@ where
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
||||||
pub struct Config {
|
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.
|
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
|
||||||
pub scrolloff: usize,
|
pub scrolloff: usize,
|
||||||
/// Number of lines to scroll at once. Defaults to 3
|
/// Number of lines to scroll at once. Defaults to 3
|
||||||
|
@ -996,6 +998,7 @@ impl Default for WordCompletion {
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
welcome_screen: true,
|
||||||
scrolloff: 5,
|
scrolloff: 5,
|
||||||
scroll_lines: 3,
|
scroll_lines: 3,
|
||||||
mouse: true,
|
mouse: true,
|
||||||
|
@ -1784,6 +1787,14 @@ impl Editor {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Use when Helix is opened with no arguments passed
|
||||||
|
pub fn new_file_welcome(&mut self) -> DocumentId {
|
||||||
|
self.new_file_from_document(
|
||||||
|
Action::VerticalSplit,
|
||||||
|
Document::default(self.config.clone(), self.syn_loader.clone()).with_welcome(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
|
pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
|
||||||
let (stdin, encoding, has_bom) = crate::document::read_to_string(&mut stdin(), None)?;
|
let (stdin, encoding, has_bom) = crate::document::read_to_string(&mut stdin(), None)?;
|
||||||
let doc = Document::from(
|
let doc = Document::from(
|
||||||
|
|
Loading…
Reference in New Issue