Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mouse selection support #509

Merged
merged 29 commits into from
Jul 30, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a14bc10
Initial mouse selection support
dsseng Jul 26, 2021
ed13b69
Disable mouse event capture if editor crashes
dsseng Jul 26, 2021
d8aa346
Translate screen coordinates to view position
dsseng Jul 27, 2021
ba70331
Select full lines by dragging on line numbers
dsseng Jul 27, 2021
6b1dd0e
editor: don't register dragging as a jump
dsseng Jul 27, 2021
de133b2
Count graphemes correctly
dsseng Jul 27, 2021
6d0fe16
Do not select lines when dragging on the line number bar
dsseng Jul 27, 2021
aa3f9cf
Split out verify_screen_coords
dsseng Jul 27, 2021
e71d487
Merge branch 'master' into mouse-support
dsseng Jul 28, 2021
9967291
Do not iterate over the graphemes twice
dsseng Jul 28, 2021
c1ee1af
Switch view by clicking on it
dsseng Jul 28, 2021
e4fc5d6
Add disable-mouse config option
dsseng Jul 28, 2021
a57eb4a
Support multiple selections with mouse
dsseng Jul 28, 2021
6b3397f
Remove unnecessary check
dsseng Jul 28, 2021
23406ec
Refactor using match expression
dsseng Jul 28, 2021
5c00a3b
Rename local variable
dsseng Jul 28, 2021
b1afad9
Rename mouse option
dsseng Jul 28, 2021
d0959da
Refactor code
dsseng Jul 28, 2021
c0aba5f
Fix dragging selection
dsseng Jul 29, 2021
c2d6cbe
Fix crash when clicking past last line
dsseng Jul 29, 2021
aa25dd7
Count characters better
dsseng Jul 29, 2021
692b873
Remove comparison not needed anymore
dsseng Jul 29, 2021
3182724
Validate coordinates before resolving position
dsseng Jul 29, 2021
9890cb1
Tidy up references to editor tree
dsseng Jul 29, 2021
3b6b835
Merge branch 'master' into mouse-support
dsseng Jul 29, 2021
6a26c6b
Better way to determine line end and avoid overflow
dsseng Jul 29, 2021
5a12542
Fix for last line
dsseng Jul 29, 2021
a7bcd46
Add unit tests for text_pos_at_screen_coords
dsseng Jul 29, 2021
5843733
Merge branch 'master' into mouse-support
dsseng Jul 30, 2021
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
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
70 changes: 68 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 @@ -770,6 +770,72 @@ 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)| {
if !view.verify_screen_coords(row as usize, column as usize) {
dsseng marked this conversation as resolved.
Show resolved Hide resolved
return None;
}

Some((
view.pos_at_screen_coords(
&editor.documents[view.doc],
row as usize,
column as usize,
),
view.id,
))
});

let tree = &mut editor.tree;
dsseng marked this conversation as resolved.
Show resolved Hide resolved

if let Some((pos, id)) = result {
let doc = &mut editor.documents[tree.get(id).doc];
let jump = (doc.id(), doc.selection(id).clone());
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));
}

cx.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);

if !view.verify_screen_coords(row as usize, column as usize) {
return EventResult::Ignored;
}

let pos = view.pos_at_screen_coords(doc, row as usize, column as usize);

let selection = doc.selection(view.id).clone();
let primary = selection.primary();

doc.set_selection(view.id, selection.push(Range::new(primary.anchor, pos)));
dsseng marked this conversation as resolved.
Show resolved Hide resolved
EventResult::Consumed(None)
}
Event::Mouse(_) => EventResult::Ignored,
}
}
Expand Down
52 changes: 52 additions & 0 deletions helix-view/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,58 @@ 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
}

/// 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, panics if out of view.
pub fn pos_at_screen_coords(&self, doc: &Document, row: usize, column: usize) -> usize {
let text = doc.text();
let line_number = row - self.area.y as usize + self.first_line;
dsseng marked this conversation as resolved.
Show resolved Hide resolved
let mut pos = text.line_to_char(line_number);

let current_line = text.line(line_number);
let tab_width = doc.tab_width();

// 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 {
dsseng marked this conversation as resolved.
Show resolved Hide resolved
break;
}
if grapheme == "\t" {
selected += tab_width;
} else if grapheme != "\n" {
dsseng marked this conversation as resolved.
Show resolved Hide resolved
let width = grapheme_width(&Cow::from(grapheme));
selected += width;
}
pos += 1;
dsseng marked this conversation as resolved.
Show resolved Hide resolved
}

pos - 1
}

// pub fn traverse<F>(&self, text: RopeSlice, start: usize, end: usize, fun: F)
// where
// F: Fn(usize, usize),
Expand Down