diff --git a/src/Components/Input.re b/src/Components/Input.re index 47833b6142..82d681bb8a 100644 --- a/src/Components/Input.re +++ b/src/Components/Input.re @@ -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.[ @@ -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, (), ) => { @@ -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 = [ @@ -205,7 +182,7 @@ let%component make = let%hook () = Hooks.effect( - If((!=), (value, cursorPosition, isFocused)), + If((!=), (value, selection, isFocused)), () => { resetCursor(); None; @@ -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())) { @@ -265,9 +242,10 @@ 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 => () }; @@ -275,7 +253,7 @@ let%component make = let cursor = () => { let (startStr, _) = - getStringParts(cursorPosition + String.length(prefix), displayValue); + getStringParts(selection.focus + String.length(prefix), displayValue); let textWidth = measureTextWidth(startStr) |> int_of_float; @@ -292,6 +270,36 @@ let%component make = ; }; + 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; + + + + int_of_float} + color=selectionColor + /> + + ; + }; + let text = () => textRef := Some(node)} @@ -302,6 +310,7 @@ let%component make = + diff --git a/src/Components/InputModel.re b/src/Components/InputModel.re index 2383163c99..dc79ca2e44 100644 --- a/src/Components/InputModel.re +++ b/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); @@ -28,13 +56,132 @@ let add = (~at as index, insert, text) => ( index + String.length(insert), ); -let handleInput = (~text, ~cursorPosition) => - fun - | "" => (text, max(0, cursorPosition - 1)) - | "" => (text, min(String.length(text), cursorPosition + 1)) - | "" => removeBefore(cursorPosition, text) - | "" => removeAfter(cursorPosition, text) - | "" => (text, 0) - | "" => (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)) { + | ("", true) => ( + text, + Selection.offsetLeft(selection) - 1 |> Selection.collapsed(~text), + ) + | ("", false) => ( + text, + Selection.offsetLeft(selection) |> Selection.collapsed(~text), + ) + | ("", true) => ( + text, + Selection.offsetLeft(selection) + 1 |> Selection.collapsed(~text), + ) + | ("", false) => ( + text, + Selection.offsetRight(selection) |> Selection.collapsed(~text), + ) + | ("", true) => removeCharBefore(text, selection) + | ("", false) => removeSelection(text, selection) + | ("", true) => removeCharAfter(text, selection) + | ("", false) => removeSelection(text, selection) + | ("", _) => (text, Selection.collapsed(~text, 0)) + | ("", _) => (text, Selection.collapsed(~text, String.length(text))) + | ("", _) => ( + text, + selection.focus - 1 |> Selection.extend(~text, ~selection), + ) + | ("", _) => ( + text, + selection.focus + 1 |> Selection.extend(~text, ~selection), + ) + | ("", _) => collapsePrevWord(text, selection) + | ("", _) => collapseNextWord(text, selection) + | ("", _) => (text, Selection.extend(~text, ~selection, 0)) + | ("", _) => ( + text, + Selection.extend(~text, ~selection, String.length(text)), + ) + | ("", _) => extendPrevWord(text, selection) + | ("", _) => extendNextWord(text, selection) + | ("", _) => ( + 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) + }; +}; diff --git a/src/Components/Selection.re b/src/Components/Selection.re new file mode 100644 index 0000000000..cd566e91e0 --- /dev/null +++ b/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); +}; diff --git a/src/Components/Selection.rei b/src/Components/Selection.rei new file mode 100644 index 0000000000..78cb5b86cd --- /dev/null +++ b/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; diff --git a/src/Feature/SCM/Feature_SCM.re b/src/Feature/SCM/Feature_SCM.re index 8b728a3a83..2fb1b4dd2f 100644 --- a/src/Feature/SCM/Feature_SCM.re +++ b/src/Feature/SCM/Feature_SCM.re @@ -3,6 +3,7 @@ open Utility; module InputModel = Oni_Components.InputModel; module ExtHostClient = Oni_Extensions.ExtHostClient; +module Selection = Oni_Components.Selection; // MODEL @@ -21,7 +22,7 @@ type model = { and inputBox = { value: string, - cursorPosition: int, + selection: Selection.t, placeholder: string, }; @@ -29,7 +30,7 @@ let initial = { providers: [], inputBox: { value: "", - cursorPosition: 0, + selection: Selection.initial, placeholder: "Do the commit thing!", }, }; @@ -91,7 +92,7 @@ type msg = command, }) | KeyPressed({key: string}) - | InputBoxClicked({cursorPosition: int}); + | InputBoxClicked({selection: Selection.t}); module Msg = { let keyPressed = key => KeyPressed({key: key}); @@ -300,10 +301,10 @@ let update = (extHostClient, model, msg) => ) | KeyPressed({key}) => - let (value, cursorPosition) = + let (value, selection) = InputModel.handleInput( ~text=model.inputBox.value, - ~cursorPosition=model.inputBox.cursorPosition, + ~selection=model.inputBox.selection, key, ); @@ -313,7 +314,7 @@ let update = (extHostClient, model, msg) => inputBox: { ...model.inputBox, value, - cursorPosition, + selection, }, }, Effect( @@ -330,12 +331,12 @@ let update = (extHostClient, model, msg) => ), ); - | InputBoxClicked({cursorPosition}) => ( + | InputBoxClicked({selection}) => ( { ...model, inputBox: { ...model.inputBox, - cursorPosition, + selection, }, }, Focus, @@ -534,10 +535,12 @@ module Pane = { style={Styles.input(~font)} cursorColor=Colors.gray value={model.inputBox.value} - cursorPosition={model.inputBox.cursorPosition} + selection={model.inputBox.selection} placeholder={model.inputBox.placeholder} isFocused - onClick={pos => dispatch(InputBoxClicked({cursorPosition: pos}))} + onClick={selection => + dispatch(InputBoxClicked({selection: selection})) + } /> {groups |> List.map(((provider, group)) => diff --git a/src/Feature/Search/Feature_Search.re b/src/Feature/Search/Feature_Search.re index 2e78a2eab0..b7db9ccec0 100644 --- a/src/Feature/Search/Feature_Search.re +++ b/src/Feature/Search/Feature_Search.re @@ -8,18 +8,23 @@ open Oni_Components; type model = { queryInput: string, query: string, - cursorPosition: int, + selection: Selection.t, hits: list(Ripgrep.Match.t), }; -let initial = {queryInput: "", query: "", cursorPosition: 0, hits: []}; +let initial = { + queryInput: "", + query: "", + selection: Selection.initial, + hits: [], +}; // UPDATE [@deriving show({with_path: false})] type msg = | Input(string) - | InputClicked(int) + | InputClicked(Selection.t) | Update([@opaque] list(Ripgrep.Match.t)) | Complete; @@ -29,7 +34,7 @@ type outmsg = let update = (model, msg) => { switch (msg) { | Input(key) => - let {queryInput, cursorPosition, _} = model; + let {queryInput, selection, _} = model; let model = switch (key) { @@ -41,17 +46,14 @@ let update = (model, msg) => { } | _ => - let (queryInput, cursorPosition) = - InputModel.handleInput(~text=queryInput, ~cursorPosition, key); - {...model, queryInput, cursorPosition}; + let (queryInput, selection) = + InputModel.handleInput(~text=queryInput, ~selection, key); + {...model, queryInput, selection}; }; (model, None); - | InputClicked(cursorPosition) => ( - {...model, cursorPosition}, - Some(Focus), - ) + | InputClicked(selection) => ({...model, selection}, Some(Focus)) | Update(items) => ({...model, hits: model.hits @ items}, None) @@ -174,11 +176,11 @@ let make = dispatch(InputClicked(pos))} + onClick={selection => dispatch(InputClicked(selection))} /> diff --git a/src/Feature/Search/Feature_Search.rei b/src/Feature/Search/Feature_Search.rei index a2bc4a4d5d..c77bdde811 100644 --- a/src/Feature/Search/Feature_Search.rei +++ b/src/Feature/Search/Feature_Search.rei @@ -1,6 +1,7 @@ open EditorCoreTypes; open Oni_Core; open Revery.UI; +open Oni_Components; type model; @@ -9,7 +10,7 @@ let initial: model; [@deriving show] type msg = | Input(string) - | InputClicked(int) + | InputClicked(Selection.t) | Update([@opaque] list(Ripgrep.Match.t)) | Complete; diff --git a/src/Model/Actions.re b/src/Model/Actions.re index 79b86acd76..7dc8bc8588 100644 --- a/src/Model/Actions.re +++ b/src/Model/Actions.re @@ -8,6 +8,7 @@ open EditorCoreTypes; open Oni_Core; open Oni_Input; open Oni_Syntax; +open Oni_Components; module Ext = Oni_Extensions; module ContextMenu = Oni_Components.ContextMenu; @@ -91,7 +92,7 @@ type t = | LanguageFeature(LanguageFeatures.action) | QuickmenuShow(quickmenuVariant) | QuickmenuInput(string) - | QuickmenuInputClicked(int) + | QuickmenuInputClicked(Selection.t) | QuickmenuCommandlineUpdated(string, int) | QuickmenuUpdateRipgrepProgress(progress) | QuickmenuUpdateFilterProgress([@opaque] array(menuItem), progress) diff --git a/src/Model/Quickmenu.re b/src/Model/Quickmenu.re index ac3c76c22f..1ece2ead32 100644 --- a/src/Model/Quickmenu.re +++ b/src/Model/Quickmenu.re @@ -1,10 +1,11 @@ open Actions; +module Selection = Oni_Components.Selection; type t = { variant, query: string, prefix: option(string), - cursorPosition: int, + selection: Selection.t, items: array(menuItem), filterProgress: progress, ripgrepProgress: progress, @@ -24,7 +25,7 @@ let defaults = variant => { variant, query: "", prefix: None, - cursorPosition: 0, + selection: Selection.initial, focused: None, items: [||], filterProgress: Complete, diff --git a/src/Model/Sneak.re b/src/Model/Sneak.re index c5e567748a..1278615048 100644 --- a/src/Model/Sneak.re +++ b/src/Model/Sneak.re @@ -2,6 +2,7 @@ open Revery.Math; module InputModel = Oni_Components.InputModel; module StringEx = Oni_Core.Utility.StringEx; +module Selection = Oni_Components.Selection; type callback = unit => unit; type bounds = unit => option(BoundingBox2d.t); @@ -84,13 +85,11 @@ module Internal = { let refine = (characterToAdd: string, sneaks: t) => { let characterToAdd = String.uppercase_ascii(characterToAdd); + let selection = + String.length(sneaks.prefix) |> Selection.collapsed(~text=sneaks.prefix); let (prefix, _) = - InputModel.handleInput( - ~text=sneaks.prefix, - ~cursorPosition=String.length(sneaks.prefix), - characterToAdd, - ); + InputModel.handleInput(~text=sneaks.prefix, ~selection, characterToAdd); {...sneaks, prefix} |> Internal.applyFilter; }; diff --git a/src/Store/InputStoreConnector.re b/src/Store/InputStoreConnector.re index bfda4db8b2..48ca7889a2 100644 --- a/src/Store/InputStoreConnector.re +++ b/src/Store/InputStoreConnector.re @@ -6,6 +6,7 @@ open Oni_Core; open Oni_Input; +open Oni_Components; module Model = Oni_Model; module State = Model.State; @@ -21,11 +22,12 @@ let conditionsOfState = (state: State.t) => { let ret: Hashtbl.t(string, bool) = Hashtbl.create(16); switch (state.quickmenu) { - | Some({variant, query, cursorPosition, _}) => + | Some({variant, query, selection, _}) => Hashtbl.add(ret, "listFocus", true); Hashtbl.add(ret, "inQuickOpen", true); - if (cursorPosition == String.length(query)) { + if (Selection.isCollapsed(selection) + && selection.focus == String.length(query)) { Hashtbl.add(ret, "quickmenuCursorEnd", true); }; diff --git a/src/Store/QuickmenuStoreConnector.re b/src/Store/QuickmenuStoreConnector.re index 722d1ce222..9614ebe05d 100644 --- a/src/Store/QuickmenuStoreConnector.re +++ b/src/Store/QuickmenuStoreConnector.re @@ -149,30 +149,30 @@ let start = (themeInfo: ThemeInfo.t) => { | QuickmenuInput(key) => ( Option.map( - (Quickmenu.{query, cursorPosition, _} as state) => { - let (text, cursorPosition) = - InputModel.handleInput(~text=query, ~cursorPosition, key); + (Quickmenu.{query, selection, _} as state) => { + let (text, selection) = + InputModel.handleInput(~text=query, ~selection, key); - Quickmenu.{ - ...state, - query: text, - cursorPosition, - focused: Some(0), - }; + Quickmenu.{...state, query: text, selection, focused: Some(0)}; }, state, ), Isolinear.Effect.none, ) - | QuickmenuInputClicked(cursorPosition) => ( - Option.map(state => Quickmenu.{...state, cursorPosition}, state), + | QuickmenuInputClicked(selection) => ( + Option.map(state => Quickmenu.{...state, selection}, state), Isolinear.Effect.none, ) | QuickmenuCommandlineUpdated(text, cursorPosition) => ( Option.map( - state => Quickmenu.{...state, query: text, cursorPosition}, + state => + Quickmenu.{ + ...state, + query: text, + selection: Selection.collapsed(~text, cursorPosition), + }, state, ), Isolinear.Effect.none, diff --git a/src/UI/QuickmenuView.re b/src/UI/QuickmenuView.re index 066c0f8ca7..09325d0394 100644 --- a/src/UI/QuickmenuView.re +++ b/src/UI/QuickmenuView.re @@ -53,8 +53,8 @@ module Styles = { let onFocusedChange = index => GlobalContext.current().dispatch(ListFocus(index)); -let onInputClicked = cursorPosition => - GlobalContext.current().dispatch(QuickmenuInputClicked(cursorPosition)); +let onInputClicked = selection => + GlobalContext.current().dispatch(QuickmenuInputClicked(selection)); let onSelect = _ => GlobalContext.current().dispatch(ListSelect); @@ -107,7 +107,7 @@ let make = ripgrepProgress, focused, query, - cursorPosition, + selection, prefix, variant, _, @@ -161,7 +161,7 @@ let make = isFocused=true onClick=onInputClicked value=query - cursorPosition + selection /> ; @@ -184,7 +184,10 @@ let make = | EditorsPicker => React.empty | _ => }} - + {switch (variant) { + | Wildmenu(SearchForward | SearchReverse) => React.empty + | _ => + }} ; diff --git a/test/Components/InputModelTests.re b/test/Components/InputModelTests.re new file mode 100644 index 0000000000..54a174837b --- /dev/null +++ b/test/Components/InputModelTests.re @@ -0,0 +1,1240 @@ +open TestFramework; + +module InputModel = Oni_Components.InputModel; +module Selection = Oni_Components.Selection; + +let testString = "Some interesting. Test. String. Isn't it? Maybe"; +let testStringLength = String.length(testString); + +let runInputHandler = (~text=testString, selection, key) => { + InputModel.handleInput(~text, ~selection, key); +}; + +let collapsedSelection = (~text=testString, position) => { + Selection.create(~text, ~anchor=position, ~focus=position); +}; + +let notCollapsedSelection = (~text=testString, ~anchor, ~focus, ()) => { + Selection.create(~text, ~anchor, ~focus); +}; + +describe("handleInputS#handleInput", ({describe, _}) => { + describe("When LEFT with no selection", ({test, _}) => { + let key = ""; + + test("Moves cursor left for 1 character", ({expect}) => { + let selection = collapsedSelection(4); + let expected = collapsedSelection(3); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor less then 0 position", ({expect}) => { + let selection = collapsedSelection(0); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + }); + + describe("When LEFT with selection", ({test, _}) => { + let key = ""; + + test( + "Moves cursor to the beginning of selection when cursor comes first", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=4, ~focus=2, ()); + let expected = notCollapsedSelection(~anchor=2, ~focus=2, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to the beginning of selection when cursor comes last", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=2, ~focus=4, ()); + let expected = notCollapsedSelection(~anchor=2, ~focus=2, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to the beginning of selection when cursor at the beginning", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=2, ~focus=4, ()); + let expected = notCollapsedSelection(~anchor=2, ~focus=2, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + }); + + describe("When with no selection", ({test, _}) => { + let key = ""; + + test("Moves cursor right for 1 character", ({expect}) => { + let selection = collapsedSelection(4); + let expected = collapsedSelection(5); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position more that string length", ({expect}) => { + let selection = collapsedSelection(testStringLength); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + }); + + describe("When RIGHT with selection", ({test, _}) => { + let key = ""; + + test( + "Moves cursor to the end of selection when cursor comes first", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=2, ~focus=4, ()); + let expected = notCollapsedSelection(~anchor=4, ~focus=4, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to the end of selection when cursor comes last", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=4, ~focus=2, ()); + let expected = notCollapsedSelection(~anchor=4, ~focus=4, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to the end of selection when cursor at the beginning", + ({expect}) => { + let selection = + notCollapsedSelection(~anchor=testStringLength, ~focus=0, ()); + let expected = + notCollapsedSelection( + ~anchor=testStringLength, + ~focus=testStringLength, + (), + ); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + }); + + describe("When with no selection", ({test, _}) => { + let key = ""; + + test("Moves cursor right for 1 character", ({expect}) => { + let selection = collapsedSelection(4); + let expected = collapsedSelection(5); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position more that string length", ({expect}) => { + let selection = collapsedSelection(testStringLength); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + }); + + describe("When with no selection", ({test, _}) => { + let key = ""; + + test("Removes character on the left of cursor", ({expect}) => { + let selection = collapsedSelection(4); + let expected = collapsedSelection(3); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual( + "Som interesting. Test. String. Isn't it? Maybe", + ); + }); + + test("Doesn't remove character if cursor at the beginnig", ({expect}) => { + let selection = collapsedSelection(0); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Don't do anything for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", -1); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + }); + + describe("When with with selection", ({test, _}) => { + let key = ""; + + test("Removes selection when cursor comes first", ({expect}) => { + let selection = notCollapsedSelection(~anchor=4, ~focus=2, ()); + let expected = notCollapsedSelection(~anchor=2, ~focus=2, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual( + "So interesting. Test. String. Isn't it? Maybe", + ); + }); + + test("Removes selection when cursor comes last", ({expect}) => { + let selection = notCollapsedSelection(~anchor=2, ~focus=4, ()); + let expected = notCollapsedSelection(~anchor=2, ~focus=2, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual( + "So interesting. Test. String. Isn't it? Maybe", + ); + }); + }); + + describe("When with no selection", ({test, _}) => { + let key = ""; + + test("Removes character on the right of cursor", ({expect}) => { + let selection = collapsedSelection(4); + let expected = collapsedSelection(4); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual( + "Someinteresting. Test. String. Isn't it? Maybe", + ); + }); + + test("Doesn't remove character if cursor at the end", ({expect}) => { + let selection = collapsedSelection(testStringLength); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Don't do anything for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + }); + + describe("When with with selection", ({test, _}) => { + let key = ""; + + test("Removes selection when cursor comes first", ({expect}) => { + let selection = notCollapsedSelection(~anchor=4, ~focus=2, ()); + let expected = notCollapsedSelection(~anchor=2, ~focus=2, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual( + "So interesting. Test. String. Isn't it? Maybe", + ); + }); + + test("Removes selection when cursor comes last", ({expect}) => { + let selection = notCollapsedSelection(~anchor=2, ~focus=4, ()); + let expected = notCollapsedSelection(~anchor=2, ~focus=2, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual( + "So interesting. Test. String. Isn't it? Maybe", + ); + }); + }); + + describe("When HOME with no selection", ({test, _}) => { + let key = ""; + + test("Moves cursor to the beginning", ({expect}) => { + let selection = collapsedSelection(4); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor if it's at the beginning", ({expect}) => { + let selection = collapsedSelection(0); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + }); + + describe("When HOME with selection", ({test, _}) => { + let key = ""; + + test("Moves cursor to the beginning and discard selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=12, ~focus=5, ()); + let expected = notCollapsedSelection(~anchor=0, ~focus=0, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Doesn't move cursor if it's at the beginning and discard selection", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=8, ~focus=0, ()); + let expected = notCollapsedSelection(~anchor=0, ~focus=0, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + }); + + describe("When END with no selection", ({test, _}) => { + let key = ""; + + test("Moves cursor to the end", ({expect}) => { + let selection = collapsedSelection(4); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor if it's at the end", ({expect}) => { + let selection = collapsedSelection(testStringLength); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + }); + + describe("When END with selection", ({test, _}) => { + let key = ""; + + test("Moves cursor to the end and discard selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=11, ~focus=5, ()); + let expected = + notCollapsedSelection( + ~anchor=testStringLength, + ~focus=testStringLength, + (), + ); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Doesn't move cursor if it's at the end and discard selection", + ({expect}) => { + let selection = + notCollapsedSelection(~anchor=testStringLength, ~focus=4, ()); + let expected = + notCollapsedSelection( + ~anchor=testStringLength, + ~focus=testStringLength, + (), + ); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + }); + + describe("When S-LEFT", ({test, _}) => { + let key = ""; + + test("Moves cursor to 1 character left and add selection", ({expect}) => { + let selection = collapsedSelection(4); + let expected = notCollapsedSelection(~anchor=4, ~focus=3, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to 1 character left and increase selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=11, ~focus=5, ()); + let expected = notCollapsedSelection(~anchor=11, ~focus=4, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position when it at the beginning", ({expect}) => { + let selection = notCollapsedSelection(~anchor=5, ~focus=0, ()); + let expected = notCollapsedSelection(~anchor=5, ~focus=0, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Doesn't move cursor position when it at the beginning and no selection", + ({expect}) => { + let selection = collapsedSelection(0); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + + test("Moves cursor to 1 character left and undo selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=4, ~focus=5, ()); + let expected = collapsedSelection(4); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to 1 character left and decrease selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=4, ~focus=8, ()); + let expected = notCollapsedSelection(~anchor=4, ~focus=7, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + }); + + describe("When S-RIGHT", ({test, _}) => { + let key = ""; + + test("Moves cursor to 1 character right and add selection", ({expect}) => { + let selection = collapsedSelection(4); + let expected = notCollapsedSelection(~anchor=4, ~focus=5, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to 1 character right and increase selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=4, ~focus=11, ()); + let expected = notCollapsedSelection(~anchor=4, ~focus=12, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position when it at the end", ({expect}) => { + let selection = + notCollapsedSelection(~anchor=5, ~focus=testStringLength, ()); + let expected = + notCollapsedSelection(~anchor=5, ~focus=testStringLength, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Doesn't move cursor position when it at the end and no selection", + ({expect}) => { + let selection = collapsedSelection(testStringLength); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + + test("Moves cursor to 1 character right and undo selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=6, ~focus=5, ()); + let expected = collapsedSelection(6); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to 1 character right and decrease selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=8, ~focus=3, ()); + let expected = notCollapsedSelection(~anchor=8, ~focus=4, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + }); + + describe("When S-HOME", ({test, _}) => { + let key = ""; + + test("Moves cursor to the beginning and add selection", ({expect}) => { + let selection = collapsedSelection(4); + let expected = notCollapsedSelection(~anchor=4, ~focus=0, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Moves cursor to the beginning and increase selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=7, ~focus=4, ()); + let expected = notCollapsedSelection(~anchor=7, ~focus=0, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position when it at the beginning", ({expect}) => { + let selection = notCollapsedSelection(~anchor=5, ~focus=0, ()); + let expected = notCollapsedSelection(~anchor=5, ~focus=0, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Doesn't move cursor position when it at the beginning and no selection", + ({expect}) => { + let selection = collapsedSelection(0); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + + test("Moves cursor to the beginning and undo selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=0, ~focus=6, ()); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Moves cursor to the beginning and decrease selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=3, ~focus=8, ()); + let expected = notCollapsedSelection(~anchor=3, ~focus=0, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + }); + + describe("When S-END", ({test, _}) => { + let key = ""; + + test("Moves cursor to the end and add selection", ({expect}) => { + let selection = collapsedSelection(4); + let expected = + notCollapsedSelection(~anchor=4, ~focus=testStringLength, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Moves cursor to the end and increase selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=4, ~focus=8, ()); + let expected = + notCollapsedSelection(~anchor=4, ~focus=testStringLength, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position when it at the end", ({expect}) => { + let selection = + notCollapsedSelection(~anchor=5, ~focus=testStringLength, ()); + let expected = + notCollapsedSelection(~anchor=5, ~focus=testStringLength, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Doesn't move cursor position when it at the and and no selection", + ({expect}) => { + let selection = collapsedSelection(testStringLength); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(0); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + + test("Moves cursor to the end and undo selection", ({expect}) => { + let selection = + notCollapsedSelection(~anchor=testStringLength, ~focus=6, ()); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Moves cursor to the end and decrease selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=7, ~focus=3, ()); + let expected = + notCollapsedSelection(~anchor=7, ~focus=testStringLength, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + }); + + describe("When S-C-LEFT", ({test, _}) => { + let key = ""; + + test("Moves cursor to previous word boundary", ({expect}) => { + let selection = collapsedSelection(10); + let expected = notCollapsedSelection(~anchor=10, ~focus=5, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Moves cursor to beginning", ({expect}) => { + let selection = collapsedSelection(3); + let expected = notCollapsedSelection(~anchor=3, ~focus=0, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position when it at the beginning", ({expect}) => { + let selection = notCollapsedSelection(~anchor=10, ~focus=0, ()); + let expected = notCollapsedSelection(~anchor=10, ~focus=0, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Doesn't move cursor position when it at the beginning and no selection", + ({expect}) => { + let selection = collapsedSelection(0); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + + test( + "Moves cursor to the previous word boundary and undo selection", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=5, ~focus=16, ()); + let expected = notCollapsedSelection(~anchor=5, ~focus=5, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to the previous word boundary and decrease selection", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=11, ~focus=16, ()); + let expected = notCollapsedSelection(~anchor=11, ~focus=5, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + }); + + describe("When C-LEFT", ({test, _}) => { + let key = ""; + + test("Moves cursor to previous word boundary", ({expect}) => { + let selection = collapsedSelection(10); + let expected = collapsedSelection(5); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Moves cursor to beginning", ({expect}) => { + let selection = collapsedSelection(3); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position when it at the beginning", ({expect}) => { + let selection = notCollapsedSelection(~anchor=10, ~focus=0, ()); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Doesn't move cursor position when it at the beginning and no selection", + ({expect}) => { + let selection = collapsedSelection(0); + let expected = collapsedSelection(0); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + + test( + "Moves cursor to the previous word boundary and undo selection", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=5, ~focus=16, ()); + let expected = collapsedSelection(5); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to the previous word boundary and decrease selection", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=11, ~focus=16, ()); + let expected = collapsedSelection(5); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + }); + + describe("When S-C-RIGHT", ({test, _}) => { + let key = ""; + + test("Moves cursor to next word boundary", ({expect}) => { + let selection = collapsedSelection(10); + let expected = notCollapsedSelection(~anchor=10, ~focus=16, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Moves cursor to end", ({expect}) => { + let selection = collapsedSelection(44); + let expected = + notCollapsedSelection(~anchor=44, ~focus=testStringLength, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position when it at the end", ({expect}) => { + let selection = + notCollapsedSelection(~anchor=10, ~focus=testStringLength, ()); + let expected = + notCollapsedSelection(~anchor=10, ~focus=testStringLength, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Doesn't move cursor position when it at the and and no selection", + ({expect}) => { + let selection = collapsedSelection(testStringLength); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + + test( + "Moves cursor to the next word boundary and undo selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=16, ~focus=5, ()); + let expected = notCollapsedSelection(~anchor=16, ~focus=16, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to the next word boundary and decrease selection", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=10, ~focus=6, ()); + let expected = notCollapsedSelection(~anchor=10, ~focus=16, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + }); + + describe("When C-RIGHT", ({test, _}) => { + let key = ""; + + test("Moves cursor to next word boundary", ({expect}) => { + let selection = collapsedSelection(10); + let expected = collapsedSelection(16); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Moves cursor to end", ({expect}) => { + let selection = collapsedSelection(44); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position when it at the end", ({expect}) => { + let selection = + notCollapsedSelection(~anchor=10, ~focus=testStringLength, ()); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Doesn't move cursor position when it at the and and no selection", + ({expect}) => { + let selection = collapsedSelection(testStringLength); + let expected = collapsedSelection(testStringLength); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Doesn't move cursor position for blank string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + + test( + "Moves cursor to the next word boundary and undo selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=16, ~focus=5, ()); + let expected = collapsedSelection(16); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test( + "Moves cursor to the next word boundary and decrease selection", + ({expect}) => { + let selection = notCollapsedSelection(~anchor=10, ~focus=6, ()); + let expected = collapsedSelection(16); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + }); + + describe("When ASCII letter when no selection", ({test, _}) => { + let key = "F"; + + test("Adds character to the beginning", ({expect}) => { + let selection = collapsedSelection(0); + let expected = collapsedSelection(1); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.string(text).toEqual( + "FSome interesting. Test. String. Isn't it? Maybe", + ); + expect.equal(expected, newSelection); + }); + + test("Adds character to the end", ({expect}) => { + let selection = collapsedSelection(testStringLength); + let expected = + collapsedSelection(~text=testString ++ key, testStringLength + 1); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual( + "Some interesting. Test. String. Isn't it? MaybeF", + ); + }); + + test("Adds character to the cursor position", ({expect}) => { + let selection = collapsedSelection(7); + let expected = collapsedSelection(8); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual( + "Some inFteresting. Test. String. Isn't it? Maybe", + ); + }); + }); + + describe("When ASCII letter when with selection", ({test, _}) => { + let key = "F"; + + test("Replaces character", ({expect}) => { + let selection = notCollapsedSelection(~anchor=0, ~focus=1, ()); + let expected = collapsedSelection(1); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual( + "Fome interesting. Test. String. Isn't it? Maybe", + ); + }); + + test("Adds character many characters", ({expect}) => { + let selection = notCollapsedSelection(~anchor=16, ~focus=4, ()); + let expected = collapsedSelection(5); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual("SomeF. Test. String. Isn't it? Maybe"); + }); + + test("Replaces all string", ({expect}) => { + let selection = + notCollapsedSelection(~anchor=testStringLength, ~focus=0, ()); + let expected = collapsedSelection(1); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual("F"); + }); + }); + + describe("When C-a", ({test, _}) => { + let key = ""; + + test("Select all when no selection", ({expect}) => { + let selection = collapsedSelection(3); + let expected = + notCollapsedSelection(~anchor=0, ~focus=testStringLength, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Select all when is selection", ({expect}) => { + let selection = notCollapsedSelection(~anchor=5, ~focus=24, ()); + let expected = + notCollapsedSelection(~anchor=0, ~focus=testStringLength, ()); + + let (text, newSelection) = runInputHandler(selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(testString); + }); + + test("Selects nothing with empty string", ({expect}) => { + let selection = collapsedSelection(~text="", 0); + let expected = collapsedSelection(~text="", 0); + + let (text, newSelection) = runInputHandler(~text="", selection, key); + + expect.equal(expected, newSelection); + expect.string(text).toEqual(""); + }); + }); +}); diff --git a/test/Components/SelectionTests.re b/test/Components/SelectionTests.re new file mode 100644 index 0000000000..b7cfdcec52 --- /dev/null +++ b/test/Components/SelectionTests.re @@ -0,0 +1,190 @@ +open TestFramework; + +module Selection = Oni_Components.Selection; + +let testString = "Some Strin"; +let testStringLength = String.length(testString); +let create = Selection.create(~text=testString); + +describe("Selection#initial", ({test, _}) => { + test("Returns initial value", ({expect}) => { + let result = Selection.initial; + + expect.int(result.anchor).toBe(0); + expect.int(result.focus).toBe(0); + }) +}); + +describe("Selection#create", ({test, _}) => { + test("Returns valid selection", ({expect}) => { + let result = create(~anchor=3, ~focus=3); + + expect.int(result.anchor).toBe(3); + expect.int(result.focus).toBe(3); + }); + + test("Handle different values", ({expect}) => { + let result = create(~anchor=3, ~focus=6); + + expect.int(result.anchor).toBe(3); + expect.int(result.focus).toBe(6); + }); + + test("Handle values below 0", ({expect}) => { + let result = create(~anchor=-1, ~focus=-3); + + expect.int(result.anchor).toBe(0); + expect.int(result.focus).toBe(0); + }); + + test("Handle above length", ({expect}) => { + let result = create(~anchor=70, ~focus=55); + + expect.int(result.anchor).toBe(testStringLength); + expect.int(result.focus).toBe(testStringLength); + }); +}); + +describe("Selection#length", ({test, _}) => { + test("Returns range when anchor comes first", ({expect}) => { + let result = Selection.length(create(~anchor=3, ~focus=5)); + + expect.int(result).toBe(2); + }); + + test("Returns range when anchor comes last", ({expect}) => { + let result = Selection.length(create(~anchor=5, ~focus=3)); + + expect.int(result).toBe(2); + }); + + test("Returns 0 for collapsed selection", ({expect}) => { + let result = Selection.length(create(~anchor=3, ~focus=3)); + + expect.int(result).toBe(0); + }); +}); + +describe("Selection#offsetLeft", ({test, _}) => { + test("Returns anchor", ({expect}) => { + let result = Selection.offsetLeft(create(~anchor=3, ~focus=5)); + + expect.int(result).toBe(3); + }); + + test("Returns focus", ({expect}) => { + let result = Selection.offsetLeft(create(~anchor=5, ~focus=3)); + + expect.int(result).toBe(3); + }); + + test("Returns any", ({expect}) => { + let result = Selection.offsetLeft(create(~anchor=3, ~focus=3)); + + expect.int(result).toBe(3); + }); +}); + +describe("Selection#offsetRight", ({test, _}) => { + test("Returns anchor", ({expect}) => { + let result = Selection.offsetRight(create(~anchor=5, ~focus=3)); + + expect.int(result).toBe(5); + }); + + test("Returns focus", ({expect}) => { + let result = Selection.offsetRight(create(~anchor=3, ~focus=5)); + + expect.int(result).toBe(5); + }); + + test("Returns any", ({expect}) => { + let result = Selection.offsetRight(create(~anchor=3, ~focus=3)); + + expect.int(result).toBe(3); + }); +}); + +describe("Selection#isCollapsed", ({test, _}) => { + test("Returns true", ({expect}) => { + let result = Selection.isCollapsed(create(~anchor=3, ~focus=3)); + + expect.bool(result).toBe(true); + }); + + test("Returns false", ({expect}) => { + let result = Selection.isCollapsed(create(~anchor=3, ~focus=7)); + + expect.bool(result).toBe(false); + }); +}); + +describe("Selection#collapse", ({test, _}) => { + let collapse = Selection.collapsed(~text=testString); + + test("Collapse selection with offset", ({expect}) => { + let result = collapse(3); + + expect.int(result.anchor).toBe(3); + expect.int(result.focus).toBe(3); + }); + + test("Collapse selection with offset less then 0", ({expect}) => { + let result = collapse(-20); + + expect.int(result.anchor).toBe(0); + expect.int(result.focus).toBe(0); + }); + + test("Collapse selection with offset is more then length", ({expect}) => { + let result = collapse(testStringLength + 70); + + expect.int(result.anchor).toBe(testStringLength); + expect.int(result.focus).toBe(testStringLength); + }); +}); + +describe("Selection#extend", ({test, _}) => { + let extend = Selection.extend(~text=testString); + + test("Extend when selection is collapsed", ({expect}) => { + let selection = create(~anchor=3, ~focus=3); + let result = extend(~selection, 5); + + expect.int(result.anchor).toBe(3); + expect.int(result.focus).toBe(5); + }); + + test("Extend when selection is not collapsed", ({expect}) => { + let selection = create(~anchor=3, ~focus=8); + let result = extend(~selection, 5); + + expect.int(result.anchor).toBe(3); + expect.int(result.focus).toBe(5); + }); + + test( + "Doesn't extend when selection is not collapsed in offset", ({expect}) => { + let selection = create(~anchor=3, ~focus=3); + let result = extend(~selection, 3); + + expect.int(result.anchor).toBe(3); + expect.int(result.focus).toBe(3); + }); + + test("Extends when offset is less than 0", ({expect}) => { + let selection = create(~anchor=3, ~focus=3); + let result = extend(~selection, -3); + + expect.int(result.anchor).toBe(3); + expect.int(result.focus).toBe(0); + }); + + test("Extends when offset is more than length", ({expect}) => { + let selection = create(~anchor=3, ~focus=3); + let result = extend(~selection, testStringLength + 70); + + expect.int(result.anchor).toBe(3); + expect.int(result.focus).toBe(testStringLength); + }); +}); diff --git a/test/Components/TestFramework.re b/test/Components/TestFramework.re new file mode 100644 index 0000000000..7ffdff90b0 --- /dev/null +++ b/test/Components/TestFramework.re @@ -0,0 +1,4 @@ +include Rely.Make({ + let config = + Rely.TestFrameworkConfig.initialize({snapshotDir: "", projectDir: ""}); +}); diff --git a/test/Components/dune b/test/Components/dune new file mode 100644 index 0000000000..52935a37ca --- /dev/null +++ b/test/Components/dune @@ -0,0 +1,5 @@ +(library + (name Oni_Components_Test) + (library_flags (-linkall -g)) + (modules (:standard)) + (libraries Oni2.components rely.lib)) diff --git a/test/OniUnitTestRunner.re b/test/OniUnitTestRunner.re index 8a8734fa82..98408ae628 100644 --- a/test/OniUnitTestRunner.re +++ b/test/OniUnitTestRunner.re @@ -9,3 +9,4 @@ Oni_ExtensionManagement_Test.TestFramework.cli(); Oni_Syntax_Test.TestFramework.cli(); Feature_Editor_Test.TestFramework.cli(); Feature_LanguageSupport_Test.TestFramework.cli(); +Oni_Components_Test.TestFramework.cli(); diff --git a/test/OniUnitTestRunnerCI.re b/test/OniUnitTestRunnerCI.re index 4413b35116..1f07b41b0b 100644 --- a/test/OniUnitTestRunnerCI.re +++ b/test/OniUnitTestRunnerCI.re @@ -46,3 +46,10 @@ Oni_ExtensionManagement_Test.TestFramework.run( Rely.RunConfig.initialize(), ), ); + +Oni_Components_Test.TestFramework.run( + Rely.RunConfig.withReporters( + [Default, JUnit("./junit.xml")], + Rely.RunConfig.initialize(), + ), +); diff --git a/test/dune b/test/dune index bfc63440b3..03f6a36ddb 100644 --- a/test/dune +++ b/test/dune @@ -6,6 +6,7 @@ (libraries yojson Oni_Core_Test + Oni_Components_Test Oni_Extensions_Test Oni_ExtensionManagement_Test Oni_Input_Test @@ -63,6 +64,7 @@ (libraries yojson Oni_Core_Test + Oni_Components_Test Oni_ExtensionManagement_Test Oni_Extensions_Test Oni_Input_Test