Mouse selection support (#509)
* Initial mouse selection support Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Disable mouse event capture if editor crashes Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Translate screen coordinates to view position Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Select full lines by dragging on line numbers Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * editor: don't register dragging as a jump Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Count graphemes correctly Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Do not select lines when dragging on the line number bar Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Split out verify_screen_coords Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Do not iterate over the graphemes twice Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Switch view by clicking on it Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Add disable-mouse config option Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Support multiple selections with mouse Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Remove unnecessary check Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Refactor using match expression Co-authored-by: Gokul Soumya <gokulps15@gmail.com> Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Rename local variable Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Rename mouse option Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Refactor code Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Fix dragging selection Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Fix crash when clicking past last line Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Count characters better Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Remove comparison not needed anymore Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Validate coordinates before resolving position Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Tidy up references to editor tree Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Better way to determine line end and avoid overflow Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Fix for last line Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Add unit tests for text_pos_at_screen_coords Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> Co-authored-by: Gokul Soumya <gokulps15@gmail.com>
This commit is contained in:
parent
0fdb626c2c
commit
8361de45dc
4 changed files with 233 additions and 3 deletions
|
@ -15,7 +15,7 @@ use std::{
|
|||
use anyhow::Error;
|
||||
|
||||
use crossterm::{
|
||||
event::{Event, EventStream},
|
||||
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
|
||||
execute, terminal,
|
||||
};
|
||||
|
||||
|
@ -449,6 +449,9 @@ impl Application {
|
|||
let mut stdout = stdout();
|
||||
|
||||
execute!(stdout, terminal::EnterAlternateScreen)?;
|
||||
if self.config.terminal.mouse {
|
||||
execute!(stdout, EnableMouseCapture)?;
|
||||
}
|
||||
|
||||
// Exit the alternate screen and disable raw mode before panicking
|
||||
let hook = std::panic::take_hook();
|
||||
|
@ -456,6 +459,7 @@ impl Application {
|
|||
// We can't handle errors properly inside this closure. And it's
|
||||
// probably not a good idea to `unwrap()` inside a panic handler.
|
||||
// So we just ignore the `Result`s.
|
||||
let _ = execute!(std::io::stdout(), DisableMouseCapture);
|
||||
let _ = execute!(std::io::stdout(), terminal::LeaveAlternateScreen);
|
||||
let _ = terminal::disable_raw_mode();
|
||||
hook(info);
|
||||
|
@ -468,6 +472,7 @@ impl Application {
|
|||
// reset cursor shape
|
||||
write!(stdout, "\x1B[2 q")?;
|
||||
|
||||
execute!(stdout, DisableMouseCapture)?;
|
||||
execute!(stdout, terminal::LeaveAlternateScreen)?;
|
||||
|
||||
terminal::disable_raw_mode()?;
|
||||
|
|
|
@ -9,6 +9,8 @@ pub struct Config {
|
|||
pub lsp: LspConfig,
|
||||
#[serde(default)]
|
||||
pub keys: Keymaps,
|
||||
#[serde(default)]
|
||||
pub terminal: TerminalConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
|
||||
|
@ -17,6 +19,18 @@ pub struct LspConfig {
|
|||
pub display_messages: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct TerminalConfig {
|
||||
pub mouse: bool,
|
||||
}
|
||||
|
||||
impl Default for TerminalConfig {
|
||||
fn default() -> Self {
|
||||
Self { mouse: true }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_keymaps_config_file() {
|
||||
use crate::keymap;
|
||||
|
|
|
@ -12,7 +12,7 @@ use helix_core::{
|
|||
syntax::{self, HighlightEvent},
|
||||
unicode::segmentation::UnicodeSegmentation,
|
||||
unicode::width::UnicodeWidthStr,
|
||||
LineEnding, Position, Range,
|
||||
LineEnding, Position, Range, Selection,
|
||||
};
|
||||
use helix_view::{
|
||||
document::Mode,
|
||||
|
@ -24,7 +24,7 @@ use helix_view::{
|
|||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crossterm::event::Event;
|
||||
use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
|
||||
use tui::buffer::Buffer as Surface;
|
||||
|
||||
pub struct EditorView {
|
||||
|
@ -805,6 +805,70 @@ impl Component for EditorView {
|
|||
|
||||
EventResult::Consumed(callback)
|
||||
}
|
||||
Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
row,
|
||||
column,
|
||||
modifiers,
|
||||
..
|
||||
}) => {
|
||||
let editor = &mut cx.editor;
|
||||
|
||||
let result = editor.tree.views().find_map(|(view, _focus)| {
|
||||
view.pos_at_screen_coords(
|
||||
&editor.documents[view.doc],
|
||||
row as usize,
|
||||
column as usize,
|
||||
)
|
||||
.map(|pos| (pos, view.id))
|
||||
});
|
||||
|
||||
if let Some((pos, id)) = result {
|
||||
let doc = &mut editor.documents[editor.tree.get(id).doc];
|
||||
let jump = (doc.id(), doc.selection(id).clone());
|
||||
editor.tree.get_mut(id).jumps.push(jump);
|
||||
|
||||
if modifiers == crossterm::event::KeyModifiers::ALT {
|
||||
let selection = doc.selection(id).clone();
|
||||
doc.set_selection(id, selection.push(Range::point(pos)));
|
||||
} else {
|
||||
doc.set_selection(id, Selection::point(pos));
|
||||
}
|
||||
|
||||
editor.tree.focus = id;
|
||||
|
||||
return EventResult::Consumed(None);
|
||||
}
|
||||
|
||||
EventResult::Ignored
|
||||
}
|
||||
|
||||
Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Drag(MouseButton::Left),
|
||||
row,
|
||||
column,
|
||||
..
|
||||
}) => {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
|
||||
let pos = view.pos_at_screen_coords(doc, row as usize, column as usize);
|
||||
|
||||
if pos == None {
|
||||
return EventResult::Ignored;
|
||||
}
|
||||
|
||||
let selection = doc.selection(view.id).clone();
|
||||
let primary_anchor = selection.primary().anchor;
|
||||
let new_selection = selection.transform(|range| -> Range {
|
||||
if range.anchor == primary_anchor {
|
||||
return Range::new(primary_anchor, pos.unwrap());
|
||||
}
|
||||
range
|
||||
});
|
||||
|
||||
doc.set_selection(view.id, new_selection);
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::Mouse(_) => EventResult::Ignored,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use crate::{graphics::Rect, Document, DocumentId, ViewId};
|
|||
use helix_core::{
|
||||
coords_at_pos,
|
||||
graphemes::{grapheme_width, RopeGraphemes},
|
||||
line_ending::line_end_char_index,
|
||||
Position, RopeSlice, Selection,
|
||||
};
|
||||
|
||||
|
@ -165,6 +166,74 @@ impl View {
|
|||
Some(Position::new(row, col))
|
||||
}
|
||||
|
||||
/// Verifies whether a screen position is inside the view
|
||||
/// Returns true when position is inside the view
|
||||
pub fn verify_screen_coords(&self, row: usize, column: usize) -> bool {
|
||||
// 2 for status
|
||||
if row < self.area.y as usize || row > self.area.y as usize + self.area.height as usize - 2
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: not ideal
|
||||
const OFFSET: usize = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
||||
|
||||
if column < self.area.x as usize + OFFSET
|
||||
|| column > self.area.x as usize + self.area.width as usize
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn text_pos_at_screen_coords(
|
||||
&self,
|
||||
text: &RopeSlice,
|
||||
row: usize,
|
||||
column: usize,
|
||||
tab_width: usize,
|
||||
) -> Option<usize> {
|
||||
if !self.verify_screen_coords(row, column) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let line_number = row - self.area.y as usize + self.first_line;
|
||||
|
||||
if line_number > text.len_lines() - 1 {
|
||||
return Some(text.len_chars());
|
||||
}
|
||||
|
||||
let mut pos = text.line_to_char(line_number);
|
||||
|
||||
let current_line = text.line(line_number);
|
||||
|
||||
// TODO: not ideal
|
||||
const OFFSET: usize = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
||||
|
||||
let target = column - OFFSET - self.area.x as usize + self.first_col;
|
||||
let mut selected = 0;
|
||||
|
||||
for grapheme in RopeGraphemes::new(current_line) {
|
||||
if selected >= target {
|
||||
break;
|
||||
}
|
||||
if grapheme == "\t" {
|
||||
selected += tab_width;
|
||||
} else {
|
||||
let width = grapheme_width(&Cow::from(grapheme));
|
||||
selected += width;
|
||||
}
|
||||
pos += grapheme.chars().count();
|
||||
}
|
||||
|
||||
Some(pos.min(line_end_char_index(&text.slice(..), line_number)))
|
||||
}
|
||||
|
||||
/// Translates a screen position to position in the text document.
|
||||
/// Returns a usize typed position in bounds of the text if found in this view, None if out of view.
|
||||
pub fn pos_at_screen_coords(&self, doc: &Document, row: usize, column: usize) -> Option<usize> {
|
||||
self.text_pos_at_screen_coords(&doc.text().slice(..), row, column, doc.tab_width())
|
||||
}
|
||||
// pub fn traverse<F>(&self, text: RopeSlice, start: usize, end: usize, fun: F)
|
||||
// where
|
||||
// F: Fn(usize, usize),
|
||||
|
@ -186,3 +255,81 @@ impl View {
|
|||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use helix_core::Rope;
|
||||
|
||||
#[test]
|
||||
fn test_text_pos_at_screen_coords() {
|
||||
let mut view = View::new(DocumentId::default());
|
||||
view.area = Rect::new(40, 40, 40, 40);
|
||||
let text = Rope::from_str("abc\n\tdef");
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 40, 2, 4),
|
||||
None
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 40, 41, 4),
|
||||
None
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 0, 2, 4),
|
||||
None
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 0, 49, 4),
|
||||
None
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 0, 41, 4),
|
||||
None
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 40, 81, 4),
|
||||
None
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 78, 41, 4),
|
||||
None
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 40, 40 + 7 + 3, 4),
|
||||
Some(3)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 40, 80, 4),
|
||||
Some(3)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 41, 40 + 7 + 1, 4),
|
||||
Some(5)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 41, 40 + 7 + 4, 4),
|
||||
Some(5)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 41, 40 + 7 + 7, 4),
|
||||
Some(8)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.text_pos_at_screen_coords(&text.slice(..), 41, 80, 4),
|
||||
Some(8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue