Skip to content

Commit

Permalink
Mouse selection support (#509)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
dsseng and sudormrfbin authored Jul 30, 2021
1 parent 0fdb626 commit 8361de4
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 3 deletions.
7 changes: 6 additions & 1 deletion helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use std::{
use anyhow::Error;

use crossterm::{
event::{Event, EventStream},
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
execute, terminal,
};

Expand Down Expand Up @@ -449,13 +449,17 @@ 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();
std::panic::set_hook(Box::new(move |info| {
// 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);
Expand All @@ -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()?;
Expand Down
14 changes: 14 additions & 0 deletions helix-term/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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;
Expand Down
68 changes: 66 additions & 2 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
}
}
Expand Down
147 changes: 147 additions & 0 deletions helix-view/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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),
Expand All @@ -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)
);
}
}

0 comments on commit 8361de4

Please sign in to comment.