Compare commits

...

37 Commits

Author SHA1 Message Date
Nik Revenco 863bd0d5bc
Merge c24ee4fe2d into fed3edcab7 2025-06-16 16:58:47 -05:00
Michael Davis fed3edcab7
queries: Fix highlighting of '#' in CSS color hex codes
This was previously highlighted as `punctuation` because the capture
applied to the whole `(color_value)` node rather than the `"#"` child
node specifically.
2025-06-16 13:43:41 -04:00
Michael Davis 4099465632
stdx: Add an iterator over grapheme indices in a rope slice 2025-06-16 13:12:54 -04:00
Michael Davis 9100bce9aa
stdx: Unify RopeSlice grapheme cluster iterators
This style for RopeGraphemes is identical to Ropey's Chars and Bytes
iterators. Being able to move the iterator types like cursors over the
bytes/chars/graphemes is useful in some cases. For example see
`helix_core::movement::<Chars as CharHelpers>::range_to_target`.

This change also adds `RopeSliceExt::graphemes_at` for flexibility.
`graphemes` and `graphemes_rev` are now implemented in terms of
`graphemes_at` and `RopeGraphemes::reversed`.
2025-06-16 13:12:13 -04:00
Michael Davis f5dc8245ea
stdx: Add `RopeSliceExt::(nth_){next,prev}_grapheme_boundary`
These functions mirror those in `helix_core::graphemes` but operate
directly on byte indices rather than character indices. These are meant
to be used as we transition to Ropey v2 and always use byte indices.
2025-06-16 13:10:30 -04:00
Michael Davis 362e97e927
Update tree-house to v0.3.0
This release contains some fixes to highlight ordering which could cause
panics in the markdown component for highlights arriving out of order.
2025-06-16 10:27:19 -04:00
Michael Davis ba54b6afe4
LSP: Short-circuit documentColors request for no servers
This fixes a deadlock when starting Helix with very many files, like
`hx runtime/queries/*/*.scm`. The tree-sitter query files don't have
an active language server on my machine and yet we were spawning a tokio
task to collect documentColors responses. We can skip that entirely.
Further debugging is needed to figure out why this lead to a deadlock
previously.
2025-06-16 09:42:48 -04:00
Tatesa Uradnik 837627dd8a
feat: allow moving nonexistent file (#13748) 2025-06-16 08:19:28 -05:00
CalebLarsen 1246549afd
Fix: update c++ highlights (#13772) 2025-06-16 08:04:22 -05:00
uncenter ada8004ea5
Highlight HTML entities (#13753) 2025-06-16 08:03:02 -05:00
Nik Revenco c24ee4fe2d chore: add debug assert 2025-05-21 21:47:36 +01:00
Nik Revenco 89da519906 chore: add comments 2025-05-21 21:29:51 +01:00
Nik Revenco 99dee0b5ac chore: add comments 2025-05-21 21:23:17 +01:00
Nik Revenco 211a8d9e96 refactor: rename variable 2025-05-21 21:21:48 +01:00
Nik Revenco 1b8228a2e8 refactor: Don't use a macro + rename variables 2025-05-21 21:18:48 +01:00
Nik Revenco 59b3fbbb2c feat: remove tip about `:config-open` 2025-05-21 20:05:25 +01:00
Nik Revenco fdcbc4e594 feat: add tip to use `tab` to select theme 2025-05-21 16:34:06 +01:00
Nik Revenco 1288b72b32 feat: render the logo of Helix in the welcome screen if there is enough space 2025-05-21 16:24:10 +01:00
Nik Revenco 0b4b8f39bc Merge branch 'master' into welcome-screen 2025-05-21 14:35:23 +01:00
Nik Revenco b1c2ca5021 fix: replace `<tab>` with `<space>` 2025-05-09 17:29:23 +01:00
Nik Revenco 1c3efb9160 fix: remove `<tab>` hint 2025-05-09 17:28:52 +01:00
Nik Revenco 13df887cca refactor: cast to u16 in a single place 2025-04-14 17:01:01 +01:00
Nik Revenco 7c7bd20159 chore: incorrect comment fix 2025-04-02 16:10:10 +01:00
Nik Revenco 761a62df86 refactor: add comments explaining various variables + rename variables 2025-04-02 16:08:37 +01:00
Nik Revenco 3c0fcb0679 chore: justify welcome! macro with comment 2025-04-02 16:02:40 +01:00
Nik Revenco b1c7bd91b9 refactor: do not pass argument unnecessarily 2025-04-02 16:00:49 +01:00
Nik Revenco a51334f00b feat: increase padding by 1 2025-03-27 16:42:01 +00:00
Nik Revenco 55ef555f87 feat: improve wording in start menu 2025-03-27 16:38:50 +00:00
Nik Revenco 603d95f5d3 feat: recommend <space>e over <space>f
_
2025-03-27 16:35:13 +00:00
Nik Revenco 593504bb27 feat: left padding 2 -> 1 2025-03-27 11:03:56 +00:00
Nik Revenco 3471d82f87 refactor: make padding more naive
no need for such a complex solution, this is already pretty good
2025-03-27 11:02:02 +00:00
Nik Revenco 1e2f1363bc feat: update welcome screen
1. remove `type ` prefix
2. add `<tab>` to themes point
2025-03-27 10:48:37 +00:00
Nik Revenco b68aa35bf4 feat: change text of :theme in welcome message
_
2025-03-26 19:10:46 +00:00
Nik Revenco 871f12b751 fix: 2 panics
_

_
2025-03-26 15:04:15 +00:00
Nik Revenco 7e9bf8a3bb feat: shift the type to the left
_
2025-03-26 13:38:36 +00:00
Nik Revenco 810ac6023e feat: update dashboard 2025-03-26 13:34:20 +00:00
Nik Revenco 5640161e38 feat: Welcome Screen
_

_
2025-03-26 02:53:55 +00:00
15 changed files with 779 additions and 126 deletions

4
Cargo.lock generated
View File

@ -2810,9 +2810,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tree-house"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "679e3296e503901cd9f6e116be5a43a9270222215bf6c78b4b1f4af5c3dcc62d"
checksum = "d00ea55222392f171ae004dd13b62edd09d995633abf0c13406a8df3547fb999"
dependencies = [
"arc-swap",
"hashbrown 0.15.4",

View File

@ -37,7 +37,7 @@ package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2
[workspace.dependencies]
tree-house = { version = "0.2.0", default-features = false }
tree-house = { version = "0.3.0", default-features = false }
nucleo = "0.5.0"
slotmap = "1.0.7"
thiserror = "2.0"

View File

@ -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

View File

@ -135,7 +135,9 @@ pub trait RopeSliceExt<'a>: Sized {
/// let graphemes: Vec<_> = text.graphemes().collect();
/// assert_eq!(graphemes.as_slice(), &["😶‍🌫️", "🏴‍☠️", "🖼️"]);
/// ```
fn graphemes(self) -> RopeGraphemes<'a>;
fn graphemes(self) -> RopeGraphemes<'a> {
self.graphemes_at(0)
}
/// Returns an iterator over the grapheme clusters in the slice, reversed.
///
/// The returned iterator starts at the end of the slice and ends at the beginning of the
@ -150,7 +152,127 @@ pub trait RopeSliceExt<'a>: Sized {
/// let graphemes: Vec<_> = text.graphemes_rev().collect();
/// assert_eq!(graphemes.as_slice(), &["🖼️", "🏴‍☠️", "😶‍🌫️"]);
/// ```
fn graphemes_rev(self) -> RevRopeGraphemes<'a>;
fn graphemes_rev(self) -> RopeGraphemes<'a>;
/// Returns an iterator over the grapheme clusters in the slice at the given byte index.
///
/// # Example
///
/// ```
/// # use ropey::Rope;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = Rope::from_str("😶‍🌫️🏴‍☠️🖼️");
/// // 14 is the byte index of the pirate flag's starting cluster boundary.
/// let graphemes: Vec<_> = text.slice(..).graphemes_at(14).collect();
/// assert_eq!(graphemes.as_slice(), &["🏴‍☠️", "🖼️"]);
/// // 27 is the byte index of the pirate flag's ending cluster boundary.
/// let graphemes: Vec<_> = text.slice(..).graphemes_at(27).reversed().collect();
/// assert_eq!(graphemes.as_slice(), &["🏴‍☠️", "😶‍🌫️"]);
/// ```
fn graphemes_at(self, byte_idx: usize) -> RopeGraphemes<'a>;
/// Returns an iterator over the grapheme clusters in a rope and the byte index where each
/// grapheme cluster starts.
///
/// # Example
///
/// ```
/// # use ropey::Rope;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = Rope::from_str("😶‍🌫️🏴‍☠️🖼️");
/// let slice = text.slice(..);
/// let graphemes: Vec<_> = slice.grapheme_indices_at(0).collect();
/// assert_eq!(
/// graphemes.as_slice(),
/// &[(0, "😶‍🌫️".into()), (14, "🏴‍☠️".into()), (27, "🖼️".into())]
/// );
/// let graphemes: Vec<_> = slice.grapheme_indices_at(slice.len_bytes()).reversed().collect();
/// assert_eq!(
/// graphemes.as_slice(),
/// &[(27, "🖼️".into()), (14, "🏴‍☠️".into()), (0, "😶‍🌫️".into())]
/// );
/// ```
fn grapheme_indices_at(self, byte_idx: usize) -> RopeGraphemeIndices<'a>;
/// Finds the byte index of the next grapheme boundary after `byte_idx`.
///
/// If the byte index lies on the last grapheme cluster in the slice then this function
/// returns `RopeSlice::len_bytes`.
///
/// # Example
///
/// ```
/// # use ropey::Rope;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = Rope::from_str("😶‍🌫️🏴‍☠️🖼️");
/// let slice = text.slice(..);
/// let mut byte_idx = 0;
/// assert_eq!(slice.graphemes_at(byte_idx).next(), Some("😶‍🌫️".into()));
/// byte_idx = slice.next_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).next(), Some("🏴‍☠️".into()));
///
/// // If `byte_idx` does not lie on a character or grapheme boundary then this function is
/// // functionally the same as `ceil_grapheme_boundary`.
/// assert_eq!(slice.next_grapheme_boundary(byte_idx - 1), byte_idx);
/// assert_eq!(slice.next_grapheme_boundary(byte_idx - 2), byte_idx);
/// assert_eq!(slice.next_grapheme_boundary(byte_idx + 1), slice.next_grapheme_boundary(byte_idx));
/// assert_eq!(slice.next_grapheme_boundary(byte_idx + 2), slice.next_grapheme_boundary(byte_idx));
///
/// byte_idx = slice.next_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).next(), Some("🖼️".into()));
/// byte_idx = slice.next_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).next(), None);
/// assert_eq!(byte_idx, slice.len_bytes());
/// ```
fn next_grapheme_boundary(self, byte_idx: usize) -> usize {
self.nth_next_grapheme_boundary(byte_idx, 1)
}
/// Finds the byte index of the `n`th grapheme cluster after the given `byte_idx`.
///
/// If there are fewer than `n` grapheme clusters after `byte_idx` in the rope then this
/// function returns `RopeSlice::len_bytes`.
///
/// This is functionally equivalent to calling `next_grapheme_boundary` `n` times but is more
/// efficient.
fn nth_next_grapheme_boundary(self, byte_idx: usize, n: usize) -> usize;
/// Finds the byte index of the previous grapheme boundary before `byte_idx`.
///
/// If the byte index lies on the first grapheme cluster in the slice then this function
/// returns zero.
///
/// # Example
///
/// ```
/// # use ropey::Rope;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = Rope::from_str("😶‍🌫️🏴‍☠️🖼️");
/// let slice = text.slice(..);
/// let mut byte_idx = text.len_bytes();
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), Some("🖼️".into()));
/// byte_idx = slice.prev_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), Some("🏴‍☠️".into()));
///
/// // If `byte_idx` does not lie on a character or grapheme boundary then this function is
/// // functionally the same as `floor_grapheme_boundary`.
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx + 1), byte_idx);
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx + 2), byte_idx);
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx - 1), slice.prev_grapheme_boundary(byte_idx));
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx - 2), slice.prev_grapheme_boundary(byte_idx));
///
/// byte_idx = slice.prev_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), Some("😶‍🌫️".into()));
/// byte_idx = slice.prev_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), None);
/// assert_eq!(byte_idx, 0);
/// ```
fn prev_grapheme_boundary(self, byte_idx: usize) -> usize {
self.nth_prev_grapheme_boundary(byte_idx, 1)
}
/// Finds the byte index of the `n`th grapheme cluster before the given `byte_idx`.
///
/// If there are fewer than `n` grapheme clusters before `byte_idx` in the rope then this
/// function returns zero.
///
/// This is functionally equivalent to calling `prev_grapheme_boundary` `n` times but is more
/// efficient.
fn nth_prev_grapheme_boundary(self, byte_idx: usize, n: usize) -> usize;
}
impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
@ -335,31 +457,111 @@ impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
}
}
fn graphemes(self) -> RopeGraphemes<'a> {
let mut chunks = self.chunks();
let first_chunk = chunks.next().unwrap_or("");
fn graphemes_rev(self) -> RopeGraphemes<'a> {
self.graphemes_at(self.len_bytes()).reversed()
}
fn graphemes_at(self, byte_idx: usize) -> RopeGraphemes<'a> {
// Bounds check
assert!(byte_idx <= self.len_bytes());
let (mut chunks, chunk_byte_idx, _, _) = self.chunks_at_byte(byte_idx);
let current_chunk = chunks.next().unwrap_or("");
RopeGraphemes {
text: self,
chunks,
cur_chunk: first_chunk,
cur_chunk_start: 0,
cursor: GraphemeCursor::new(0, self.len_bytes(), true),
current_chunk,
chunk_byte_idx,
cursor: GraphemeCursor::new(byte_idx, self.len_bytes(), true),
is_reversed: false,
}
}
fn graphemes_rev(self) -> RevRopeGraphemes<'a> {
let (mut chunks, mut cur_chunk_start, _, _) = self.chunks_at_byte(self.len_bytes());
chunks.reverse();
let first_chunk = chunks.next().unwrap_or("");
cur_chunk_start -= first_chunk.len();
RevRopeGraphemes {
text: self,
chunks,
cur_chunk: first_chunk,
cur_chunk_start,
cursor: GraphemeCursor::new(self.len_bytes(), self.len_bytes(), true),
fn grapheme_indices_at(self, byte_idx: usize) -> RopeGraphemeIndices<'a> {
// Bounds check
assert!(byte_idx <= self.len_bytes());
RopeGraphemeIndices {
front_offset: byte_idx,
iter: self.graphemes_at(byte_idx),
is_reversed: false,
}
}
fn nth_next_grapheme_boundary(self, mut byte_idx: usize, n: usize) -> usize {
// Bounds check
assert!(byte_idx <= self.len_bytes());
byte_idx = self.floor_char_boundary(byte_idx);
// Get the chunk with our byte index in it.
let (mut chunk, mut chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
// Find the nth next grapheme cluster boundary.
for _ in 0..n {
loop {
match gc.next_boundary(chunk, chunk_byte_idx) {
Ok(None) => return self.len_bytes(),
Ok(Some(boundary)) => {
byte_idx = boundary;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
chunk_byte_idx += chunk.len();
let (a, _, _, _) = self.chunk_at_byte(chunk_byte_idx);
chunk = a;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = self.chunk_at_byte(n - 1).0;
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
byte_idx
}
fn nth_prev_grapheme_boundary(self, mut byte_idx: usize, n: usize) -> usize {
// Bounds check
assert!(byte_idx <= self.len_bytes());
byte_idx = self.ceil_char_boundary(byte_idx);
// Get the chunk with our byte index in it.
let (mut chunk, mut chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
for _ in 0..n {
loop {
match gc.prev_boundary(chunk, chunk_byte_idx) {
Ok(None) => return 0,
Ok(Some(boundary)) => {
byte_idx = boundary;
break;
}
Err(GraphemeIncomplete::PrevChunk) => {
let (a, b, _, _) = self.chunk_at_byte(chunk_byte_idx - 1);
chunk = a;
chunk_byte_idx = b;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = self.chunk_at_byte(n - 1).0;
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
byte_idx
}
}
// copied from std
@ -370,13 +572,19 @@ const fn is_utf8_char_boundary(b: u8) -> bool {
}
/// An iterator over the graphemes of a `RopeSlice`.
///
/// This iterator is cursor-like: rather than implementing DoubleEndedIterator it can be reversed
/// like a cursor. This style matches `Bytes` and `Chars` iterator types in Ropey and is more
/// natural and useful for wrapping `GraphemeCursor`.
#[derive(Clone)]
pub struct RopeGraphemes<'a> {
text: RopeSlice<'a>,
chunks: Chunks<'a>,
cur_chunk: &'a str,
cur_chunk_start: usize,
current_chunk: &'a str,
/// Byte index of the start of the current chunk.
chunk_byte_idx: usize,
cursor: GraphemeCursor,
is_reversed: bool,
}
impl fmt::Debug for RopeGraphemes<'_> {
@ -384,112 +592,178 @@ impl fmt::Debug for RopeGraphemes<'_> {
f.debug_struct("RopeGraphemes")
.field("text", &self.text)
.field("chunks", &self.chunks)
.field("cur_chunk", &self.cur_chunk)
.field("cur_chunk_start", &self.cur_chunk_start)
.field("current_chunk", &self.current_chunk)
.field("chunk_byte_idx", &self.chunk_byte_idx)
// .field("cursor", &self.cursor)
.field("is_reversed", &self.is_reversed)
.finish()
}
}
impl<'a> RopeGraphemes<'a> {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<RopeSlice<'a>> {
if self.is_reversed {
self.prev_impl()
} else {
self.next_impl()
}
}
pub fn prev(&mut self) -> Option<RopeSlice<'a>> {
if self.is_reversed {
self.next_impl()
} else {
self.prev_impl()
}
}
pub fn reverse(&mut self) {
self.is_reversed = !self.is_reversed;
}
#[must_use]
pub fn reversed(mut self) -> Self {
self.reverse();
self
}
fn next_impl(&mut self) -> Option<RopeSlice<'a>> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.next_boundary(self.current_chunk, self.chunk_byte_idx)
{
Ok(None) => return None,
Ok(Some(boundary)) => {
b = boundary;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
self.chunk_byte_idx += self.current_chunk.len();
self.current_chunk = self.chunks.next().unwrap_or("");
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}
if a < self.chunk_byte_idx {
Some(self.text.byte_slice(a..b))
} else {
let a2 = a - self.chunk_byte_idx;
let b2 = b - self.chunk_byte_idx;
Some((&self.current_chunk[a2..b2]).into())
}
}
fn prev_impl(&mut self) -> Option<RopeSlice<'a>> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.prev_boundary(self.current_chunk, self.chunk_byte_idx)
{
Ok(None) => return None,
Ok(Some(boundary)) => {
b = boundary;
break;
}
Err(GraphemeIncomplete::PrevChunk) => {
self.current_chunk = self.chunks.prev().unwrap_or("");
self.chunk_byte_idx -= self.current_chunk.len();
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}
if a >= self.chunk_byte_idx + self.current_chunk.len() {
Some(self.text.byte_slice(b..a))
} else {
let a2 = a - self.chunk_byte_idx;
let b2 = b - self.chunk_byte_idx;
Some((&self.current_chunk[b2..a2]).into())
}
}
}
impl<'a> Iterator for RopeGraphemes<'a> {
type Item = RopeSlice<'a>;
fn next(&mut self) -> Option<Self::Item> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.next_boundary(self.cur_chunk, self.cur_chunk_start)
{
Ok(None) => {
return None;
}
Ok(Some(n)) => {
b = n;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
self.cur_chunk_start += self.cur_chunk.len();
self.cur_chunk = self.chunks.next().unwrap_or("");
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
RopeGraphemes::next(self)
}
}
if a < self.cur_chunk_start {
Some(self.text.byte_slice(a..b))
/// An iterator over the grapheme clusters in a rope and the byte indices where each grapheme
/// cluster starts.
///
/// This iterator wraps `RopeGraphemes` and is also cursor-like. Use `reverse` or `reversed` to
/// toggle the direction of the iterator. See [RopeGraphemes].
#[derive(Debug, Clone)]
pub struct RopeGraphemeIndices<'a> {
front_offset: usize,
iter: RopeGraphemes<'a>,
is_reversed: bool,
}
impl<'a> RopeGraphemeIndices<'a> {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<(usize, RopeSlice<'a>)> {
if self.is_reversed {
self.prev_impl()
} else {
let a2 = a - self.cur_chunk_start;
let b2 = b - self.cur_chunk_start;
Some((&self.cur_chunk[a2..b2]).into())
self.next_impl()
}
}
pub fn prev(&mut self) -> Option<(usize, RopeSlice<'a>)> {
if self.is_reversed {
self.next_impl()
} else {
self.prev_impl()
}
}
pub fn reverse(&mut self) {
self.is_reversed = !self.is_reversed;
}
#[must_use]
pub fn reversed(mut self) -> Self {
self.reverse();
self
}
fn next_impl(&mut self) -> Option<(usize, RopeSlice<'a>)> {
let slice = self.iter.next()?;
let idx = self.front_offset;
self.front_offset += slice.len_bytes();
Some((idx, slice))
}
fn prev_impl(&mut self) -> Option<(usize, RopeSlice<'a>)> {
let slice = self.iter.prev()?;
self.front_offset -= slice.len_bytes();
Some((self.front_offset, slice))
}
}
/// An iterator over the graphemes of a `RopeSlice` in reverse.
#[derive(Clone)]
pub struct RevRopeGraphemes<'a> {
text: RopeSlice<'a>,
chunks: Chunks<'a>,
cur_chunk: &'a str,
cur_chunk_start: usize,
cursor: GraphemeCursor,
}
impl fmt::Debug for RevRopeGraphemes<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RevRopeGraphemes")
.field("text", &self.text)
.field("chunks", &self.chunks)
.field("cur_chunk", &self.cur_chunk)
.field("cur_chunk_start", &self.cur_chunk_start)
// .field("cursor", &self.cursor)
.finish()
}
}
impl<'a> Iterator for RevRopeGraphemes<'a> {
type Item = RopeSlice<'a>;
impl<'a> Iterator for RopeGraphemeIndices<'a> {
type Item = (usize, RopeSlice<'a>);
fn next(&mut self) -> Option<Self::Item> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.prev_boundary(self.cur_chunk, self.cur_chunk_start)
{
Ok(None) => {
return None;
}
Ok(Some(n)) => {
b = n;
break;
}
Err(GraphemeIncomplete::PrevChunk) => {
self.cur_chunk = self.chunks.next().unwrap_or("");
self.cur_chunk_start -= self.cur_chunk.len();
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}
if a >= self.cur_chunk_start + self.cur_chunk.len() {
Some(self.text.byte_slice(b..a))
} else {
let a2 = a - self.cur_chunk_start;
let b2 = b - self.cur_chunk_start;
Some((&self.cur_chunk[b2..a2]).into())
}
RopeGraphemeIndices::next(self)
}
}

View File

@ -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();
} else {
editor
.new_file_from_stdin(Action::VerticalSplit)
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
.unwrap_or_else(|_| editor.new_file_welcome());
}
#[cfg(windows)]

View File

@ -81,6 +81,10 @@ fn request_document_colors(editor: &mut Editor, doc_id: DocumentId) {
})
.collect();
if futures.is_empty() {
return;
}
tokio::spawn(async move {
let mut all_colors = Vec::new();
loop {

View File

@ -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},
@ -31,9 +32,12 @@ use helix_view::{
keyboard::{KeyCode, KeyModifiers},
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 keymaps: Keymaps,
@ -74,6 +78,261 @@ impl EditorView {
&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(
&self,
editor: &Editor,
@ -161,6 +420,15 @@ 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,
config.true_color || crate::true_color(),
);
}
let primary_cursor = doc
.selection(view.id)
.primary()

View File

@ -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> {
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
Spans(spans)

View File

@ -210,6 +210,8 @@ pub struct Document {
// 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,
// 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
// `ArcSwap` directly.
@ -727,6 +729,7 @@ impl Document {
jump_labels: HashMap::new(),
color_swatches: None,
color_swatch_controller: TaskController::new(),
is_welcome: false,
syn_loader,
}
}
@ -740,6 +743,11 @@ impl Document {
Self::from(text, None, config, syn_loader)
}
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.

View File

@ -250,6 +250,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
@ -974,6 +976,7 @@ pub enum PopupBorderConfig {
impl Default for Config {
fn default() -> Self {
Self {
welcome_screen: true,
scrolloff: 5,
scroll_lines: 3,
mouse: true,
@ -1437,7 +1440,11 @@ impl Editor {
log::error!("failed to apply workspace edit: {err:?}")
}
}
if old_path.exists() {
fs::rename(old_path, &new_path)?;
}
if let Some(doc) = self.document_by_path(old_path) {
self.set_doc_path(doc.id(), &new_path);
}
@ -1753,6 +1760,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> {
let (stdin, encoding, has_bom) = crate::document::read_to_string(&mut stdin(), None)?;
let doc = Document::from(

View File

@ -937,7 +937,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "html"
source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "29f53d8f4f2335e61bf6418ab8958dac3282077a" }
source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "cbb91a0ff3621245e890d1c50cc811bffb77a26b" }
[[language]]
name = "python"

View File

@ -1,3 +1,48 @@
; inherits: html
(tag_name) @tag
(erroneous_end_tag_name) @error
(doctype) @constant
(attribute_name) @attribute
(comment) @comment
((attribute
(attribute_name) @attribute
(quoted_attribute_value (attribute_value) @markup.link.url))
(#any-of? @attribute "href" "src"))
((element
(start_tag
(tag_name) @tag)
(text) @markup.link.label)
(#eq? @tag "a"))
(attribute [(attribute_value) (quoted_attribute_value)] @string)
((element
(start_tag
(tag_name) @tag)
(text) @markup.bold)
(#any-of? @tag "strong" "b"))
((element
(start_tag
(tag_name) @tag)
(text) @markup.italic)
(#any-of? @tag "em" "i"))
((element
(start_tag
(tag_name) @tag)
(text) @markup.strikethrough)
(#any-of? @tag "s" "del"))
[
"<"
">"
"</"
"/>"
"<!"
] @punctuation.bracket
"=" @punctuation.delimiter
["---"] @punctuation.delimiter

View File

@ -12,8 +12,6 @@
(namespace_definition name: (namespace_identifier) @namespace)
(namespace_identifier) @namespace
(qualified_identifier name: (identifier) @type.enum.variant)
(auto) @type
"decltype" @type
@ -21,12 +19,29 @@
(reference_declarator ["&" "&&"] @type.builtin)
(abstract_reference_declarator ["&" "&&"] @type.builtin)
; -------
; Functions
; -------
; Support up to 4 levels of nesting of qualifiers
; i.e. a::b::c::d::func();
(call_expression
function: (qualified_identifier
name: (identifier) @function))
(call_expression
function: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function)))
(call_expression
function: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function))))
(call_expression
function: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function)))))
(template_function
name: (identifier) @function)
@ -34,26 +49,42 @@
(template_method
name: (field_identifier) @function)
; Support up to 3 levels of nesting of qualifiers
; i.e. a::b::c::func();
; Support up to 4 levels of nesting of qualifiers
; i.e. a::b::c::d::func();
(function_declarator
declarator: (qualified_identifier
name: (identifier) @function))
(function_declarator
declarator: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function)))
(function_declarator
declarator: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function))))
(function_declarator
declarator: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function)))))
(function_declarator
declarator: (field_identifier) @function)
; Constructors
(class_specifier
(type_identifier) @type
(field_declaration_list
(function_definition
(function_declarator
(identifier) @constructor)))
(#eq? @type @constructor))
(destructor_name "~" @constructor
(identifier) @constructor)
; Parameters
(parameter_declaration

View File

@ -65,7 +65,7 @@
] @punctuation
(string_value) @string
((color_value) "#") @string.special
(color_value "#" @string.special)
(color_value) @string.special
(integer_value) @constant.numeric.integer

View File

@ -2,6 +2,7 @@
(erroneous_end_tag_name) @error
(doctype) @constant
(attribute_name) @attribute
(entity) @string.special.symbol
(comment) @comment
((attribute