diff --git a/.dependency-cruiser.js b/.dependency-cruiser.js index 39e89c4..b96d2e4 100644 --- a/.dependency-cruiser.js +++ b/.dependency-cruiser.js @@ -4,21 +4,19 @@ module.exports = { { name: "no-circular", severity: "error", - from: { - pathNot: "^src/api", - }, + from: {}, to: { circular: true, }, }, { - name: "api-only-depends-on-api/index-and-utils", + name: "api-only-depends-on-api-and-utils", severity: "error", from: { path: "^src/api/(?!index)", }, to: { - pathNot: "^src/(api/index|utils)", + pathNot: "^src/(api/(?!index)|utils)", }, }, { @@ -32,10 +30,10 @@ module.exports = { }, }, { - name: "only-api/index-depends-on-api", + name: "only-api-depends-on-api", severity: "error", from: { - pathNot: "^src/api/index", + pathNot: "^src/api", }, to: { path: "^src/api/(?!index)", diff --git a/src/api/clipboard.ts b/src/api/clipboard.ts index f4b1472..a078489 100644 --- a/src/api/clipboard.ts +++ b/src/api/clipboard.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; -import { Context } from "."; +import { Context } from "./context"; /** * Copies the given text to the clipboard. diff --git a/src/api/context.ts b/src/api/context.ts index 9ef3894..7eadaea 100644 --- a/src/api/context.ts +++ b/src/api/context.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; -import { SelectionBehavior, Selections } from "."; +import { SelectionBehavior } from "./types"; import type { CommandDescriptor } from "../commands"; import type { PerEditorState } from "../state/editors"; import type { Extension } from "../state/extension"; @@ -302,7 +302,7 @@ export class Context extends ContextWithoutActiveEditor { const editor = this.editor as vscode.TextEditor; if (this.selectionBehavior === SelectionBehavior.Character) { - return Selections.fromCharacterMode(editor.selections, editor.document); + return selectionsFromCharacterMode(editor.selections, editor.document); } return editor.selections; @@ -318,7 +318,7 @@ export class Context extends ContextWithoutActiveEditor { const editor = this.editor as vscode.TextEditor; if (this.selectionBehavior === SelectionBehavior.Character) { - selections = Selections.toCharacterMode(selections, editor.document); + selections = selectionsToCharacterMode(selections, editor.document); } editor.selections = selections as vscode.Selection[]; @@ -333,7 +333,7 @@ export class Context extends ContextWithoutActiveEditor { const editor = this.editor as vscode.TextEditor; if (this.selectionBehavior === SelectionBehavior.Character) { - return Selections.fromCharacterMode([editor.selection], editor.document)[0]; + return selectionsFromCharacterMode([editor.selection], editor.document)[0]; } return editor.selection; @@ -581,3 +581,221 @@ export function insertUndoStop(editor?: vscode.TextEditor) { return Context.current.insertUndoStop(); } + +/** + * Transforms a list of caret-mode selections (that is, regular selections as + * manipulated internally) into a list of character-mode selections (that is, + * selections modified to include a block character in them). + * + * This function should be used before setting the selections of a + * `vscode.TextEditor` if the selection behavior is `Character`. + * + * ### Example + * Forward-facing, non-empty selections are reduced by one character. + * + * ```js + * // One-character selection becomes empty. + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 0, 1)]), "to satisfy", [ + * expect.it("to be empty at coords", 0, 0), + * ]); + * + * // One-character selection becomes empty (at line break). + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 1, 0)]), "to satisfy", [ + * expect.it("to be empty at coords", 0, 1), + * ]); + * + * // Forward-facing selection becomes shorter. + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 1, 1)]), "to satisfy", [ + * expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 1, 0), + * ]); + * + * // One-character selection becomes empty (reversed). + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 0, 0)]), "to satisfy", [ + * expect.it("to be empty at coords", 0, 0), + * ]); + * + * // One-character selection becomes empty (reversed, at line break). + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 0, 0, 1)]), "to satisfy", [ + * expect.it("to be empty at coords", 0, 1), + * ]); + * + * // Reversed selection stays as-is. + * expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 1, 0, 0)]), "to satisfy", [ + * expect.it("to have anchor at coords", 1, 1).and("to have cursor at coords", 0, 0), + * ]); + * + * // Empty selection stays as-is. + * expect(Selections.toCharacterMode([Selections.empty(1, 1)]), "to satisfy", [ + * expect.it("to be empty at coords", 1, 1), + * ]); + * ``` + * + * With: + * ``` + * a + * b + * ``` + */ +export function selectionsToCharacterMode( + selections: readonly vscode.Selection[], + document?: vscode.TextDocument, +) { + const characterModeSelections = [] as vscode.Selection[]; + + for (const selection of selections) { + const selectionActive = selection.active, + selectionActiveLine = selectionActive.line, + selectionActiveCharacter = selectionActive.character, + selectionAnchor = selection.anchor, + selectionAnchorLine = selectionAnchor.line, + selectionAnchorCharacter = selectionAnchor.character; + let active = selectionActive, + anchor = selectionAnchor, + changed = false; + + if (selectionAnchorLine === selectionActiveLine) { + if (selectionAnchorCharacter + 1 === selectionActiveCharacter) { + // Selection is one-character long: make it empty. + active = selectionAnchor; + changed = true; + } else if (selectionAnchorCharacter - 1 === selectionActiveCharacter) { + // Selection is reversed and one-character long: make it empty. + anchor = selectionActive; + changed = true; + } else if (selectionAnchorCharacter < selectionActiveCharacter) { + // Selection is strictly forward-facing: make it shorter. + active = new vscode.Position(selectionActiveLine, selectionActiveCharacter - 1); + changed = true; + } else { + // Selection is reversed or empty: do nothing. + } + } else if (selectionAnchorLine < selectionActiveLine) { + // Selection is strictly forward-facing: make it shorter. + if (selectionActiveCharacter > 0) { + active = new vscode.Position(selectionActiveLine, selectionActiveCharacter - 1); + changed = true; + } else { + // The active character is the first one, so we have to get some + // information from the document. + if (document === undefined) { + document = Context.current.document; + } + + const activePrevLine = selectionActiveLine - 1, + activePrevLineLength = document.lineAt(activePrevLine).text.length; + + active = new vscode.Position(activePrevLine, activePrevLineLength); + changed = true; + } + } else if (selectionAnchorLine === selectionActiveLine + 1 + && selectionAnchorCharacter === 0 + && selectionActiveCharacter === (document ?? (document = Context.current.document)) + .lineAt(selectionActiveLine).text.length) { + // Selection is reversed and one-character long: make it empty. + anchor = selectionActive; + changed = true; + } else { + // Selection is reversed: do nothing. + } + + characterModeSelections.push(changed ? new vscode.Selection(anchor, active) : selection); + } + + return characterModeSelections; +} + +/** + * Reverses the changes made by `toCharacterMode` by increasing by one the + * length of every empty or forward-facing selection. + * + * This function should be used on the selections of a `vscode.TextEditor` if + * the selection behavior is `Character`. + * + * ### Example + * Selections remain empty in empty documents. + * + * ```js + * expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ + * expect.it("to be empty at coords", 0, 0), + * ]); + * ``` + * + * With: + * ``` + * ``` + * + * ### Example + * Empty selections automatically become 1-character selections. + * + * ```js + * expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ + * expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 0, 1), + * ]); + * + * // At the end of the line, it selects the line ending: + * expect(Selections.fromCharacterMode([Selections.empty(0, 1)]), "to satisfy", [ + * expect.it("to have anchor at coords", 0, 1).and("to have cursor at coords", 1, 0), + * ]); + * + * // But it does nothing at the end of the document: + * expect(Selections.fromCharacterMode([Selections.empty(2, 0)]), "to satisfy", [ + * expect.it("to be empty at coords", 2, 0), + * ]); + * ``` + * + * With: + * ``` + * a + * b + * + * ``` + */ +export function selectionsFromCharacterMode( + selections: readonly vscode.Selection[], + document?: vscode.TextDocument, +) { + const caretModeSelections = [] as vscode.Selection[]; + + for (const selection of selections) { + const selectionActive = selection.active, + selectionActiveLine = selectionActive.line, + selectionActiveCharacter = selectionActive.character, + selectionAnchor = selection.anchor, + selectionAnchorLine = selectionAnchor.line, + selectionAnchorCharacter = selectionAnchor.character; + let active = selectionActive, + changed = false; + + const isEmptyOrForwardFacing = selectionAnchorLine < selectionActiveLine + || (selectionAnchorLine === selectionActiveLine + && selectionAnchorCharacter <= selectionActiveCharacter); + + if (isEmptyOrForwardFacing) { + // Selection is empty or forward-facing: extend it if possible. + if (document === undefined) { + document = Context.current.document; + } + + const lineLength = document.lineAt(selectionActiveLine).text.length; + + if (selectionActiveCharacter === lineLength) { + // Character is at the end of the line. + if (selectionActiveLine + 1 < document.lineCount) { + // This is not the last line: we can extend the selection. + active = new vscode.Position(selectionActiveLine + 1, 0); + changed = true; + } else { + // This is the last line: we cannot do anything. + } + } else { + // Character is not at the end of the line: we can extend the selection. + active = new vscode.Position(selectionActiveLine, selectionActiveCharacter + 1); + changed = true; + } + } + + caretModeSelections.push(changed ? new vscode.Selection(selectionAnchor, active) : selection); + } + + return caretModeSelections; +} diff --git a/src/api/edit/index.ts b/src/api/edit/index.ts index f2b09db..6c8832d 100644 --- a/src/api/edit/index.ts +++ b/src/api/edit/index.ts @@ -1,6 +1,8 @@ import * as vscode from "vscode"; -import { Context, edit, Positions, Selections } from ".."; +import { Context, edit } from "../context"; +import { Positions } from "../positions"; +import { Selections } from "../selections"; import { TrackedSelection } from "../../utils/tracked-selection"; const enum Constants { diff --git a/src/api/edit/linewise.ts b/src/api/edit/linewise.ts index 612e99d..10b38f6 100644 --- a/src/api/edit/linewise.ts +++ b/src/api/edit/linewise.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; -import { Context, edit } from ".."; +import { Context, edit } from "../context"; import { blankCharacters } from "../../utils/charset"; /** diff --git a/src/api/errors.ts b/src/api/errors.ts index 354883f..519c0b6 100644 --- a/src/api/errors.ts +++ b/src/api/errors.ts @@ -1,4 +1,4 @@ -import { Context } from "."; +import { Context } from "./context"; export * from "../utils/errors"; diff --git a/src/api/history.ts b/src/api/history.ts index 885cf22..b176831 100644 --- a/src/api/history.ts +++ b/src/api/history.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; -import { Context } from "."; +import { Context } from "./context"; /** * Un-does the last action. diff --git a/src/api/index.ts b/src/api/index.ts index cbd4bf8..29dc718 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -23,61 +23,7 @@ export * from "./search/pairs"; export * from "./search/range"; export * from "./search/word"; export * from "./selections"; - -/** - * Direction of an operation. - */ -export const enum Direction { - /** - * Forward direction (`1`). - */ - Forward = 1, - - /** - * Backward direction (`-1`). - */ - Backward = -1, -} - -/** - * Behavior of a shift. - */ -export const enum Shift { - /** - * Jump to the position. - */ - Jump, - - /** - * Select to the position. - */ - Select, - - /** - * Extend to the position. - */ - Extend, -} - -/** - * Selection behavior of an operation. - */ -export const enum SelectionBehavior { - /** - * VS Code-like caret selections. - */ - Caret = 1, - /** - * Kakoune-like character selections. - */ - Character = 2, -} - -export const Forward = Direction.Forward, - Backward = Direction.Backward, - Jump = Shift.Jump, - Select = Shift.Select, - Extend = Shift.Extend; +export * from "./types"; /** * Returns the module exported by the extension with the given identifier. diff --git a/src/api/lines.ts b/src/api/lines.ts index df5d061..99190ad 100644 --- a/src/api/lines.ts +++ b/src/api/lines.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; -import { Context } from "."; +import { Context } from "./context"; /** * Returns the 0-based number of the first visible line in the current editor. diff --git a/src/api/menu.ts b/src/api/menu.ts index 3c563b2..f46e2ed 100644 --- a/src/api/menu.ts +++ b/src/api/menu.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; -import { Context, keypress, prompt } from "."; +import { Context } from "./context"; +import { keypress, prompt } from "./prompt"; export interface Menu { readonly items: Menu.Items; diff --git a/src/api/modes.ts b/src/api/modes.ts index 4061f8c..8b861ba 100644 --- a/src/api/modes.ts +++ b/src/api/modes.ts @@ -1,4 +1,4 @@ -import { Context } from "."; +import { Context } from "./context"; /** * Switches to the mode with the given name. diff --git a/src/api/positions.ts b/src/api/positions.ts index e687419..2f44c91 100644 --- a/src/api/positions.ts +++ b/src/api/positions.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; -import { Context, Direction } from "."; +import { Context } from "./context"; +import { Direction } from "./types"; /** * Returns the position right after the given position, or `undefined` if diff --git a/src/api/prompt.ts b/src/api/prompt.ts index 77d805a..e1d10da 100644 --- a/src/api/prompt.ts +++ b/src/api/prompt.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; -import { Context, Selections } from "."; +import { Context } from "./context"; +import { Selections } from "./selections"; import type { Input, SetInput } from "../commands"; import { CancellationError } from "../utils/errors"; diff --git a/src/api/registers.ts b/src/api/registers.ts index c4a6b6e..b62fa62 100644 --- a/src/api/registers.ts +++ b/src/api/registers.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; -import { Context } from "."; +import { Context } from "./context"; import type { Register } from "../state/registers"; /** diff --git a/src/api/run.ts b/src/api/run.ts index cdba389..a25024e 100644 --- a/src/api/run.ts +++ b/src/api/run.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; -import { Context } from "."; +import { Context } from "./context"; import type { CommandDescriptor } from "../commands"; import { parseRegExpWithReplacement } from "../utils/regexp"; diff --git a/src/api/search/index.ts b/src/api/search/index.ts index 75983b5..f89646b 100644 --- a/src/api/search/index.ts +++ b/src/api/search/index.ts @@ -1,6 +1,8 @@ import * as vscode from "vscode"; -import { Context, Direction, Positions } from ".."; +import { Context } from "../context"; +import { Positions } from "../positions"; +import { Direction } from "../types"; import { canMatchLineFeed, execLast, matchesStaticStrings } from "../../utils/regexp"; /** diff --git a/src/api/search/lines.ts b/src/api/search/lines.ts index 3e752ce..8fddfe5 100644 --- a/src/api/search/lines.ts +++ b/src/api/search/lines.ts @@ -1,6 +1,8 @@ import * as vscode from "vscode"; -import { Context, lineByLine, Positions } from ".."; +import { lineByLine } from "./move"; +import { Context } from "../context"; +import { Positions } from "../positions"; /** * Returns the range of lines matching the given `RegExp` before and after diff --git a/src/api/search/move-to.ts b/src/api/search/move-to.ts index 11b2aaa..8ef3a77 100644 --- a/src/api/search/move-to.ts +++ b/src/api/search/move-to.ts @@ -1,6 +1,8 @@ import * as vscode from "vscode"; -import { Context, Direction, Positions } from ".."; +import { Context } from "../context"; +import { Positions } from "../positions"; +import { Direction } from "../types"; /** * Moves the given position towards the given direction until the given string diff --git a/src/api/search/move.ts b/src/api/search/move.ts index 0319e0b..6a7d245 100644 --- a/src/api/search/move.ts +++ b/src/api/search/move.ts @@ -1,6 +1,8 @@ import * as vscode from "vscode"; -import { Context, Direction, Positions } from ".."; +import { Context } from "../context"; +import { Positions } from "../positions"; +import { Direction } from "../types"; /** * Moves the given position towards the given direction as long as the given diff --git a/src/api/search/pairs.ts b/src/api/search/pairs.ts index fc61f69..080f42d 100644 --- a/src/api/search/pairs.ts +++ b/src/api/search/pairs.ts @@ -1,6 +1,9 @@ import * as vscode from "vscode"; -import { Context, Direction, Positions, search } from ".."; +import { search } from "./index"; +import { Context } from "../context"; +import { Positions } from "../positions"; +import { Direction } from "../types"; import { ArgumentError } from "../../utils/errors"; import { anyRegExp, escapeForRegExp } from "../../utils/regexp"; diff --git a/src/api/search/range.ts b/src/api/search/range.ts index b099e72..e4d9626 100644 --- a/src/api/search/range.ts +++ b/src/api/search/range.ts @@ -1,6 +1,10 @@ import * as vscode from "vscode"; -import { Context, Direction, Lines, moveWhile, Positions } from ".."; +import { moveWhile } from "./move"; +import { Context } from "../context"; +import { Lines } from "../lines"; +import { Positions } from "../positions"; +import { Direction } from "../types"; import { CharSet, getCharSetFunction } from "../../utils/charset"; import { CharCodes } from "../../utils/regexp"; diff --git a/src/api/search/word.ts b/src/api/search/word.ts index 476540d..b4f3c0f 100644 --- a/src/api/search/word.ts +++ b/src/api/search/word.ts @@ -1,6 +1,8 @@ import * as vscode from "vscode"; -import { Context, Direction, SelectionBehavior, skipEmptyLines } from ".."; +import { skipEmptyLines } from "./move"; +import { Context } from "../context"; +import { Direction, SelectionBehavior } from "../types"; import { CharSet, getCharSetFunction } from "../../utils/charset"; const enum WordCategory { diff --git a/src/api/selections.ts b/src/api/selections.ts index 6c9859c..43cceb2 100644 --- a/src/api/selections.ts +++ b/src/api/selections.ts @@ -1,6 +1,9 @@ import * as vscode from "vscode"; -import { Context, Direction, Lines, NotASelectionError, Positions, SelectionBehavior, Shift } from "."; +import { Context, selectionsFromCharacterMode, selectionsToCharacterMode } from "./context"; +import { NotASelectionError } from "./errors"; +import { Positions } from "./positions"; +import { Direction, SelectionBehavior, Shift } from "./types"; import { execRange, splitRange } from "../utils/regexp"; /** @@ -1768,222 +1771,8 @@ export namespace Selections { } } - /** - * Transforms a list of caret-mode selections (that is, regular selections as - * manipulated internally) into a list of character-mode selections (that is, - * selections modified to include a block character in them). - * - * This function should be used before setting the selections of a - * `vscode.TextEditor` if the selection behavior is `Character`. - * - * ### Example - * Forward-facing, non-empty selections are reduced by one character. - * - * ```js - * // One-character selection becomes empty. - * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 0, 1)]), "to satisfy", [ - * expect.it("to be empty at coords", 0, 0), - * ]); - * - * // One-character selection becomes empty (at line break). - * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 1, 0)]), "to satisfy", [ - * expect.it("to be empty at coords", 0, 1), - * ]); - * - * // Forward-facing selection becomes shorter. - * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 1, 1)]), "to satisfy", [ - * expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 1, 0), - * ]); - * - * // One-character selection becomes empty (reversed). - * expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 0, 0)]), "to satisfy", [ - * expect.it("to be empty at coords", 0, 0), - * ]); - * - * // One-character selection becomes empty (reversed, at line break). - * expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 0, 0, 1)]), "to satisfy", [ - * expect.it("to be empty at coords", 0, 1), - * ]); - * - * // Reversed selection stays as-is. - * expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 1, 0, 0)]), "to satisfy", [ - * expect.it("to have anchor at coords", 1, 1).and("to have cursor at coords", 0, 0), - * ]); - * - * // Empty selection stays as-is. - * expect(Selections.toCharacterMode([Selections.empty(1, 1)]), "to satisfy", [ - * expect.it("to be empty at coords", 1, 1), - * ]); - * ``` - * - * With: - * ``` - * a - * b - * ``` - */ - export function toCharacterMode( - selections: readonly vscode.Selection[], - document?: vscode.TextDocument, - ) { - const characterModeSelections = [] as vscode.Selection[]; - - for (const selection of selections) { - const selectionActive = selection.active, - selectionActiveLine = selectionActive.line, - selectionActiveCharacter = selectionActive.character, - selectionAnchor = selection.anchor, - selectionAnchorLine = selectionAnchor.line, - selectionAnchorCharacter = selectionAnchor.character; - let active = selectionActive, - anchor = selectionAnchor, - changed = false; - - if (selectionAnchorLine === selectionActiveLine) { - if (selectionAnchorCharacter + 1 === selectionActiveCharacter) { - // Selection is one-character long: make it empty. - active = selectionAnchor; - changed = true; - } else if (selectionAnchorCharacter - 1 === selectionActiveCharacter) { - // Selection is reversed and one-character long: make it empty. - anchor = selectionActive; - changed = true; - } else if (selectionAnchorCharacter < selectionActiveCharacter) { - // Selection is strictly forward-facing: make it shorter. - active = new vscode.Position(selectionActiveLine, selectionActiveCharacter - 1); - changed = true; - } else { - // Selection is reversed or empty: do nothing. - } - } else if (selectionAnchorLine < selectionActiveLine) { - // Selection is strictly forward-facing: make it shorter. - if (selectionActiveCharacter > 0) { - active = new vscode.Position(selectionActiveLine, selectionActiveCharacter - 1); - changed = true; - } else { - // The active character is the first one, so we have to get some - // information from the document. - if (document === undefined) { - document = Context.current.document; - } - - const activePrevLine = selectionActiveLine - 1, - activePrevLineLength = document.lineAt(activePrevLine).text.length; - - active = new vscode.Position(activePrevLine, activePrevLineLength); - changed = true; - } - } else if (selectionAnchorLine === selectionActiveLine + 1 - && selectionAnchorCharacter === 0 - && selectionActiveCharacter === Lines.length(selectionActiveLine, document)) { - // Selection is reversed and one-character long: make it empty. - anchor = selectionActive; - changed = true; - } else { - // Selection is reversed: do nothing. - } - - characterModeSelections.push(changed ? new vscode.Selection(anchor, active) : selection); - } - - return characterModeSelections; - } - - /** - * Reverses the changes made by `toCharacterMode` by increasing by one the - * length of every empty or forward-facing selection. - * - * This function should be used on the selections of a `vscode.TextEditor` if - * the selection behavior is `Character`. - * - * ### Example - * Selections remain empty in empty documents. - * - * ```js - * expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ - * expect.it("to be empty at coords", 0, 0), - * ]); - * ``` - * - * With: - * ``` - * ``` - * - * ### Example - * Empty selections automatically become 1-character selections. - * - * ```js - * expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ - * expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 0, 1), - * ]); - * - * // At the end of the line, it selects the line ending: - * expect(Selections.fromCharacterMode([Selections.empty(0, 1)]), "to satisfy", [ - * expect.it("to have anchor at coords", 0, 1).and("to have cursor at coords", 1, 0), - * ]); - * - * // But it does nothing at the end of the document: - * expect(Selections.fromCharacterMode([Selections.empty(2, 0)]), "to satisfy", [ - * expect.it("to be empty at coords", 2, 0), - * ]); - * ``` - * - * With: - * ``` - * a - * b - * - * ``` - */ - export function fromCharacterMode( - selections: readonly vscode.Selection[], - document?: vscode.TextDocument, - ) { - const caretModeSelections = [] as vscode.Selection[]; - - for (const selection of selections) { - const selectionActive = selection.active, - selectionActiveLine = selectionActive.line, - selectionActiveCharacter = selectionActive.character, - selectionAnchor = selection.anchor, - selectionAnchorLine = selectionAnchor.line, - selectionAnchorCharacter = selectionAnchor.character; - let active = selectionActive, - changed = false; - - const isEmptyOrForwardFacing = selectionAnchorLine < selectionActiveLine - || (selectionAnchorLine === selectionActiveLine - && selectionAnchorCharacter <= selectionActiveCharacter); - - if (isEmptyOrForwardFacing) { - // Selection is empty or forward-facing: extend it if possible. - if (document === undefined) { - document = Context.current.document; - } - - const lineLength = document.lineAt(selectionActiveLine).text.length; - - if (selectionActiveCharacter === lineLength) { - // Character is at the end of the line. - if (selectionActiveLine + 1 < document.lineCount) { - // This is not the last line: we can extend the selection. - active = new vscode.Position(selectionActiveLine + 1, 0); - changed = true; - } else { - // This is the last line: we cannot do anything. - } - } else { - // Character is not at the end of the line: we can extend the selection. - active = new vscode.Position(selectionActiveLine, selectionActiveCharacter + 1); - changed = true; - } - } - - caretModeSelections.push(changed ? new vscode.Selection(selectionAnchor, active) : selection); - } - - return caretModeSelections; - } + export const fromCharacterMode = selectionsFromCharacterMode, + toCharacterMode = selectionsToCharacterMode; } function sortTopToBottom(a: vscode.Selection, b: vscode.Selection) { diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..5b567f7 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,54 @@ +/** + * Direction of an operation. + */ +export const enum Direction { + /** + * Forward direction (`1`). + */ + Forward = 1, + + /** + * Backward direction (`-1`). + */ + Backward = -1, +} + +/** + * Behavior of a shift. + */ +export const enum Shift { + /** + * Jump to the position. + */ + Jump, + + /** + * Select to the position. + */ + Select, + + /** + * Extend to the position. + */ + Extend, +} + +/** + * Selection behavior of an operation. + */ +export const enum SelectionBehavior { + /** + * VS Code-like caret selections. + */ + Caret = 1, + /** + * Kakoune-like character selections. + */ + Character = 2, +} + +export const Forward = Direction.Forward, + Backward = Direction.Backward, + Jump = Shift.Jump, + Select = Shift.Select, + Extend = Shift.Extend; diff --git a/test/suite/api.test.ts b/test/suite/api.test.ts index 27d8c6c..58c0439 100644 --- a/test/suite/api.test.ts +++ b/test/suite/api.test.ts @@ -89,6 +89,103 @@ suite("API tests", function () { // No expected end document. }); + test("function selectionsToCharacterMode", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a + b + `); + + await before.apply(editor); + + await context.runAsync(async () => { + // One-character selection becomes empty. + expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 0, 1)]), "to satisfy", [ + expect.it("to be empty at coords", 0, 0), + ]); + + // One-character selection becomes empty (at line break). + expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 1, 0)]), "to satisfy", [ + expect.it("to be empty at coords", 0, 1), + ]); + + // Forward-facing selection becomes shorter. + expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 1, 1)]), "to satisfy", [ + expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 1, 0), + ]); + + // One-character selection becomes empty (reversed). + expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 0, 0)]), "to satisfy", [ + expect.it("to be empty at coords", 0, 0), + ]); + + // One-character selection becomes empty (reversed, at line break). + expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 0, 0, 1)]), "to satisfy", [ + expect.it("to be empty at coords", 0, 1), + ]); + + // Reversed selection stays as-is. + expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 1, 0, 0)]), "to satisfy", [ + expect.it("to have anchor at coords", 1, 1).and("to have cursor at coords", 0, 0), + ]); + + // Empty selection stays as-is. + expect(Selections.toCharacterMode([Selections.empty(1, 1)]), "to satisfy", [ + expect.it("to be empty at coords", 1, 1), + ]); + }); + + // No expected end document. + }); + + test("function selectionsFromCharacterMode", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ + expect.it("to be empty at coords", 0, 0), + ]); + }); + + // No expected end document. + }); + + test("function selectionsFromCharacterMode#1", async function () { + const editorState = extension.editors.getState(editor)!, + context = new Context(editorState, cancellationToken), + before = ExpectedDocument.parseIndented(14, String.raw` + a + b + + `); + + await before.apply(editor); + + await context.runAsync(async () => { + expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ + expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 0, 1), + ]); + + // At the end of the line, it selects the line ending: + expect(Selections.fromCharacterMode([Selections.empty(0, 1)]), "to satisfy", [ + expect.it("to have anchor at coords", 0, 1).and("to have cursor at coords", 1, 0), + ]); + + // But it does nothing at the end of the document: + expect(Selections.fromCharacterMode([Selections.empty(2, 0)]), "to satisfy", [ + expect.it("to be empty at coords", 2, 0), + ]); + }); + + // No expected end document. + }); + }); suite("./src/api/functional.ts", function () { @@ -1308,102 +1405,5 @@ suite("API tests", function () { // No expected end document. }); - test("function toCharacterMode", async function () { - const editorState = extension.editors.getState(editor)!, - context = new Context(editorState, cancellationToken), - before = ExpectedDocument.parseIndented(14, String.raw` - a - b - `); - - await before.apply(editor); - - await context.runAsync(async () => { - // One-character selection becomes empty. - expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 0, 1)]), "to satisfy", [ - expect.it("to be empty at coords", 0, 0), - ]); - - // One-character selection becomes empty (at line break). - expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 1, 0)]), "to satisfy", [ - expect.it("to be empty at coords", 0, 1), - ]); - - // Forward-facing selection becomes shorter. - expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 0, 1, 1)]), "to satisfy", [ - expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 1, 0), - ]); - - // One-character selection becomes empty (reversed). - expect(Selections.toCharacterMode([Selections.fromAnchorActive(0, 1, 0, 0)]), "to satisfy", [ - expect.it("to be empty at coords", 0, 0), - ]); - - // One-character selection becomes empty (reversed, at line break). - expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 0, 0, 1)]), "to satisfy", [ - expect.it("to be empty at coords", 0, 1), - ]); - - // Reversed selection stays as-is. - expect(Selections.toCharacterMode([Selections.fromAnchorActive(1, 1, 0, 0)]), "to satisfy", [ - expect.it("to have anchor at coords", 1, 1).and("to have cursor at coords", 0, 0), - ]); - - // Empty selection stays as-is. - expect(Selections.toCharacterMode([Selections.empty(1, 1)]), "to satisfy", [ - expect.it("to be empty at coords", 1, 1), - ]); - }); - - // No expected end document. - }); - - test("function fromCharacterMode", async function () { - const editorState = extension.editors.getState(editor)!, - context = new Context(editorState, cancellationToken), - before = ExpectedDocument.parseIndented(14, String.raw` - `); - - await before.apply(editor); - - await context.runAsync(async () => { - expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ - expect.it("to be empty at coords", 0, 0), - ]); - }); - - // No expected end document. - }); - - test("function fromCharacterMode#1", async function () { - const editorState = extension.editors.getState(editor)!, - context = new Context(editorState, cancellationToken), - before = ExpectedDocument.parseIndented(14, String.raw` - a - b - - `); - - await before.apply(editor); - - await context.runAsync(async () => { - expect(Selections.fromCharacterMode([Selections.empty(0, 0)]), "to satisfy", [ - expect.it("to have anchor at coords", 0, 0).and("to have cursor at coords", 0, 1), - ]); - - // At the end of the line, it selects the line ending: - expect(Selections.fromCharacterMode([Selections.empty(0, 1)]), "to satisfy", [ - expect.it("to have anchor at coords", 0, 1).and("to have cursor at coords", 1, 0), - ]); - - // But it does nothing at the end of the document: - expect(Selections.fromCharacterMode([Selections.empty(2, 0)]), "to satisfy", [ - expect.it("to be empty at coords", 2, 0), - ]); - }); - - // No expected end document. - }); - }); });