From 28c5b1f65a56ae3c3b6d23c972697a6db1f2c8a5 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 22 May 2017 21:24:35 +0200 Subject: [PATCH] Generate screen reader explanations for cursor movements --- .../editor/common/controller/accGenerator.ts | 194 ++++ src/vs/editor/common/controller/cursor.ts | 17 +- src/vs/editor/common/view/viewEvents.ts | 7 +- .../wordOperations/common/wordOperations.ts | 4 +- .../test/common/controller/cursor.test.ts | 997 +++++++++++++++++- 5 files changed, 1212 insertions(+), 7 deletions(-) create mode 100644 src/vs/editor/common/controller/accGenerator.ts diff --git a/src/vs/editor/common/controller/accGenerator.ts b/src/vs/editor/common/controller/accGenerator.ts new file mode 100644 index 00000000000..6f6bb813e6b --- /dev/null +++ b/src/vs/editor/common/controller/accGenerator.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { Position } from 'vs/editor/common/core/position'; +import * as nls from 'vs/nls'; +import { Range } from 'vs/editor/common/core/range'; +import { IModel } from "vs/editor/common/editorCommon"; +import { Selection } from 'vs/editor/common/core/selection'; + +export class ScreenReaderMessageGenerator { + + public static xSelected(x: string): string { + return nls.localize( + { + key: 'x.selected', + comment: ['A piece of text was added to the selection (this should be a message suitable for a Screen Reader).'] + }, + "{0}\nSelected", + x + ); + } + + public static xUnselected(x: string): string { + return nls.localize( + { + key: 'x.unselected', + comment: ['A piece of text was removed from the selection (this should be a message suitable for a Screen Reader).'] + }, + "{0}\nUnselected", + x + ); + } + + public static xCharsSelected(x: number): string { + return nls.localize( + { + key: 'x.chars.selected', + comment: ['A large number of characters were added to the selection (this should be a message suitable for a Screen Reader).'] + }, + "{0}\nCharacters selected", + x + ); + } + + public static xCharsUnselected(x: number): string { + return nls.localize( + { + key: 'x.chars.unselected', + comment: ['A large number of characters were removed from the selection (this should be a message suitable for a Screen Reader).'] + }, + "{0}\nCharacters unselected", + x + ); + } + + public static generateMessage(source: string, model: IModel, oldModelId: number, oldSelection: Selection, newModelId: number, newSelection: Selection): string { + if (oldModelId === newModelId) { + return this._cursorChangeMessage(source, model, oldSelection, newSelection); + } + return 'TODO'; + } + + private static _cursorChangeMessage(source: string, model: IModel, oldSelection: Selection, newSelection: Selection): string { + if (oldSelection.equalsRange(newSelection)) { + return ''; + } + + if (oldSelection.isEmpty()) { + + if (newSelection.isEmpty()) { + // ...[]... => ...[]... + return this._cursorMoveMessage(source, model, oldSelection.getPosition(), newSelection.getPosition()); + } + + // ...[]... => ...[x]...: + return this._cursorSelectedMessage(model, newSelection); + } + + if (newSelection.isEmpty()) { + if (oldSelection.containsPosition(newSelection.getPosition())) { + // ...a[xy]b... => ...a[]xyb... or ...ax[]yb... or ...axy[]b... + return this._cursorUnselectedMessage(model, oldSelection); + } + + // moved away from the old selection and collapsed it + return this._cursorMoveMessage(source, model, oldSelection.getPosition(), newSelection.getPosition()) + '\n' + this._cursorUnselectedMessage(model, oldSelection); + } + + // ...[x]... => ...[y]... + + if (newSelection.getStartPosition().equals(oldSelection.getStartPosition())) { + + // ...a[x]... => ...a[y]... + + if (newSelection.getEndPosition().isBefore(oldSelection.getEndPosition())) { + // ...a[xy]... => ...a[x]y... + return this._cursorUnselectedMessage(model, new Range(newSelection.endLineNumber, newSelection.endColumn, oldSelection.endLineNumber, oldSelection.endColumn)); + + } + + // ...a[x]y... => ...a[xy]... + return this._cursorSelectedMessage(model, new Range(oldSelection.endLineNumber, oldSelection.endColumn, newSelection.endLineNumber, newSelection.endColumn)); + + } + + if (newSelection.getEndPosition().equals(oldSelection.getEndPosition())) { + + // ...[x]a... => ...[y]a... + + if (newSelection.getStartPosition().isBefore(oldSelection.getStartPosition())) { + // ...y[x]a... => ...[yx]a... + return this._cursorSelectedMessage(model, new Range(newSelection.startLineNumber, newSelection.startColumn, oldSelection.startLineNumber, oldSelection.startColumn)); + } + + // ...[yx]a... => ...y[x]a... + return this._cursorUnselectedMessage(model, new Range(oldSelection.startLineNumber, oldSelection.startColumn, newSelection.startLineNumber, newSelection.startColumn)); + + } + + // weird jump + return this._cursorSelectedMessage(model, newSelection) + '\n' + this._cursorUnselectedMessage(model, oldSelection); + + } + + private static _cursorMoveMessage(source: string, model: IModel, oldPosition: Position, newPosition: Position): string { + + if (source === 'moveWordCommand') { + return model.getValueInRange(new Range(oldPosition.lineNumber, oldPosition.column, newPosition.lineNumber, newPosition.column)); + } + + const oldLineNumber = oldPosition.lineNumber; + const oldColumn = oldPosition.column; + const newLineNumber = newPosition.lineNumber; + const newColumn = newPosition.column; + + // check going down via right arrow + if (newLineNumber === oldLineNumber + 1 && newColumn === 1 && oldColumn === model.getLineMaxColumn(oldLineNumber)) { + return this._cursorCharMessage(model, newPosition); + } + + // check going up via up arrow + if (newLineNumber === oldLineNumber - 1 && newColumn === model.getLineMaxColumn(newLineNumber) && oldColumn === 1) { + return this._cursorCharMessage(model, newPosition); + } + + const lineCount = model.getLineCount(); + if (oldLineNumber !== newLineNumber) { + if (newLineNumber === lineCount) { + // Last line does not have an EOL + return model.getLineContent(newLineNumber); + } + return model.getLineContent(newLineNumber) + model.getEOL(); + } + + return this._cursorCharMessage(model, newPosition); + } + + private static _cursorCharMessage(model: IModel, position: Position): string { + const lineNumber = position.lineNumber; + const column = position.column; + + const maxLineColumn = model.getLineMaxColumn(lineNumber); + if (column === maxLineColumn) { + const lineCount = model.getLineCount(); + if (lineNumber === lineCount) { + // At the end of the file + return ''; + } + return model.getEOL(); + } + return model.getLineContent(lineNumber).charAt(column - 1); + } + + private static _cursorSelectedMessage(model: IModel, range: Range): string { + const valueLength = model.getValueLengthInRange(range); + if (valueLength > 512) { + return this.xCharsSelected(valueLength); + } + return this.xSelected(model.getValueInRange(range)); + } + + private static _cursorUnselectedMessage(model: IModel, range: Range): string { + const valueLength = model.getValueLengthInRange(range); + if (valueLength > 512) { + return this.xCharsUnselected(valueLength); + } + return this.xUnselected(model.getValueInRange(range)); + } + +} diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index bcec892c75d..73128638939 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -21,6 +21,7 @@ import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { IViewModel } from "vs/editor/common/viewModel/viewModel"; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import Event, { Emitter } from 'vs/base/common/event'; +import { ScreenReaderMessageGenerator } from "vs/editor/common/controller/accGenerator"; function containsLineMappingChanged(events: viewEvents.ViewEvent[]): boolean { for (let i = 0, len = events.length; i < len; i++) { @@ -56,7 +57,7 @@ export class CursorStateChangedEvent { /** * A snapshot of the cursor and the model state */ -class CursorModelState { +export class CursorModelState { public readonly modelVersionId: number; public readonly cursorState: CursorState[]; @@ -380,8 +381,20 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { const selections = this._cursors.getSelections(); const viewSelections = this._cursors.getViewSelections(); + let screenReaderMessage: string = null; + if (oldState) { + screenReaderMessage = ScreenReaderMessageGenerator.generateMessage( + source, + this._model, + oldState.modelVersionId, + oldState.cursorState[0].modelState.selection, + newState.modelVersionId, + newState.cursorState[0].modelState.selection + ); + } + // Let the view get the event first. - this._emit([new viewEvents.ViewCursorStateChangedEvent(viewSelections, isInEditableRange)]); + this._emit([new viewEvents.ViewCursorStateChangedEvent(viewSelections, isInEditableRange, screenReaderMessage)]); // Only after the view has been notified, let the rest of the world know... this._onDidChange.fire(new CursorStateChangedEvent(selections, source || 'keyboard', reason)); diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index e6388d47dd4..875b465166e 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -70,10 +70,15 @@ export class ViewCursorStateChangedEvent { * Is the primary cursor in the editable range? */ public readonly isInEditableRange: boolean; + /** + * A message that can be presented to screen readers. + */ + public readonly screenReaderMessage: string; - constructor(selections: Selection[], isInEditableRange: boolean) { + constructor(selections: Selection[], isInEditableRange: boolean, screenReaderMessage: string) { this.selections = selections; this.isInEditableRange = isInEditableRange; + this.screenReaderMessage = screenReaderMessage; } } diff --git a/src/vs/editor/contrib/wordOperations/common/wordOperations.ts b/src/vs/editor/contrib/wordOperations/common/wordOperations.ts index b09317b09e6..469e9446eb2 100644 --- a/src/vs/editor/contrib/wordOperations/common/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/common/wordOperations.ts @@ -15,6 +15,8 @@ import { Range } from 'vs/editor/common/core/range'; import { WordNavigationType, WordOperations } from 'vs/editor/common/controller/cursorWordOperations'; import { ReplaceCommand } from 'vs/editor/common/commands/replaceCommand'; import { getMapForWordSeparators, WordCharacterClassifier } from "vs/editor/common/controller/wordCharacterClassifier"; +import { CursorState } from "vs/editor/common/controller/cursorCommon"; +import { CursorChangeReason } from "vs/editor/common/controller/cursorEvents"; export interface MoveWordOptions extends ICommandOptions { inSelectionMode: boolean; @@ -44,7 +46,7 @@ export abstract class MoveWordCommand extends EditorCommand { return this._moveTo(sel, outPosition, this._inSelectionMode); }); - editor.setSelections(result); + editor._getCursors().setStates('moveWordCommand', CursorChangeReason.NotSet, result.map(r => CursorState.fromModelSelection(r))); if (result.length === 1) { const pos = new Position(result[0].positionLineNumber, result[0].positionColumn); editor.revealPosition(pos, false, true); diff --git a/src/vs/editor/test/common/controller/cursor.test.ts b/src/vs/editor/test/common/controller/cursor.test.ts index 3eb3703c7c7..1d4b8353c77 100644 --- a/src/vs/editor/test/common/controller/cursor.test.ts +++ b/src/vs/editor/test/common/controller/cursor.test.ts @@ -13,7 +13,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { EndOfLinePreference, Handler, DefaultEndOfLine, ITextModelCreationOptions, ICommand, - ITokenizedModel, IEditOperationBuilder, ICursorStateComputerData, EndOfLineSequence + ITokenizedModel, IEditOperationBuilder, ICursorStateComputerData, EndOfLineSequence, ICommonCodeEditor } from 'vs/editor/common/editorCommon'; import { Model } from 'vs/editor/common/model/model'; import { IndentAction, IndentationRule } from 'vs/editor/common/modes/languageConfiguration'; @@ -23,10 +23,15 @@ import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { CoreNavigationCommands, CoreEditingCommands } from 'vs/editor/common/controller/coreCommands'; -import { withMockCodeEditor } from "vs/editor/test/common/mocks/mockCodeEditor"; +import { withMockCodeEditor, MockCodeEditor } from "vs/editor/test/common/mocks/mockCodeEditor"; import { TextModel } from "vs/editor/common/model/textModel"; import { ViewModel } from "vs/editor/common/viewModel/viewModelImpl"; - +import * as viewEvents from 'vs/editor/common/view/viewEvents'; +import { ScreenReaderMessageGenerator } from "vs/editor/common/controller/accGenerator"; +import { + CursorWordLeft, CursorWordLeftSelect, CursorWordRight, CursorWordRightSelect +} from 'vs/editor/contrib/wordOperations/common/wordOperations'; +import { EditorCommand } from "vs/editor/common/editorCommonExtensions"; let H = Handler; // --------- utils @@ -3317,3 +3322,989 @@ suite('autoClosingPairs', () => { mode.dispose(); }); }); + + +suite('cursor screen reader message', () => { + + const _cursorWordLeft = new CursorWordLeft(); + const _cursorWordLeftSelect = new CursorWordLeftSelect(); + const _cursorWordRight = new CursorWordRight(); + const _cursorWordRightSelect = new CursorWordRightSelect(); + + function runEditorCommand(editor: ICommonCodeEditor, command: EditorCommand): void { + command.runEditorCommand(null, editor, null); + } + function moveWordLeft(editor: ICommonCodeEditor, inSelectionMode: boolean = false): void { + runEditorCommand(editor, inSelectionMode ? _cursorWordLeftSelect : _cursorWordLeft); + } + function moveWordRight(editor: ICommonCodeEditor, inSelectionMode: boolean = false): void { + runEditorCommand(editor, inSelectionMode ? _cursorWordRightSelect : _cursorWordRight); + } + + function assertScreenReaderMessage(lines: string[], initialSelection: Selection, expectedScreenReaderMessage: string, callback: (editor: MockCodeEditor, cursor: Cursor) => void): void { + withMockCodeEditor(lines, {}, (editor, cursor) => { + editor.setSelection(initialSelection); + + let actualScreenReaderMessage: string = null; + cursor.addEventListener((events: viewEvents.ViewEvent[]) => { + for (let i = 0, len = events.length; i < len; i++) { + const event = events[i]; + if (event.type === viewEvents.ViewEventType.ViewCursorStateChanged) { + actualScreenReaderMessage = event.screenReaderMessage; + } + } + }); + + callback(editor, cursor); + + assert.equal(actualScreenReaderMessage, expectedScreenReaderMessage); + }); + } + + suite('move down', () => { + test('start of line no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 1), + 'How are you?\n', + (editor, cursor) => { + moveDown(cursor, false); + } + ); + }); + test('start of line no selection - last line', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(2, 1, 2, 1), + 'I am good, thanks', + (editor, cursor) => { + moveDown(cursor, false); + } + ); + }); + test('middle of line no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 3, 1, 3), + 'How are you?\n', + (editor, cursor) => { + moveDown(cursor, false); + } + ); + }); + test('middle of line no selection - last line', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(2, 3, 2, 3), + 'I am good, thanks', + (editor, cursor) => { + moveDown(cursor, false); + } + ); + }); + + test('begin a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 1), + ScreenReaderMessageGenerator.xSelected('Hello world!\n'), + (editor, cursor) => { + moveDown(cursor, true); + } + ); + }); + + test('increase a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 6), + ScreenReaderMessageGenerator.xSelected(' world!\nHow a'), + (editor, cursor) => { + moveDown(cursor, true); + } + ); + }); + + test('decrease a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 1, 1, 1), + ScreenReaderMessageGenerator.xUnselected('Hello world!\n'), + (editor, cursor) => { + moveDown(cursor, true); + } + ); + }); + + test('cancel a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 1, 1, 1), + 'I am good, thanks\nHello world!\nHow are you?\n\nUnselected', + (editor, cursor) => { + moveDown(cursor, false); + } + ); + }); + }); + + suite('move up', () => { + test('start of line no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 1, 3, 1), + 'How are you?\n', + (editor, cursor) => { + moveUp(cursor, false); + } + ); + }); + test('start of line no selection - first line', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(2, 1, 2, 1), + 'Hello world!\n', + (editor, cursor) => { + moveUp(cursor, false); + } + ); + }); + test('middle of line no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 3, 3, 3), + 'How are you?\n', + (editor, cursor) => { + moveUp(cursor, false); + } + ); + }); + test('middle of line no selection - last line', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(2, 3, 2, 3), + 'Hello world!\n', + (editor, cursor) => { + moveUp(cursor, false); + } + ); + }); + + test('begin a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 1, 3, 1), + ScreenReaderMessageGenerator.xSelected('How are you?\n'), + (editor, cursor) => { + moveUp(cursor, true); + } + ); + }); + + test('increase a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 6, 3, 1), + ScreenReaderMessageGenerator.xSelected('How are you?\n'), + (editor, cursor) => { + moveUp(cursor, true); + } + ); + }); + + test('decrease a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 3, 1), + ScreenReaderMessageGenerator.xUnselected('How are you?\n'), + (editor, cursor) => { + moveUp(cursor, true); + } + ); + }); + + test('cancel a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 3, 1), + ScreenReaderMessageGenerator.xUnselected('Hello world!\nHow are you?\n'), + (editor, cursor) => { + moveUp(cursor, false); + } + ); + }); + }); + + suite('move right', () => { + test('start of line no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 1), + 'e', + (editor, cursor) => { + moveRight(cursor, false); + } + ); + }); + test('middle of line no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 2, 1, 2), + 'l', + (editor, cursor) => { + moveRight(cursor, false); + } + ); + }); + test('end of line no selection 1', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 12, 1, 12), + '\n', + (editor, cursor) => { + moveRight(cursor, false); + } + ); + }); + test('end of line no selection 2', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 13, 1, 13), + 'H', + (editor, cursor) => { + moveRight(cursor, false); + } + ); + }); + test('end of file no selection 1', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 17, 3, 17), + '', + (editor, cursor) => { + moveRight(cursor, false); + } + ); + }); + test('end of file no selection 2', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 18, 3, 18), + null, + (editor, cursor) => { + moveRight(cursor, false); + } + ); + }); + + test('begin a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 1), + ScreenReaderMessageGenerator.xSelected('H'), + (editor, cursor) => { + moveRight(cursor, true); + } + ); + }); + + test('increase a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 7), + ScreenReaderMessageGenerator.xSelected('w'), + (editor, cursor) => { + moveRight(cursor, true); + } + ); + }); + + test('decrease a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 7, 1, 1), + ScreenReaderMessageGenerator.xUnselected('H'), + (editor, cursor) => { + moveRight(cursor, true); + } + ); + }); + + test('cancel a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 8), + ScreenReaderMessageGenerator.xUnselected('Hello w'), + (editor, cursor) => { + moveRight(cursor, false); + } + ); + }); + }); + + suite('move left', () => { + test('end of line no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 18, 3, 18), + 's', + (editor, cursor) => { + moveLeft(cursor, false); + } + ); + }); + test('middle of line no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 4, 3, 4), + 'a', + (editor, cursor) => { + moveLeft(cursor, false); + } + ); + }); + test('start of line no selection 1', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 2, 3, 2), + 'I', + (editor, cursor) => { + moveLeft(cursor, false); + } + ); + }); + test('start of line no selection 2', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 1, 3, 1), + '\n', + (editor, cursor) => { + moveLeft(cursor, false); + } + ); + }); + test('start of file no selection 1', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 2, 1, 2), + 'H', + (editor, cursor) => { + moveLeft(cursor, false); + } + ); + }); + test('start of file no selection 2', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 1), + null, + (editor, cursor) => { + moveLeft(cursor, false); + } + ); + }); + + test('begin a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 6, 1, 6), + ScreenReaderMessageGenerator.xSelected('o'), + (editor, cursor) => { + moveLeft(cursor, true); + } + ); + }); + + test('increase a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 6, 1, 5), + ScreenReaderMessageGenerator.xSelected('l'), + (editor, cursor) => { + moveLeft(cursor, true); + } + ); + }); + + test('decrease a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 6), + ScreenReaderMessageGenerator.xUnselected('o'), + (editor, cursor) => { + moveLeft(cursor, true); + } + ); + }); + + test('cancel a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 2, 1, 8), + ScreenReaderMessageGenerator.xUnselected('ello w'), + (editor, cursor) => { + moveLeft(cursor, false); + } + ); + }); + }); + + suite('home', () => { + test('no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 18, 3, 18), + 'I', + (editor, cursor) => { + moveToBeginningOfLine(cursor, false); + } + ); + }); + + test('begin a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 6, 1, 6), + ScreenReaderMessageGenerator.xSelected('Hello'), + (editor, cursor) => { + moveToBeginningOfLine(cursor, true); + } + ); + }); + + test('increase a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 6, 1, 5), + ScreenReaderMessageGenerator.xSelected('Hell'), + (editor, cursor) => { + moveToBeginningOfLine(cursor, true); + } + ); + }); + + test('decrease a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 2, 4), + ScreenReaderMessageGenerator.xUnselected('How'), + (editor, cursor) => { + moveToBeginningOfLine(cursor, true); + } + ); + }); + + test('cancel a selection 1', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 8), + ScreenReaderMessageGenerator.xUnselected('Hello w'), + (editor, cursor) => { + moveToBeginningOfLine(cursor, false); + } + ); + }); + + test('cancel a selection 2', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 7, 1, 12), + ScreenReaderMessageGenerator.xUnselected('H\nworld'), + (editor, cursor) => { + moveToBeginningOfLine(cursor, false); + } + ); + }); + }); + + suite('end', () => { + test('no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 1), + '\n', + (editor, cursor) => { + moveToEndOfLine(cursor, false); + } + ); + }); + + test('no selection at end of file', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 1, 3, 1), + '', + (editor, cursor) => { + moveToEndOfLine(cursor, false); + } + ); + }); + + test('begin a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 7, 1, 7), + ScreenReaderMessageGenerator.xSelected('world!'), + (editor, cursor) => { + moveToEndOfLine(cursor, true); + } + ); + }); + + test('increase a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 7), + ScreenReaderMessageGenerator.xSelected('world!'), + (editor, cursor) => { + moveToEndOfLine(cursor, true); + } + ); + }); + + test('decrease a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(2, 13, 1, 1), + ScreenReaderMessageGenerator.xUnselected('Hello world!'), + (editor, cursor) => { + moveToEndOfLine(cursor, true); + } + ); + }); + + test('cancel a selection 1', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 13, 1, 8), + ScreenReaderMessageGenerator.xUnselected('orld!'), + (editor, cursor) => { + moveToEndOfLine(cursor, false); + } + ); + }); + + test('cancel a selection 2', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 6), + ScreenReaderMessageGenerator.xUnselected('\n\nHello'), + (editor, cursor) => { + moveToEndOfLine(cursor, false); + } + ); + }); + }); + + suite('ctrl+right', () => { + test('no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 1), + 'Hello', + (editor, cursor) => { + moveWordRight(editor, false); + } + ); + }); + + test('accross lines', () => { + assertScreenReaderMessage( + [ + 'Hello world', + 'How are you', + 'I am good, thanks' + ], + new Selection(1, 12, 1, 12), + '\nHow', + (editor, cursor) => { + moveWordRight(editor, false); + } + ); + }); + + test('no selection at end of file', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(3, 12, 3, 12), + 'thanks', + (editor, cursor) => { + moveWordRight(editor, false); + } + ); + }); + + test('begin a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 1), + ScreenReaderMessageGenerator.xSelected('Hello'), + (editor, cursor) => { + moveWordRight(editor, true); + } + ); + }); + + test('increase a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 6), + ScreenReaderMessageGenerator.xSelected(' world'), + (editor, cursor) => { + moveWordRight(editor, true); + } + ); + }); + + test('decrease a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 12, 1, 1), + ScreenReaderMessageGenerator.xUnselected('Hello'), + (editor, cursor) => { + moveWordRight(editor, true); + } + ); + }); + + test('cancel a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(2, 1, 2, 4), + ScreenReaderMessageGenerator.xUnselected(' are\nHow'), + (editor, cursor) => { + moveWordRight(editor, false); + } + ); + }); + }); + + suite('ctrl+left', () => { + test('no selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 12, 1, 12), + 'world', + (editor, cursor) => { + moveWordLeft(editor, false); + } + ); + }); + + test('accross lines', () => { + assertScreenReaderMessage( + [ + 'Hello world', + 'How are you', + 'I am good, thanks' + ], + new Selection(2, 1, 2, 1), + 'world\n', + (editor, cursor) => { + moveWordLeft(editor, false); + } + ); + }); + + test('no selection at start of file', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 6, 1, 6), + 'Hello', + (editor, cursor) => { + moveWordLeft(editor, false); + } + ); + }); + + test('begin a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 12, 1, 12), + ScreenReaderMessageGenerator.xSelected('world'), + (editor, cursor) => { + moveWordLeft(editor, true); + } + ); + }); + + test('increase a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 12, 1, 7), + ScreenReaderMessageGenerator.xSelected('Hello '), + (editor, cursor) => { + moveWordLeft(editor, true); + } + ); + }); + + test('decrease a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(1, 1, 1, 12), + ScreenReaderMessageGenerator.xUnselected('world'), + (editor, cursor) => { + moveWordLeft(editor, true); + } + ); + }); + + test('cancel a selection', () => { + assertScreenReaderMessage( + [ + 'Hello world!', + 'How are you?', + 'I am good, thanks' + ], + new Selection(2, 4, 2, 1), + ScreenReaderMessageGenerator.xUnselected('!\n\nHow'), + (editor, cursor) => { + moveWordLeft(editor, false); + } + ); + }); + }); + + +}); -- GitLab