Skip to content
This repository was archived by the owner on Jul 23, 2019. It is now read-only.

Commit c4515bd

Browse files
maxbrunsfeldNathan SoboAntonio Scandurra
committed
Blink cursors, start on cursor movement and editing
Co-authored-by: Nathan Sobo <[email protected]> Co-authored-by: Antonio Scandurra <[email protected]>
1 parent c75db56 commit c4515bd

File tree

3 files changed

+135
-9
lines changed

3 files changed

+135
-9
lines changed

xray_core/src/buffer_view.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ struct SelectionProps {
3939
enum BufferViewAction {
4040
UpdateScrollTop{delta: f64},
4141
SetDimensions{width: u64, height: u64},
42+
ReplaceSelectedText{text: String},
43+
MoveUp,
44+
MoveDown,
45+
MoveLeft,
46+
MoveRight,
4247
}
4348

4449
impl BufferView {
@@ -91,6 +96,20 @@ impl BufferView {
9196
self
9297
}
9398

99+
pub fn replace_selected_text(&mut self, text: &str) {
100+
{
101+
let mut buffer = self.buffer.borrow_mut();
102+
for selection in self.selections.iter().rev() {
103+
let start = buffer.offset_for_anchor(&selection.start).unwrap();
104+
let end = buffer.offset_for_anchor(&selection.end).unwrap();
105+
buffer.splice(start..end, text);
106+
}
107+
}
108+
109+
self.merge_selections();
110+
self.updated();
111+
}
112+
94113
pub fn add_selection(&mut self, start: Point, end: Point) {
95114
debug_assert!(start <= end); // TODO: Reverse selection if end < start
96115

@@ -489,6 +508,11 @@ impl View for BufferView {
489508
self.set_width(width as f64);
490509
self.set_height(height as f64);
491510
},
511+
Ok(BufferViewAction::ReplaceSelectedText{text}) => self.replace_selected_text(text.as_str()),
512+
Ok(BufferViewAction::MoveUp) => self.move_up(),
513+
Ok(BufferViewAction::MoveDown) => self.move_down(),
514+
Ok(BufferViewAction::MoveLeft) => self.move_left(),
515+
Ok(BufferViewAction::MoveRight) => self.move_right(),
492516
action @ _ => eprintln!("Unrecognized action {:?}", action),
493517
}
494518
}
@@ -866,6 +890,24 @@ mod tests {
866890
);
867891
}
868892

893+
#[test]
894+
fn test_replace_selected_text() {
895+
let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(1))));
896+
897+
editor.buffer.borrow_mut().splice(0..0, "abcdefgh\nhijklmno");
898+
899+
// Three selections on the same line
900+
editor.select_right();
901+
editor.select_right();
902+
editor.add_selection(Point::new(0, 3), Point::new(0, 5));
903+
editor.add_selection(Point::new(0, 7), Point::new(1, 1));
904+
editor.replace_selected_text("-");
905+
assert_eq!(
906+
String::from_utf16_lossy(editor.buffer.borrow().to_u16_chars().as_slice()),
907+
"-c-fg-ijklmno"
908+
);
909+
}
910+
869911
#[test]
870912
fn test_render() {
871913
let buffer = Rc::new(RefCell::new(Buffer::new(1)));
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module.exports =
2+
function debounce (fn, wait) {
3+
let timestamp, timeout
4+
5+
function later () {
6+
const last = Date.now() - timestamp
7+
if (last < wait && last >= 0) {
8+
timeout = setTimeout(later, wait - last)
9+
} else {
10+
timeout = null
11+
fn()
12+
}
13+
}
14+
15+
return function () {
16+
timestamp = Date.now()
17+
if (!timeout) timeout = setTimeout(later, wait)
18+
}
19+
}

xray_electron/lib/render_process/text_editor/text_editor.js

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ const ReactDOM = require("react-dom");
33
const PropTypes = require("prop-types");
44
const { styled } = require("styletron-react");
55
const TextPlane = require("./text_plane");
6+
const debounce = require('../debounce');
67
const $ = React.createElement;
78

9+
const CURSOR_BLINK_RESUME_DELAY = 300;
10+
const CURSOR_BLINK_PERIOD = 800;
11+
812
const Root = styled("div", {
913
width: "100%",
1014
height: "100%",
@@ -14,7 +18,12 @@ const Root = styled("div", {
1418
class TextEditor extends React.Component {
1519
constructor(props) {
1620
super(props);
17-
this.onWheel = this.onWheel.bind(this);
21+
this.handleMouseWheel = this.handleMouseWheel.bind(this);
22+
this.handleKeyDown = this.handleKeyDown.bind(this);
23+
this.debouncedStartCursorBlinking = debounce(
24+
this.startCursorBlinking.bind(this),
25+
CURSOR_BLINK_RESUME_DELAY
26+
);
1827

1928
if (props.initialText) {
2029
buffer.splice(0, 0, props.initialText);
@@ -39,18 +48,16 @@ class TextEditor extends React.Component {
3948
height: element.offsetHeight
4049
});
4150

42-
element.addEventListener('wheel', this.onWheel, {passive: true});
51+
element.addEventListener('wheel', this.handleMouseWheel, {passive: true});
4352

44-
this.state.cursorBlinkIntervalHandle = window.setInterval(() => {
45-
this.setState({ showCursors: !this.state.showCursors });
46-
}, 500);
53+
this.startCursorBlinking();
4754
}
4855

4956
componentWillUnmount() {
57+
this.stopCursorBlinking();
5058
const element = ReactDOM.findDOMNode(this);
51-
element.removeEventListener('wheel', this.onWheel, {passive: true});
59+
element.removeEventListener('wheel', this.handleMouseWheel, {passive: true});
5260
this.state.resizeObserver.disconnect();
53-
window.clearInterval(this.state.cursorBlinkIntervalHandle);
5461
}
5562

5663
componentDidResize(measurements) {
@@ -64,7 +71,10 @@ class TextEditor extends React.Component {
6471
render() {
6572
return $(
6673
Root,
67-
{},
74+
{
75+
tabIndex: -1,
76+
onKeyDown: this.handleKeyDown
77+
},
6878
$(TextPlane, {
6979
showCursors: this.state.showCursors,
7080
lineHeight: this.props.line_height,
@@ -78,9 +88,64 @@ class TextEditor extends React.Component {
7888
);
7989
}
8090

81-
onWheel (event) {
91+
handleMouseWheel(event) {
8292
this.props.dispatch({type: 'UpdateScrollTop', delta: event.deltaY});
8393
}
94+
95+
handleKeyDown(event) {
96+
if (event.key.length === 1) {
97+
this.props.dispatch({type: 'ReplaceSelectedText', text: event.key});
98+
return;
99+
}
100+
101+
switch (event.key) {
102+
case 'ArrowUp':
103+
this.pauseCursorBlinking();
104+
this.props.dispatch({type: 'MoveUp'});
105+
break;
106+
case 'ArrowDown':
107+
this.pauseCursorBlinking();
108+
this.props.dispatch({type: 'MoveDown'});
109+
break;
110+
case 'ArrowLeft':
111+
this.pauseCursorBlinking();
112+
this.props.dispatch({type: 'MoveLeft'});
113+
break;
114+
case 'ArrowRight':
115+
this.pauseCursorBlinking();
116+
this.props.dispatch({type: 'MoveRight'});
117+
break;
118+
}
119+
}
120+
121+
pauseCursorBlinking () {
122+
this.stopCursorBlinking()
123+
this.debouncedStartCursorBlinking()
124+
}
125+
126+
stopCursorBlinking () {
127+
if (this.state.cursorsBlinking) {
128+
window.clearInterval(this.cursorBlinkIntervalHandle)
129+
this.cursorBlinkIntervalHandle = null
130+
this.setState({
131+
showCursors: true,
132+
cursorsBlinking: false
133+
});
134+
}
135+
}
136+
137+
startCursorBlinking () {
138+
if (!this.state.cursorsBlinking) {
139+
this.cursorBlinkIntervalHandle = window.setInterval(() => {
140+
this.setState({ showCursors: !this.state.showCursors });
141+
}, CURSOR_BLINK_PERIOD / 2);
142+
143+
this.setState({
144+
cursorsBlinking: true,
145+
showCursors: false
146+
});
147+
}
148+
}
84149
}
85150

86151
TextEditor.contextTypes = {

0 commit comments

Comments
 (0)