Skip to content
This repository was archived by the owner on May 14, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 43 additions & 0 deletions xray_core/src/buffer_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ enum BufferViewAction {
SelectDown,
SelectLeft,
SelectRight,
SelectTo {
row: u32,
column: u32,
},
SelectToBeginningOfWord,
SelectToEndOfWord,
SelectToBeginningOfLine,
Expand Down Expand Up @@ -510,6 +514,20 @@ impl BufferView {
self.autoscroll_to_cursor(false);
}

pub fn select_to(&mut self, position: Point) {
self.buffer
.borrow_mut()
.mutate_selections(self.selection_set_id, |buffer, selections| {
for selection in selections.iter_mut() {
let anchor = buffer.anchor_before_point(position).unwrap();
selection.set_head(buffer, anchor);
selection.goal_column = None;
}
})
.unwrap();
self.autoscroll_to_cursor(false);
}

pub fn move_up(&mut self) {
self.buffer
.borrow_mut()
Expand Down Expand Up @@ -1087,6 +1105,10 @@ impl View for BufferView {
Ok(BufferViewAction::SelectDown) => self.select_down(),
Ok(BufferViewAction::SelectLeft) => self.select_left(),
Ok(BufferViewAction::SelectRight) => self.select_right(),
Ok(BufferViewAction::SelectTo {
row,
column
}) => self.select_to(Point::new(row, column)),
Ok(BufferViewAction::SelectToBeginningOfWord) => self.select_to_beginning_of_word(),
Ok(BufferViewAction::SelectToEndOfWord) => self.select_to_end_of_word(),
Ok(BufferViewAction::SelectToBeginningOfLine) => self.select_to_beginning_of_line(),
Expand Down Expand Up @@ -1301,6 +1323,27 @@ mod tests {
editor.move_up();
editor.move_up();
assert_eq!(render_selections(&editor), vec![empty_selection(0, 1)]);

// Select to a direct point in front of cursor position
editor.select_to(Point::new(1, 0));
assert_eq!(render_selections(&editor), vec![selection((0, 1), (1, 0))]);
editor.move_right(); // cancel selection
assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]);
editor.move_right();
editor.move_right();
assert_eq!(render_selections(&editor), vec![empty_selection(2, 1)]);

// Selection can even go to a point before the cursor (with reverse)
editor.select_to(Point::new(0, 0));
assert_eq!(render_selections(&editor), vec![rev_selection((0, 0), (2, 1))]);

// A selection can switch to a new point and the selection will update
editor.select_to(Point::new(0, 3));
assert_eq!(render_selections(&editor), vec![rev_selection((0, 3), (2, 1))]);

// A selection can even swing around the cursor without having to unselect
editor.select_to(Point::new(2, 3));
assert_eq!(render_selections(&editor), vec![selection((2, 1), (2, 3))]);
}

#[test]
Expand Down
164 changes: 140 additions & 24 deletions xray_ui/lib/text_editor/text_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ const { ActionContext, Action } = require("../action_dispatcher");

const CURSOR_BLINK_RESUME_DELAY = 300;
const CURSOR_BLINK_PERIOD = 800;
const MOUSE_DRAG_AUTOSCROLL_MARGIN = 40;

function scaleMouseDragAutoscrollDelta(delta) {
return Math.pow(delta / 3, 3) / 280;
}

const Root = styled("div", {
width: "100%",
Expand Down Expand Up @@ -38,7 +43,10 @@ class TextEditor extends React.Component {

constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseWheel = this.handleMouseWheel.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.pauseCursorBlinking = this.pauseCursorBlinking.bind(this);
Expand All @@ -47,7 +55,7 @@ class TextEditor extends React.Component {
CURSOR_BLINK_RESUME_DELAY
);
this.paddingLeft = 5;
this.state = { scrollLeft: 0, showLocalCursors: true };
this.state = { scrollLeft: 0, showLocalCursors: true, mouseDown: false };
}

componentDidMount() {
Expand All @@ -74,6 +82,13 @@ class TextEditor extends React.Component {
passive: true
});

document.addEventListener("mousemove", this.handleMouseMove, {
passive: true
});
document.addEventListener("mouseup", this.handleMouseUp, {
passive: true
});

this.startCursorBlinking();
}

Expand All @@ -83,6 +98,16 @@ class TextEditor extends React.Component {
element.removeEventListener("wheel", this.handleMouseWheel, {
passive: true
});
element.removeEventListener("mousedown", this.handleMouseDown, {
passive: true
});

document.removeEventListener("mousemove", this.handleMouseMove, {
passive: true
});
document.removeEventListener("mouseup", this.handleMouseUp, {
passive: true
});
this.resizeObserver.disconnect();
}

Expand Down Expand Up @@ -210,21 +235,7 @@ class TextEditor extends React.Component {
);
}

handleMouseDown(event) {
if (this.canUseTextPlane()) {
this.handleClick(event);
switch (event.detail) {
case 2:
this.handleDoubleClick();
break;
case 3:
this.handleTripleClick();
break;
}
}
}

handleClick({ clientX, clientY }) {
getPositionFromMouseEvent({ clientX, clientY }) {
const { scroll_top, line_height, first_visible_row, lines } = this.props;
const { scrollLeft } = this.state;
const targetX =
Expand All @@ -245,14 +256,115 @@ class TextEditor extends React.Component {
break;
}
}
return { row, column };
}
}

this.pauseCursorBlinking();
this.props.dispatch({
type: "SetCursorPosition",
row,
column,
autoscroll: false
});
autoscrollOnMouseDrag({ clientX, clientY }) {
const top = 0 + MOUSE_DRAG_AUTOSCROLL_MARGIN;
const bottom = this.props.height - MOUSE_DRAG_AUTOSCROLL_MARGIN;
const left = 0 + MOUSE_DRAG_AUTOSCROLL_MARGIN;
const right = this.props.width - MOUSE_DRAG_AUTOSCROLL_MARGIN;

let yDelta, yDirection;
if (clientY < top) {
yDelta = top - clientY;
yDirection = -1;
} else if (clientY > bottom) {
yDelta = clientY - bottom;
yDirection = 1;
}

let xDelta, xDirection;
if (clientX < left) {
xDelta = left - clientX;
xDirection = -1;
} else if (clientX > right) {
xDelta = clientX - right;
xDirection = 1;
}

if (yDelta != null) {
const scaledDelta = scaleMouseDragAutoscrollDelta(yDelta) * yDirection;
this.updateScrollTop(scaledDelta);
}

if (xDelta != null) {
const scaledDelta = scaleMouseDragAutoscrollDelta(xDelta) * xDirection;
this.setScrollLeft(this.getScrollLeft() + scaledDelta);
}
}

handleMouseMove(event) {
if (!this.state.mouseDown) {
return;
}

window.requestAnimationFrame(() => {
if (this.canUseTextPlane() && this.state.mouseDown) {
const boundedPositions = {
clientX: Math.min(Math.max(event.clientX, 0), this.props.width),
clientY: Math.min(Math.max(event.clientY, 0), this.props.height)
};
this.autoscrollOnMouseDrag(event);
const pos = this.getPositionFromMouseEvent(boundedPositions);
if (pos) {
this.props.dispatch(
Object.assign(
{
type: "SelectTo"
},
pos
)
);
}
}
});
}

handleMouseUp() {
this.setState({ mouseDown: false });
}

handleMouseDown(event) {
this.setState({ mouseDown: true });
if (this.canUseTextPlane()) {
this.handleClick(event);
switch (event.detail) {
case 2:
this.handleDoubleClick();
break;
case 3:
this.handleTripleClick();
break;
}
}
}

handleClick(event) {
this.pauseCursorBlinking();
const pos = this.getPositionFromMouseEvent(event);
if (pos) {
if (event.shiftKey) {
this.props.dispatch(
Object.assign(
{
type: "SelectTo"
},
pos
)
);
} else {
this.props.dispatch(
Object.assign(
{
type: "SetCursorPosition",
autoscroll: false
},
pos
)
);
}
}
}

Expand All @@ -270,7 +382,7 @@ class TextEditor extends React.Component {
if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
this.setScrollLeft(this.state.scrollLeft + event.deltaX);
} else {
this.props.dispatch({ type: "UpdateScrollTop", delta: event.deltaY });
this.updateScrollTop(event.deltaY);
}
}

Expand Down Expand Up @@ -368,6 +480,10 @@ class TextEditor extends React.Component {
}
}

updateScrollTop(deltaY) {
this.props.dispatch({ type: "UpdateScrollTop", delta: deltaY });
}

setScrollLeft(scrollLeft) {
this.setState({
scrollLeft: this.constrainScrollLeft(scrollLeft)
Expand Down