Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Selection behavior for inputs (onivim#1264)
* Selection behavior for inputs
  - Adds ability for input to have selected text
  - Adds handlers for Shift/Control key combination with control keys
  - Adds test to cover new behavior

Removed unused code. Updated to handle <Right> key at the end of selection

Update to have better variable names

* Update to fit new skia render. Update wild menu to don't have a dropdown.

* Added Selection module

* Updated InputModel to work with Selection model
 - Added tests

* Attempt to integrate new selection module.

* Fixed type issues.

* Refactored Selection module in functional way

* Added new line at the end of file

* Update SCM feature to use new input handler

* Formatted the code

* Code refactoring
  - Updates to don't have realative methods for Selection module
  - Update to have better names
  - Update to have more DRY code

* Better variable names

* Better variable names

* Update to don't have Selection focus/anchor
  • Loading branch information
fanantoxa committed Mar 1, 2020
1 parent b723829 commit b9253a1
Show file tree
Hide file tree
Showing 20 changed files with 1,776 additions and 99 deletions.
85 changes: 47 additions & 38 deletions src/Components/Input.re
Expand Up @@ -70,45 +70,15 @@ let getStringParts = (index, str) => {
};
};

let getSafeStringBounds = (str, cursorPosition, change) => {
let nextPosition = cursorPosition + change;
let currentLength = String.length(str);
nextPosition > currentLength
? currentLength : nextPosition < 0 ? 0 : nextPosition;
};

let removeCharacterBefore = (word, cursorPosition) => {
let (startStr, endStr) = getStringParts(cursorPosition, word);
let nextPosition = getSafeStringBounds(startStr, cursorPosition, -1);
let newString = Str.string_before(startStr, nextPosition) ++ endStr;
(newString, nextPosition);
};

let removeCharacterAfter = (word, cursorPosition) => {
let (startStr, endStr) = getStringParts(cursorPosition, word);
let newString =
startStr
++ (
switch (endStr) {
| "" => ""
| _ => Str.last_chars(endStr, String.length(endStr) - 1)
}
);
(newString, cursorPosition);
};

let addCharacter = (word, char, index) => {
let (startStr, endStr) = getStringParts(index, word);
(startStr ++ char ++ endStr, String.length(startStr) + 1);
};

module Constants = {
let cursorWidth = 2;
let selectionOpacity = 0.75;
};

module Styles = {
let defaultPlaceholderColor = Colors.grey;
let defaultCursorColor = Colors.black;
let defaultSelectionColor = Color.hex("#42557b");

let default =
Style.[
Expand All @@ -125,11 +95,12 @@ let%component make =
~style=Styles.default,
~placeholderColor=Styles.defaultPlaceholderColor,
~cursorColor=Styles.defaultCursorColor,
~selectionColor=Styles.defaultSelectionColor,
~placeholder="",
~prefix="",
~isFocused,
~value,
~cursorPosition,
~selection: Selection.t,
~onClick,
(),
) => {
Expand Down Expand Up @@ -175,6 +146,12 @@ let%component make =
transform(Transform.[TranslateX(float(offset))]),
];

let selection = offset => [
position(`Absolute),
marginTop(2),
transform(Transform.[TranslateX(float(offset))]),
];

let textContainer = [flexGrow(1), overflow(`Hidden)];

let text = [
Expand Down Expand Up @@ -205,7 +182,7 @@ let%component make =

let%hook () =
Hooks.effect(
If((!=), (value, cursorPosition, isFocused)),
If((!=), (value, selection, isFocused)),
() => {
resetCursor();
None;
Expand All @@ -214,7 +191,7 @@ let%component make =

let () = {
let cursorOffset =
measureTextWidth(String.sub(displayValue, 0, cursorPosition))
measureTextWidth(String.sub(displayValue, 0, selection.focus))
|> int_of_float;

switch (Option.bind(textRef^, r => r#getParent())) {
Expand Down Expand Up @@ -265,17 +242,18 @@ let%component make =
| Some(node) =>
let offset =
int_of_float(event.mouseX) - offsetLeft(node) + scrollOffset^;
let cursorPosition = indexNearestOffset(offset);
let nearestOffset = indexNearestOffset(offset);
let selection = Selection.collapsed(~text=value, nearestOffset);
resetCursor();
onClick(cursorPosition);
onClick(selection);

| None => ()
};
};

let cursor = () => {
let (startStr, _) =
getStringParts(cursorPosition + String.length(prefix), displayValue);
getStringParts(selection.focus + String.length(prefix), displayValue);

let textWidth = measureTextWidth(startStr) |> int_of_float;

Expand All @@ -292,6 +270,36 @@ let%component make =
</View>;
};

let selectionView = () =>
if (Selection.isCollapsed(selection)) {
React.empty;
} else {
let startOffset = Selection.offsetLeft(selection);
let endOffset = Selection.offsetRight(selection);

let (beginnigStartStr, _) =
getStringParts(startOffset + String.length(prefix), displayValue);
let beginningTextWidth =
measureTextWidth(beginnigStartStr) |> int_of_float;
let startOffset = beginningTextWidth - scrollOffset^;

let (endingStartStr, _) =
getStringParts(endOffset + String.length(prefix), displayValue);
let endingTextWidth = measureTextWidth(endingStartStr) |> int_of_float;
let endOffset = endingTextWidth - scrollOffset^;
let width = endOffset - startOffset + Constants.cursorWidth;

<View style={Styles.selection(startOffset)}>
<Opacity opacity=Constants.selectionOpacity>
<Container
width
height={Styles.fontSize |> int_of_float}
color=selectionColor
/>
</Opacity>
</View>;
};

let text = () =>
<Text
ref={node => textRef := Some(node)}
Expand All @@ -302,6 +310,7 @@ let%component make =
<Clickable onAnyClick=handleClick>
<View style=Styles.box>
<View style=Styles.marginContainer>
<selectionView />
<cursor />
<View style=Styles.textContainer> <text /> </View>
</View>
Expand Down
167 changes: 157 additions & 10 deletions src/Components/InputModel.re
@@ -1,6 +1,34 @@
open Oni_Core;
open Utility;

let wordSeparators = " ./\\()\"'-:,.;<>~!@#$%^&*|+=[]{}`~?";

let separatorOnIndexExn = (index, text) => {
String.contains(wordSeparators, text.[index]);
};

let findNextWordBoundary = (text, focus) => {
let finalIndex = String.length(text);
let index = ref(min(focus + 1, finalIndex));

while (index^ < finalIndex && !separatorOnIndexExn(index^, text)) {
index := index^ + 1;
};

index^;
};

let findPrevWordBoundary = (text, focus) => {
let finalIndex = 0;
let index = ref(max(focus - 1, finalIndex));

while (index^ > finalIndex && !separatorOnIndexExn(index^ - 1, text)) {
index := index^ - 1;
};

index^;
};

let slice = (~start=0, ~stop=?, str) => {
let length = String.length(str);
let start = IntEx.clamp(~lo=0, ~hi=length, start);
Expand Down Expand Up @@ -28,13 +56,132 @@ let add = (~at as index, insert, text) => (
index + String.length(insert),
);

let handleInput = (~text, ~cursorPosition) =>
fun
| "<LEFT>" => (text, max(0, cursorPosition - 1))
| "<RIGHT>" => (text, min(String.length(text), cursorPosition + 1))
| "<BS>" => removeBefore(cursorPosition, text)
| "<DEL>" => removeAfter(cursorPosition, text)
| "<HOME>" => (text, 0)
| "<END>" => (text, String.length(text))
| key when String.length(key) == 1 => add(~at=cursorPosition, key, text)
| _ => (text, cursorPosition);
let removeCharBefore = (text, selection: Selection.t) => {
let (textSlice, _) = removeBefore(selection.focus, text);

(
textSlice,
Selection.offsetLeft(selection)
- 1
|> Selection.collapsed(~text=textSlice),
);
};

let removeSelection = (text, selection) => {
let (textSlice, focus) =
removeAfter(
Selection.offsetLeft(selection),
text,
~count=Selection.length(selection),
);

(textSlice, Selection.collapsed(~text=textSlice, focus));
};

let removeCharAfter = (text, selection: Selection.t) => {
let (textSlice, focus) = removeAfter(selection.focus, text);

(textSlice, Selection.collapsed(~text=textSlice, focus));
};

let collapsePrevWord = (text, selection: Selection.t) => {
let newSelection =
selection.focus
|> findPrevWordBoundary(text)
|> Selection.collapsed(~text);

(text, newSelection);
};

let collapseNextWord = (text, selection: Selection.t) => {
let newSelection =
selection.focus
|> findNextWordBoundary(text)
|> Selection.collapsed(~text);

(text, newSelection);
};

let extendPrevWord = (text, selection: Selection.t) => {
let newSelection =
selection.focus
|> findPrevWordBoundary(text)
|> Selection.extend(~text, ~selection);

(text, newSelection);
};

let extendNextWord = (text, selection: Selection.t) => {
let newSelection =
selection.focus
|> findNextWordBoundary(text)
|> Selection.extend(~text, ~selection);

(text, newSelection);
};

let addCharacter = (key, text, selection: Selection.t) => {
let (newText, focus) = add(~at=selection.focus, key, text);

(newText, Selection.collapsed(~text=newText, focus));
};

let replacesSelection = (key, text, selection: Selection.t) => {
let (textSlice, selectionSlice) = removeSelection(text, selection);
let (newText, focus) = add(~at=selectionSlice.focus, key, textSlice);

(newText, Selection.collapsed(~text=newText, focus));
};

let handleInput = (~text, ~selection: Selection.t, key) => {
switch (key, Selection.isCollapsed(selection)) {
| ("<LEFT>", true) => (
text,
Selection.offsetLeft(selection) - 1 |> Selection.collapsed(~text),
)
| ("<LEFT>", false) => (
text,
Selection.offsetLeft(selection) |> Selection.collapsed(~text),
)
| ("<RIGHT>", true) => (
text,
Selection.offsetLeft(selection) + 1 |> Selection.collapsed(~text),
)
| ("<RIGHT>", false) => (
text,
Selection.offsetRight(selection) |> Selection.collapsed(~text),
)
| ("<BS>", true) => removeCharBefore(text, selection)
| ("<BS>", false) => removeSelection(text, selection)
| ("<DEL>", true) => removeCharAfter(text, selection)
| ("<DEL>", false) => removeSelection(text, selection)
| ("<HOME>", _) => (text, Selection.collapsed(~text, 0))
| ("<END>", _) => (text, Selection.collapsed(~text, String.length(text)))
| ("<S-LEFT>", _) => (
text,
selection.focus - 1 |> Selection.extend(~text, ~selection),
)
| ("<S-RIGHT>", _) => (
text,
selection.focus + 1 |> Selection.extend(~text, ~selection),
)
| ("<C-LEFT>", _) => collapsePrevWord(text, selection)
| ("<C-RIGHT>", _) => collapseNextWord(text, selection)
| ("<S-HOME>", _) => (text, Selection.extend(~text, ~selection, 0))
| ("<S-END>", _) => (
text,
Selection.extend(~text, ~selection, String.length(text)),
)
| ("<S-C-LEFT>", _) => extendPrevWord(text, selection)
| ("<S-C-RIGHT>", _) => extendNextWord(text, selection)
| ("<C-a>", _) => (
text,
Selection.create(~text, ~anchor=0, ~focus=String.length(text)),
)
| (key, true) when String.length(key) == 1 =>
addCharacter(key, text, selection)
| (key, false) when String.length(key) == 1 =>
replacesSelection(key, text, selection)
| (_, _) => (text, selection)
};
};
42 changes: 42 additions & 0 deletions src/Components/Selection.re
@@ -0,0 +1,42 @@
open Utility;

[@deriving show({with_path: false})]
type t = {
anchor: int,
focus: int,
};

let initial: t = {anchor: 0, focus: 0};

let create = (~text: string, ~anchor: int, ~focus: int): t => {
let safeOffset = IntEx.clamp(~lo=0, ~hi=String.length(text));

let safeAnchor = safeOffset(anchor);
let safeFocus = safeOffset(focus);

{anchor: safeAnchor, focus: safeFocus};
};

let length = (selection: t): int => {
abs(selection.focus - selection.anchor);
};

let offsetLeft = (selection: t): int => {
min(selection.focus, selection.anchor);
};

let offsetRight = (selection: t): int => {
max(selection.focus, selection.anchor);
};

let isCollapsed = (selection: t): bool => {
selection.anchor == selection.focus;
};

let collapsed = (~text: string, offset: int): t => {
create(~text, ~anchor=offset, ~focus=offset);
};

let extend = (~text: string, ~selection: t, offset: int): t => {
create(~text, ~anchor=selection.anchor, ~focus=offset);
};
18 changes: 18 additions & 0 deletions src/Components/Selection.rei
@@ -0,0 +1,18 @@
[@deriving show({with_path: false})]
type t =
pri {
anchor: int,
focus: int,
};

let initial: t;

let create: (~text: string, ~anchor: int, ~focus: int) => t;
let length: t => int;
let offsetLeft: t => int;
let offsetRight: t => int;
let isCollapsed: t => bool;

let collapsed: (~text: string, int) => t;

let extend: (~text: string, ~selection: t, int) => t;

0 comments on commit b9253a1

Please sign in to comment.