on master help sign in
Mouse selection support (#509)
8361de4Dmitry Sharshakov
helix-term/src/application.rs+6−1
@@ -15,7 +15,7 @@ use std::{
15use anyhow::Error;
15use anyhow::Error;
16
16
17use crossterm::{
17use crossterm::{
18− event::{Event, EventStream},
18+ event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
19 execute, terminal,
19 execute, terminal,
20};
20};
21
21
@@ -449,6 +449,9 @@ impl Application {
449 let mut stdout = stdout();
449 let mut stdout = stdout();
450
450
451 execute!(stdout, terminal::EnterAlternateScreen)?;
451 execute!(stdout, terminal::EnterAlternateScreen)?;
452+ if self.config.terminal.mouse {
453+ execute!(stdout, EnableMouseCapture)?;
454+ }
452
455
453 // Exit the alternate screen and disable raw mode before panicking
456 // Exit the alternate screen and disable raw mode before panicking
454 let hook = std::panic::take_hook();
457 let hook = std::panic::take_hook();
@@ -456,6 +459,7 @@ impl Application {
456 // We can't handle errors properly inside this closure. And it's
459 // We can't handle errors properly inside this closure. And it's
457 // probably not a good idea to `unwrap()` inside a panic handler.
460 // probably not a good idea to `unwrap()` inside a panic handler.
458 // So we just ignore the `Result`s.
461 // So we just ignore the `Result`s.
462+ let _ = execute!(std::io::stdout(), DisableMouseCapture);
459 let _ = execute!(std::io::stdout(), terminal::LeaveAlternateScreen);
463 let _ = execute!(std::io::stdout(), terminal::LeaveAlternateScreen);
460 let _ = terminal::disable_raw_mode();
464 let _ = terminal::disable_raw_mode();
461 hook(info);
465 hook(info);
@@ -468,6 +472,7 @@ impl Application {
468 // reset cursor shape
472 // reset cursor shape
469 write!(stdout, "\x1B[2 q")?;
473 write!(stdout, "\x1B[2 q")?;
470
474
475+ execute!(stdout, DisableMouseCapture)?;
471 execute!(stdout, terminal::LeaveAlternateScreen)?;
476 execute!(stdout, terminal::LeaveAlternateScreen)?;
472
477
473 terminal::disable_raw_mode()?;
478 terminal::disable_raw_mode()?;
helix-term/src/config.rs+14
@@ -9,6 +9,8 @@ pub struct Config {
9 pub lsp: LspConfig,
9 pub lsp: LspConfig,
10 #[serde(default)]
10 #[serde(default)]
11 pub keys: Keymaps,
11 pub keys: Keymaps,
12+ #[serde(default)]
13+ pub terminal: TerminalConfig,
12}
14}
13
15
14#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
16#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
@@ -17,6 +19,18 @@ pub struct LspConfig {
17 pub display_messages: bool,
19 pub display_messages: bool,
18}
20}
19
21
22+#[derive(Debug, Clone, PartialEq, Deserialize)]
23+#[serde(rename_all = "kebab-case")]
24+pub struct TerminalConfig {
25+ pub mouse: bool,
26+}
27+
28+impl Default for TerminalConfig {
29+ fn default() -> Self {
30+ Self { mouse: true }
31+ }
32+}
33+
20#[test]
34#[test]
21fn parsing_keymaps_config_file() {
35fn parsing_keymaps_config_file() {
22 use crate::keymap;
36 use crate::keymap;
helix-term/src/ui/editor.rs+66−2
@@ -12,7 +12,7 @@ use helix_core::{
12 syntax::{self, HighlightEvent},
12 syntax::{self, HighlightEvent},
13 unicode::segmentation::UnicodeSegmentation,
13 unicode::segmentation::UnicodeSegmentation,
14 unicode::width::UnicodeWidthStr,
14 unicode::width::UnicodeWidthStr,
15− LineEnding, Position, Range,
15+ LineEnding, Position, Range, Selection,
16};
16};
17use helix_view::{
17use helix_view::{
18 document::Mode,
18 document::Mode,
@@ -24,7 +24,7 @@ use helix_view::{
24};
24};
25use std::borrow::Cow;
25use std::borrow::Cow;
26
26
27−use crossterm::event::Event;
27+use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
28use tui::buffer::Buffer as Surface;
28use tui::buffer::Buffer as Surface;
29
29
30pub struct EditorView {
30pub struct EditorView {
@@ -805,6 +805,70 @@ impl Component for EditorView {
805
805
806 EventResult::Consumed(callback)
806 EventResult::Consumed(callback)
807 }
807 }
808+ Event::Mouse(MouseEvent {
809+ kind: MouseEventKind::Down(MouseButton::Left),
810+ row,
811+ column,
812+ modifiers,
813+ ..
814+ }) => {
815+ let editor = &mut cx.editor;
816+
817+ let result = editor.tree.views().find_map(|(view, _focus)| {
818+ view.pos_at_screen_coords(
819+ &editor.documents[view.doc],
820+ row as usize,
821+ column as usize,
822+ )
823+ .map(|pos| (pos, view.id))
824+ });
825+
826+ if let Some((pos, id)) = result {
827+ let doc = &mut editor.documents[editor.tree.get(id).doc];
828+ let jump = (doc.id(), doc.selection(id).clone());
829+ editor.tree.get_mut(id).jumps.push(jump);
830+
831+ if modifiers == crossterm::event::KeyModifiers::ALT {
832+ let selection = doc.selection(id).clone();
833+ doc.set_selection(id, selection.push(Range::point(pos)));
834+ } else {
835+ doc.set_selection(id, Selection::point(pos));
836+ }
837+
838+ editor.tree.focus = id;
839+
840+ return EventResult::Consumed(None);
841+ }
842+
843+ EventResult::Ignored
844+ }
845+
846+ Event::Mouse(MouseEvent {
847+ kind: MouseEventKind::Drag(MouseButton::Left),
848+ row,
849+ column,
850+ ..
851+ }) => {
852+ let (view, doc) = current!(cx.editor);
853+
854+ let pos = view.pos_at_screen_coords(doc, row as usize, column as usize);
855+
856+ if pos == None {
857+ return EventResult::Ignored;
858+ }
859+
860+ let selection = doc.selection(view.id).clone();
861+ let primary_anchor = selection.primary().anchor;
862+ let new_selection = selection.transform(|range| -> Range {
863+ if range.anchor == primary_anchor {
864+ return Range::new(primary_anchor, pos.unwrap());
865+ }
866+ range
867+ });
868+
869+ doc.set_selection(view.id, new_selection);
870+ EventResult::Consumed(None)
871+ }
808 Event::Mouse(_) => EventResult::Ignored,
872 Event::Mouse(_) => EventResult::Ignored,
809 }
873 }
810 }
874 }
helix-view/src/view.rs+147
@@ -4,6 +4,7 @@ use crate::{graphics::Rect, Document, DocumentId, ViewId};
4use helix_core::{
4use helix_core::{
5 coords_at_pos,
5 coords_at_pos,
6 graphemes::{grapheme_width, RopeGraphemes},
6 graphemes::{grapheme_width, RopeGraphemes},
7+ line_ending::line_end_char_index,
7 Position, RopeSlice, Selection,
8 Position, RopeSlice, Selection,
8};
9};
9
10
@@ -165,6 +166,74 @@ impl View {
165 Some(Position::new(row, col))
166 Some(Position::new(row, col))
166 }
167 }
167
168
169+ /// Verifies whether a screen position is inside the view
170+ /// Returns true when position is inside the view
171+ pub fn verify_screen_coords(&self, row: usize, column: usize) -> bool {
172+ // 2 for status
173+ if row < self.area.y as usize || row > self.area.y as usize + self.area.height as usize - 2
174+ {
175+ return false;
176+ }
177+
178+ // TODO: not ideal
179+ const OFFSET: usize = 7; // 1 diagnostic + 5 linenr + 1 gutter
180+
181+ if column < self.area.x as usize + OFFSET
182+ || column > self.area.x as usize + self.area.width as usize
183+ {
184+ return false;
185+ }
186+ true
187+ }
188+
189+ pub fn text_pos_at_screen_coords(
190+ &self,
191+ text: &RopeSlice,
192+ row: usize,
193+ column: usize,
194+ tab_width: usize,
195+ ) -> Option<usize> {
196+ if !self.verify_screen_coords(row, column) {
197+ return None;
198+ }
199+
200+ let line_number = row - self.area.y as usize + self.first_line;
201+
202+ if line_number > text.len_lines() - 1 {
203+ return Some(text.len_chars());
204+ }
205+
206+ let mut pos = text.line_to_char(line_number);
207+
208+ let current_line = text.line(line_number);
209+
210+ // TODO: not ideal
211+ const OFFSET: usize = 7; // 1 diagnostic + 5 linenr + 1 gutter
212+
213+ let target = column - OFFSET - self.area.x as usize + self.first_col;
214+ let mut selected = 0;
215+
216+ for grapheme in RopeGraphemes::new(current_line) {
217+ if selected >= target {
218+ break;
219+ }
220+ if grapheme == "\t" {
221+ selected += tab_width;
222+ } else {
223+ let width = grapheme_width(&Cow::from(grapheme));
224+ selected += width;
225+ }
226+ pos += grapheme.chars().count();
227+ }
228+
229+ Some(pos.min(line_end_char_index(&text.slice(..), line_number)))
230+ }
231+
232+ /// Translates a screen position to position in the text document.
233+ /// Returns a usize typed position in bounds of the text if found in this view, None if out of view.
234+ pub fn pos_at_screen_coords(&self, doc: &Document, row: usize, column: usize) -> Option<usize> {
235+ self.text_pos_at_screen_coords(&doc.text().slice(..), row, column, doc.tab_width())
236+ }
168 // pub fn traverse<F>(&self, text: RopeSlice, start: usize, end: usize, fun: F)
237 // pub fn traverse<F>(&self, text: RopeSlice, start: usize, end: usize, fun: F)
169 // where
238 // where
170 // F: Fn(usize, usize),
239 // F: Fn(usize, usize),
@@ -186,3 +255,81 @@ impl View {
186 // }
255 // }
187 // }
256 // }
188}
257}
258+
259+#[cfg(test)]
260+mod tests {
261+ use super::*;
262+ use helix_core::Rope;
263+
264+ #[test]
265+ fn test_text_pos_at_screen_coords() {
266+ let mut view = View::new(DocumentId::default());
267+ view.area = Rect::new(40, 40, 40, 40);
268+ let text = Rope::from_str("abc\n\tdef");
269+
270+ assert_eq!(
271+ view.text_pos_at_screen_coords(&text.slice(..), 40, 2, 4),
272+ None
273+ );
274+
275+ assert_eq!(
276+ view.text_pos_at_screen_coords(&text.slice(..), 40, 41, 4),
277+ None
278+ );
279+
280+ assert_eq!(
281+ view.text_pos_at_screen_coords(&text.slice(..), 0, 2, 4),
282+ None
283+ );
284+
285+ assert_eq!(
286+ view.text_pos_at_screen_coords(&text.slice(..), 0, 49, 4),
287+ None
288+ );
289+
290+ assert_eq!(
291+ view.text_pos_at_screen_coords(&text.slice(..), 0, 41, 4),
292+ None
293+ );
294+
295+ assert_eq!(
296+ view.text_pos_at_screen_coords(&text.slice(..), 40, 81, 4),
297+ None
298+ );
299+
300+ assert_eq!(
301+ view.text_pos_at_screen_coords(&text.slice(..), 78, 41, 4),
302+ None
303+ );
304+
305+ assert_eq!(
306+ view.text_pos_at_screen_coords(&text.slice(..), 40, 40 + 7 + 3, 4),
307+ Some(3)
308+ );
309+
310+ assert_eq!(
311+ view.text_pos_at_screen_coords(&text.slice(..), 40, 80, 4),
312+ Some(3)
313+ );
314+
315+ assert_eq!(
316+ view.text_pos_at_screen_coords(&text.slice(..), 41, 40 + 7 + 1, 4),
317+ Some(5)
318+ );
319+
320+ assert_eq!(
321+ view.text_pos_at_screen_coords(&text.slice(..), 41, 40 + 7 + 4, 4),
322+ Some(5)
323+ );
324+
325+ assert_eq!(
326+ view.text_pos_at_screen_coords(&text.slice(..), 41, 40 + 7 + 7, 4),
327+ Some(8)
328+ );
329+
330+ assert_eq!(
331+ view.text_pos_at_screen_coords(&text.slice(..), 41, 80, 4),
332+ Some(8)
333+ );
334+ }
335+}