Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions helix-core/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,16 @@ pub use helix_stdx::range::Range;
use serde::{Deserialize, Serialize};

/// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
#[default]
Hint,
Info,
Warning,
Error,
}

impl Default for Severity {
fn default() -> Self {
Self::Hint
}
}

Comment on lines +8 to -22

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this and other changes (like slice_from_raw_parts_mut below) are unrelated and are probably lints from newer versions of Rust. If you install Rust using rustup it will respect the version set in rust-toolchain.toml. We keep an MSRV that is intentionally older, see https://github.com/helix-editor/helix/blob/master/docs/CONTRIBUTING.md#minimum-stable-rust-version-msrv-policy

These extra changes should be reset

#[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)]
pub enum NumberOrString {
Number(i32),
Expand Down
2 changes: 1 addition & 1 deletion helix-core/src/graphemes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ impl Drop for GraphemeStr<'_> {
if self.len & Self::MASK_OWNED != 0 {
// free allocation
unsafe {
drop(Box::from_raw(slice::from_raw_parts_mut(
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
self.ptr.as_ptr(),
self.compute_len(),
)));
Expand Down
2 changes: 1 addition & 1 deletion helix-core/tests/indent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ fn test_treesitter_indent(
.unwrap()
.to_string(&indent_style, tab_width);
assert!(
line.get_slice(..pos).map_or(false, |s| s == suggested_indent),
line.get_slice(..pos).is_some_and(|s| s == suggested_indent),
"Wrong indentation for file {:?} on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
test_name,
i+1,
Expand Down
44 changes: 44 additions & 0 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ impl Application {
let backend = TestBackend::new(120, 150);

let theme_mode = backend.get_theme_mode();

#[cfg(all(not(windows), not(feature = "integration")))]
let kitty_multi_cursor_support = backend.supports_kitty_multi_cursor();
#[cfg(any(windows, feature = "integration"))]
let kitty_multi_cursor_support = false;

let terminal = Terminal::new(backend)?;
let area = terminal.size();
let mut compositor = Compositor::new(area);
Expand All @@ -138,6 +144,8 @@ impl Application {
})),
handlers,
);
editor.kitty_multi_cursor_support = kitty_multi_cursor_support;

Self::load_configured_theme(
&mut editor,
&config.load(),
Expand Down Expand Up @@ -298,7 +306,43 @@ impl Application {
self.editor.cursor_cache.reset();

let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));

use helix_view::graphics::CursorKind;
let secondary_cursors = if !matches!(kind, CursorKind::Block | CursorKind::Hidden) {
self.get_secondary_cursor_positions()
} else {
Vec::new()
};

self.terminal.draw(pos, kind).unwrap();
// Always update kitty cursors (clears if empty, sets if not)
self.terminal
.set_multiple_cursors(&secondary_cursors)
.unwrap();
}

fn get_secondary_cursor_positions(&self) -> Vec<(u16, u16)> {
use helix_view::current_ref;

let (view, doc) = current_ref!(&self.editor);
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
let primary_idx = selection.primary_index();
let inner = view.inner_area(doc);

selection
.iter()
.enumerate()
.filter(|(idx, _)| *idx != primary_idx)
.filter_map(|(_, range)| {
let cursor = range.cursor(text);
view.screen_coords_at_pos(doc, text, cursor).map(|pos| {
let x = (pos.col + inner.x as usize) as u16;
let y = (pos.row + inner.y as usize) as u16;
(x, y)
})
})
.collect()
}

pub async fn event_loop<S>(&mut self, input_stream: &mut S)
Expand Down
13 changes: 5 additions & 8 deletions helix-term/src/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,14 +483,11 @@ mod tests {
.len()
> 1
);
assert!(
merged_keyamp
.get(&Mode::Insert)
.and_then(|key_trie| key_trie.node())
.unwrap()
.len()
> 0
);
assert!(!merged_keyamp
.get(&Mode::Insert)
.and_then(|key_trie| key_trie.node())
.unwrap()
.is_empty());
}

#[test]
Expand Down
34 changes: 17 additions & 17 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use helix_core::{
use helix_view::{
annotations::diagnostics::DiagnosticFilter,
document::{Mode, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig},
editor::CompleteAction,
graphics::{Color, CursorKind, Modifier, Rect, Style},
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
keyboard::{KeyCode, KeyModifiers},
Expand Down Expand Up @@ -146,11 +146,10 @@ impl EditorView {
overlays.push(tabstops);
}
overlays.push(Self::doc_selection_highlights(
editor.mode(),
editor,
doc,
view,
theme,
&config.cursor_shape,
self.terminal_focused,
));
if let Some(overlay) = Self::highlight_focused_view_elements(view, doc, theme) {
Expand Down Expand Up @@ -461,20 +460,24 @@ impl EditorView {

/// Get highlight spans for selections in a document view.
pub fn doc_selection_highlights(
mode: Mode,
editor: &Editor,
doc: &Document,
view: &View,
theme: &Theme,
cursor_shape_config: &CursorShapeConfig,
is_terminal_focused: bool,
) -> OverlayHighlights {
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
let primary_idx = selection.primary_index();

let mode = editor.mode();
let cursor_shape_config = &editor.config().cursor_shape;
let cursorkind = cursor_shape_config.from_mode(mode);
let cursor_is_block = cursorkind == CursorKind::Block;

// Skip rendering secondary cursors when kitty protocol handles them
let skip_secondary_cursors = editor.kitty_multi_cursor_support && !cursor_is_block;

let selection_scope = theme
.find_highlight_exact("ui.selection")
.expect("could not find `ui.selection` scope in the theme!");
Expand Down Expand Up @@ -514,12 +517,9 @@ impl EditorView {

// Special-case: cursor at end of the rope.
if range.head == range.anchor && range.head == text.len_chars() {
if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
// Bar and underline cursors are drawn by the terminal
// BUG: If the editor area loses focus while having a bar or
// underline cursor (eg. when a regex prompt has focus) then
// the primary cursor will be invisible. This doesn't happen
// with block cursors since we manually draw *all* cursors.
if (selection_is_primary || !skip_secondary_cursors)
&& (!selection_is_primary || (cursor_is_block && is_terminal_focused))
{
spans.push((cursor_scope, range.head..range.head + 1));
}
continue;
Expand All @@ -537,17 +537,17 @@ impl EditorView {
cursor_start
};
spans.push((selection_scope, range.anchor..selection_end));
// add block cursors
// skip primary cursor if terminal is unfocused - terminal cursor is used in that case
if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
if (selection_is_primary || !skip_secondary_cursors)
&& (!selection_is_primary || (cursor_is_block && is_terminal_focused))
{
spans.push((cursor_scope, cursor_start..range.head));
}
} else {
// Reverse case.
let cursor_end = next_grapheme_boundary(text, range.head);
// add block cursors
// skip primary cursor if terminal is unfocused - terminal cursor is used in that case
if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
if (selection_is_primary || !skip_secondary_cursors)
&& (!selection_is_primary || (cursor_is_block && is_terminal_focused))
{
spans.push((cursor_scope, range.head..cursor_end));
}
// non block cursors look like they exclude the cursor
Expand Down
4 changes: 4 additions & 0 deletions helix-tui/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ pub trait Backend {
fn show_cursor(&mut self, kind: CursorKind) -> Result<(), io::Error>;
/// Sets the cursor to the given position
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
/// Sets multiple cursors using terminal-specific protocols (e.g., kitty)
fn set_multiple_cursors(&mut self, _cursors: &[(u16, u16)]) -> Result<(), io::Error> {
Ok(())
}
/// Clears the terminal
fn clear(&mut self) -> Result<(), io::Error>;
/// Gets the size of the terminal in cells
Expand Down
60 changes: 57 additions & 3 deletions helix-tui/src/backend/termina.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ fn vte_version() -> Option<usize> {
#[derive(Debug, Default, Clone, Copy)]
struct Capabilities {
kitty_keyboard: KittyKeyboardSupport,
kitty_multi_cursor: bool,
synchronized_output: bool,
true_color: bool,
extended_underlines: bool,
Expand Down Expand Up @@ -125,7 +126,6 @@ impl TerminaBackend {
) -> io::Result<(Capabilities, String)> {
use std::time::{Duration, Instant};

// Colibri "midnight"
const TEST_COLOR: RgbColor = RgbColor::new(59, 34, 76);

terminal.enter_raw_mode()?;
Expand All @@ -149,13 +149,15 @@ impl TerminaBackend {
// If we only receive the device attributes then we know it is not.
write!(
terminal,
"{}{}{}{}{}{}{}",
"{}{}{}{}{}{}{}{}",
// Synchronized output
Csi::Mode(csi::Mode::QueryDecPrivateMode(csi::DecPrivateMode::Code(
csi::DecPrivateModeCode::SynchronizedOutput
))),
// Mode 2031 theme updates. Query the current theme.
Csi::Mode(csi::Mode::QueryTheme),
// Kitty multi-cursor protocol support query
Csi::Cursor(csi::Cursor::QueryCursorShape),
// True color and while we're at it, extended underlines:
// <https://github.com/termstandard/colors?tab=readme-ov-file#querying-the-terminal>
Csi::Sgr(csi::Sgr::Background(TEST_COLOR.into())),
Expand Down Expand Up @@ -199,7 +201,21 @@ impl TerminaBackend {
capabilities.extended_underlines =
sgrs.contains(&csi::Sgr::UnderlineColor(TEST_COLOR.into()));
}
_ => (),
Event::Csi(Csi::Cursor(csi::Cursor::QueryCursorShape)) => {
// Empty response to the query still means the protocol is supported
capabilities.kitty_multi_cursor = true;
log::debug!("Detected kitty multi-cursor support via protocol query");
}
Event::Csi(Csi::Cursor(csi::Cursor::CursorShapeQueryResponse(shapes))) => {
capabilities.kitty_multi_cursor = true;
log::debug!(
"Detected kitty multi-cursor support via protocol query. Supported shapes: {:?}",
shapes
);
}
event => {
log::trace!("Unhandled capability detection event: {event:?}");
}
}
}

Expand Down Expand Up @@ -544,6 +560,38 @@ impl Backend for TerminaBackend {
self.flush()
}

fn set_multiple_cursors(&mut self, cursors: &[(u16, u16)]) -> io::Result<()> {
if !self.capabilities.kitty_multi_cursor {
return Ok(());
}

// Always clear existing cursors first
write!(
self.terminal,
"{}",
Csi::Cursor(csi::Cursor::ClearSecondaryCursors)
)?;

if !cursors.is_empty() {
write!(
self.terminal,
"{}",
Csi::Cursor(csi::Cursor::SetMultipleCursors {
shape: csi::MultiCursorShape::FollowMainCursor,
positions: cursors
.iter()
.map(|(x, y)| (
OneBased::from_zero_based(*y),
OneBased::from_zero_based(*x),
))
.collect(),
})
)?;
}

self.flush()
}

fn clear(&mut self) -> io::Result<()> {
self.start_synchronized_render()?;
write!(
Expand Down Expand Up @@ -572,6 +620,12 @@ impl Backend for TerminaBackend {
}
}

impl TerminaBackend {
pub fn supports_kitty_multi_cursor(&self) -> bool {
self.capabilities.kitty_multi_cursor
}
}

impl Drop for TerminaBackend {
fn drop(&mut self) {
// Avoid resetting the terminal while panicking because we set a panic hook above in
Expand Down
4 changes: 4 additions & 0 deletions helix-tui/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ where
self.backend.set_cursor(x, y)
}

pub fn set_multiple_cursors(&mut self, cursors: &[(u16, u16)]) -> io::Result<()> {
self.backend.set_multiple_cursors(cursors)
}

/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
self.backend.clear()?;
Expand Down
2 changes: 2 additions & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,7 @@ pub struct Editor {

pub mouse_down_range: Option<Range>,
pub cursor_cache: CursorCache,
pub kitty_multi_cursor_support: bool,
}

pub type Motion = Box<dyn Fn(&mut Editor)>;
Expand Down Expand Up @@ -1340,6 +1341,7 @@ impl Editor {
handlers,
mouse_down_range: None,
cursor_cache: CursorCache::default(),
kitty_multi_cursor_support: false,
}
}

Expand Down
9 changes: 2 additions & 7 deletions helix-view/src/graphics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ use std::{
str::FromStr,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
#[serde(rename_all = "lowercase")]
/// UNSTABLE
pub enum CursorKind {
/// █
#[default]
Block,
/// |
Bar,
Expand All @@ -19,12 +20,6 @@ pub enum CursorKind {
Hidden,
}

impl Default for CursorKind {
fn default() -> Self {
Self::Block
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Margin {
pub horizontal: u16,
Expand Down
3 changes: 1 addition & 2 deletions helix-view/src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -957,8 +957,7 @@ mod test {

assert_eq!(10, tree.views().count());
assert_eq!(
std::iter::repeat(7)
.take(9)
std::iter::repeat_n(7, 9)
.chain(Some(8)) // Rounding in `recalculate`.
.collect::<Vec<_>>(),
tree.views()
Expand Down