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
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
36 changes: 18 additions & 18 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,13 +517,10 @@ 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.
spans.push((cursor_scope, range.head..range.head + 1));
if selection_is_primary || !skip_secondary_cursors {
if !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
36 changes: 35 additions & 1 deletion 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 Down Expand Up @@ -238,6 +238,15 @@ impl TerminaBackend {
log::debug!("terminfo could not be read, using default cursor reset escape sequence: {reset_cursor_command:?}");
}

// Detect kitty multi-cursor support (available in kitty >= 0.43.0)
if matches!(
term_program().as_deref(),
Some("kitty") | Some("xterm-kitty")
) {
capabilities.kitty_multi_cursor = true;
log::debug!("Detected kitty terminal - enabling multi-cursor protocol support");
}

terminal.enter_cooked_mode()?;

Ok((capabilities, reset_cursor_command))
Expand Down Expand Up @@ -544,6 +553,25 @@ 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, "\x1b[>0;4 q")?;

if !cursors.is_empty() {
write!(self.terminal, "\x1b[>29")?; // Shape 29 = follow main cursor
for (x, y) in cursors {
write!(self.terminal, ";2:{}:{}", y + 1, x + 1)?; // 1-indexed coords
}
write!(self.terminal, " q")?;
}

self.flush()
}

fn clear(&mut self) -> io::Result<()> {
self.start_synchronized_render()?;
write!(
Expand Down Expand Up @@ -572,6 +600,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