diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a27c1afc..68aa3b9d8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,10 +7,12 @@ "ahlbrecht", "alnum", "Alsos", + "analyse", "analysing", "arglist", "arglists", "autoindent", + "avli", "Batsov", "behaviour", "bencode", @@ -24,11 +26,15 @@ "calva", "Calva's", "calvapretty", + "ccls", "chmod", "cibuilds", "circleci", + "clangd", "classpath", + "clientrc", "cljc", + "cljd", "cljfmt", "cljfx", "cljify", @@ -44,7 +50,9 @@ "Cognitect", "Configurability", "cospaia", + "cpcache", "Dallo", + "darcs", "datafication", "debugable", "debugadapter", @@ -68,10 +76,12 @@ "Elisp", "enablement", "enablements", + "ensime", "entrypoint", "errored", "ESPACEIALLY", "être", + "eunit", "eval", "evals", "falsesomething", @@ -82,6 +92,7 @@ "filipe", "fipp", "foob", + "fslckout", "FUBAR", "gifs", "Girardi", @@ -139,6 +150,7 @@ "parinfer", "pidfile", "piggieback", + "pijul", "polyrepos", "postrelease", "pprint", @@ -189,6 +201,7 @@ "unpromote", "unpromoted", "unsets", + "uuidv", "visibles", "vsce", "vscodevim", diff --git a/CHANGELOG.md b/CHANGELOG.md index 2511919d2..1de4167e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ Changes to Calva. ## [Unreleased] +- [Multi cursor support for Paredit](https://github.com/BetterThanTomorrow/calva/issues/610) +- [Remove when-contexts as a way to control when commands move by sexp or by word](https://github.com/BetterThanTomorrow/calva/pull/1606#issuecomment-1086567905) ## [2.0.267] - 2022-04-13 - [Add command for formatting away multiple space between forms on the same line](https://github.com/BetterThanTomorrow/calva/issues/1677) @@ -10,6 +12,8 @@ Changes to Calva. ## [2.0.266] - 2022-04-11 - Fix: [Jump between source/test does not work properly with multiple workspace folders](https://github.com/BetterThanTomorrow/calva/issues/1219) +- [New diagnostics command, Print TextNotation from current document](https://github.com/BetterThanTomorrow/calva/issues/1675) (See [this CalvaTV video](https://www.youtube.com/watch?v=Sy3arG-Degw)) +- [New diagnostics command, Create untitled document from TextNotation](https://github.com/BetterThanTomorrow/calva/issues/1676) ## [2.0.265] - 2022-04-08 - [Update paredit sexp forward/backward command labels](https://github.com/BetterThanTomorrow/calva/issues/1660) diff --git a/docs/site/customizing.md b/docs/site/customizing.md index 4f45bf2e2..6e78d4b71 100644 --- a/docs/site/customizing.md +++ b/docs/site/customizing.md @@ -75,7 +75,7 @@ The versions used are configurable via the VS Code settings `calva.jackInDepende ## Key bindings -Most of Calva's commands have default keybindings. They are only defaults, though, and you can change keybindings as you wish. To facilitate precision in binding keys Calva keeps some [when clause contexts](https://code.visualstudio.com/api/references/when-clause-contexts) updated. +Most of Calva's commands have default keybindings. They are only defaults, though, and you can change keybindings as you wish. To facilitate precision in binding keys Calva keeps some [when clause contexts](https://code.visualstudio.com/api/references/when-clause-contexts) updated. ### When Clause Contexts @@ -87,15 +87,15 @@ The following contexts are available with Calva: * `calva:outputWindowActive`: `true` when the [Output/REPL window](output.md) has input focus * `calva:replHistoryCommandsActive`: `true` when the cursor is in the Output/REPL window at the top level after the last prompt * `calva:outputWindowSubmitOnEnter`: `true` when the cursor is adjacent after the last top level form in the Output/REPL window -* `calva:cursorInString`: `true` when the cursor/caret is in a string or a regexp -* `calva:cursorInComment`: `true` when the cursor is in, or adjacent to a line comment -* `calva:cursorBeforeComment`: `true` when the cursor is adjacent before a line comment -* `calva:cursorAfterComment`: `true` when the cursor is adjacent after a line comment -* `calva:cursorAtStartOfLine`: `true` when the cursor is at the start of a line including any leading whitespace -* `calva:cursorAtEndOfLine`: `true` when the cursor is at the end of a line including any trailing whitespace + + + + + + * `calva:showReplUi`: `false` when Calva's REPL UI is disabled through the corresponding setting -### Some Custom Bindings +### Some Custom Bindings Here is a collection of custom keybindings from here and there. diff --git a/package.json b/package.json index a9f5e42b2..01e99c160 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ }, "printEngine": { "type": "string", - "description": "The print engine to use. 'calva' means that the nREPL server will first plain print it and then Calva will prettify. The other options will make the server use the chosen printer-function to print the result, and Calva will not reformat it. To use some other function (on the server), configure `printFn` instead. To use the `nREPL` default (the equivalent of `clojure.core/pr`), set neither this nore `printFn`.", + "description": "The print engine to use. 'calva' means that the nREPL server will first plain print it and then Calva will prettify. The other options will make the server use the chosen printer-function to print the result, and Calva will not reformat it. To use some other function (on the server), configure `printFn` instead. To use the `nREPL` default (the equivalent of `clojure.core/pr`), set neither this nor `printFn`.", "enum": [ "calva", "pprint", @@ -917,6 +917,16 @@ "title": "Toggle nREPL Logging Enabled", "category": "Calva Diagnostics" }, + { + "command": "calva.diagnostics.printTextNotationFromDocument", + "title": "Print TextNotation from the current document to Calva says", + "category": "Calva Diagnostics" + }, + { + "command": "calva.diagnostics.createDocumentFromTextNotation", + "title": "Create a new Clojure Document from TextNotation", + "category": "Calva Diagnostics" + }, { "command": "calva.linting.resolveMacroAs", "title": "Resolve Macro As", @@ -1895,28 +1905,28 @@ "mac": "ctrl+left", "win": "alt+left", "linux": "alt+left", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorBeforeComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.backwardSexp", "mac": "alt+left", "win": "ctrl+left", "linux": "ctrl+left", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorBeforeComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.forwardSexp", "mac": "ctrl+right", "win": "alt+right", "linux": "alt+right", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorAfterComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.forwardSexp", "mac": "alt+right", "win": "ctrl+right", "linux": "ctrl+right", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorAfterComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.forwardDownSexp", @@ -2009,14 +2019,14 @@ "mac": "ctrl+w", "win": "shift+alt+right", "linux": "shift+alt+right", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" }, { "command": "paredit.sexpRangeContraction", "mac": "ctrl+shift+w", "win": "shift+alt+left", "linux": "shift+alt+left", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" }, { "command": "paredit.slurpSexpForward", @@ -2068,22 +2078,22 @@ { "command": "paredit.dragSexprBackward", "key": "ctrl+shift+alt+b", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" }, { "command": "paredit.dragSexprForward", "key": "ctrl+shift+alt+f", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" }, { "command": "paredit.dragSexprBackward", "key": "alt+up", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /strict/ && config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.dragSexprForward", "key": "alt+down", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /strict/ && config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.dragSexprBackwardUp", @@ -2190,12 +2200,12 @@ { "command": "paredit.deleteForward", "key": "delete", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap == strict && !editorReadOnly && !editorHasMultipleSelections && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap == strict && !editorReadOnly && !editorHasMultipleSelections" }, { "command": "paredit.deleteBackward", "key": "backspace", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap == strict && !editorReadOnly && !editorHasMultipleSelections && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap == strict && !editorReadOnly && !editorHasMultipleSelections" }, { "command": "paredit.forceDeleteForward", @@ -2729,7 +2739,7 @@ "calva-lib-test": "node ./out/cljs-lib/test/cljs-lib-tests.js", "integration-test": "node ./out/extension-test/integration/runTests.js", "unit-test": "npx mocha --require ts-node/register 'src/extension-test/unit/**/*-test.ts'", - "unit-test-watch": "npx mocha --watch --require ts-node/register 'src/extension-test/unit/**/*-test.ts'", + "unit-test-watch": "npx mocha --watch --require ts-node/register --watch-extensions ts --watch-files src 'src/extension-test/unit/**/*-test.ts'", "publish": "bb publish.clj", "prettier-format": "npx prettier --write './**/*.{ts,js,json}'", "prettier-check": "npx prettier --check './**/*.{ts,js,json}'", diff --git a/src/calva-fmt/src/providers/ontype_formatter.ts b/src/calva-fmt/src/providers/ontype_formatter.ts index cc7107370..316184080 100644 --- a/src/calva-fmt/src/providers/ontype_formatter.ts +++ b/src/calva-fmt/src/providers/ontype_formatter.ts @@ -7,7 +7,7 @@ import { getConfig } from '../../../config'; import * as util from '../../../utilities'; export class FormatOnTypeEditProvider implements vscode.OnTypeFormattingEditProvider { - async provideOnTypeFormattingEdits( + provideOnTypeFormattingEdits( document: vscode.TextDocument, _position: vscode.Position, ch: string, @@ -22,10 +22,11 @@ export class FormatOnTypeEditProvider implements vscode.OnTypeFormattingEditProv if (tokenCursor.withinComment()) { return undefined; } - return paredit.backspace(mDoc).then((fulfilled) => { - paredit.close(mDoc, ch); + void paredit.backspace(mDoc).then((fulfilled) => { + void paredit.close(mDoc, ch); return undefined; }); + return; } else { return undefined; } diff --git a/src/cursor-doc/cursor-context.ts b/src/cursor-doc/cursor-context.ts index f80a2bc19..63b1beffc 100644 --- a/src/cursor-doc/cursor-context.ts +++ b/src/cursor-doc/cursor-context.ts @@ -1,12 +1,12 @@ import { EditableDocument } from './model'; export const allCursorContexts = [ - 'calva:cursorInString', - 'calva:cursorInComment', - 'calva:cursorAtStartOfLine', - 'calva:cursorAtEndOfLine', - 'calva:cursorBeforeComment', - 'calva:cursorAfterComment', + // 'calva:cursorInString', + // 'calva:cursorInComment', + // 'calva:cursorAtStartOfLine', + // 'calva:cursorAtEndOfLine', + // 'calva:cursorBeforeComment', + // 'calva:cursorAfterComment', ] as const; export type CursorContext = typeof allCursorContexts[number]; @@ -15,7 +15,7 @@ export type CursorContext = typeof allCursorContexts[number]; * Returns true if documentOffset is either at the first char of the token under the cursor, or * in the whitespace between the token and the first preceding EOL, otherwise false */ -export function isAtLineStartInclWS(doc: EditableDocument, offset = doc.selection.active) { +export function isAtLineStartInclWS(doc: EditableDocument, offset = doc.selections[0].active) { const tokenCursor = doc.getTokenCursor(offset); let startOfLine = false; // only at start if we're in ws, or at the 1st char of a non-ws sexp @@ -33,7 +33,7 @@ export function isAtLineStartInclWS(doc: EditableDocument, offset = doc.selectio * Returns true if position is after the last char of the last lisp token on the line, including * any trailing whitespace or EOL, otherwise false */ -export function isAtLineEndInclWS(doc: EditableDocument, offset = doc.selection.active) { +export function isAtLineEndInclWS(doc: EditableDocument, offset = doc.selections[0].active) { const tokenCursor = doc.getTokenCursor(offset); if (tokenCursor.getToken().type === 'eol') { return true; @@ -56,27 +56,29 @@ export function isAtLineEndInclWS(doc: EditableDocument, offset = doc.selection. return false; } +// TODO: setting the vscode ext context for cursor context might not work for multi-cursor export function determineContexts( doc: EditableDocument, - offset = doc.selection.active -): CursorContext[] { + offset = doc.selections[0].active +): readonly CursorContext[] { const tokenCursor = doc.getTokenCursor(offset); - const contexts: CursorContext[] = []; + // const contexts: CursorContext[] = []; + const contexts: readonly CursorContext[] = allCursorContexts; if (isAtLineStartInclWS(doc)) { - contexts.push('calva:cursorAtStartOfLine'); + // contexts.push('calva:cursorAtStartOfLine'); } else if (isAtLineEndInclWS(doc)) { - contexts.push('calva:cursorAtEndOfLine'); + // contexts.push('calva:cursorAtEndOfLine'); } if (tokenCursor.withinString()) { - contexts.push('calva:cursorInString'); + // contexts.push('calva:cursorInString'); } else if (tokenCursor.withinComment()) { - contexts.push('calva:cursorInComment'); + // contexts.push('calva:cursorInComment'); } // Compound contexts - if (contexts.includes('calva:cursorInComment')) { + /* if (contexts.includes('calva:cursorInComment')) { if (contexts.includes('calva:cursorAtEndOfLine')) { tokenCursor.forwardWhitespace(false); if (tokenCursor.getToken().type != 'comment') { @@ -88,7 +90,7 @@ export function determineContexts( contexts.push('calva:cursorBeforeComment'); } } - } + } */ return contexts; } diff --git a/src/cursor-doc/cursor-doc-utils.ts b/src/cursor-doc/cursor-doc-utils.ts new file mode 100644 index 000000000..b5af5fc63 --- /dev/null +++ b/src/cursor-doc/cursor-doc-utils.ts @@ -0,0 +1,69 @@ +import { first, isArray, isNumber, last, ListIteratee, ListIterator } from 'lodash'; +import _ = require('lodash'); +import { ModelEditSelection } from './model'; + +export type SimpleRange = [start: number, end: number]; +export type SimpleDirectedRange = [anchor: number, active: number]; + +export const mapRangeOrSelectionToOffset = + /* */ + + + (whichOffset: 'start' | 'end' | 'anchor' | 'active' = 'start') => + (t: T | [T, number]): number => { + const rangeOrSel = isModelEditSelection(t) || isSimpleRange(t) ? t : first(t); + + if (rangeOrSel instanceof ModelEditSelection) { + return rangeOrSel[whichOffset]; + } else if (Array.isArray(rangeOrSel)) { + let fn; + switch (whichOffset) { + case 'start': + fn = Math.min; + break; + case 'end': + fn = Math.max; + break; + case 'anchor': + fn = (...x) => first(x); + break; + case 'active': + fn = (...x) => last(x); + break; + default: + first; + } + + return fn(...rangeOrSel); + } + }; + +export function repositionRangeOrSelectionByCumulativeOffsets( + offsetGetter: ListIterator | number +) { + return ( + s: ModelEditSelection, + index: number, + array: ModelEditSelection[] + ): ModelEditSelection => { + const newSel = s.clone(); + + const getItemOffset = isNumber(offsetGetter) ? () => offsetGetter : offsetGetter; + + const offset = _(array) + .filter((x) => x.start < s.start) + .map(getItemOffset) + .sum(); + + newSel.reposition(offset); + return newSel; + }; +} + +export function isSimpleRange(o: any): o is SimpleRange { + return isArray(o) && o.length === 2 && isNumber(o[0]) && isNumber(o[1]); +} + +export function isModelEditSelection(o: any): o is ModelEditSelection { + return o instanceof ModelEditSelection; +} diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index fcca51ff1..dbcadfb8a 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -1,7 +1,8 @@ -import { Scanner, Token, ScannerState } from './clojure-lexer'; -import { LispTokenCursor } from './token-cursor'; +import { isUndefined, max, min, isNumber } from 'lodash'; import { deepEqual as equal } from '../util/object'; -import { isUndefined } from 'lodash'; +import { Scanner, ScannerState, Token } from './clojure-lexer'; +import { LispTokenCursor } from './token-cursor'; +import type { Selection, TextDocument } from 'vscode'; let scanner: Scanner; @@ -27,9 +28,14 @@ export class TextLine { } export type ModelEditFunction = 'insertString' | 'changeRange' | 'deleteRange'; - -export class ModelEdit { - constructor(public editFn: ModelEditFunction, public args: any[]) {} +export type ModelEditFunctionArgs = T extends 'insertString' + ? Parameters + : T extends 'changeRange' + ? Parameters + : Parameters; + +export class ModelEdit { + constructor(public editFn: T, public args: ModelEditFunctionArgs) {} } /** @@ -45,26 +51,59 @@ export class ModelEdit { * * This will be in line with vscode when it comes to anchor/active, but introduce our own terminology for the span of the selection. It will also keep the tradition of paredit with backward/forward and up/down. */ - export class ModelEditSelection { private _anchor: number; private _active: number; - - constructor(anchor: number, active?: number) { - this._anchor = anchor; - if (active !== undefined) { - this._active = active; + private _start: number; + private _end: number; + private _isReversed: boolean; + + constructor(anchor: number, active?: number, start?: number, end?: number, isReversed?: boolean); + constructor(selection: Selection, doc: TextDocument); + constructor( + anchorOrSelection: number | Selection, + activeOrDoc?: number | TextDocument, + start?: number, + end?: number, + isReversed?: boolean + ) { + if (isNumber(anchorOrSelection)) { + const anchor = anchorOrSelection; + this._anchor = anchor; + if (activeOrDoc !== undefined && isNumber(activeOrDoc)) { + this._active = activeOrDoc; + } else { + this._active = anchor; + } + isReversed = isReversed ?? this._anchor > this._active; + this._isReversed = isReversed; + this._start = start ?? isReversed ? this._active : Math.min(anchor, this._active); + this._end = end ?? isReversed ? anchor : Math.max(anchor, this._active); } else { - this._active = anchor; + const { active, anchor, start, end, isReversed } = anchorOrSelection; + // const doc = getActiveTextEditor().document; + const doc = activeOrDoc as TextDocument; + this._active = doc.offsetAt(active); + this._anchor = doc.offsetAt(anchor); + this._start = doc.offsetAt(start); + this._end = doc.offsetAt(end); + this._isReversed = isReversed; } } + private _updateDirection() { + this._start = Math.min(this._anchor, this._active); + this._end = Math.max(this._anchor, this._active); + this._isReversed = this._active < this._anchor; + } + get anchor() { return this._anchor; } set anchor(v: number) { this._anchor = v; + this._updateDirection(); } get active() { @@ -73,29 +112,144 @@ export class ModelEditSelection { set active(v: number) { this._active = v; + this._updateDirection(); + } + + get start() { + this._updateDirection(); + return this._start; + } + + /* set start(v: number) { + // TODO: figure out .start setter logic + this._start = v; + if (this._start === this._anchor) { + this._isReversed = false; + } else if (this._start === this._active) { + this._isReversed = true; + } else if (this._isReversed) { + this._active = this._start; + } else if (!this._isReversed) { + this._anchor = this._start; + } + } */ + + get end() { + this._updateDirection(); + return this._end; + } + + /* set end(v: number) { + // TODO: figure out .end setter logic + // TODO: figure out .start setter logic + this.end = v; + + if (this._end < this._start) { + this._start; + } + + if (this.end === this._anchor) { + this._isReversed = true; + } else if (this.end === this._active) { + this._isReversed = false; + } else if (this._isReversed) { + this._anchor = this.end; + } else if (!this._isReversed) { + this._active = this.end; + } + } */ + + get isCursor() { + return this.anchor === this.active; + } + + get isSelection() { + return this.anchor !== this.active; + } + + get isReversed() { + this._updateDirection(); + return this._isReversed; + } + + set isReversed(isReversed: boolean) { + this._isReversed = isReversed; + if (this._isReversed) { + this._start = this._active; + this._end = this._anchor; + } else { + this._start = this._anchor; + this._end = this._active; + } + } + + get distance() { + return this._end - this._start; } clone() { return new ModelEditSelection(this._anchor, this._active); } + + /** + * Returns a simple 2-item tuple representing the + * [leftmost/earliest/start position, rightmost, farthest, end position]. + */ + get asRange() { + return [this.start, this.end] as [start: number, end: number]; + } + + /** + * Same as `ModelEditSelection.asRange` but with the leftmost item being the anchor position, and the rightmost item + * being the active position. This way, you can tell if it's reversed by checking if the leftmost item is greater + * than the rightmost item. + */ + get asDirectedRange() { + return [this.anchor, this.active] as [anchor: number, active: number]; + } + + /** + * Mutates itself! + * Very basic, offsets both active/anchor by a positive or negative number lol, with no attempt at clamping. + * + * Returns self for convenience + * @param offset number + */ + reposition(offset: number) { + this.active += offset; + this.anchor += offset; + + this._updateDirection(); + + return this; + } } export type ModelEditOptions = { undoStopBefore?: boolean; formatDepth?: number; skipFormat?: boolean; - selection?: ModelEditSelection; + selections?: ModelEditSelection[]; }; +export type ModelEditResult = { + edits: ModelEdit[]; + selections: ModelEditSelection[]; + success: boolean; +}; export interface EditableModel { readonly lineEndingLength: number; + readonly lineEnding: string; /** * Performs a model edit batch. * For some EditableModel's these are performed as one atomic set of edits. * @param edits */ - edit: (edits: ModelEdit[], options: ModelEditOptions) => Thenable; + edit: ( + edits: ModelEdit[], + options: ModelEditOptions + ) => Thenable; getText: (start: number, end: number, mustBeWithin?: boolean) => string; getLineText: (line: number) => string; @@ -104,14 +258,27 @@ export interface EditableModel { } export interface EditableDocument { - selection: ModelEditSelection; + selections: ModelEditSelection[]; model: EditableModel; - selectionStack: ModelEditSelection[]; + /** + * A stack of selections - that is, a 2d array, where the outer array index is a point in "selection/form nesting order" and the inner array index is which cursor that ModelEditSelection belongs to. That "selection/form nesting order" axis can be thought of as the axis for time, or something close to that. That is, .selectionStacks + * is only used when the user invokes the "Expand Selection" or "Shrink Selection" Paredit commands, such that each time the user invokes "Expand", it pushes an item onto the stack. Similarly, when "Shrink" is invoked, the last item + * is popped. In essence, it's sort of an undo/history/time stack for the selection of forms/text in the current document. + * + * Each item in the stack however, is not a single selection, but rather an array of selections, one for each active cursor. Recall that vscode users may have as many cursors as they like, and will expect that selection expansion/shrinking commands should work equally well for each cursor, with respect to their particular locations, as they do outside of Paredit, eg with vscode's native selection expansion/shrinking commands. + * + * A further detail is that, along the "selection/form nesting order" axis, the selections are not an undo history of + * the user's arbitrary selections anywhere in the document, but specifically of the order in which selection expansion operates. That is, if we for simplicity pretend there can only be one cursor, each selection stack is a stack whose items hold forms/s-exps, such that each item is the form immediately enclosing the previous one. As such, we can imagine traversing forward (towards the top/right) of the stack + * as representing expanding the selection of forms by each nesting level, and backwards as shrinking the selection back down to the starting form/cursor position. + * + */ + selectionsStack: ModelEditSelection[][]; getTokenCursor: (offset?: number, previous?: boolean) => LispTokenCursor; insertString: (text: string) => void; - getSelectionText: () => string; - delete: () => Thenable; - backspace: () => Thenable; + getSelectionTexts: () => string[]; + getSelectionText: (index: number) => string; + delete: () => Thenable; + backspace: () => Thenable; } /** The underlying model for the REPL readline. */ @@ -119,6 +286,10 @@ export class LineInputModel implements EditableModel { /** How many characters in the line endings of the text of this model? */ constructor(readonly lineEndingLength: number = 1, private document?: EditableDocument) {} + get lineEnding() { + return this.lineEndingLength === 1 ? '\n' : '\r\n'; + } + /** The input lines. */ lines: TextLine[] = [new TextLine('', this.getStateForLine(0))]; @@ -356,7 +527,7 @@ export class LineInputModel implements EditableModel { * Doesn't need to be atomic in the LineInputModel. * @param edits */ - edit(edits: ModelEdit[], options: ModelEditOptions): Thenable { + edit(edits: ModelEdit[], options: ModelEditOptions): Thenable { return new Promise((resolve, reject) => { for (const edit of edits) { switch (edit.editFn) { @@ -379,10 +550,10 @@ export class LineInputModel implements EditableModel { break; } } - if (this.document && options.selection) { - this.document.selection = options.selection; + if (this.document && options.selections) { + this.document.selections = options.selections; } - resolve(true); + resolve({ edits, selections: options.selections, success: true }); }); } @@ -397,13 +568,7 @@ export class LineInputModel implements EditableModel { * @param oldSelection the old selection * @param newSelection the new selection */ - private changeRange( - start: number, - end: number, - text: string, - oldSelection?: [number, number], - newSelection?: [number, number] - ) { + private changeRange(start: number, end: number, text: string) { const t1 = new Date(); const startPos = Math.min(start, end); @@ -469,13 +634,8 @@ export class LineInputModel implements EditableModel { * @param text the text to insert * @param oldCursor the [row,col] of the cursor at the start of the operation */ - insertString( - offset: number, - text: string, - oldSelection?: [number, number], - newSelection?: [number, number] - ): number { - this.changeRange(offset, offset, text, oldSelection, newSelection); + insertString(offset: number, text: string): number { + this.changeRange(offset, offset, text); return text.length; } @@ -494,7 +654,7 @@ export class LineInputModel implements EditableModel { oldSelection?: [number, number], newSelection?: [number, number] ) { - this.changeRange(offset, offset + count, '', oldSelection, newSelection); + this.changeRange(offset, offset + count, ''); } /** Return the offset of the last character in this model. */ @@ -532,11 +692,11 @@ export class StringDocument implements EditableDocument { } } - selection: ModelEditSelection; + selections: ModelEditSelection[]; model: LineInputModel = new LineInputModel(1, this); - selectionStack: ModelEditSelection[] = []; + selectionsStack: ModelEditSelection[][] = []; getTokenCursor(offset?: number, previous?: boolean): LispTokenCursor { if (isUndefined(offset)) { @@ -550,19 +710,24 @@ export class StringDocument implements EditableDocument { this.model.insertString(0, text); } - getSelectionText: () => string; + getSelectionTexts: () => string[]; + getSelectionText: (index: number) => string; delete() { - const p = this.selection.anchor; - return this.model.edit([new ModelEdit('deleteRange', [p, 1])], { - selection: new ModelEditSelection(p), - }); + return this.model.edit( + this.selections.map(({ anchor: p }) => new ModelEdit('deleteRange', [p, 1])), + { + selections: this.selections.map(({ anchor: p }) => new ModelEditSelection(p)), + } + ); } backspace() { - const p = this.selection.anchor; - return this.model.edit([new ModelEdit('deleteRange', [p - 1, 1])], { - selection: new ModelEditSelection(p - 1), - }); + return this.model.edit( + this.selections.map(({ anchor: p }) => new ModelEdit('deleteRange', [p - 1, 1])), + { + selections: this.selections.map(({ anchor: p }) => new ModelEditSelection(p - 1)), + } + ); } } diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index cba3ac35e..39c18b2b8 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -1,6 +1,19 @@ +import { isEqual, last, pick, property, clone, isBoolean, orderBy, isNil, flatten } from 'lodash'; +import _ = require('lodash'); import { validPair } from './clojure-lexer'; -import { ModelEdit, EditableDocument, ModelEditSelection } from './model'; +import { + EditableDocument, + ModelEdit, + ModelEditSelection, + ModelEditResult, + ModelEditFunctionArgs, +} from './model'; import { LispTokenCursor } from './token-cursor'; +import { mapToItemAndOrder, mapToOriginalItem, mapToOriginalOrder, replaceAt } from '../util/array'; +import { + mapRangeOrSelectionToOffset, + repositionRangeOrSelectionByCumulativeOffsets, +} from './cursor-doc-utils'; // NB: doc.model.edit returns a Thenable, so that the vscode Editor can compose commands. // But don't put such chains in this module because that won't work in the repl-console. @@ -12,113 +25,207 @@ import { LispTokenCursor } from './token-cursor'; // Example: paredit.moveToRangeRight(this.readline, paredit.forwardSexpRange(this.readline)) // => paredit.moveForwardSexp(this.readline) -export function killRange( - doc: EditableDocument, - range: [number, number], - start = doc.selection.anchor, - end = doc.selection.active -) { - const [left, right] = [Math.min(...range), Math.max(...range)]; - void doc.model.edit([new ModelEdit('deleteRange', [left, right - left, [start, end]])], { - selection: new ModelEditSelection(left), +export function killRange(doc: EditableDocument, ranges: Array<[number, number]>) { + const edits = [], + // use tuple to track + // [selection, original selection/cursor order before sorting by location, and amount of text deleted] + selections: [ModelEditSelection, number, number][] = []; + + /** + * Sorting by location backwards simplifies the underlying "deleteRange" operation, + * as the operation logic doesn't have to translate the range by the sum of each prior deletion. + * + * We still however have to do the aforementioned translation/relocation for the post-delete cursor replacement, + * but that happens ONCE, at the end of this whole range killing series, + * and also seems to not be subject to as many strange bugs as + * when we do the translation/relocation for deletion operations. + * + * For example, it appears that sometimes a series of deletions DOESN'T take into account + * prior (ascending order of location) deletions, + * so in order to prevent incorrect text being deleted, we might want to precalculate the updated offsets + * on behalf of the delete operations, as we suggested NOT doing above. + * + * However, sometimes, it DOES take into account the prior deletions (or at least some of them) + * + * Of course, if the latter occurs, yet we thought the former would, our preventative + * precalculated offset adjustments would then cause + * incorrect text to be deleted anyways. + */ + + _(ranges) + .map((r, idx) => [r, idx] as const) + .orderBy(([r]) => Math.min(...r), 'desc') + .forEach(([range, idx]) => { + // we assume the ranges passed above are some transformation of the current selections + // therefore doc.selections[index] should be the range before the transformation... maybe + const [left, right] = [Math.min(...range), Math.max(...range)]; + const length = right - left; + edits.push(new ModelEdit('deleteRange', [left, length])); + selections.push([new ModelEditSelection(left), idx, length]); + }); + + return doc.model.edit(edits, { + selections: _(selections) + // return to original selection/cursor order + .orderBy(([_, idx]) => idx) + // pull each cursor backwards by the amount of text deleted by every prior (by location) cursor's delete operation + .map( + ([selection], _index, others) => + new ModelEditSelection( + selection.start - + _(others) + .filter(([s]) => s.start < selection.start) + .reduce((sum, [_, __, length]) => sum + length, 0) + ) + ) + .value(), }); } -export function moveToRangeLeft(doc: EditableDocument, range: [number, number]) { - doc.selection = new ModelEditSelection(Math.min(range[0], range[1])); +export function moveToRangeLeft(doc: EditableDocument, ranges: Array<[number, number]>) { + // doc.selections = [new ModelEditSelection(Math.min(range[0], range[1]))]; + doc.selections = ranges.map((range) => new ModelEditSelection(Math.min(range[0], range[1]))); } -export function moveToRangeRight(doc: EditableDocument, range: [number, number]) { - doc.selection = new ModelEditSelection(Math.max(range[0], range[1])); +export function moveToRangeRight(doc: EditableDocument, ranges: Array<[number, number]>) { + doc.selections = ranges.map((range) => new ModelEditSelection(Math.max(range[0], range[1]))); } -export function selectRange(doc: EditableDocument, range: [number, number]) { - growSelectionStack(doc, range); +export function selectRange(doc: EditableDocument, ranges: Array<[number, number]>) { + growSelectionStack(doc, ranges); } -export function selectRangeForward(doc: EditableDocument, range: [number, number]) { - const selectionLeft = doc.selection.anchor; - const rangeRight = Math.max(range[0], range[1]); - growSelectionStack(doc, [selectionLeft, rangeRight]); +export function selectRangeForward( + doc: EditableDocument, + // selections: Array<[number, number]> = doc.selections.map(s => ([s.anchor, s.active])) + ranges: Array<[number, number]> = doc.selections.map((s) => [s.anchor, s.active]) +) { + growSelectionStack( + doc, + ranges.map((range, index) => { + const selectionLeft = doc.selections[index].anchor; + const rangeRight = Math.max(range[0], range[1]); + return [selectionLeft, rangeRight]; + }) + ); } -export function selectRangeBackward(doc: EditableDocument, range: [number, number]) { - const selectionRight = doc.selection.anchor; - const rangeLeft = Math.min(range[0], range[1]); - growSelectionStack(doc, [selectionRight, rangeLeft]); +export function selectRangeBackward(doc: EditableDocument, ranges: Array<[number, number]>) { + growSelectionStack( + doc, + ranges.map((range, index) => { + const selectionRight = doc.selections[index].anchor; + const rangeLeft = Math.min(range[0], range[1]); + return [selectionRight, rangeLeft]; + }) + ); } +// TODO: could prob use ModelEditSelection semantics for `end` versus checking for active >= anchor export function selectForwardSexp(doc: EditableDocument) { - const rangeFn = - doc.selection.active >= doc.selection.anchor - ? forwardSexpRange - : (doc: EditableDocument) => forwardSexpRange(doc, doc.selection.active, true); - selectRangeForward(doc, rangeFn(doc)); + const ranges = doc.selections.map((selection) => { + const rangeFn = + selection.active >= selection.anchor + ? forwardSexpRange + : (doc: EditableDocument) => forwardSexpRange(doc, [selection.active], true); + return rangeFn(doc, [selection.start])[0]; + }); + selectRangeForward(doc, ranges); } +// TODO: could prob use ModelEditSelection semantics for `end` versus checking for active >= anchor export function selectRight(doc: EditableDocument) { - const rangeFn = - doc.selection.active >= doc.selection.anchor - ? forwardHybridSexpRange - : (doc: EditableDocument) => forwardHybridSexpRange(doc, doc.selection.active, true); - selectRangeForward(doc, rangeFn(doc)); + const ranges = doc.selections.map((selection) => { + const rangeFn = + selection.active >= selection.anchor + ? (doc) => forwardHybridSexpRange(doc, [selection.end])[0] + : (doc: EditableDocument) => forwardHybridSexpRange(doc, [selection.active], true)[0]; + return rangeFn(doc); + }); + selectRangeForward(doc, ranges); } export function selectForwardSexpOrUp(doc: EditableDocument) { - const rangeFn = - doc.selection.active >= doc.selection.anchor - ? forwardSexpOrUpRange - : (doc: EditableDocument) => forwardSexpOrUpRange(doc, doc.selection.active, true); - - selectRangeForward(doc, rangeFn(doc)); + const ranges = doc.selections.map((selection) => { + const rangeFn = + selection.active >= selection.anchor + ? (doc) => forwardSexpOrUpRange(doc, [selection.end])[0] + : (doc: EditableDocument) => forwardSexpOrUpRange(doc, [selection.active], true)[0]; + return rangeFn(doc); + }); + selectRangeForward(doc, ranges); } export function selectBackwardSexp(doc: EditableDocument) { - const rangeFn = - doc.selection.active <= doc.selection.anchor - ? backwardSexpRange - : (doc: EditableDocument) => backwardSexpRange(doc, doc.selection.active, false); - selectRangeBackward(doc, rangeFn(doc)); + const ranges = doc.selections.map((selection) => { + const rangeFn = + selection.active <= selection.anchor + ? backwardSexpRange + : (doc: EditableDocument) => backwardSexpRange(doc, [selection.active], false); + return rangeFn(doc, [selection.start])[0]; + }); + selectRangeBackward(doc, ranges); } export function selectForwardDownSexp(doc: EditableDocument) { - const rangeFn = - doc.selection.active >= doc.selection.anchor - ? (doc: EditableDocument) => rangeToForwardDownList(doc, doc.selection.active, true) - : (doc: EditableDocument) => rangeToForwardDownList(doc, doc.selection.active, true); - selectRangeForward(doc, rangeFn(doc)); + const ranges = doc.selections.map((selection) => { + const rangeFn = + selection.active >= selection.anchor + ? (doc: EditableDocument) => rangeToForwardDownList(doc, selection.active, true) + : (doc: EditableDocument) => rangeToForwardDownList(doc, selection.active, true); + return rangeFn(doc); + }); + selectRangeForward(doc, ranges); } export function selectBackwardDownSexp(doc: EditableDocument) { - selectRangeBackward(doc, rangeToBackwardDownList(doc)); + selectRangeBackward( + doc, + doc.selections.map((selection) => rangeToBackwardDownList(doc, selection.start)) + ); } export function selectForwardUpSexp(doc: EditableDocument) { - selectRangeForward(doc, rangeToForwardUpList(doc, doc.selection.active)); + selectRangeForward( + doc, + doc.selections.map((selection) => rangeToForwardUpList(doc, selection.end)) + ); } export function selectBackwardUpSexp(doc: EditableDocument) { - const rangeFn = - doc.selection.active <= doc.selection.anchor - ? (doc: EditableDocument) => rangeToBackwardUpList(doc, doc.selection.active, false) - : (doc: EditableDocument) => rangeToBackwardUpList(doc, doc.selection.active, false); - selectRangeBackward(doc, rangeFn(doc)); + const ranges = doc.selections.map((selection) => { + const rangeFn = + selection.active <= selection.anchor + ? (doc: EditableDocument) => rangeToBackwardUpList(doc, selection.active, false) + : (doc: EditableDocument) => rangeToBackwardUpList(doc, selection.active, false); + return rangeFn(doc); + }); + selectRangeBackward(doc, ranges); } export function selectBackwardSexpOrUp(doc: EditableDocument) { - const rangeFn = - doc.selection.active <= doc.selection.anchor - ? (doc: EditableDocument) => backwardSexpOrUpRange(doc, doc.selection.active, false) - : (doc: EditableDocument) => backwardSexpOrUpRange(doc, doc.selection.active, false); - selectRangeBackward(doc, rangeFn(doc)); + const ranges = doc.selections.map((selection) => { + const rangeFn = + selection.active <= selection.anchor + ? (doc: EditableDocument) => backwardSexpOrUpRange(doc, [selection.active], false) + : (doc: EditableDocument) => backwardSexpOrUpRange(doc, [selection.active], false); + return rangeFn(doc)[0]; + }); + selectRangeBackward(doc, ranges); } export function selectCloseList(doc: EditableDocument) { - selectRangeForward(doc, rangeToForwardList(doc, doc.selection.active)); + selectRangeForward( + doc, + doc.selections.map((selection) => rangeToForwardList(doc, selection.end)) + ); } export function selectOpenList(doc: EditableDocument) { - selectRangeBackward(doc, rangeToBackwardList(doc)); + selectRangeBackward( + doc, + doc.selections.map((selection) => rangeToBackwardList(doc, selection.start)) + ); } /** @@ -127,7 +234,7 @@ export function selectOpenList(doc: EditableDocument) { */ export function rangeForDefun( doc: EditableDocument, - offset: number = doc.selection.active, + offset: number = doc.selections[0].active, commentCreatesTopLevel = true ): [number, number] { const cursor = doc.getTokenCursor(offset); @@ -150,34 +257,36 @@ enum GoUpSexpOption { */ function _forwardSexpRange( doc: EditableDocument, - offset = Math.max(doc.selection.anchor, doc.selection.active), + offsets = doc.selections.map((s) => s.end), goUpSexp: GoUpSexpOption, goPastWhitespace = false -): [number, number] { - const cursor = doc.getTokenCursor(offset); +): Array<[number, number]> { + return offsets.map((offset) => { + const cursor = doc.getTokenCursor(offset); - if (goUpSexp == GoUpSexpOption.Never || goUpSexp == GoUpSexpOption.WhenAtLimit) { - // Normalize our position by scooting to the beginning of the closest sexp - cursor.forwardWhitespace(); + if (goUpSexp == GoUpSexpOption.Never || goUpSexp == GoUpSexpOption.WhenAtLimit) { + // Normalize our position by scooting to the beginning of the closest sexp + cursor.forwardWhitespace(); - if (cursor.forwardSexp(true, true)) { - if (goPastWhitespace) { - cursor.forwardWhitespace(); + if (cursor.forwardSexp(true, true)) { + if (goPastWhitespace) { + cursor.forwardWhitespace(); + } + return [offset, cursor.offsetStart]; } - return [offset, cursor.offsetStart]; } - } - if (goUpSexp == GoUpSexpOption.Required || goUpSexp == GoUpSexpOption.WhenAtLimit) { - cursor.forwardList(); - if (cursor.upList()) { - if (goPastWhitespace) { - cursor.forwardWhitespace(); + if (goUpSexp == GoUpSexpOption.Required || goUpSexp == GoUpSexpOption.WhenAtLimit) { + cursor.forwardList(); + if (cursor.upList()) { + if (goPastWhitespace) { + cursor.forwardWhitespace(); + } + return [offset, cursor.offsetStart]; } - return [offset, cursor.offsetStart]; } - } - return [offset, offset]; + return [offset, offset]; + }); } /** @@ -185,62 +294,64 @@ function _forwardSexpRange( */ function _backwardSexpRange( doc: EditableDocument, - offset: number = Math.min(doc.selection.anchor, doc.selection.active), + offsets: number[] = doc.selections.map((s) => s.start), goUpSexp: GoUpSexpOption, goPastWhitespace = false -): [number, number] { - const cursor = doc.getTokenCursor(offset); - - if (goUpSexp == GoUpSexpOption.Never || goUpSexp == GoUpSexpOption.WhenAtLimit) { - if (!cursor.isWhiteSpace() && cursor.offsetStart < offset) { - // This is because cursor.backwardSexp() can't move backwards when "on" the first sexp inside a list - // TODO: Try to fix this in LispTokenCursor instead. - cursor.forwardSexp(); - } - cursor.backwardWhitespace(); +): Array<[number, number]> { + return offsets.map((offset) => { + const cursor = doc.getTokenCursor(offset); + + if (goUpSexp == GoUpSexpOption.Never || goUpSexp == GoUpSexpOption.WhenAtLimit) { + if (!cursor.isWhiteSpace() && cursor.offsetStart < offset) { + // This is because cursor.backwardSexp() can't move backwards when "on" the first sexp inside a list + // TODO: Try to fix this in LispTokenCursor instead. + cursor.forwardSexp(); + } + cursor.backwardWhitespace(); - if (cursor.backwardSexp(true, true)) { - if (goPastWhitespace) { - cursor.backwardWhitespace(); + if (cursor.backwardSexp(true, true)) { + if (goPastWhitespace) { + cursor.backwardWhitespace(); + } + return [cursor.offsetStart, offset]; } - return [cursor.offsetStart, offset]; } - } - if (goUpSexp == GoUpSexpOption.Required || goUpSexp == GoUpSexpOption.WhenAtLimit) { - cursor.backwardList(); - if (cursor.backwardUpList()) { - cursor.forwardSexp(true, true); - cursor.backwardSexp(true, true); - if (goPastWhitespace) { - cursor.backwardWhitespace(); + if (goUpSexp == GoUpSexpOption.Required || goUpSexp == GoUpSexpOption.WhenAtLimit) { + cursor.backwardList(); + if (cursor.backwardUpList()) { + cursor.forwardSexp(true, true); + cursor.backwardSexp(true, true); + if (goPastWhitespace) { + cursor.backwardWhitespace(); + } + return [cursor.offsetStart, offset]; } - return [cursor.offsetStart, offset]; } - } - return [offset, offset]; + return [offset, offset]; + }); } export function forwardSexpRange( doc: EditableDocument, - offset = Math.max(doc.selection.anchor, doc.selection.active), + offsets: number[] = doc.selections.map((s) => s.end), goPastWhitespace = false -): [number, number] { - return _forwardSexpRange(doc, offset, GoUpSexpOption.Never, goPastWhitespace); +): Array<[number, number]> { + return _forwardSexpRange(doc, offsets, GoUpSexpOption.Never, goPastWhitespace); } export function backwardSexpRange( doc: EditableDocument, - offset: number = Math.min(doc.selection.anchor, doc.selection.active), + offsets: number[] = doc.selections.map((s) => s.start), goPastWhitespace = false -): [number, number] { - return _backwardSexpRange(doc, offset, GoUpSexpOption.Never, goPastWhitespace); +): Array<[number, number]> { + return _backwardSexpRange(doc, offsets, GoUpSexpOption.Never, goPastWhitespace); } export function forwardListRange( doc: EditableDocument, - start: number = doc.selection.active + start: number = doc.selections[0].active ): [number, number] { const cursor = doc.getTokenCursor(start); cursor.forwardList(); @@ -249,7 +360,7 @@ export function forwardListRange( export function backwardListRange( doc: EditableDocument, - start: number = doc.selection.active + start: number = doc.selections[0].active ): [number, number] { const cursor = doc.getTokenCursor(start); cursor.backwardList(); @@ -270,108 +381,120 @@ export function backwardListRange( * @param goPastWhitespace * @returns [number, number] */ + export function forwardHybridSexpRange( doc: EditableDocument, - offset = Math.max(doc.selection.anchor, doc.selection.active), + // offset = Math.max(doc.selections.anchor, doc.selections.active), + // offset?: number = doc.selections[0].end, + // selections: ModelEditSelection[] = doc.selections, + offsets: number[] = doc.selections.map((s) => s.end), goPastWhitespace = false -): [number, number] { - let cursor = doc.getTokenCursor(offset); - if (cursor.getToken().type === 'open') { - return forwardSexpRange(doc); - } else if (cursor.getToken().type === 'close') { - return [offset, offset]; - } +): Array<[number, number]> { + return offsets.map<[number, number]>((offset) => { + // const { end: offset } = selection; + + let cursor = doc.getTokenCursor(offset); + if (cursor.getToken().type === 'open') { + const [forwarded] = forwardSexpRange(doc, [offset]); + return forwarded; + } else if (cursor.getToken().type === 'close') { + return [offset, offset]; + } - const currentLineText = doc.model.getLineText(cursor.line); - const lineStart = doc.model.getOffsetForLine(cursor.line); - const currentLineNewlineOffset = lineStart + currentLineText.length; - const remainderLineText = doc.model.getText(offset, currentLineNewlineOffset + 1); - - cursor.forwardList(); // move to the end of the current form - const currentFormEndToken = cursor.getToken(); - // when we've advanced the cursor but start is behind us then go to the end - // happens when in a clojure comment i.e: ;; ---- - const cursorOffsetEnd = cursor.offsetStart <= offset ? cursor.offsetEnd : cursor.offsetStart; - const text = doc.model.getText(offset, cursorOffsetEnd); - let hasNewline = text.indexOf('\n') > -1; - let end = cursorOffsetEnd; - - // Want the min of closing token or newline - // After moving forward, the cursor is not yet at the end of the current line, - // and it is not a close token. So we include the newline - // because what forms are here extend beyond the end of the current line - if (currentLineNewlineOffset > cursor.offsetEnd && currentFormEndToken.type != 'close') { - hasNewline = true; - end = currentLineNewlineOffset; - } + const currentLineText = doc.model.getLineText(cursor.line); + const lineStart = doc.model.getOffsetForLine(cursor.line); + const currentLineNewlineOffset = lineStart + currentLineText.length; + const remainderLineText = doc.model.getText(offset, currentLineNewlineOffset + 1); + + cursor.forwardList(); // move to the end of the current form + const currentFormEndToken = cursor.getToken(); + // when we've advanced the cursor but start is behind us then go to the end + // happens when in a clojure comment i.e: ;; ---- + const cursorOffsetEnd = cursor.offsetStart <= offset ? cursor.offsetEnd : cursor.offsetStart; + const text = doc.model.getText(offset, cursorOffsetEnd); + let hasNewline = text.indexOf('\n') > -1; + let end = cursorOffsetEnd; + + // Want the min of closing token or newline + // After moving forward, the cursor is not yet at the end of the current line, + // and it is not a close token. So we include the newline + // because what forms are here extend beyond the end of the current line + if (currentLineNewlineOffset > cursor.offsetEnd && currentFormEndToken.type != 'close') { + hasNewline = true; + end = currentLineNewlineOffset; + } - if (remainderLineText === '' || remainderLineText === '\n') { - end = currentLineNewlineOffset + doc.model.lineEndingLength; - } else if (hasNewline) { - // Try to find the first open token to the right of the document's cursor location if any - let nearestOpenTokenOffset = -1; - - // Start at the newline. - // Work backwards to find the smallest open token offset - // greater than the document's cursor location if any - cursor = doc.getTokenCursor(currentLineNewlineOffset); - while (cursor.offsetStart > offset) { - while (cursor.backwardSexp()) { - // move backward until the cursor cannot move backward anymore - } - if (cursor.offsetStart > offset) { - nearestOpenTokenOffset = cursor.offsetStart; - cursor = doc.getTokenCursor(cursor.offsetStart - 1); + if (remainderLineText === '' || remainderLineText === '\n') { + end = currentLineNewlineOffset + doc.model.lineEndingLength; + } else if (hasNewline) { + // Try to find the first open token to the right of the document's cursor location if any + let nearestOpenTokenOffset = -1; + + // Start at the newline. + // Work backwards to find the smallest open token offset + // greater than the document's cursor location if any + cursor = doc.getTokenCursor(currentLineNewlineOffset); + while (cursor.offsetStart > offset) { + while (cursor.backwardSexp()) { + // move backward until the cursor cannot move backward anymore + } + if (cursor.offsetStart > offset) { + nearestOpenTokenOffset = cursor.offsetStart; + cursor = doc.getTokenCursor(cursor.offsetStart - 1); + } } - } - if (nearestOpenTokenOffset > 0) { - cursor = doc.getTokenCursor(nearestOpenTokenOffset); - cursor.forwardList(); - end = cursor.offsetEnd; // include the closing token - } else { - // no open tokens found so the end is the newline - end = currentLineNewlineOffset; + if (nearestOpenTokenOffset > 0) { + cursor = doc.getTokenCursor(nearestOpenTokenOffset); + cursor.forwardList(); + end = cursor.offsetEnd; // include the closing token + } else { + // no open tokens found so the end is the newline + end = currentLineNewlineOffset; + } } - } - return [offset, end]; + return [offset, end]; + }); } export function rangeToForwardUpList( doc: EditableDocument, - offset: number = Math.max(doc.selection.anchor, doc.selection.active), + // offset: number = Math.max(doc.selections.anchor, doc.selections.active), + offset: number = doc.selections[0].end, goPastWhitespace = false ): [number, number] { - return _forwardSexpRange(doc, offset, GoUpSexpOption.Required, goPastWhitespace); + return _forwardSexpRange(doc, [offset], GoUpSexpOption.Required, goPastWhitespace)[0]; } export function rangeToBackwardUpList( doc: EditableDocument, - offset: number = Math.min(doc.selection.anchor, doc.selection.active), + // offset: number = Math.min(doc.selections.anchor, doc.selections.active), + offset: number = doc.selections[0].start, goPastWhitespace = false ): [number, number] { - return _backwardSexpRange(doc, offset, GoUpSexpOption.Required, goPastWhitespace); + return _backwardSexpRange(doc, [offset], GoUpSexpOption.Required, goPastWhitespace)[0]; } export function forwardSexpOrUpRange( doc: EditableDocument, - offset = Math.max(doc.selection.anchor, doc.selection.active), + offsets: number[] = doc.selections.map((s) => s.end), goPastWhitespace = false -): [number, number] { - return _forwardSexpRange(doc, offset, GoUpSexpOption.WhenAtLimit, goPastWhitespace); +): Array<[number, number]> { + return _forwardSexpRange(doc, offsets, GoUpSexpOption.WhenAtLimit, goPastWhitespace); } export function backwardSexpOrUpRange( doc: EditableDocument, - offset: number = Math.min(doc.selection.anchor, doc.selection.active), + offsets: number[] = doc.selections.map((s) => s.start), goPastWhitespace = false -): [number, number] { - return _backwardSexpRange(doc, offset, GoUpSexpOption.WhenAtLimit, goPastWhitespace); +): Array<[number, number]> { + return _backwardSexpRange(doc, offsets, GoUpSexpOption.WhenAtLimit, goPastWhitespace); } export function rangeToForwardDownList( doc: EditableDocument, - offset: number = Math.max(doc.selection.anchor, doc.selection.active), + // offset: number = Math.max(doc.selections.anchor, doc.selections.active), + offset: number = doc.selections[0].end, goPastWhitespace = false ): [number, number] { const cursor = doc.getTokenCursor(offset); @@ -387,7 +510,8 @@ export function rangeToForwardDownList( export function rangeToBackwardDownList( doc: EditableDocument, - offset: number = Math.min(doc.selection.anchor, doc.selection.active), + // offsets: number[] = Math.min(doc.selections.anchor, doc.selections.active), + offset: number = doc.selections[0].start, goPastWhitespace = false ): [number, number] { const cursor = doc.getTokenCursor(offset); @@ -409,7 +533,8 @@ export function rangeToBackwardDownList( export function rangeToForwardList( doc: EditableDocument, - offset: number = Math.max(doc.selection.anchor, doc.selection.active) + // offset: number = Math.max(doc.selections.anchor, doc.selections.active) + offset: number = doc.selections[0].end ): [number, number] { const cursor = doc.getTokenCursor(offset); if (cursor.forwardList()) { @@ -421,7 +546,8 @@ export function rangeToForwardList( export function rangeToBackwardList( doc: EditableDocument, - offset: number = Math.min(doc.selection.anchor, doc.selection.active) + // offset: number = Math.min(doc.selections.anchor, doc.selections.active) + offset: number = doc.selections[0].start ): [number, number] { const cursor = doc.getTokenCursor(offset); if (cursor.backwardList()) { @@ -435,91 +561,108 @@ export function wrapSexpr( doc: EditableDocument, open: string, close: string, - start: number = doc.selection.anchor, - end: number = doc.selection.active, options = { skipFormat: false } -): Thenable { - const cursor = doc.getTokenCursor(end); - if (cursor.withinString() && open == '"') { - open = close = '\\"'; - } - if (start == end) { - // No selection - const currentFormRange = cursor.rangeForCurrentForm(start); - if (currentFormRange) { - const range = currentFormRange; - return doc.model.edit( - [ - new ModelEdit('insertString', [range[1], close]), - new ModelEdit('insertString', [ - range[0], - open, - [end, end], - [start + open.length, start + open.length], - ]), - ], - { - selection: new ModelEditSelection(start + open.length), - skipFormat: options.skipFormat, +): Thenable { + // TODO: (The following applies to all multi cursor work that involved a ModelEditResult and where not every cursor has a valid operation) Restore all cursors to their original positions after batch edit, except those explicitly operated upon. + + const edits = [], + // selections = clone(doc.selections).map(mapToItemAndOrder); + selections = clone(doc.selections); + + _(doc.selections) + // iterate backwards to simplify dealing with cumulative cursor position offsets as document's text is added to with + // parens or whatever is being wrapped with. + .map(mapToItemAndOrder) + .orderBy(([r]) => mapRangeOrSelectionToOffset('start')(r), 'desc') + .forEach(([sel, index]) => { + const { start, end } = sel; + const cursor = doc.getTokenCursor(end); + if (cursor.withinString() && open == '"') { + open = close = '\\"'; + } + if (start == end) { + // No selection + const currentFormRange = cursor.rangeForCurrentForm(start); + if (currentFormRange) { + const range = currentFormRange; + edits.push( + new ModelEdit('insertString', [range[1], close]), + new ModelEdit('insertString', [ + range[0], + open, + // [end, end], + // [start + open.length, start + open.length], + ]) + ); + // don't forget to include the index with the selection for later reordering to original order; + // selections[index] = [new ModelEditSelection(start + open.length), index]; + selections[index] = new ModelEditSelection(start + open.length); } - ); - } - } else { - // there is a selection - const range = [Math.min(start, end), Math.max(start, end)]; - return doc.model.edit( - [ - new ModelEdit('insertString', [range[1], close]), - new ModelEdit('insertString', [range[0], open]), - ], - { - selection: new ModelEditSelection(start + open.length), - skipFormat: options.skipFormat, + } else { + // there is a selection + const range = [Math.min(start, end), Math.max(start, end)]; + edits.push( + new ModelEdit('insertString', [range[1], close]), + new ModelEdit('insertString', [range[0], open]) + ); + // don't forget to include the index with the selection for later reordering to original order; + // selections[index] = [new ModelEditSelection(start + open.length), index]; + selections[index] = new ModelEditSelection(start + open.length); } - ); - } + return undefined; + }); + return doc.model.edit(edits, { + selections: selections.map(repositionRangeOrSelectionByCumulativeOffsets(2)), + skipFormat: options.skipFormat, + }); } -export function rewrapSexpr( - doc: EditableDocument, - open: string, - close: string, - start: number = doc.selection.anchor, - end: number = doc.selection.active -): Thenable { - const cursor = doc.getTokenCursor(end); - if (cursor.backwardList()) { - const openStart = cursor.offsetStart - 1, - openEnd = cursor.offsetStart; - if (cursor.forwardList()) { - const closeStart = cursor.offsetStart, - closeEnd = cursor.offsetEnd; - return doc.model.edit( - [ - new ModelEdit('changeRange', [closeStart, closeEnd, close]), - new ModelEdit('changeRange', [openStart, openEnd, open]), - ], - { selection: new ModelEditSelection(end) } - ); +// TODO: test +export function rewrapSexpr(doc: EditableDocument, open: string, close: string) { + doc.selections.forEach((sel) => { + const { start, end } = sel; + const cursor = doc.getTokenCursor(end); + if (cursor.backwardList()) { + const openStart = cursor.offsetStart - 1, + openEnd = cursor.offsetStart; + if (cursor.forwardList()) { + const closeStart = cursor.offsetStart, + closeEnd = cursor.offsetEnd; + void doc.model.edit( + [ + new ModelEdit('changeRange', [closeStart, closeEnd, close]), + new ModelEdit('changeRange', [openStart, openEnd, open]), + ], + { selections: [new ModelEditSelection(end)] } + ); + } } - } + }); } -export function splitSexp(doc: EditableDocument, start: number = doc.selection.active) { - const cursor = doc.getTokenCursor(start); - if (!cursor.withinString() && !(cursor.isWhiteSpace() || cursor.previousIsWhiteSpace())) { - cursor.forwardWhitespace(); - } - const splitPos = cursor.withinString() ? start : cursor.offsetStart; - if (cursor.backwardList()) { - const open = cursor.getPrevToken().raw; - if (cursor.forwardList()) { - const close = cursor.getToken().raw; - void doc.model.edit([new ModelEdit('changeRange', [splitPos, splitPos, `${close}${open}`])], { - selection: new ModelEditSelection(splitPos + 1), - }); +// TODO: test +export function splitSexp(doc: EditableDocument) { + const edits = [], + selections = clone(doc.selections); + doc.selections.forEach((selection, index) => { + const { start, end } = selection; + const cursor = doc.getTokenCursor(start); + if (!cursor.withinString() && !(cursor.isWhiteSpace() || cursor.previousIsWhiteSpace())) { + cursor.forwardWhitespace(); } - } + const splitPos = cursor.withinString() ? start : cursor.offsetStart; + if (cursor.backwardList()) { + const open = cursor.getPrevToken().raw; + if (cursor.forwardList()) { + const close = cursor.getToken().raw; + edits.push(new ModelEdit('changeRange', [splitPos, splitPos, `${close}${open}`])); + selections[index] = new ModelEditSelection(splitPos + 1); + } + } + }); + return doc.model.edit(edits, { + selections, + }); } /** @@ -527,99 +670,218 @@ export function splitSexp(doc: EditableDocument, start: number = doc.selection.a * @param doc * @param start */ -export function joinSexp( - doc: EditableDocument, - start: number = doc.selection.active -): Thenable { - const cursor = doc.getTokenCursor(start); - cursor.backwardWhitespace(); - const prevToken = cursor.getPrevToken(), - prevEnd = cursor.offsetStart; - if (['close', 'str-end', 'str'].includes(prevToken.type)) { - cursor.forwardWhitespace(); - const nextToken = cursor.getToken(), - nextStart = cursor.offsetStart; - if (validPair(nextToken.raw[0], prevToken.raw[prevToken.raw.length - 1])) { - return doc.model.edit( - [ +// TODO: test +export function joinSexp(doc: EditableDocument): Thenable { + const edits = [], + selections = clone(doc.selections); + doc.selections.forEach((selection, index) => { + const { start, end } = selection; + + const cursor = doc.getTokenCursor(start); + cursor.backwardWhitespace(); + const prevToken = cursor.getPrevToken(), + prevEnd = cursor.offsetStart; + if (['close', 'str-end', 'str'].includes(prevToken.type)) { + cursor.forwardWhitespace(); + const nextToken = cursor.getToken(), + nextStart = cursor.offsetStart; + if (validPair(nextToken.raw[0], prevToken.raw[prevToken.raw.length - 1])) { + // + edits.push( new ModelEdit('changeRange', [ prevEnd - 1, nextStart + 1, prevToken.type === 'close' ? ' ' : '', - [start, start], - [prevEnd, prevEnd], - ]), - ], - { selection: new ModelEditSelection(prevEnd), formatDepth: 2 } - ); + // [start, start], + // [prevEnd, prevEnd], + ]) + ); + selections[index] = new ModelEditSelection(prevEnd); + } } - } + }); + return doc.model.edit(edits, { selections, formatDepth: 2 }); } export function spliceSexp( doc: EditableDocument, - start: number = doc.selection.active, + // start: number = doc.selections.active, undoStopBefore = true -): Thenable { - const cursor = doc.getTokenCursor(start); - // TODO: this should unwrap the string, not the enclosing list. +): Thenable { + const edits = [], + selections = clone(doc.selections); + + _(doc.selections) + .map(mapToItemAndOrder) + .orderBy(mapRangeOrSelectionToOffset(), 'desc') + .forEach(([selection, originalOrder]) => { + const { start, end } = selection; + const cursor = doc.getTokenCursor(start); + // TODO: this should unwrap the string, not the enclosing list. + cursor.backwardList(); + const open = cursor.getPrevToken(); + const beginning = cursor.offsetStart; + if (open.type == 'open') { + cursor.forwardList(); + const close = cursor.getToken(); + const end = cursor.offsetStart; + if (close.type == 'close' && validPair(open.raw, close.raw)) { + edits.push( + new ModelEdit('changeRange', [end, end + close.raw.length, '']), + new ModelEdit('changeRange', [beginning - open.raw.length, beginning, '']) + ); + selections[originalOrder] = new ModelEditSelection(start - 1); + } + } + }); - cursor.backwardList(); - const open = cursor.getPrevToken(); - const beginning = cursor.offsetStart; - if (open.type == 'open') { - cursor.forwardList(); - const close = cursor.getToken(); - const end = cursor.offsetStart; - if (close.type == 'close' && validPair(open.raw, close.raw)) { - return doc.model.edit( - [ - new ModelEdit('changeRange', [end, end + close.raw.length, '']), - new ModelEdit('changeRange', [beginning - open.raw.length, beginning, '']), - ], - { undoStopBefore, selection: new ModelEditSelection(start - 1) } - ); - } + return doc.model.edit(edits, { + undoStopBefore, + selections: _(selections).map(repositionRangeOrSelectionByCumulativeOffsets(-2)).value(), + }); +} + +export function killSexpBackward( + doc: EditableDocument, + shouldKillAlsoCutToClipboard = () => false, + copyRangeToClipboard = (doc: EditableDocument, range: Array<[number, number]>) => undefined +) { + const ranges = backwardSexpRange( + doc, + doc.selections.map((s) => s.active) + ); + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, ranges); + } + return killRange(doc, ranges); +} + +export function killSexpForward( + doc: EditableDocument, + shouldKillAlsoCutToClipboard = () => false, + copyRangeToClipboard = (doc: EditableDocument, range: Array<[number, number]>) => undefined +) { + const ranges = forwardSexpRange(doc); + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, ranges); } + return killRange(doc, ranges); } +/** + * In making this compatible with multi-cursor, + * we had to complicate the logic somewhat to make sure + * deletions by prior cursors are taken into account by + * later ones, and are relocated accordingly. + * + * See comments in paredit.killRange() for more details. + */ export function killBackwardList( doc: EditableDocument, - [start, end]: [number, number] -): Thenable { - return doc.model.edit( - [new ModelEdit('changeRange', [start, end, '', [end, end], [start, start]])], - { - selection: new ModelEditSelection(start), - } - ); + ranges: Array<[number, number]> = doc.selections.map((s) => [s.start, s.end]) +): Thenable { + const edits: ModelEdit<'deleteRange'>[] = [], + selections: [ModelEditSelection, number, number][] = []; + + _(ranges) + .map((r, idx) => [r, idx] as const) + .orderBy(([r]) => Math.min(...r), 'desc') + .forEach(([r, originalIndex]) => { + const [left, right] = r; + const cursor = doc.getTokenCursor(left); + cursor.backwardList(); + // const offset = selections + // .filter((s) => s.start < left) + // .reduce((sum, s) => sum + s.distance, 0); + // const start = cursor.offsetStart - offset; + // const end = right - offset; + // edits.push(new ModelEdit('deleteRange', [start, Math.abs(end - start)])); + const start = cursor.offsetStart; + const end = right; + const length = Math.abs(end - start); + edits.push(new ModelEdit('deleteRange', [start, length])); + // selections.push([new ModelEditSelection(start, end), originalIndex, length]); + selections.push([new ModelEditSelection(start, end), originalIndex, length]); + }); + + return doc.model.edit(edits, { + selections: _(selections) + .orderBy(([_, originalIndex]) => originalIndex) + .map( + ([selection], _idx, others) => + new ModelEditSelection( + selection.start - + _(others) + .filter(([s]) => s.start < selection.start) + .reduce((sum, [_, __, lengthDeleted]) => sum + lengthDeleted, 0) + ) + ) + .value(), + }); } +/** + * In making this compatible with multi-cursor, + * we had to complicate the logic somewhat to make sure + * deletions by prior cursors are taken into account by + * later ones, and are relocated accordingly. + * + * See comments in paredit.killRange() for more details. + */ export function killForwardList( doc: EditableDocument, - [start, end]: [number, number] -): Thenable { - const cursor = doc.getTokenCursor(start); - const inComment = - (cursor.getToken().type == 'comment' && start > cursor.offsetStart) || - cursor.getPrevToken().type == 'comment'; - return doc.model.edit( - [ - new ModelEdit('changeRange', [ - start, - end, - inComment ? '\n' : '', - [start, start], - [start, start], - ]), - ], - { selection: new ModelEditSelection(start) } - ); + ranges: Array<[number, number]> = doc.selections.map((s) => [s.start, s.end]) +): Thenable { + const edits: ModelEdit<'changeRange'>[] = [], + selections: [ModelEditSelection, number, number][] = []; + + _(ranges) + .map((r, idx) => [r, idx] as const) + .orderBy(([r]) => Math.min(...r), 'desc') + .forEach(([r, originalIndex]) => { + const [left, right] = r; + const cursor = doc.getTokenCursor(left); + cursor.forwardList(); + const inComment = + (cursor.getToken().type == 'comment' && left > cursor.offsetStart) || + cursor.getPrevToken().type == 'comment'; + + const start = cursor.offsetStart; + const end = right; + const length = Math.abs(end - start); + edits.push(new ModelEdit('changeRange', [start, end, inComment ? '\n' : ''])); + selections.push([new ModelEditSelection(start, end), originalIndex, length]); + }); + + return doc.model.edit(edits, { + selections: _(selections) + .orderBy(([_, originalIndex]) => originalIndex) + .map( + ([selection], _idx, others) => + new ModelEditSelection( + selection.start - + _(others) + .filter(([s]) => s.start < selection.start) + .reduce((sum, [_, __, lengthDeleted]) => sum + lengthDeleted, 0) + ) + ) + .value(), + }); } -export function forwardSlurpSexp( +// FIXME: check if this forEach solution works vs map into modelEdit batch +export function forwardSlurpSexp(doc: EditableDocument) { + const startOffsets: number[] = doc.selections.map(property('active')); + startOffsets.forEach((offset) => { + const extraOpts = { formatDepth: 1 }; + + _forwardSlurpSexpSingle(doc, offset, extraOpts); + }); +} + +export function _forwardSlurpSexpSingle( doc: EditableDocument, - start: number = doc.selection.active, + start: number, extraOpts = { formatDepth: 1 } ) { const cursor = doc.getTokenCursor(start); @@ -632,12 +894,13 @@ export function forwardSlurpSexp( const wsStartOffset = wsInsideCursor.offsetStart; cursor.upList(); const wsOutSideCursor = cursor.clone(); + // check if we're about to hit the end of our current scope if (cursor.forwardSexp(true, true)) { wsOutSideCursor.forwardWhitespace(false); const wsEndOffset = wsOutSideCursor.offsetStart; const newCloseOffset = cursor.offsetStart; const replacedText = doc.model.getText(wsStartOffset, wsEndOffset); - const changeArgs = + const changeArgs: ModelEditFunctionArgs<'changeRange'> = replacedText.indexOf('\n') >= 0 ? [currentCloseOffset, currentCloseOffset + close.length, ''] : [wsStartOffset, wsEndOffset, ' ']; @@ -654,19 +917,24 @@ export function forwardSlurpSexp( } ); } else { + // the const formatDepth = extraOpts['formatDepth'] ? extraOpts['formatDepth'] : 1; - forwardSlurpSexp(doc, cursor.offsetStart, { + _forwardSlurpSexpSingle(doc, cursor.offsetStart, { formatDepth: formatDepth + 1, }); } } } -export function backwardSlurpSexp( - doc: EditableDocument, - start: number = doc.selection.active, - extraOpts = {} -) { +// FIXME: check if this forEach solution works vs map into modelEdit batch +export function backwardSlurpSexp(doc: EditableDocument) { + doc.selections.forEach((selection) => { + const extraOpts = { formatDepth: 1 }; + _backwardSlurpSexpSingle(doc, selection.active, extraOpts); + }); +} + +export function _backwardSlurpSexpSingle(doc: EditableDocument, start: number, extraOpts = {}) { const cursor = doc.getTokenCursor(start); cursor.backwardList(); const tk = cursor.getPrevToken(); @@ -691,78 +959,97 @@ export function backwardSlurpSexp( ); } else { const formatDepth = extraOpts['formatDepth'] ? extraOpts['formatDepth'] : 1; - backwardSlurpSexp(doc, cursor.offsetStart, { + _backwardSlurpSexpSingle(doc, cursor.offsetStart, { formatDepth: formatDepth + 1, }); } } } -export function forwardBarfSexp(doc: EditableDocument, start: number = doc.selection.active) { - const cursor = doc.getTokenCursor(start); - cursor.forwardList(); - if (cursor.getToken().type == 'close') { - const offset = cursor.offsetStart, - close = cursor.getToken().raw; - cursor.backwardSexp(true, true); - cursor.backwardWhitespace(); - void doc.model.edit( - [ +export function forwardBarfSexp(doc: EditableDocument) { + const edits = [], + selections = clone(doc.selections); + doc.selections.forEach((selection, index) => { + const { start, end } = selection; + const cursor = doc.getTokenCursor(start); + cursor.forwardList(); + if (cursor.getToken().type == 'close') { + const offset = cursor.offsetStart, + close = cursor.getToken().raw; + cursor.backwardSexp(true, true); + cursor.backwardWhitespace(); + edits.push( new ModelEdit('deleteRange', [offset, close.length]), - new ModelEdit('insertString', [cursor.offsetStart, close]), - ], - start >= cursor.offsetStart - ? { - selection: new ModelEditSelection(cursor.offsetStart), - formatDepth: 2, - } - : { formatDepth: 2 } - ); - } + new ModelEdit('insertString', [cursor.offsetStart, close]) + ); + if (start >= cursor.offsetStart) { + selections[index] = new ModelEditSelection(cursor.offsetStart); + } else { + selections[index] = selection; + } + } + }); + void doc.model.edit(edits, { selections, formatDepth: 2 }); } -export function backwardBarfSexp(doc: EditableDocument, start: number = doc.selection.active) { - const cursor = doc.getTokenCursor(start); - cursor.backwardList(); - const tk = cursor.getPrevToken(); - if (tk.type == 'open') { - cursor.previous(); - const offset = cursor.offsetStart; - const close = cursor.getToken().raw; - cursor.next(); - cursor.forwardSexp(true, true); - cursor.forwardWhitespace(false); - void doc.model.edit( - [ +export function backwardBarfSexp(doc: EditableDocument) { + const edits = [], + selections = clone(doc.selections); + + doc.selections.forEach((sel, index) => { + const { start, end } = sel; + const cursor = doc.getTokenCursor(start); + cursor.backwardList(); + const tk = cursor.getPrevToken(); + if (tk.type == 'open') { + cursor.previous(); + const offset = cursor.offsetStart; + const close = cursor.getToken().raw; + cursor.next(); + cursor.forwardSexp(true, true); + cursor.forwardWhitespace(false); + + edits.push( new ModelEdit('changeRange', [cursor.offsetStart, cursor.offsetStart, close]), - new ModelEdit('deleteRange', [offset, tk.raw.length]), - ], - start <= cursor.offsetStart - ? { - selection: new ModelEditSelection(cursor.offsetStart), - formatDepth: 2, - } - : { formatDepth: 2 } - ); - } + new ModelEdit('deleteRange', [offset, tk.raw.length]) + ); + + if (start <= cursor.offsetStart) { + selections[index] = new ModelEditSelection(cursor.offsetStart); + } + } + }); + + void doc.model.edit(edits, { selections, formatDepth: 2 }); } +// FIXME: open() is defined and tested but is never used or referenced? export function open( doc: EditableDocument, open: string, close: string, - start: number = doc.selection.active + start: number = doc.selections[0].active ) { - const [cs, ce] = [doc.selection.anchor, doc.selection.active]; - doc.insertString(open + doc.getSelectionText() + close); + const [cs, ce] = [doc.selections[0].anchor, doc.selections[0].active]; + doc.insertString(open + doc.getSelectionTexts() + close); if (cs != ce) { - doc.selection = new ModelEditSelection(cs + open.length, ce + open.length); + // TODO(multi-cursor): make multi cursor compat? + doc.selections = replaceAt( + doc.selections, + 0, + new ModelEditSelection(cs + open.length, ce + open.length) + ); } else { - doc.selection = new ModelEditSelection(start + open.length); + // TODO(multi-cursor): make multi cursor compat? + doc.selections = replaceAt(doc.selections, 0, new ModelEditSelection(start + open.length)); } } -function docIsBalanced(doc: EditableDocument, start: number = doc.selection.active): boolean { +// TODO: docIsBalanced() needs testing +function docIsBalanced( + doc: EditableDocument + // start: number = doc.selections.active +): boolean { const cursor = doc.getTokenCursor(0); while (cursor.forwardSexp(true, true, true)) { // move forward until the cursor cannot move forward anymore @@ -771,102 +1058,187 @@ function docIsBalanced(doc: EditableDocument, start: number = doc.selection.acti return cursor.atEnd(); } -export function close(doc: EditableDocument, close: string, start: number = doc.selection.active) { - const cursor = doc.getTokenCursor(start); - const inString = cursor.withinString(); - cursor.forwardWhitespace(false); - if (cursor.getToken().raw === close) { - doc.selection = new ModelEditSelection(cursor.offsetEnd); - } else { - if (!inString && docIsBalanced(doc)) { - // Do nothing when there is balance +export function close( + doc: EditableDocument, + close: string, + startOffsets: number[] = doc.selections.map(property('active')) +) { + const edits = [], + selections = clone(doc.selections); + + startOffsets.forEach((start, index) => { + const cursor = doc.getTokenCursor(start); + const inString = cursor.withinString(); + cursor.forwardWhitespace(false); + if (cursor.getToken().raw === close) { + selections[index] = new ModelEditSelection(cursor.offsetEnd); } else { - void doc.model.edit([new ModelEdit('insertString', [start, close])], { - selection: new ModelEditSelection(start + close.length), - }); + if (!inString && docIsBalanced(doc)) { + // Do nothing when there is balance + } else { + edits.push(new ModelEdit('insertString', [start, close])); + selections[index] = new ModelEditSelection(start + close.length); + } } - } + }); + return doc.model.edit(edits, { + selections, + }); } export function backspace( - doc: EditableDocument, - start: number = doc.selection.anchor, - end: number = doc.selection.active -): Thenable { - if (start != end) { - return doc.backspace(); - } else { - const cursor = doc.getTokenCursor(start); - const nextToken = cursor.getToken(); - const p = start; - const prevToken = - p > cursor.offsetStart && !['open', 'close'].includes(nextToken.type) - ? nextToken - : cursor.getPrevToken(); - if (prevToken.type == 'prompt') { - return new Promise((resolve) => resolve(true)); - } else if (nextToken.type == 'prompt') { - return new Promise((resolve) => resolve(true)); - } else if (doc.model.getText(p - 2, p, true) == '\\"') { - return doc.model.edit([new ModelEdit('deleteRange', [p - 2, 2])], { - selection: new ModelEditSelection(p - 2), - }); - } else if (prevToken.type === 'open' && nextToken.type === 'close') { - return doc.model.edit( - [new ModelEdit('deleteRange', [p - prevToken.raw.length, prevToken.raw.length + 1])], - { - selection: new ModelEditSelection(p - prevToken.raw.length), + doc: EditableDocument + // _start: number, // = doc.selections.anchor, + // _end: number // = doc.selections.active + // ): Thenable { +): Thenable { + // const selections = clone(doc.selections); + + return Promise.all( + doc.selections.map>( + async (selection, index) => { + const { start, end } = selection; + + if (start != end) { + // const res = await doc.backspace(); + + // return res.selections[0]; + // return [res.success, res.selections[0]]; + return ['backspace', selection, index] as const; + } else { + const cursor = doc.getTokenCursor(start); + const nextToken = cursor.getToken(); + const p = start; + const prevToken = + p > cursor.offsetStart && !['open', 'close'].includes(nextToken.type) + ? nextToken + : cursor.getPrevToken(); + if (prevToken.type == 'prompt') { + // return new Promise((resolve) => resolve(true)); + // return selection; + return [true, selection] as const; + } else if (nextToken.type == 'prompt') { + // return new Promise((resolve) => resolve(true)); + return [true, selection] as const; + // return selection; + } else if (doc.model.getText(p - 2, p, true) == '\\"') { + // return doc.model.edit([new ModelEdit('deleteRange', [p - 2, 2])], { + const sel = new ModelEditSelection(p - 2); + // selections[index] = sel; + const res = await doc.model.edit([new ModelEdit('deleteRange', [p - 2, 2])], { + // selections: [new ModelEditSelection(p - 2)], + // selections: Object.assign([...selections], {[index]: new ModelEditSelection(p - 2)}) + selections: replaceAt(doc.selections, index, sel), + }); + // return sel; + // selections[index] = res.selections[index]; + return [res.success, res.selections[index]] as const; + } else if (prevToken.type === 'open' && nextToken.type === 'close') { + // return doc.model.edit( + const sel = new ModelEditSelection(p - prevToken.raw.length); + // selections[index] = sel; + const res = await doc.model.edit( + [new ModelEdit('deleteRange', [p - prevToken.raw.length, prevToken.raw.length + 1])], + { + // selections: [new ModelEditSelection(p - prevToken.raw.length)], + // selections: Object.assign([...selections], {[index]: new ModelEditSelection(p - prevToken.raw.length)}, + selections: replaceAt(doc.selections, index, sel), + } + ); + // selections[index] = res.selections[index]; + return [res.success, res.selections[index]] as const; + } else { + if (['open', 'close'].includes(prevToken.type) && docIsBalanced(doc)) { + // doc.selection = new ModelEditSelection(p - prevToken.raw.length); + // return new ModelEditSelection(p - prevToken.raw.length); + // selections[index] = new ModelEditSelection(p - prevToken.raw.length); + // return new Promise((resolve) => resolve(true)); + return [true, new ModelEditSelection(p - prevToken.raw.length)] as const; + } else { + // const res = await doc.backspace(); + // selections[index] = res.selections[0]; + // return [res.success, res.selections[0]] as const; + return ['backspace', selection, index] as const; + } + } } - ); - } else { - if (['open', 'close'].includes(prevToken.type) && docIsBalanced(doc)) { - doc.selection = new ModelEditSelection(p - prevToken.raw.length); - return new Promise((resolve) => resolve(true)); - } else { - return doc.backspace(); } - } - } + ) + ).then>(async (results) => { + // run native backspace on non edited cursors + const nativeBackspaceSelections = results + .filter((result) => result[0] === 'backspace') + .map((result) => result[1]); + doc.selections = nativeBackspaceSelections; + await doc.backspace(); + // set edited selections + doc.selections.push(...results.filter((r) => r[0] === true).map(([_, sel]) => sel)); + return results.map(([success]) => (success === 'backspace' ? true : success)); + }); } -export function deleteForward( - doc: EditableDocument, - start: number = doc.selection.anchor, - end: number = doc.selection.active +export async function deleteForward( + doc: EditableDocument + // _start: number = doc.selections.anchor, + // _end: number = doc.selections.active ) { - if (start != end) { - void doc.delete(); - } else { - const cursor = doc.getTokenCursor(start); - const prevToken = cursor.getPrevToken(); - const nextToken = cursor.getToken(); - const p = start; - if (doc.model.getText(p, p + 2, true) == '\\"') { - return doc.model.edit([new ModelEdit('deleteRange', [p, 2])], { - selection: new ModelEditSelection(p), - }); - } else if (prevToken.type === 'open' && nextToken.type === 'close') { - void doc.model.edit( - [new ModelEdit('deleteRange', [p - prevToken.raw.length, prevToken.raw.length + 1])], - { - selection: new ModelEditSelection(p - prevToken.raw.length), - } - ); - } else { - if (['open', 'close'].includes(nextToken.type) && docIsBalanced(doc)) { - doc.selection = new ModelEditSelection(p + 1); - return new Promise((resolve) => resolve(true)); + const results = await Promise.all( + doc.selections.map(async (selection, index) => { + const { start, end } = selection; + if (start != end) { + // await doc.delete(); + // return selection; + return ['delete', selection, index] as const; } else { - return doc.delete(); + const cursor = doc.getTokenCursor(start); + const prevToken = cursor.getPrevToken(); + const nextToken = cursor.getToken(); + const p = start; + if (doc.model.getText(p, p + 2, true) == '\\"') { + const res = await doc.model.edit([new ModelEdit('deleteRange', [p, 2])], { + selections: replaceAt(doc.selections, index, new ModelEditSelection(p)), + }); + return [res.success, res.selections[index], index] as const; + } else if (prevToken.type === 'open' && nextToken.type === 'close') { + const res = await doc.model.edit( + [new ModelEdit('deleteRange', [p - prevToken.raw.length, prevToken.raw.length + 1])], + { + selections: replaceAt( + doc.selections, + index, + new ModelEditSelection(p - prevToken.raw.length) + ), + } + ); + return [res.success, res.selections[index], index] as const; + } else { + if (['open', 'close'].includes(nextToken.type) && docIsBalanced(doc)) { + // doc.selections = replaceAt(doc.selections, index, new ModelEditSelection(p + 1)); + // return new Promise((resolve) => resolve(true)); + return [true, new ModelEditSelection(p + 1), index] as const; + } else { + // return doc.delete(); + // return selection; + return ['delete', selection, index] as const; + } + } } - } - } + }) + ); + const postCalvaEditSelections = results.filter((r) => isBoolean(r[0])); + const cursorsNeedingNativeDeletion = results.filter((r) => r[0] === 'delete'); + doc.selections = cursorsNeedingNativeDeletion.map((r) => r[1]); + await doc.delete(); + + doc.selections.push(...postCalvaEditSelections.map((s) => s[1])); + return results.map((r) => (r[0] === 'delete' ? true : r[0])); } +// FIXME: stringQuote() is defined and tested but is never used or referenced? export function stringQuote( doc: EditableDocument, - start: number = doc.selection.anchor, - end: number = doc.selection.active + start: number = doc.selections[0].start, + end: number = doc.selections[0].end ) { if (start != end) { doc.insertString('"'); @@ -877,229 +1249,333 @@ export function stringQuote( if (cursor.getToken().type == 'close') { if (doc.model.getText(0, start).endsWith('\\')) { void doc.model.edit([new ModelEdit('changeRange', [start, start, '"'])], { - selection: new ModelEditSelection(start + 1), + selections: [new ModelEditSelection(start + 1)], }); } else { - close(doc, '"', start); + // close(doc, '"', start); + // close(doc, '"', replaceAt(doc.selections.map(property('active')), 0, start)); + void close(doc, '"', replaceAt(doc.selections.map(property('active')), 0, start)); } } else { if (doc.model.getText(0, start).endsWith('\\')) { void doc.model.edit([new ModelEdit('changeRange', [start, start, '"'])], { - selection: new ModelEditSelection(start + 1), + selections: [new ModelEditSelection(start + 1)], }); } else { void doc.model.edit([new ModelEdit('changeRange', [start, start, '\\"'])], { - selection: new ModelEditSelection(start + 2), + selections: [new ModelEditSelection(start + 2)], }); } } } else { void doc.model.edit([new ModelEdit('changeRange', [start, start, '""'])], { - selection: new ModelEditSelection(start + 1), + selections: [new ModelEditSelection(start + 1)], }); } } } +/** + * Given the set of selections in the given document, + * edit the set of selections therein such that for each selection: + * the selection expands to encompass just the contents of the form + * (where this selection or its cursor lies), the entire form itself + * (including open/close symbols) or the full contents of the form + * immediately enclosing this one, repeating each time the function is + * called, for each selection in the doc. + * + * (Or in other words, the S-expression powered equivalent to vs-code's + * built-in Expand Selection/Shrink Selection commands) + */ export function growSelection( - doc: EditableDocument, - start: number = doc.selection.anchor, - end: number = doc.selection.active + doc: EditableDocument + // start: number = doc.selections.anchor, + // end: number = doc.selections.active ) { - const startC = doc.getTokenCursor(start), - endC = doc.getTokenCursor(end), - emptySelection = startC.equals(endC); - - if (emptySelection) { - const currentFormRange = startC.rangeForCurrentForm(start); - if (currentFormRange) { - growSelectionStack(doc, currentFormRange); - } - } else { - if (startC.getPrevToken().type == 'open' && endC.getToken().type == 'close') { - startC.backwardList(); - startC.backwardUpList(); - endC.forwardList(); - growSelectionStack(doc, [startC.offsetStart, endC.offsetEnd]); + const newRanges = doc.selections.map<[number, number]>(({ anchor: start, active: end }) => { + // init start/end TokenCursors, ascertain emptiness of selection + const startC = doc.getTokenCursor(start), + endC = doc.getTokenCursor(end), + emptySelection = startC.equals(endC); + + // check if selection is empty - means just a cursor + if (emptySelection) { + const currentFormRange = startC.rangeForCurrentForm(start); + // check if there's a form associated with the current cursor + if (currentFormRange) { + // growSelectionStack(doc, currentFormRange); + return currentFormRange; + } + // if there's not, do nothing, we will not be expanding this cursor + return [start, end]; } else { - if (startC.backwardList()) { - // we are in an sexpr. + if (startC.getPrevToken().type == 'open' && endC.getToken().type == 'close') { + startC.backwardList(); + startC.backwardUpList(); endC.forwardList(); - endC.previous(); + // growSelectionStack(doc, [startC.offsetStart, endC.offsetEnd]); + return [startC.offsetStart, endC.offsetEnd]; } else { - if (startC.backwardDownList()) { - startC.backwardList(); - if (emptySelection) { - endC.set(startC); - endC.forwardList(); - endC.next(); - } - startC.previous(); - } else if (startC.downList()) { - if (emptySelection) { - endC.set(startC); - endC.forwardList(); - endC.next(); + if (startC.backwardList()) { + // we are in an sexpr. + endC.forwardList(); + endC.previous(); + } else { + if (startC.backwardDownList()) { + startC.backwardList(); + if (emptySelection) { + endC.set(startC); + endC.forwardList(); + endC.next(); + } + startC.previous(); + } else if (startC.downList()) { + if (emptySelection) { + endC.set(startC); + endC.forwardList(); + endC.next(); + } + startC.previous(); } - startC.previous(); } + // growSelectionStack(doc, [startC.offsetStart, endC.offsetEnd]); + return [startC.offsetStart, endC.offsetEnd]; } - growSelectionStack(doc, [startC.offsetStart, endC.offsetEnd]); } - } + }); + growSelectionStack(doc, newRanges); } -export function growSelectionStack(doc: EditableDocument, range: [number, number]) { - const [start, end] = range; - if (doc.selectionStack.length > 0) { - const prev = doc.selectionStack[doc.selectionStack.length - 1]; - if (!(doc.selection.anchor == prev.anchor && doc.selection.active == prev.active)) { +/** + * When growing a stack, we want a temporary selection undo/redo history, + * so to speak, such that each time the growSelection (labelled "Expand Selection") + * command is invoked, the selection keeps expanding by s-expression level, without + * "losing" prior selections. + * + * Recall that one cannot arbitrarily "Shrink Selection" _without_ such a history stack + * or outside of the context of a series of "Expand Selection" operations, + * as there is no way to know to which symbol(s) a user could hypothetically intend on + * "zooming in" on via selection shrinking. + * + * Thus to implement the above, we store a stack of selections (each item in the stack + * is an array of selection items, one per cursor) and grow or traverse this stack + * as the user expands or shrinks their selection(s). + * + * @param doc EditableDocument + * @param ranges the new ranges to grow the selection into + * @returns + */ +export function growSelectionStack(doc: EditableDocument, ranges: Array<[number, number]>) { + // Check if user has already at least once invoked "Expand Selection": + if (doc.selectionsStack.length > 0) { + // User indeed already has a selection set expansion history. + const prev = last(doc.selectionsStack); + // Check if the current document selection set DOES NOT match the widest (latest) selection set + // in the history. + if ( + !( + isEqual(doc.selections.map(property('anchor')), prev.map(property('anchor'))) && + isEqual(doc.selections.map(property('active')), prev.map(property('active'))) + ) + ) { + // FIXME(multi-cursor): This means there's some kind of mismatch. Why? + // Therefore, let's reset the selection set history setSelectionStack(doc); - } else if (prev.anchor === range[0] && prev.active === range[1]) { + + // Check if the intended new selection set to grow into is already the widest (latest) selection set + // in the history. + } else if ( + isEqual(prev.map(property('anchor')), ranges.map(property(0))) && + isEqual(prev.map(property('active')), ranges.map(property(1))) + ) { return; } } else { - doc.selectionStack = [doc.selection]; + // start a "fresh" selection set expansion history + // FIXME(multi-cursor): why doesn't this use `setSelectionStack(doc)` from below? + doc.selectionsStack = [doc.selections]; } - doc.selection = new ModelEditSelection(start, end); - doc.selectionStack.push(doc.selection); + doc.selections = ranges.map((range) => new ModelEditSelection(...range)); + doc.selectionsStack.push(doc.selections); } +// FIXME(multi-cursor): prob needs rethinking export function shrinkSelection(doc: EditableDocument) { - if (doc.selectionStack.length) { - const latest = doc.selectionStack.pop(); + if (doc.selectionsStack.length) { + const latest = doc.selectionsStack.pop(); if ( - doc.selectionStack.length && - latest.anchor == doc.selection.anchor && - latest.active == doc.selection.active + doc.selectionsStack.length && + latest.every((selection, index) => + isEqual( + pick(selection, ['anchor, active']), + pick(doc.selections[index], ['anchor, active']) + ) + ) ) { - doc.selection = doc.selectionStack[doc.selectionStack.length - 1]; + doc.selections = last(doc.selectionsStack); } } } -export function setSelectionStack(doc: EditableDocument, selection = doc.selection) { - doc.selectionStack = [selection]; -} - -export function raiseSexp( +export function setSelectionStack( doc: EditableDocument, - start = doc.selection.anchor, - end = doc.selection.active + selections: ModelEditSelection[][] = [doc.selections] ) { - const cursor = doc.getTokenCursor(end); - const [formStart, formEnd] = cursor.rangeForCurrentForm(start); - const isCaretTrailing = formEnd - start < start - formStart; - const startCursor = doc.getTokenCursor(formStart); - const endCursor = startCursor.clone(); - if (endCursor.forwardSexp()) { - const raised = doc.model.getText(startCursor.offsetStart, endCursor.offsetStart); - startCursor.backwardList(); - endCursor.forwardList(); - if (startCursor.getPrevToken().type == 'open') { - startCursor.previous(); - if (endCursor.getToken().type == 'close') { - void doc.model.edit( - [new ModelEdit('changeRange', [startCursor.offsetStart, endCursor.offsetEnd, raised])], - { - selection: new ModelEditSelection( - isCaretTrailing ? startCursor.offsetStart + raised.length : startCursor.offsetStart - ), + doc.selectionsStack = selections; +} + +/** + * In making this compatible with multi-cursor, + * we had to complicate the logic somewhat to make sure + * deletions by prior cursors are taken into account by + * later ones, and are relocated accordingly. + * + * See comments in paredit.killRange() for more details. + */ +export function raiseSexp(doc: EditableDocument) { + const edits: ModelEdit<'changeRange'>[] = [], + selections = doc.selections.map((s) => [0, s.clone()] as [number, ModelEditSelection]); + + _(doc.selections) + .map((s, index) => [s, index] as const) + .orderBy(([s]) => s.start, 'desc') + .forEach(([selection, originalIndex], index) => { + const { start, end } = selection; + + const cursor = doc.getTokenCursor(end); + const [formStart, formEnd] = cursor.rangeForCurrentForm(start); + const isCaretTrailing = formEnd - start < start - formStart; + const startCursor = doc.getTokenCursor(formStart); + const endCursor = startCursor.clone(); + if (endCursor.forwardSexp()) { + const raised = doc.model.getText(startCursor.offsetStart, endCursor.offsetStart); + startCursor.backwardList(); + endCursor.forwardList(); + if (startCursor.getPrevToken().type == 'open') { + startCursor.previous(); + if (endCursor.getToken().type == 'close') { + edits.push( + new ModelEdit('changeRange', [startCursor.offsetStart, endCursor.offsetEnd, raised]) + ); + const cursorPos = isCaretTrailing + ? startCursor.offsetStart + raised.length + : startCursor.offsetStart; + + selections[originalIndex] = [ + endCursor.offsetEnd - startCursor.offsetStart - raised.length, + new ModelEditSelection(cursorPos), + ]; } - ); + } } - } - } + }); + return doc.model.edit(edits, { + selections: selections.map(([_, selection], __, others) => { + const s = selection.clone(); + + const offsetSum = others + .filter(([_, otherSel]) => otherSel.start < selection.start) + .reduce((sum, o) => sum + o[0], 0); + + s.anchor -= offsetSum; + s.active -= offsetSum; + + return s; + }), + }); } export function convolute( - doc: EditableDocument, - start = doc.selection.anchor, - end = doc.selection.active + doc: EditableDocument + // start = doc.selections.anchor, + // end = doc.selections.active ) { - if (start == end) { - const cursorStart = doc.getTokenCursor(end); - const cursorEnd = cursorStart.clone(); - - if (cursorStart.backwardList()) { - if (cursorEnd.forwardList()) { - const head = doc.model.getText(cursorStart.offsetStart, end); - if (cursorStart.getPrevToken().type == 'open') { - cursorStart.previous(); - const headStart = cursorStart.clone(); - - if (headStart.backwardList() && headStart.backwardUpList()) { - const headEnd = cursorStart.clone(); - if (headEnd.forwardList() && cursorEnd.getToken().type == 'close') { - void doc.model.edit( - [ - new ModelEdit('changeRange', [headEnd.offsetEnd, headEnd.offsetEnd, ')']), - new ModelEdit('changeRange', [cursorEnd.offsetStart, cursorEnd.offsetEnd, '']), - new ModelEdit('changeRange', [cursorStart.offsetStart, end, '']), - new ModelEdit('changeRange', [ - headStart.offsetStart, - headStart.offsetStart, - '(' + head, - ]), - ], - {} - ); + doc.selections.forEach((selection) => { + const { start, end } = selection; + + if (start == end) { + const cursorStart = doc.getTokenCursor(end); + const cursorEnd = cursorStart.clone(); + + if (cursorStart.backwardList()) { + if (cursorEnd.forwardList()) { + const head = doc.model.getText(cursorStart.offsetStart, end); + if (cursorStart.getPrevToken().type == 'open') { + cursorStart.previous(); + const headStart = cursorStart.clone(); + + if (headStart.backwardList() && headStart.backwardUpList()) { + const headEnd = cursorStart.clone(); + if (headEnd.forwardList() && cursorEnd.getToken().type == 'close') { + void doc.model.edit( + [ + new ModelEdit('changeRange', [headEnd.offsetEnd, headEnd.offsetEnd, ')']), + new ModelEdit('changeRange', [cursorEnd.offsetStart, cursorEnd.offsetEnd, '']), + new ModelEdit('changeRange', [cursorStart.offsetStart, end, '']), + new ModelEdit('changeRange', [ + headStart.offsetStart, + headStart.offsetStart, + '(' + head, + ]), + ], + {} + ); + } } } } } } - } + }); } export function transpose( doc: EditableDocument, - left = doc.selection.anchor, - right = doc.selection.active, + // left = doc.selections.anchor, + // right = doc.selections.active, newPosOffset: { fromLeft?: number; fromRight?: number } = {} ) { - const cursor = doc.getTokenCursor(right); - cursor.backwardWhitespace(); - if (cursor.getPrevToken().type == 'open') { - cursor.forwardSexp(); - } - cursor.forwardWhitespace(); - if (cursor.getToken().type == 'close') { - cursor.backwardSexp(); - } - if (cursor.getToken().type != 'close') { - const rightStart = cursor.offsetStart; - if (cursor.forwardSexp()) { - const rightEnd = cursor.offsetStart; + const edits = [], + selections = clone(doc.selections); + doc.selections.forEach((selection, index) => { + const { start: left, end: right } = selection; + + const cursor = doc.getTokenCursor(right); + cursor.backwardWhitespace(); + if (cursor.getPrevToken().type == 'open') { + cursor.forwardSexp(); + } + cursor.forwardWhitespace(); + if (cursor.getToken().type == 'close') { cursor.backwardSexp(); - cursor.backwardWhitespace(); - const leftEnd = cursor.offsetStart; - if (cursor.backwardSexp()) { - const leftStart = cursor.offsetStart, - leftText = doc.model.getText(leftStart, leftEnd), - rightText = doc.model.getText(rightStart, rightEnd); - let newCursorPos = leftStart + rightText.length; - if (newPosOffset.fromLeft != undefined) { - newCursorPos = leftStart + newPosOffset.fromLeft; - } else if (newPosOffset.fromRight != undefined) { - newCursorPos = rightEnd - newPosOffset.fromRight; - } - void doc.model.edit( - [ + } + if (cursor.getToken().type != 'close') { + const rightStart = cursor.offsetStart; + if (cursor.forwardSexp()) { + const rightEnd = cursor.offsetStart; + cursor.backwardSexp(); + cursor.backwardWhitespace(); + const leftEnd = cursor.offsetStart; + if (cursor.backwardSexp()) { + const leftStart = cursor.offsetStart, + leftText = doc.model.getText(leftStart, leftEnd), + rightText = doc.model.getText(rightStart, rightEnd); + let newCursorPos = leftStart + rightText.length; + if (newPosOffset.fromLeft != undefined) { + newCursorPos = leftStart + newPosOffset.fromLeft; + } else if (newPosOffset.fromRight != undefined) { + newCursorPos = rightEnd - newPosOffset.fromRight; + } + edits.push( new ModelEdit('changeRange', [rightStart, rightEnd, leftText]), - new ModelEdit('changeRange', [ - leftStart, - leftEnd, - rightText, - [left, left], - [newCursorPos, newCursorPos], - ]), - ], - { selection: new ModelEditSelection(newCursorPos) } - ); + new ModelEdit('changeRange', [leftStart, leftEnd, rightText]) + ); + selections[index] = new ModelEditSelection(newCursorPos); + } } } - } + }); + return doc.model.edit(edits, { selections }); } export const bindingForms = [ @@ -1164,60 +1640,71 @@ function currentSexpsRange( export function dragSexprBackward( doc: EditableDocument, - pairForms = bindingForms, - left = doc.selection.anchor, - right = doc.selection.active + pairForms = bindingForms + // left = doc.selections.anchor, + // right = doc.selections.active ) { - const cursor = doc.getTokenCursor(right); - const usePairs = isInPairsList(cursor, pairForms); - const currentRange = currentSexpsRange(doc, cursor, right, usePairs); - const newPosOffset = right - currentRange[0]; - const backCursor = doc.getTokenCursor(currentRange[0]); - backCursor.backwardSexp(); - const backRange = currentSexpsRange(doc, backCursor, backCursor.offsetStart, usePairs); - if (backRange[0] !== currentRange[0]) { - // there is a sexp to the left - const leftText = doc.model.getText(backRange[0], backRange[1]); - const currentText = doc.model.getText(currentRange[0], currentRange[1]); - void doc.model.edit( - [ + const edits = [], + selections = clone(doc.selections); + + doc.selections.forEach((selection, index) => { + const { start: left, end: right } = selection; + + const cursor = doc.getTokenCursor(right); + const usePairs = isInPairsList(cursor, pairForms); + const currentRange = currentSexpsRange(doc, cursor, right, usePairs); + const newPosOffset = right - currentRange[0]; + const backCursor = doc.getTokenCursor(currentRange[0]); + backCursor.backwardSexp(); + const backRange = currentSexpsRange(doc, backCursor, backCursor.offsetStart, usePairs); + if (backRange[0] !== currentRange[0]) { + // there is a sexp to the left + const leftText = doc.model.getText(backRange[0], backRange[1]); + const currentText = doc.model.getText(currentRange[0], currentRange[1]); + edits.push( new ModelEdit('changeRange', [currentRange[0], currentRange[1], leftText]), - new ModelEdit('changeRange', [backRange[0], backRange[1], currentText]), - ], - { selection: new ModelEditSelection(backRange[0] + newPosOffset) } - ); - } + new ModelEdit('changeRange', [backRange[0], backRange[1], currentText]) + ); + selections[index] = new ModelEditSelection(backRange[0] + newPosOffset); + } + }); + return doc.model.edit(edits, { selections }); } +// TODO: multi export function dragSexprForward( doc: EditableDocument, - pairForms = bindingForms, - left = doc.selection.anchor, - right = doc.selection.active + pairForms = bindingForms + // left = doc.selections.anchor, + // right = doc.selections.active ) { - const cursor = doc.getTokenCursor(right); - const usePairs = isInPairsList(cursor, pairForms); - const currentRange = currentSexpsRange(doc, cursor, right, usePairs); - const newPosOffset = currentRange[1] - right; - const forwardCursor = doc.getTokenCursor(currentRange[1]); - forwardCursor.forwardSexp(); - const forwardRange = currentSexpsRange(doc, forwardCursor, forwardCursor.offsetStart, usePairs); - if (forwardRange[0] !== currentRange[0]) { - // there is a sexp to the right - const rightText = doc.model.getText(forwardRange[0], forwardRange[1]); - const currentText = doc.model.getText(currentRange[0], currentRange[1]); - void doc.model.edit( - [ + const edits = [], + selections = clone(doc.selections); + + doc.selections.forEach((selection, index) => { + const { start: left, end: right } = selection; + const cursor = doc.getTokenCursor(right); + const usePairs = isInPairsList(cursor, pairForms); + const currentRange = currentSexpsRange(doc, cursor, right, usePairs); + const newPosOffset = currentRange[1] - right; + const forwardCursor = doc.getTokenCursor(currentRange[1]); + forwardCursor.forwardSexp(); + const forwardRange = currentSexpsRange(doc, forwardCursor, forwardCursor.offsetStart, usePairs); + if (forwardRange[0] !== currentRange[0]) { + // there is a sexp to the right + const rightText = doc.model.getText(forwardRange[0], forwardRange[1]); + const currentText = doc.model.getText(currentRange[0], currentRange[1]); + edits.push( new ModelEdit('changeRange', [forwardRange[0], forwardRange[1], currentText]), - new ModelEdit('changeRange', [currentRange[0], currentRange[1], rightText]), - ], - { - selection: new ModelEditSelection( - currentRange[1] + (forwardRange[1] - currentRange[1]) - newPosOffset - ), - } - ); - } + new ModelEdit('changeRange', [currentRange[0], currentRange[1], rightText]) + ); + + selections[index] = new ModelEditSelection( + currentRange[1] + (forwardRange[1] - currentRange[1]) - newPosOffset + ); + } + }); + return doc.model.edit(edits, { selections }); } export type WhitespaceInfo = { @@ -1238,7 +1725,7 @@ export type WhitespaceInfo = { */ export function collectWhitespaceInfo( doc: EditableDocument, - p = doc.selection.active + p /* = doc.selections.active */ ): WhitespaceInfo { const cursor = doc.getTokenCursor(p); const currentRange = cursor.rangeForCurrentForm(p); @@ -1266,151 +1753,178 @@ export function collectWhitespaceInfo( }; } -export function dragSexprBackwardUp(doc: EditableDocument, p = doc.selection.active) { - const wsInfo = collectWhitespaceInfo(doc, p); - const cursor = doc.getTokenCursor(p); - const currentRange = cursor.rangeForCurrentForm(p); - if (cursor.backwardList() && cursor.backwardUpList()) { - const listStart = cursor.offsetStart; - const newPosOffset = p - currentRange[0]; - const newCursorPos = listStart + newPosOffset; - const listIndent = cursor.getToken().offset; - let dragText: string, deleteEdit: ModelEdit; - if (wsInfo.hasLeftWs) { - dragText = - doc.model.getText(...currentRange) + - (wsInfo.leftWsHasNewline ? '\n' + ' '.repeat(listIndent) : ' '); - const lineCommentCursor = doc.getTokenCursor(wsInfo.leftWsRange[0]); - const havePrecedingLineComment = lineCommentCursor.getPrevToken().type === 'comment'; - const wsLeftStart = wsInfo.leftWsRange[0] + (havePrecedingLineComment ? 1 : 0); - deleteEdit = new ModelEdit('deleteRange', [wsLeftStart, currentRange[1] - wsLeftStart]); - } else { - dragText = - doc.model.getText(...currentRange) + - (wsInfo.rightWsHasNewline ? '\n' + ' '.repeat(listIndent) : ' '); - deleteEdit = new ModelEdit('deleteRange', [ - currentRange[0], - wsInfo.rightWsRange[1] - currentRange[0], - ]); - } - void doc.model.edit( - [ - deleteEdit, - new ModelEdit('insertString', [listStart, dragText, [p, p], [newCursorPos, newCursorPos]]), - ], - { - selection: new ModelEditSelection(newCursorPos), - skipFormat: false, - undoStopBefore: true, - } - ); - } -} - -export function dragSexprForwardDown(doc: EditableDocument, p = doc.selection.active) { - const wsInfo = collectWhitespaceInfo(doc, p); - const currentRange = doc.getTokenCursor(p).rangeForCurrentForm(p); - const newPosOffset = p - currentRange[0]; - const cursor = doc.getTokenCursor(currentRange[0]); - while (cursor.forwardSexp()) { - cursor.forwardWhitespace(); - const token = cursor.getToken(); - if (token.type === 'open') { +// TODO: multi +export function dragSexprBackwardUp( + doc: EditableDocument + // p = doc.selections.active +) { + const edits = [], + selections = clone(doc.selections); + + doc.selections.forEach((selection, index) => { + const { active: p } = selection; + const wsInfo = collectWhitespaceInfo(doc, p); + const cursor = doc.getTokenCursor(p); + const currentRange = cursor.rangeForCurrentForm(p); + if (cursor.backwardList() && cursor.backwardUpList()) { const listStart = cursor.offsetStart; - const deleteLength = wsInfo.rightWsRange[1] - currentRange[0]; - const insertStart = listStart + token.raw.length; - const newCursorPos = insertStart - deleteLength + newPosOffset; - const insertText = - doc.model.getText(...currentRange) + (wsInfo.rightWsHasNewline ? '\n' : ' '); - void doc.model.edit( - [ - new ModelEdit('insertString', [ - insertStart, - insertText, - [p, p], - [newCursorPos, newCursorPos], - ]), - new ModelEdit('deleteRange', [currentRange[0], deleteLength]), - ], - { - selection: new ModelEditSelection(newCursorPos), - skipFormat: false, - undoStopBefore: true, - } - ); - break; + const newPosOffset = p - currentRange[0]; + const newCursorPos = listStart + newPosOffset; + const listIndent = cursor.getToken().offset; + let dragText: string, deleteEdit: ModelEdit; + if (wsInfo.hasLeftWs) { + dragText = + doc.model.getText(...currentRange) + + (wsInfo.leftWsHasNewline ? '\n' + ' '.repeat(listIndent) : ' '); + const lineCommentCursor = doc.getTokenCursor(wsInfo.leftWsRange[0]); + const havePrecedingLineComment = lineCommentCursor.getPrevToken().type === 'comment'; + const wsLeftStart = wsInfo.leftWsRange[0] + (havePrecedingLineComment ? 1 : 0); + deleteEdit = new ModelEdit('deleteRange', [wsLeftStart, currentRange[1] - wsLeftStart]); + } else { + dragText = + doc.model.getText(...currentRange) + + (wsInfo.rightWsHasNewline ? '\n' + ' '.repeat(listIndent) : ' '); + deleteEdit = new ModelEdit('deleteRange', [ + currentRange[0], + wsInfo.rightWsRange[1] - currentRange[0], + ]); + } + edits.push(deleteEdit, new ModelEdit('insertString', [listStart, dragText])); + selections[index] = new ModelEditSelection(newCursorPos); } - } + }); + void doc.model.edit(edits, { + selections, + skipFormat: false, + undoStopBefore: true, + }); } -export function dragSexprForwardUp(doc: EditableDocument, p = doc.selection.active) { - const wsInfo = collectWhitespaceInfo(doc, p); - const cursor = doc.getTokenCursor(p); - const currentRange = cursor.rangeForCurrentForm(p); - if (cursor.forwardList() && cursor.upList()) { - const listEnd = cursor.offsetStart; +// TODO: test +// TODO: either forEach and batch edit or forEach sequential +export function dragSexprForwardDown( + doc: EditableDocument + // p = doc.selections.active +) { + const edits = [], + selections = clone(doc.selections); + + doc.selections.forEach((selection, index) => { + const { active: p } = selection; + + const wsInfo = collectWhitespaceInfo(doc, p); + const currentRange = doc.getTokenCursor(p).rangeForCurrentForm(p); const newPosOffset = p - currentRange[0]; - const listWsInfo = collectWhitespaceInfo(doc, listEnd); - const dragText = - (listWsInfo.rightWsHasNewline ? '\n' : ' ') + doc.model.getText(...currentRange); - let deleteStart = wsInfo.leftWsRange[0]; - let deleteLength = currentRange[1] - deleteStart; - if (wsInfo.hasRightWs) { - deleteStart = currentRange[0]; - deleteLength = wsInfo.rightWsRange[1] - deleteStart; - } - const newCursorPos = listEnd + newPosOffset + 1 - deleteLength; - void doc.model.edit( - [ - new ModelEdit('insertString', [listEnd, dragText, [p, p], [newCursorPos, newCursorPos]]), - new ModelEdit('deleteRange', [deleteStart, deleteLength]), - ], - { - selection: new ModelEditSelection(newCursorPos), - skipFormat: false, - undoStopBefore: true, + const cursor = doc.getTokenCursor(currentRange[0]); + while (cursor.forwardSexp()) { + cursor.forwardWhitespace(); + const token = cursor.getToken(); + if (token.type === 'open') { + const listStart = cursor.offsetStart; + const deleteLength = wsInfo.rightWsRange[1] - currentRange[0]; + const insertStart = listStart + token.raw.length; + const newCursorPos = insertStart - deleteLength + newPosOffset; + const insertText = + doc.model.getText(...currentRange) + (wsInfo.rightWsHasNewline ? '\n' : ' '); + edits.push( + new ModelEdit('insertString', [insertStart, insertText]), + new ModelEdit('deleteRange', [currentRange[0], deleteLength]) + ); + selections[index] = new ModelEditSelection(newCursorPos); + break; } - ); - } + } + }); + void doc.model.edit(edits, { + selections, + skipFormat: false, + undoStopBefore: true, + }); } -export function dragSexprBackwardDown(doc: EditableDocument, p = doc.selection.active) { - const wsInfo = collectWhitespaceInfo(doc, p); - const currentRange = doc.getTokenCursor(p).rangeForCurrentForm(p); - const newPosOffset = p - currentRange[0]; - const cursor = doc.getTokenCursor(currentRange[1]); - while (cursor.backwardSexp()) { - cursor.backwardWhitespace(); - const token = cursor.getPrevToken(); - if (token.type === 'close') { - cursor.previous(); +// TODO: multi +export function dragSexprForwardUp( + doc: EditableDocument + // p = doc.selections.active +) { + const edits = [], + selections = clone(doc.selections); + + doc.selections.forEach((selection, index) => { + const { active: p } = selection; + + const wsInfo = collectWhitespaceInfo(doc, p); + const cursor = doc.getTokenCursor(p); + const currentRange = cursor.rangeForCurrentForm(p); + if (cursor.forwardList() && cursor.upList()) { const listEnd = cursor.offsetStart; + const newPosOffset = p - currentRange[0]; + const listWsInfo = collectWhitespaceInfo(doc, listEnd); + const dragText = + (listWsInfo.rightWsHasNewline ? '\n' : ' ') + doc.model.getText(...currentRange); + let deleteStart = wsInfo.leftWsRange[0]; + let deleteLength = currentRange[1] - deleteStart; + if (wsInfo.hasRightWs) { + deleteStart = currentRange[0]; + deleteLength = wsInfo.rightWsRange[1] - deleteStart; + } + const newCursorPos = listEnd + newPosOffset + 1 - deleteLength; + edits.push( + new ModelEdit('insertString', [listEnd, dragText]), + new ModelEdit('deleteRange', [deleteStart, deleteLength]) + ); + selections[index] = new ModelEditSelection(newCursorPos); + } + }); + void doc.model.edit(edits, { + selections, + skipFormat: false, + undoStopBefore: true, + }); +} + +// TODO: multi +export function dragSexprBackwardDown( + doc: EditableDocument + // p = doc.selections.active +) { + const edits = [], + selections = clone(doc.selections); + + doc.selections.forEach((selection, index) => { + const { active: p } = selection; + + const wsInfo = collectWhitespaceInfo(doc, p); + const currentRange = doc.getTokenCursor(p).rangeForCurrentForm(p); + const newPosOffset = p - currentRange[0]; + const cursor = doc.getTokenCursor(currentRange[1]); + while (cursor.backwardSexp()) { cursor.backwardWhitespace(); - const siblingWsInfo = collectWhitespaceInfo(doc, cursor.offsetStart); - const deleteLength = currentRange[1] - wsInfo.leftWsRange[0]; - const insertStart = listEnd; - const newCursorPos = insertStart + newPosOffset + 1; - let insertText = doc.model.getText(...currentRange); - insertText = (siblingWsInfo.leftWsHasNewline ? '\n' : ' ') + insertText; - void doc.model.edit( - [ + const token = cursor.getPrevToken(); + if (token.type === 'close') { + cursor.previous(); + const listEnd = cursor.offsetStart; + cursor.backwardWhitespace(); + const siblingWsInfo = collectWhitespaceInfo(doc, cursor.offsetStart); + const deleteLength = currentRange[1] - wsInfo.leftWsRange[0]; + const insertStart = listEnd; + const newCursorPos = insertStart + newPosOffset + 1; + let insertText = doc.model.getText(...currentRange); + insertText = (siblingWsInfo.leftWsHasNewline ? '\n' : ' ') + insertText; + edits.push( new ModelEdit('deleteRange', [wsInfo.leftWsRange[0], deleteLength]), - new ModelEdit('insertString', [ - insertStart, - insertText, - [p, p], - [newCursorPos, newCursorPos], - ]), - ], - { - selection: new ModelEditSelection(newCursorPos), - skipFormat: false, - undoStopBefore: true, - } - ); - break; + new ModelEdit('insertString', [insertStart, insertText]) + ); + selections[index] = new ModelEditSelection(newCursorPos); + break; + } } - } + }); + void doc.model.edit(edits, { + selections, + skipFormat: false, + undoStopBefore: true, + }); } function adaptContentsToRichComment(contents: string): string { @@ -1421,7 +1935,12 @@ function adaptContentsToRichComment(contents: string): string { .trim(); } -export function addRichComment(doc: EditableDocument, p = doc.selection.active, contents?: string) { +// it only warrants multi cursor when invoking the simple "Add Rich Comment" command, if even that +export function addRichComment( + doc: EditableDocument, + p = doc.selections[0].active, + contents?: string +) { const richComment = `(comment\n ${contents ? adaptContentsToRichComment(contents) : ''}\n )`; let cursor = doc.getTokenCursor(p); const topLevelRange = rangeForDefun(doc, p, false); @@ -1448,21 +1967,11 @@ export function addRichComment(doc: EditableDocument, p = doc.selection.active, checkIfRichCommentExistsCursor.forwardWhitespace(false); // insert nothing, just place cursor const newCursorPos = checkIfRichCommentExistsCursor.offsetStart; - void doc.model.edit( - [ - new ModelEdit('insertString', [ - newCursorPos, - '', - [newCursorPos, newCursorPos], - [newCursorPos, newCursorPos], - ]), - ], - { - selection: new ModelEditSelection(newCursorPos), - skipFormat: true, - undoStopBefore: false, - } - ); + void doc.model.edit([new ModelEdit('insertString', [newCursorPos, ''])], { + selections: [new ModelEditSelection(newCursorPos)], + skipFormat: true, + undoStopBefore: false, + }); return; } } @@ -1476,19 +1985,9 @@ export function addRichComment(doc: EditableDocument, p = doc.selection.active, const append = '\n'.repeat(numAppendNls); const insertText = `${prepend}${richComment}${append}`; const newCursorPos = insertStart + 11 + numPrependNls * doc.model.lineEndingLength; - void doc.model.edit( - [ - new ModelEdit('insertString', [ - insertStart, - insertText, - [insertStart, insertStart], - [newCursorPos, newCursorPos], - ]), - ], - { - selection: new ModelEditSelection(newCursorPos), - skipFormat: false, - undoStopBefore: true, - } - ); + void doc.model.edit([new ModelEdit('insertString', [insertStart, insertText])], { + selections: [new ModelEditSelection(newCursorPos)], + skipFormat: false, + undoStopBefore: true, + }); } diff --git a/src/doc-mirror/index.ts b/src/doc-mirror/index.ts index 4d6ec962f..d2838361a 100644 --- a/src/doc-mirror/index.ts +++ b/src/doc-mirror/index.ts @@ -1,17 +1,18 @@ export { getIndent } from '../cursor-doc/indent'; +import { isUndefined } from 'lodash'; import * as vscode from 'vscode'; -import * as utilities from '../utilities'; import * as formatter from '../calva-fmt/src/format'; -import { LispTokenCursor } from '../cursor-doc/token-cursor'; import { - ModelEdit, EditableDocument, EditableModel, - ModelEditOptions, LineInputModel, + ModelEdit, + ModelEditOptions, ModelEditSelection, + ModelEditResult, } from '../cursor-doc/model'; -import { isUndefined } from 'lodash'; +import { LispTokenCursor } from '../cursor-doc/token-cursor'; +import * as utilities from '../utilities'; const documents = new Map(); @@ -24,7 +25,11 @@ export class DocumentModel implements EditableModel { this.lineInputModel = new LineInputModel(this.lineEndingLength); } - edit(modelEdits: ModelEdit[], options: ModelEditOptions): Thenable { + get lineEnding() { + return this.lineEndingLength == 2 ? '\r\n' : '\n'; + } + + edit(modelEdits: ModelEdit[], options: ModelEditOptions): Thenable { const editor = utilities.getActiveTextEditor(), undoStopBefore = !!options.undoStopBefore; return editor @@ -48,18 +53,22 @@ export class DocumentModel implements EditableModel { }, { undoStopBefore, undoStopAfter: false } ) - .then((isFulfilled) => { - if (isFulfilled) { - if (options.selection) { - this.document.selection = options.selection; + .then(async (success) => { + if (success) { + if (options.selections) { + this.document.selections = options.selections; } if (!options.skipFormat) { - return formatter.formatPosition(editor, false, { - 'format-depth': options.formatDepth ? options.formatDepth : 1, - }); + return { + edits: modelEdits, + selections: options.selections, + success: await formatter.formatPosition(editor, false, { + 'format-depth': options.formatDepth ? options.formatDepth : 1, + }), + }; } } - return isFulfilled; + return { edits: modelEdits, selections: options.selections, success }; }); } @@ -124,55 +133,78 @@ export class MirroredDocument implements EditableDocument { model = new DocumentModel(this); - selectionStack: ModelEditSelection[] = []; + selectionsStack: ModelEditSelection[][] = []; public getTokenCursor( - offset: number = this.selection.active, + offset: number = this.selections[0].active, previous: boolean = false ): LispTokenCursor { return this.model.getTokenCursor(offset, previous); } public insertString(text: string) { - const editor = utilities.getActiveTextEditor(), - selection = editor.selection, + const editor = utilities.tryToGetActiveTextEditor(), + selections = editor.selections, wsEdit = new vscode.WorkspaceEdit(), // TODO: prob prefer selection.active or .start - edit = vscode.TextEdit.insert(this.document.positionAt(this.selection.anchor), text); - wsEdit.set(this.document.uri, [edit]); + edits = this.selections.map(({ anchor: left }) => + vscode.TextEdit.insert(this.document.positionAt(left), text) + ); + wsEdit.set(this.document.uri, edits); void vscode.workspace.applyEdit(wsEdit).then((_v) => { - editor.selection = selection; + editor.selections = selections; }); } - set selection(selection: ModelEditSelection) { + get selection() { + return this.selections[0]; + } + + set selection(sel: ModelEditSelection) { + this.selections = [sel]; + } + + get selections(): ModelEditSelection[] { const editor = utilities.getActiveTextEditor(), - document = editor.document, - anchor = document.positionAt(selection.anchor), - active = document.positionAt(selection.active); - editor.selection = new vscode.Selection(anchor, active); + document = editor.document; + return editor.selections.map((sel) => { + const anchor = document.offsetAt(sel.anchor), + active = document.offsetAt(sel.active); + return new ModelEditSelection(anchor, active); + }); + } + + set selections(selections: ModelEditSelection[]) { + const editor = utilities.getActiveTextEditor(), + document = editor.document; + editor.selections = selections.map((selection) => { + const anchor = document.positionAt(selection.anchor), + active = document.positionAt(selection.active); + return new vscode.Selection(anchor, active); + }); + + const primarySelection = selections[0]; + const active = document.positionAt(primarySelection.active); editor.revealRange(new vscode.Range(active, active)); } - get selection(): ModelEditSelection { + public getSelectionTexts() { const editor = utilities.getActiveTextEditor(), - document = editor.document, - anchor = document.offsetAt(editor.selection.anchor), - active = document.offsetAt(editor.selection.active); - return new ModelEditSelection(anchor, active); + selections = editor.selections; + return selections.map((selection) => this.document.getText(selection)); } - public getSelectionText() { + public getSelectionText(index: number = 0) { const editor = utilities.getActiveTextEditor(), - selection = editor.selection; + selection = editor.selections[index]; return this.document.getText(selection); } - public delete(): Thenable { + public delete(): Thenable { return vscode.commands.executeCommand('deleteRight'); } - public backspace(): Thenable { + public backspace(): Thenable { return vscode.commands.executeCommand('deleteLeft'); } } @@ -209,7 +241,7 @@ export function tryToGetDocument(doc: vscode.TextDocument) { return documents.get(doc); } -export function getDocument(doc: vscode.TextDocument) { +export function getDocument(doc: vscode.TextDocument): MirroredDocument { const mirrorDoc = tryToGetDocument(doc); if (isUndefined(mirrorDoc)) { diff --git a/src/extension-test/unit/common/text-notation-test.ts b/src/extension-test/unit/common/text-notation-test.ts new file mode 100644 index 000000000..e74637f6e --- /dev/null +++ b/src/extension-test/unit/common/text-notation-test.ts @@ -0,0 +1,86 @@ +import * as expect from 'expect'; +import * as textNotation from '../common/text-notation'; + +describe('text-notation test utils', () => { + describe('textNotationFromDoc', () => { + it('returns the same input text to textNotationFromDoc', () => { + const inputText = '(a b|1) (a b|) (a <2(b)<2)'; + const doc = textNotation.docFromTextNotation(inputText); + expect(textNotation.textNotationFromDoc(doc)).toEqual(inputText); + }); + it('has cursor at end when no trailing newline', () => { + const inputText = '()|'; + const doc = textNotation.docFromTextNotation(inputText); + expect(textNotation.textNotationFromDoc(doc)).toEqual(inputText); + }); + }); + describe('docFromTextNotation', () => { + it('creates single cursor position', () => { + const tn = '(a b|)'; + const text = '(a b)'; + const selections = [[4, 4]]; + const doc = textNotation.docFromTextNotation(tn); + expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); + }); + it('creates multiple cursor positions', () => { + const tn = '(|1a |2b|)'; + const text = '(a b)'; + const selections = [ + [4, 4], + [1, 1], + [3, 3], + ]; + const doc = textNotation.docFromTextNotation(tn); + expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); + }); + it('creates single range/selection', () => { + const tn = '(a |b|)'; + const text = '(a b)'; + const selections = [[3, 4]]; + const doc = textNotation.docFromTextNotation(tn); + expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); + }); + it('creates multiple ranges/selections', () => { + const tn = '|1(|1|2a|2 |b|)'; + const text = '(a b)'; + const selections = [ + [3, 4], + [0, 1], + [1, 2], + ]; + const doc = textNotation.docFromTextNotation(tn); + expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); + }); + it('creates single directed r->l range/selection', () => { + const tn = '(a l range/selection', () => { + const tn = '(<1a<1 { + const tn = '|3a|3 <1b<1 >2c>2 d§>0e>0 f |4g<5<5'; + const text = 'a b c d§e f g'.replace(/§/g, '\n'); + const selections = [ + [8, 9], + [3, 2], + [4, 5], + [0, 1], + [12, 12], + [13, 13], + ]; + const doc = textNotation.docFromTextNotation(tn); + expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); + }); + }); +}); diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index ed674cf4f..e960c5a0b 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -1,67 +1,177 @@ import * as model from '../../../cursor-doc/model'; +import { + clone, + entries, + cond, + toInteger, + last, + first, + cloneDeep, + orderBy, + partialRight, +} from 'lodash'; /** * Text Notation for expressing states of a document, including * current text and selection. + * See this video for an intro: https://www.youtube.com/watch?v=Sy3arG-Degw * * Since JavasScript makes it clumsy with multiline strings, - * newlines are denoted with a middle dot character: `•` + * newlines are denoted with the paragraph character: `§` * * Selections are denoted like so - * * Single position selections are denoted with a single `|`. - * * Selections w/o direction are denoted with `|` at the range's boundaries. - * * Selections with direction left->right are denoted with `|>|` at the range boundaries - * * Selections with direction left->right are denoted with `|<|` at the range boundaries + * * Cursors, (which are actually selections with identical start and end) are denoted with a single `|`, with <= 10 multiple cursors defined by `|1`, `|2`, ... `|9`, etc, or in regex: /\|\d/. 0-indexed, so `|` is 0, `|1` is 1, etc. + * * This is however actually an alternative for the following `>\d?` notation, it has the same left->right semantics, but looks more like a cursor/caret lol, and more importantly, drops the 0 for the first cursor. + * * Selections with direction left->right are denoted with `>0`, `>1`, `>2`, ... `>9` etc at the range boundaries + * * Selections with direction right->left are denoted with `<0`, `<1`, `<2`, ... `<9` etc at the range boundaries */ +function textNotationToTextAndSelection(content: string): [string, model.ModelEditSelection[]] { + const cursorSymbolRegExPattern = + /(?(?:(?<|>(?=\d{1})))|(?:\|))(?\d{1})?/g; + const text = clone(content) + .replace(/§/g, '\n') + // .replace(/\|?[<>]?\|\d?/g, ''); + .replace(cursorSymbolRegExPattern, ''); -function textNotationToTextAndSelection(s: string): [string, { anchor: number; active: number }] { - const text = s.replace(/•/g, '\n').replace(/\|?[<>]?\|/g, ''); - let anchor: undefined | number = undefined; - let active: undefined | number = undefined; - anchor = s.indexOf('|>|'); - if (anchor >= 0) { - active = s.lastIndexOf('|>|') - 3; - } else { - anchor = s.lastIndexOf('|<|'); - if (anchor >= 0) { - anchor -= 3; - active = s.indexOf('|<|'); - } else { - anchor = s.indexOf('|'); - if (anchor >= 0) { - active = s.lastIndexOf('|'); - if (active !== anchor) { - active -= 1; - } else { - active = anchor; - } - } - } - } - return [text, { anchor, active }]; + /** + * 3 capt groups: + * 0 = total cursor, with number, + * 1 = just the cursor type, no number, + * 2 = only for directional selection cursors: + * the > or <, + * 3 = only if there's a number, the number itself (eg multi cursor) + */ + const matches = Array.from(content.matchAll(cursorSymbolRegExPattern)); + + // a map of cursor symbols (eg '>3' - including the cursor number if >1 ) to an an array of matches (for their positions mostly) in content string where that cursor is + // for now, we hope that there are at most two positions per symbol + const cursorMatchInstances = matches.reduce((acc, curr, index) => { + const nextAcc = { ...acc }; + + const sumOfPreviousCursorOffsets = Array.from(matches) + .slice(0, index) + .reduce((sum, m) => sum + m[0].length, 0); + + curr.index = curr.index - sumOfPreviousCursorOffsets; + + // const cursorMatchStr = curr.groups['cursorType'] ?? curr[0]; + const cursorMatchStr = curr[0]; + const matchesForCursor = nextAcc[cursorMatchStr] ?? []; + nextAcc[cursorMatchStr] = [...matchesForCursor, curr]; + return nextAcc; + }, {} as { [key: string]: RegExpMatchArray[] }); + + const selections = [].fill(0, matches.length, undefined); + + entries(cursorMatchInstances).forEach(([_, matches]) => { + const firstMatch = first(matches); + const secondMatch = last(matches) ?? firstMatch; + + const isReversed = + (firstMatch.groups['selectionDirection'] ?? firstMatch[2] ?? '') === '<' ? true : false; + + const start = firstMatch.index; + const end = secondMatch.index === firstMatch.index ? secondMatch.index : secondMatch.index; + + const anchor = isReversed ? end : start; + const active = isReversed ? start : end; + + const cursorNumber = toInteger(firstMatch.groups['cursorNumber'] ?? firstMatch[3] ?? '0'); + + // return new model.ModelEditSelection(anchor, active, start, end, isReversed); + selections[cursorNumber] = new model.ModelEditSelection(anchor, active, start, end, isReversed); + }); + + return [text, selections]; } /** * Utility function to create a doc from text-notated strings */ export function docFromTextNotation(s: string): model.StringDocument { - const [text, selection] = textNotationToTextAndSelection(s); + const [text, selections] = textNotationToTextAndSelection(s); const doc = new model.StringDocument(text); - doc.selection = new model.ModelEditSelection(selection.anchor, selection.active); + doc.selections = selections; return doc; } +export function textNotationFromDoc(doc: model.EditableDocument, prettyPrint = false): string { + const selections = doc.selections ?? []; + const ranges = selections.map((s) => s.asDirectedRange); + + const text = getText(doc, true); + + return textNotationFromTextAndSelections(text, ranges, prettyPrint); +} + +export function textNotationFromTextAndSelections( + text: string, + ranges: Array<[number, number]>, + prettyPrint = false +): string { + let cursorSymbols: [number, string][] = []; + ranges.forEach((r, cursorNumber) => { + const [anchor, active] = r; + const isReversed = anchor > active; + const isSelection = anchor - active !== 0; + const start = Math.min(anchor, active); + const end = Math.max(anchor, active); + + const cursorType = isReversed ? '<' : '|'; + cursorSymbols.push([start, `${cursorType}${cursorNumber || ''}`]); + if (isSelection) { + cursorSymbols.push([end, `${cursorType}${cursorNumber || ''}`]); + } + }); + + cursorSymbols = orderBy(cursorSymbols, (c) => c[0]); + + // basically split up the text into chunks separated by where they'd have had cursor symbols, and append cursor symbols after each chunk, before joining back together + // this way we can insert the cursor symbols in the right place without having to worry about the cumulative offsets created by appending the cursor symbols + const textSegments = cursorSymbols + .reduce( + (acc, [offset, symbol], index) => { + const lastSection = last(acc)[1]; + const sections = acc.slice(0, -1); + + const lastSectionOffset = + offset - sections.filter((s) => s[0]).reduce((sum, sec) => sum + sec[1].length, 0); + const newSectionOfText = [true, lastSection.slice(0, lastSectionOffset)]; + const newSectionWithCursor = [false, symbol]; + const restOfText = lastSection.slice(lastSectionOffset); + + return [...sections, newSectionOfText, newSectionWithCursor, [true, restOfText]]; + }, + [ + [true, text] as [ + boolean /* is an actual text segment instead of cursor symbol? */, + string /* text segment or cursor symbol */ + ], + ] + ) + .map((s) => s[1]); + + const textNotation = textSegments.join(''); + return prettyPrint ? textNotation.replace(/§/g, '\n') : textNotation; +} + +textNotationFromDoc.pretty = partialRight(textNotationFromDoc, true); +textNotationFromTextAndSelections.pretty = partialRight(textNotationFromTextAndSelections, true); + /** * Utility function to get the text from a document. * @param doc * @returns string */ -export function text(doc: model.StringDocument): string { - return doc.model.getText(0, Infinity); +export function getText(doc: model.EditableDocument, replaceNewLine = false): string { + const text = doc.model.getText(0, Infinity); + + return replaceNewLine ? text.split(doc.model.lineEnding).join('§') : text; } /** * Utility function to create a comparable structure with the text and * selection from a document */ -export function textAndSelection(doc: model.StringDocument): [string, [number, number]] { - return [text(doc), [doc.selection.anchor, doc.selection.active]]; +export function textAndSelection(doc: model.EditableDocument): [string, [number, number][]] { + // return [text(doc), [doc.selection.anchor, doc.selection.active]]; + return [getText(doc), doc.selections.map((s) => [s.anchor, s.active])]; } diff --git a/src/extension-test/unit/cursor-doc/cursor-context-test.ts b/src/extension-test/unit/cursor-doc/cursor-context-test.ts index bc58d0ff3..ba854b957 100644 --- a/src/extension-test/unit/cursor-doc/cursor-context-test.ts +++ b/src/extension-test/unit/cursor-doc/cursor-context-test.ts @@ -3,347 +3,347 @@ import * as context from '../../../cursor-doc/cursor-context'; import { docFromTextNotation, textAndSelection } from '../common/text-notation'; describe('Cursor Contexts', () => { - describe('cursorInString', () => { + xdescribe('cursorInString', () => { it('is true in string', () => { - const contexts = context.determineContexts(docFromTextNotation('foo• "bar• |baz"•gaz')); - expect(contexts.includes('calva:cursorInString')).toBe(true); + const contexts = context.determineContexts(docFromTextNotation('foo§ "bar§ |baz"§gaz')); + // expect(contexts.includes('calva:cursorInString')).toBe(true); }); it('is false outside after string', () => { - const contexts = context.determineContexts(docFromTextNotation('foo• "bar• baz"|•gaz')); - expect(contexts.includes('calva:cursorInString')).toBe(false); + const contexts = context.determineContexts(docFromTextNotation('foo§ "bar§ baz"|§gaz')); + // expect(contexts.includes('calva:cursorInString')).toBe(false); }); it('is true in regexp', () => { - const contexts = context.determineContexts(docFromTextNotation('foo• #"bar• ba|z"•gaz')); - expect(contexts.includes('calva:cursorInString')).toBe(true); + const contexts = context.determineContexts(docFromTextNotation('foo§ #"bar§ ba|z"§gaz')); + // expect(contexts.includes('calva:cursorInString')).toBe(true); }); it('is false in regexp open token', () => { const contexts = context.determineContexts( - docFromTextNotation('foo• #|"bat bar• baz"•gaz') + docFromTextNotation('foo§ #|"bat bar§ baz"§gaz') ); - expect(contexts.includes('calva:cursorInString')).toBe(false); + // expect(contexts.includes('calva:cursorInString')).toBe(false); }); }); describe('cursorInComment', () => { it('is true in comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; f|oo• ;; bar• ;; baz •gaz') + docFromTextNotation(';; f|oo§ ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorInComment')).toBe(true); + // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is true adjacent before comment', () => { const contexts = context.determineContexts( - docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz') + docFromTextNotation('|;; foo§ ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorInComment')).toBe(true); + // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is true in whitespace between SOL and comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo•| ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo§| ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorInComment')).toBe(true); + // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is true adjacent after comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo |• ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo |§ ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorInComment')).toBe(true); + // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is false in symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •g|az ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §g|az ;; ') ); - expect(contexts.includes('calva:cursorInComment')).toBe(false); + // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); it('is false adjacent after symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz| ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz| ;; ') ); - expect(contexts.includes('calva:cursorInComment')).toBe(false); + // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); it('is false in whitespace after symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz | ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz | ;; ') ); - expect(contexts.includes('calva:cursorInComment')).toBe(false); + // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); it('is true after symbol adjacent before comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz |;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz |;; ') ); - expect(contexts.includes('calva:cursorInComment')).toBe(true); + // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is false in leading ws on line after comment', () => { - const contexts = context.determineContexts(docFromTextNotation('(+• ;foo• | 2)')); - expect(contexts.includes('calva:cursorInComment')).toBe(false); + const contexts = context.determineContexts(docFromTextNotation('(+§ ;foo§ | 2)')); + // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); }); describe('cursorBeforeComment', () => { it('is false in comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; fo|o• ;; bar• ;; baz •gaz') + docFromTextNotation(';; fo|o§ ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is true adjacent before comment', () => { const contexts = context.determineContexts( - docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz') + docFromTextNotation('|;; foo§ ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(true); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(true); }); - it('is false adjacent before comment on line with leading witespace and preceding comment line', () => { - const contexts = context.determineContexts(docFromTextNotation(' ;; foo• |;; bar')); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + it('is false adjacent before comment on line with leading whitespace and preceding comment line', () => { + const contexts = context.determineContexts(docFromTextNotation(' ;; foo§ |;; bar')); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is true after symbol in whitespace between SOL and comment', () => { const contexts = context.determineContexts( - docFromTextNotation(' foo•| ;; bar• ;; baz •gaz') + docFromTextNotation(' foo§| ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(true); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(true); }); it('is false at SOL on a comment line with more comment lines following', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo •| ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo §| ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false at empty line squeezed in along comments lines', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • •|• • ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo § §|§ § ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false after comment lines before symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz |•gaz ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz |§gaz ;; ') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false in symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •g|az ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §g|az ;; ') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false adjacent after symbol after comment lines', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz| ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz| ;; ') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false in whitespace after symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz | ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz | ;; ') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is true adjacent before comment after symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz |;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz |;; ') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false at EOT after comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz ;; |') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz ;; |') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); }); describe('cursorAfterComment', () => { it('is false in comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; fo|o• ;; bar• ;; baz •gaz') + docFromTextNotation(';; fo|o§ ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false adjacent before comment', () => { const contexts = context.determineContexts( - docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz') + docFromTextNotation('|;; foo§ ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false in whitespace between SOL and comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo•| ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo§| ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false at EOL on a comment line with more comment lines following', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo |• ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo |§ ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false at empty line squeezed in along comments lines', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • •|• • ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo § §|§ § ;; bar§ ;; baz §gaz') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is true after comment lines before symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz |•gaz ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz |§gaz ;; ') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(true); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(true); }); it('is false in symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •g|az ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §g|az ;; ') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false adjacent after symbol after comment lines', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz| ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz| ;; ') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false in whitespace after symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz | ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz | ;; ') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is true at EOT after comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz ;; |') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz ;; |') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(true); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(true); }); }); describe('isAtLineStartInclWS', () => { it('returns true at the start of a line', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation('|;; foo§ ;; bar§ ;; baz §gaz')) ).toBe(true); expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• ;; bar• ;; baz •|gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ ;; bar§ ;; baz §|gaz')) ).toBe(true); }); it('returns true at line start with leading whitespace', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• |;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ |;; bar§ ;; baz §gaz')) ).toBe(true); expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• | ;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ | ;; bar§ ;; baz §gaz')) ).toBe(true); expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo•| ;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§| ;; bar§ ;; baz §gaz')) ).toBe(true); }); it('returns true at end of line with only whitespace', () => { - expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo• |• ;; baz •gaz'))).toBe( + expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo§ |§ ;; baz §gaz'))).toBe( true ); }); it('returns true at start of line with only whitespace', () => { - expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo•| • ;; baz •gaz'))).toBe( + expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo§| § ;; baz §gaz'))).toBe( true ); }); it('returns true in middle of line with only whitespace', () => { - expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo• | • ;; baz •gaz'))).toBe( + expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo§ | § ;; baz §gaz'))).toBe( true ); }); it('returns true at empty line', () => { - expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo•|• ;; baz •gaz'))).toBe( + expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo§|§ ;; baz §gaz'))).toBe( true ); }); it('returns true at line start with leading & trailing whitespace', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• ;; bar• | ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ ;; bar§ | ;; baz §gaz')) ).toBe(true); }); it('returns false within a line (non-whitespace)', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';|; foo• ;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';|; foo§ ;; bar§ ;; baz §gaz')) ).toBe(false); }); it('returns false within a line with leading whitespace', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• ;; |bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ ;; |bar§ ;; baz §gaz')) ).toBe(false); }); it('returns false at the end of a line', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo|• ;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo|§ ;; bar§ ;; baz §gaz')) ).toBe(false); }); it('returns false at the end of document', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• ;; bar• ;; baz •gaz|')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ ;; bar§ ;; baz §gaz|')) ).toBe(false); }); }); describe('isAtLineEndInclWS', () => { it('returns true at the end of a line', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar • ;; baz •gaz|')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar § ;; baz §gaz|')) ).toBe(true); expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo|• ;; bar • ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo|§ ;; bar § ;; baz §gaz')) ).toBe(true); }); it('returns true at line end with trailing whitespace', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar| • ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar| § ;; baz §gaz')) ).toBe(true); expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar |• ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar |§ ;; baz §gaz')) ).toBe(true); }); it('returns true at line end with leading & trailing whitespace', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar | • ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar | § ;; baz §gaz')) ).toBe(true); }); it('returns true at end of line with only whitespace', () => { - expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo• |• ;; baz •gaz'))).toBe( + expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo§ |§ ;; baz §gaz'))).toBe( true ); }); it('returns true at start of line with only whitespace', () => { - expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo•| • ;; baz •gaz'))).toBe( + expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo§| § ;; baz §gaz'))).toBe( true ); }); it('returns true in middle of line with only whitespace', () => { - expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo• | • ;; baz •gaz'))).toBe( + expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo§ | § ;; baz §gaz'))).toBe( true ); }); it('returns true at empty line', () => { - expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo•|• ;; baz •gaz'))).toBe(true); + expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo§|§ ;; baz §gaz'))).toBe(true); }); it('returns true at line start with only leading & trailing whitespace', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar | • ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar | § ;; baz §gaz')) ).toBe(true); }); it('returns false within a line (non-whitespace)', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar• ;; ba|z •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar§ ;; ba|z §gaz')) ).toBe(false); expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar• ;; ba|z •gaz ;|;')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar§ ;; ba|z §gaz ;|;')) ).toBe(false); }); it('returns false within a line with trailing whitespace', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar• ;|; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar§ ;|; baz §gaz')) ).toBe(false); expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar• ;; b|az •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar§ ;; b|az §gaz')) ).toBe(false); }); it('returns false at the start of a line', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar•| ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar§| ;; baz §gaz')) ).toBe(false); expect( - context.isAtLineEndInclWS(docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation('|;; foo§ ;; bar§ ;; baz §gaz')) ).toBe(false); }); }); diff --git a/src/extension-test/unit/cursor-doc/indent-test.ts b/src/extension-test/unit/cursor-doc/indent-test.ts index 1b75a0580..d1c93303a 100644 --- a/src/extension-test/unit/cursor-doc/indent-test.ts +++ b/src/extension-test/unit/cursor-doc/indent-test.ts @@ -1,7 +1,7 @@ import * as expect from 'expect'; import * as model from '../../../cursor-doc/model'; import * as indent from '../../../cursor-doc/indent'; -import { docFromTextNotation, textAndSelection, text } from '../common/text-notation'; +import { docFromTextNotation, textAndSelection, getText } from '../common/text-notation'; import { ModelEditSelection } from '../../../cursor-doc/model'; model.initScanner(20000); @@ -11,22 +11,22 @@ describe('indent', () => { describe('lists', () => { it('calculates indents for cursor in empty list', () => { const doc = docFromTextNotation('(|)'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(1); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(1); }); it('calculates indents for cursor in empty list prepended by text', () => { const doc = docFromTextNotation(' a b (|)'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(7); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(7); }); it('calculates indents for empty list inside vector', () => { const doc = docFromTextNotation('[(|)]'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(2); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(2); }); it("calculates indents for cursor in at arg 0 in `[['inner' 0]]`", () => { const doc = docFromTextNotation('(foo|)'); expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['inner', 0]], }) @@ -38,7 +38,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['inner', 0]], }) @@ -50,7 +50,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['block', 1]], }) @@ -62,7 +62,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['block', 1]], }) @@ -74,11 +74,11 @@ describe('indent', () => { describe('vectors', () => { it('calculates indents for cursor in empty vector', () => { const doc = docFromTextNotation('[|]'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(1); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(1); }); it('calculates indents for cursor in empty vector inside list', () => { const doc = docFromTextNotation('([|])'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(2); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(2); }); it('does not use indent rules for vectors with symbols at ”call” position', () => { // https://github.com/BetterThanTomorrow/calva/issues/1622 @@ -86,7 +86,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['inner', 0]], }) @@ -98,11 +98,11 @@ describe('indent', () => { describe('maps', () => { it('calculates indents for cursor in empty map', () => { const doc = docFromTextNotation('{|}'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(1); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(1); }); it('calculates indents for cursor in empty map inside list inside a vector', () => { const doc = docFromTextNotation('([{|}])'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(3); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(3); }); it('does not use indent rules for maps with symbols at ”call” position', () => { // https://github.com/BetterThanTomorrow/calva/issues/1622 @@ -110,7 +110,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['inner', 0]], }) @@ -122,11 +122,11 @@ describe('indent', () => { describe('sets', () => { it('calculates indents for cursor in empty set', () => { const doc = docFromTextNotation('#{|}'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(2); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(2); }); it('calculates indents for cursor in empty set inside list inside a vector', () => { const doc = docFromTextNotation('([#{|}])'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(4); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(4); }); it('does not use indent rules for maps with symbols at ”call” position', () => { // https://github.com/BetterThanTomorrow/calva/issues/1622 @@ -134,7 +134,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['inner', 0]], }) @@ -153,7 +153,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); @@ -166,14 +166,14 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); expect(state[0].rules).toEqual([]); }); it('collects indents for cursor in nested structure', () => { - const doc = docFromTextNotation('[]•(aa []•(bb•(cc :dd|)))•[]'); + const doc = docFromTextNotation('[]§(aa []§(bb§(cc :dd|)))§[]'); const rule1: indent.IndentRule[] = [ ['inner', 0], ['block', 1], @@ -183,7 +183,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); @@ -196,7 +196,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); @@ -210,7 +210,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); @@ -228,7 +228,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); @@ -246,7 +246,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index eff0299dc..858776187 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -1,8 +1,15 @@ import * as expect from 'expect'; import * as paredit from '../../../cursor-doc/paredit'; import * as model from '../../../cursor-doc/model'; -import { docFromTextNotation, textAndSelection, text } from '../common/text-notation'; +import { + docFromTextNotation, + textAndSelection, + getText, + textNotationFromDoc, + textNotationFromTextAndSelections, +} from '../common/text-notation'; import { ModelEditSelection } from '../../../cursor-doc/model'; +import { last, method } from 'lodash'; model.initScanner(20000); @@ -13,28 +20,48 @@ model.initScanner(20000); describe('paredit', () => { const docText = '(def foo [:foo :bar :baz])'; let doc: model.StringDocument; - const startSelection = new ModelEditSelection(0, 0); + const startSelections = [new ModelEditSelection(0, 0)]; beforeEach(() => { doc = new model.StringDocument(docText); - doc.selection = startSelection.clone(); + doc.selections = startSelections.map((s) => s.clone()); }); describe('movement', () => { describe('rangeToSexprForward', () => { it('Finds the list in front', () => { - const a = docFromTextNotation('|(def foo [vec])'); - const b = docFromTextNotation('|(def foo [vec])|'); + const a = docFromTextNotation('|(a b [c])'); + const b = docFromTextNotation('|(a b [c])|'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors find the list in front', () => { + const a = docFromTextNotation('|(a b [c])§|1(a b [c])'); + const b = docFromTextNotation('|(a b [c])|§|1(a b [c])|1'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds the list in front through metadata', () => { - const a = docFromTextNotation('|^:foo (def foo [vec])'); - const b = docFromTextNotation('|^:foo (def foo [vec])|'); + const a = docFromTextNotation('|^:a (b c [d])'); + const b = docFromTextNotation('|^:a (b c [d])|'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors find the list in front through metadata', () => { + const a = docFromTextNotation('|^:a (b c [d])§|1^:a (b c [d])'); + const b = docFromTextNotation('|^:a (b c [d])|§|1^:a (b c [d])|1'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds the list in front through metadata and readers', () => { - const a = docFromTextNotation('|^:f #a #b (def foo [vec])'); - const b = docFromTextNotation('|^:f #a #b (def foo [vec])|'); + const a = docFromTextNotation('|^:f #a #b (c d [e])'); + const b = docFromTextNotation('|^:f #a #b (c d [e])|'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors find the list in front through metadata and readers', () => { + const a = docFromTextNotation('|^:f #a #b (c d [e])§|1^:f #a #b (c d [e])'); + const b = docFromTextNotation('|^:f #a #b (c d [e])|§|1^:f #a #b (c d [e])|1'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors find the list in front through metadata and readers', () => { + const a = docFromTextNotation('|^:f #a #b (c d [e])§|1^:f #a #b (c d [e])'); + const b = docFromTextNotation('|^:f #a #b (c d [e])|§|1^:f #a #b (c d [e])|1'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds the list in front through reader metadata reader', () => { @@ -72,24 +99,34 @@ describe('paredit', () => { const b = docFromTextNotation('(def foo [:foo :bar |:baz|])'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); - it('Returns empty range when no forward sexp', () => { - const a = docFromTextNotation('(def foo [:foo :bar :baz|])'); - const b = docFromTextNotation('(def foo [:foo :bar :baz|])'); + it('Does not find anything at end of list', () => { + const a = docFromTextNotation('(|)'); + const b = docFromTextNotation('(|)'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors do not find anything at end of list', () => { + const a = docFromTextNotation('(|)§(|1)'); + const b = docFromTextNotation('(|)§(|1)'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Cursor 1 finds fwd sexp, cursor 2 does not, cursor 3 does', () => { + const a = docFromTextNotation('(|a)§(|1)§|2(b)'); + const b = docFromTextNotation('(|a|)§(|1)§|2(b)|2'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds next symbol, including leading space', () => { - const a = docFromTextNotation('(|>|def|>| foo [vec])'); - const b = docFromTextNotation('(def|>| foo|>| [vec])'); + const a = docFromTextNotation('(|def| foo [vec])'); + const b = docFromTextNotation('(def| foo| [vec])'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds following vector including leading space', () => { - const a = docFromTextNotation('(|>|def foo|>| [vec])'); - const b = docFromTextNotation('(def foo|>| [vec]|>|)'); + const a = docFromTextNotation('(|def foo| [vec])'); + const b = docFromTextNotation('(def foo| [vec]|)'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Reverses direction of selection and finds next sexp', () => { const a = docFromTextNotation('(|<|def foo|<| [vec])'); - const b = docFromTextNotation('(def foo|>| [vec]|>|)'); + const b = docFromTextNotation('(def foo| [vec]|)'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); }); @@ -118,7 +155,7 @@ describe('paredit', () => { it('Finds previous form, including space, and reverses direction', () => { // TODO: Should we really be reversing the direction here? const a = docFromTextNotation('(def |<|foo [vec]|<|)'); - const b = docFromTextNotation('(|>|def |>|foo [vec])'); + const b = docFromTextNotation('(|def |foo [vec])'); expect(paredit.backwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); }); @@ -131,6 +168,13 @@ describe('paredit', () => { const actual = paredit.forwardHybridSexpRange(a); expect(actual).toEqual(expected); }); + it('Multi-cursors find end of string', () => { + const a = docFromTextNotation('"a |b c"§"c |1d e"'); + const b = docFromTextNotation('"a |b c|"§"c |1d e|1"'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); it('Finds newline in multi line string', () => { const a = docFromTextNotation('"This |needs to find the end\n of the string."'); @@ -139,6 +183,13 @@ describe('paredit', () => { const actual = paredit.forwardHybridSexpRange(a); expect(actual).toEqual(expected); }); + it('Multi-cursors find newline in multi line string', () => { + const a = docFromTextNotation('"a |b §c"§"c |1d §e"'); + const b = docFromTextNotation('"a |b |cc"§"c |1d |1§e"'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); it('Finds newline in multi line string (Windows)', () => { const a = docFromTextNotation('"This |needs to find the end\r\n of the string."'); @@ -149,8 +200,15 @@ describe('paredit', () => { }); it('Finds end of comment', () => { - const a = docFromTextNotation('(a |;; foo\n e)'); - const b = docFromTextNotation('(a |;; foo|\n e)'); + const a = docFromTextNotation('(a |;; foo§ e)'); + const b = docFromTextNotation('(a |;; foo|§ e)'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + it('Multi-cursors find end of comment', () => { + const a = docFromTextNotation('(a |; b§ c)§(c |1; d§ e)'); + const b = docFromTextNotation('(a |; b|§ c)§(c |1; d|1§ e)'); const expected = textAndSelection(b)[1]; const actual = paredit.forwardHybridSexpRange(a); expect(actual).toEqual(expected); @@ -165,8 +223,15 @@ describe('paredit', () => { }); it('Maintains balanced delimiters 1', () => { - const a = docFromTextNotation('(a| b (c\n d) e)'); - const b = docFromTextNotation('(a| b (c\n d)| e)'); + const a = docFromTextNotation('(a| b (c§ d) e)'); + const b = docFromTextNotation('(a| b (c§ d)| e)'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + it('Multi-cursors maintain balanced delimiters 1', () => { + const a = docFromTextNotation('(a| b (c§ d) e)§(f|1 g (h§ i) j)'); + const b = docFromTextNotation('(a| b (c§ d)| e)§(f|1 g (h§ i)|1 j)'); const expected = textAndSelection(b)[1]; const actual = paredit.forwardHybridSexpRange(a); expect(actual).toEqual(expected); @@ -175,15 +240,25 @@ describe('paredit', () => { it('Maintains balanced delimiters 1 (Windows)', () => { const a = docFromTextNotation('(a| b (c\r\n d) e)'); const b = docFromTextNotation('(a| b (c\r\n d)| e)'); - const [start, end] = textAndSelection(b)[1]; - const actual = paredit.forwardHybridSexpRange(a); + const [start, end] = textAndSelection(b)[1][0]; + const actual = paredit.forwardHybridSexpRange(a)[0]; // off by 1 because \r\n is treated as 1 char? expect(actual).toEqual([start, end - 1]); }); it('Maintains balanced delimiters 2', () => { - const a = docFromTextNotation('(aa| (c (e\nf)) g)'); - const b = docFromTextNotation('(aa| (c (e\nf))|g)'); + const a = docFromTextNotation('(aa| (c d(e§f)) g)'); + const b = docFromTextNotation('(aa| (c d(e§f))|g)'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + it('Multi-cursors maintain balanced delimiters 2', () => { + const a = docFromTextNotation('(a| (c d (e§f)) g)§(h|1 (i j (k§l)) m)'); + // TODO: This behaves in VS Code, but the test fails + const b = docFromTextNotation('(a| (c d (e§f))| g)§(h|1 (i j (k§l))|1 m)'); + // the result matches this (the second cursor is where it differs) + //const b = docFromTextNotation('(a| (c d (e§f))|g)§(h |1(i j (k§l)) |1m)'); const expected = textAndSelection(b)[1]; const actual = paredit.forwardHybridSexpRange(a); expect(actual).toEqual(expected); @@ -192,8 +267,8 @@ describe('paredit', () => { it('Maintains balanced delimiters 2 (Windows)', () => { const a = docFromTextNotation('(aa| (c (e\r\nf)) g)'); const b = docFromTextNotation('(aa| (c (e\r\nf))|g)'); - const [start, end] = textAndSelection(b)[1]; - const actual = paredit.forwardHybridSexpRange(a); + const [start, end] = textAndSelection(b)[1][0]; + const actual = paredit.forwardHybridSexpRange(a)[0]; // off by 1 because \r\n is treated as 1 char? expect(actual).toEqual([start, end - 1]); }); @@ -329,10 +404,10 @@ describe('paredit', () => { it('Finds the full form after an ignore marker', () => { // https://github.com/BetterThanTomorrow/calva/pull/1293#issuecomment-927123696 const a = docFromTextNotation( - '(comment• #_|[a b (c d• e• f) g]• :a•)' + '(comment§ #_|[a b (c d§ e§ f) g]§ :a§)' ); const b = docFromTextNotation( - '(comment• #_|[a b (c d• e• f) g]|• :a•)' + '(comment§ #_|[a b (c d§ e§ f) g]|§ :a§)' ); const expected = textAndSelection(b)[1]; const actual = paredit.forwardHybridSexpRange(a); @@ -391,24 +466,34 @@ describe('paredit', () => { const b = docFromTextNotation('(def foo [:foo :bar |:baz|])'); expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); - it('Leaves an sexp if at the end', () => { + it('Leaves a sexp if at the end', () => { const a = docFromTextNotation('(def foo [:foo :bar :baz|])'); const b = docFromTextNotation('(def foo [:foo :bar :baz|]|)'); expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); + it('Multi-cursors leave sexp if at the end', () => { + const a = docFromTextNotation('(a|)§(b|1)'); + const b = docFromTextNotation('(a|)|§(b|1)|1'); + expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors one goes fwd a sexp, one leaves sexp b/c at the end', () => { + const a = docFromTextNotation('(|a)§(b|1)'); + const b = docFromTextNotation('(|a|)§(b|1)|1'); + expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); + }); it('Finds next symbol, including leading space', () => { - const a = docFromTextNotation('(|>|def|>| foo [vec])'); - const b = docFromTextNotation('(def|>| foo|>| [vec])'); + const a = docFromTextNotation('(|def| foo [vec])'); + const b = docFromTextNotation('(def| foo| [vec])'); expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds following vector including leading space', () => { - const a = docFromTextNotation('(|>|def foo|>| [vec])'); - const b = docFromTextNotation('(def foo|>| [vec]|>|)'); + const a = docFromTextNotation('(|def foo| [vec])'); + const b = docFromTextNotation('(def foo| [vec]|)'); expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Reverses direction of selection and finds next sexp', () => { const a = docFromTextNotation('(|<|def foo|<| [vec])'); - const b = docFromTextNotation('(def foo|>| [vec]|>|)'); + const b = docFromTextNotation('(def foo| [vec]|)'); expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); }); @@ -437,7 +522,7 @@ describe('paredit', () => { it('Finds previous form, including space, and reverses direction', () => { // TODO: Should we really be reversing the direction here? const a = docFromTextNotation('(def |<|foo [vec]|<|)'); - const b = docFromTextNotation('(|>|def |>|foo [vec])'); + const b = docFromTextNotation('(|def |foo [vec])'); expect(paredit.backwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Goes up when at front bounds', () => { @@ -445,24 +530,34 @@ describe('paredit', () => { const b = docFromTextNotation('(def x |(|inc 1))'); expect(paredit.backwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); + it('Multi-cursors go up when at front bounds', () => { + const a = docFromTextNotation('(|1a (|b 1))'); + const b = docFromTextNotation('|1(|1a |(|b 1))'); + expect(paredit.backwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors, one go up when at front bounds, the other back sexp', () => { + const a = docFromTextNotation('(|1a (b c|))'); + const b = docFromTextNotation('|1(|1a (b |c|))'); + expect(paredit.backwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); + }); }); describe('moveToRangeRight', () => { it('Places cursor at the right end of the selection', () => { - const a = docFromTextNotation('(def |>|foo|>| [vec])'); + const a = docFromTextNotation('(def |foo| [vec])'); const b = docFromTextNotation('(def foo| [vec])'); paredit.moveToRangeRight(a, textAndSelection(a)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Places cursor at the right end of the selection 2', () => { - const a = docFromTextNotation('(|>|def foo|>| [vec])'); + const a = docFromTextNotation('(|def foo| [vec])'); const b = docFromTextNotation('(def foo| [vec])'); paredit.moveToRangeRight(a, textAndSelection(a)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Move to right of given range, regardless of previous selection', () => { const a = docFromTextNotation('(|<|def|<| foo [vec])'); - const b = docFromTextNotation('(def foo |>|[vec]|>|)'); + const b = docFromTextNotation('(def foo |[vec]|)'); const c = docFromTextNotation('(def foo [vec]|)'); paredit.moveToRangeRight(a, textAndSelection(b)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(c)); @@ -471,20 +566,20 @@ describe('paredit', () => { describe('moveToRangeLeft', () => { it('Places cursor at the left end of the selection', () => { - const a = docFromTextNotation('(def |>|foo|>| [vec])'); + const a = docFromTextNotation('(def |foo| [vec])'); const b = docFromTextNotation('(def |foo [vec])'); paredit.moveToRangeLeft(a, textAndSelection(a)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Places cursor at the left end of the selection 2', () => { - const a = docFromTextNotation('(|>|def foo|>| [vec])'); + const a = docFromTextNotation('(|def foo| [vec])'); const b = docFromTextNotation('(|def foo [vec])'); paredit.moveToRangeLeft(a, textAndSelection(a)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Move to left of given range, regardless of previous selection', () => { const a = docFromTextNotation('(|<|def|<| foo [vec])'); - const b = docFromTextNotation('(def foo |>|[vec]|>|)'); + const b = docFromTextNotation('(def foo |[vec]|)'); const c = docFromTextNotation('(def foo |[vec])'); paredit.moveToRangeLeft(a, textAndSelection(b)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(c)); @@ -493,104 +588,104 @@ describe('paredit', () => { describe('Forward to end of list', () => { it('rangeToForwardList', () => { - const a = docFromTextNotation('(|c•(#b •[:f :b :z])•#z•1)'); - const b = docFromTextNotation('(|c•(#b •[:f :b :z])•#z•1|)'); - expect(paredit.rangeToForwardList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(|c§(#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(|c§(#b §[:f :b :z])§#z§1|)'); + expect(paredit.rangeToForwardList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardList through readers and meta', () => { - const a = docFromTextNotation('(|^e #a ^{:c d}•#b•[:f]•#z•1)'); - const b = docFromTextNotation('(|^e #a ^{:c d}•#b•[:f]•#z•1|)'); - expect(paredit.rangeToForwardList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(|^e #a ^{:c d}§#b§[:f]§#z§1)'); + const b = docFromTextNotation('(|^e #a ^{:c d}§#b§[:f]§#z§1|)'); + expect(paredit.rangeToForwardList(a)).toEqual(textAndSelection(b)[1][0]); }); }); describe('Backward to start of list', () => { it('rangeToBackwardList', () => { - const a = docFromTextNotation('(c•(#b •[:f :b :z])•#z•1|)'); - const b = docFromTextNotation('(|c•(#b •[:f :b :z])•#z•1|)'); - expect(paredit.rangeToBackwardList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(c§(#b §[:f :b :z])§#z§1|)'); + const b = docFromTextNotation('(|c§(#b §[:f :b :z])§#z§1|)'); + expect(paredit.rangeToBackwardList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToBackwardList through readers and meta', () => { - const a = docFromTextNotation('(^e #a ^{:c d}•#b•[:f]•#z•1|)'); - const b = docFromTextNotation('(|^e #a ^{:c d}•#b•[:f]•#z•1|)'); - expect(paredit.rangeToBackwardList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(^e #a ^{:c d}§#b§[:f]§#z§1|)'); + const b = docFromTextNotation('(|^e #a ^{:c d}§#b§[:f]§#z§1|)'); + expect(paredit.rangeToBackwardList(a)).toEqual(textAndSelection(b)[1][0]); }); }); describe('Down list', () => { it('rangeToForwardDownList', () => { - const a = docFromTextNotation('(|c•(#b •[:f :b :z])•#z•1)'); - const b = docFromTextNotation('(|c•(|#b •[:f :b :z])•#z•1)'); - expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(|c§(#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(|c§(|#b §[:f :b :z])§#z§1)'); + expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardDownList through readers', () => { - const a = docFromTextNotation('(|c•#f•(#b •[:f :b :z])•#z•1)'); - const b = docFromTextNotation('(|c•#f•(|#b •[:f :b :z])•#z•1)'); - expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(|c§#f§(#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(|c§#f§(|#b §[:f :b :z])§#z§1)'); + expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardDownList through metadata', () => { - const a = docFromTextNotation('(|c•^f•(#b •[:f :b]))'); - const b = docFromTextNotation('(|c•^f•(|#b •[:f :b]))'); - expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(|c§^f§(#b §[:f :b]))'); + const b = docFromTextNotation('(|c§^f§(|#b §[:f :b]))'); + expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardDownList through metadata collection', () => { - const a = docFromTextNotation('(|c•^{:f 1}•(#b •[:f :b]))'); - const b = docFromTextNotation('(|c•^{:f 1}•(|#b •[:f :b]))'); - expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(|c§^{:f 1}§(#b §[:f :b]))'); + const b = docFromTextNotation('(|c§^{:f 1}§(|#b §[:f :b]))'); + expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardDownList through metadata and readers', () => { - const a = docFromTextNotation('(|c•^:a #f•(#b •[:f :b]))'); - const b = docFromTextNotation('(|c•^:a #f•(|#b •[:f :b]))'); - expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(|c§^:a #f§(#b §[:f :b]))'); + const b = docFromTextNotation('(|c§^:a #f§(|#b §[:f :b]))'); + expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardDownList through metadata collection and reader', () => { - const a = docFromTextNotation('(|c•^{:f 1}•#a •(#b •[:f :b]))'); - const b = docFromTextNotation('(|c•^{:f 1}•#a •(|#b •[:f :b]))'); - expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(|c§^{:f 1}§#a §(#b §[:f :b]))'); + const b = docFromTextNotation('(|c§^{:f 1}§#a §(|#b §[:f :b]))'); + expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); }); describe('Backward Up list', () => { it('rangeToBackwardUpList', () => { - const a = docFromTextNotation('(c•(|#b •[:f :b :z])•#z•1)'); - const b = docFromTextNotation('(c•|(|#b •[:f :b :z])•#z•1)'); - expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(c§(|#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(c§|(|#b §[:f :b :z])§#z§1)'); + expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToBackwardUpList through readers', () => { - const a = docFromTextNotation('(c•#f•(|#b •[:f :b :z])•#z•1)'); - const b = docFromTextNotation('(c•|#f•(|#b •[:f :b :z])•#z•1)'); - expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(c§#f§(|#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(c§|#f§(|#b §[:f :b :z])§#z§1)'); + expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToBackwardUpList through metadata', () => { - const a = docFromTextNotation('(c•^f•(|#b •[:f :b]))'); - const b = docFromTextNotation('(c•|^f•(|#b •[:f :b]))'); - expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(c§^f§(|#b §[:f :b]))'); + const b = docFromTextNotation('(c§|^f§(|#b §[:f :b]))'); + expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToBackwardUpList through metadata and readers', () => { - const a = docFromTextNotation('(c•^:a #f•(|#b •[:f :b]))'); - const b = docFromTextNotation('(c•|^:a #f•(|#b •[:f :b]))'); - expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(c§^:a #f§(|#b §[:f :b]))'); + const b = docFromTextNotation('(c§|^:a #f§(|#b §[:f :b]))'); + expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToBackwardUpList 2', () => { // TODO: This is wrong! But real Paredit behaves as it should... - const a = docFromTextNotation('(a(b(c•#f•(#b •|[:f :b :z])•#z•1)))'); - const b = docFromTextNotation('(a(b|(c•#f•(#b •|[:f :b :z])•#z•1)))'); - expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(a(b(c§#f§(#b §|[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(b|(c§#f§(#b §|[:f :b :z])§#z§1)))'); + expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1][0]); }); }); }); describe('Reader tags', () => { it('dragSexprBackward', () => { - const a = docFromTextNotation('(a(b(c•#f•|(#b •[:f :b :z])•#z•1)))'); - const b = docFromTextNotation('(a(b(#f•|(#b •[:f :b :z])•c•#z•1)))'); - paredit.dragSexprBackward(a); + const a = docFromTextNotation('(a(b(c§#f§|(#b §[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(b(#f§|(#b §[:f :b :z])§c§#z§1)))'); + void paredit.dragSexprBackward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('dragSexprForward', () => { - const a = docFromTextNotation('(a(b(c•#f•|(#b •[:f :b :z])•#z•1)))'); - const b = docFromTextNotation('(a(b(c•#z•1•#f•|(#b •[:f :b :z]))))'); - paredit.dragSexprForward(a); + const a = docFromTextNotation('(a(b(c§#f§|(#b §[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(b(c§#z§1§#f§|(#b §[:f :b :z]))))'); + void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); describe('Stacked readers', () => { @@ -600,16 +695,16 @@ describe('paredit', () => { beforeEach(() => { doc = new model.StringDocument(docText); }); - it('dragSexprBackward', () => { - const a = docFromTextNotation('(c•#f•(#b •[:f :b :z])•#x•#y•|1)'); - const b = docFromTextNotation('(c•#x•#y•|1•#f•(#b •[:f :b :z]))'); - paredit.dragSexprBackward(a); + it('dragSexprBackward', async () => { + const a = docFromTextNotation('(c§#f§(#b §[:f :b :z])§#x§#y§|a)'); + const b = docFromTextNotation('(c§#x§#y§|a§#f§(#b §[:f :b :z]))'); + await paredit.dragSexprBackward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('dragSexprForward', () => { - const a = docFromTextNotation('(c•#f•|(#b •[:f :b :z])•#x•#y•1)'); - const b = docFromTextNotation('(c•#x•#y•1•#f•|(#b •[:f :b :z]))'); - paredit.dragSexprForward(a); + const a = docFromTextNotation('(c§#f§|(#b §[:f :b :z])§#x§#y§1)'); + const b = docFromTextNotation('(c§#x§#y§1§#f§|(#b §[:f :b :z]))'); + void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); @@ -621,22 +716,22 @@ describe('paredit', () => { beforeEach(() => { doc = new model.StringDocument(docText); }); - it('dragSexprBackward: #f•(#b •[:f :b :z])•#x•#y•|1•#å#ä#ö => #x•#y•1•#f•(#b •[:f :b :z])•#å#ä#ö', () => { - doc.selection = new ModelEditSelection(26, 26); - paredit.dragSexprBackward(doc); + it('dragSexprBackward: #f§(#b §[:f :b :z])§#x§#y§|a§#å#ä#ö => #x§#y§1§#f§(#b §[:f :b :z])§#å#ä#ö', () => { + doc.selections = [new ModelEditSelection(26, 26)]; + void paredit.dragSexprBackward(doc); expect(doc.model.getText(0, Infinity)).toBe('#x\n#y\n1\n#f\n(#b \n[:f :b :z])\n#å#ä#ö'); }); - it('dragSexprForward: #f•|(#b •[:f :b :z])•#x•#y•1#å#ä#ö => #x•#y•1•#f•|(#b •[:f :b :z])•#å#ä#ö', () => { - doc.selection = new ModelEditSelection(3, 3); - paredit.dragSexprForward(doc); + it('dragSexprForward: #f§|(#b §[:f :b :z])§#x§#y§1#å#ä#ö => #x§#y§1§#f§|(#b §[:f :b :z])§#å#ä#ö', () => { + doc.selections = [new ModelEditSelection(3, 3)]; + void paredit.dragSexprForward(doc); expect(doc.model.getText(0, Infinity)).toBe('#x\n#y\n1\n#f\n(#b \n[:f :b :z])\n#å#ä#ö'); - expect(doc.selection).toEqual(new ModelEditSelection(11)); + expect(doc.selections).toEqual([new ModelEditSelection(11)]); }); - it('dragSexprForward: #f•(#b •[:f :b :z])•#x•#y•|1•#å#ä#ö => #f•(#b •[:f :b :z])•#x•#y•|1•#å#ä#ö', () => { - doc.selection = new ModelEditSelection(26, 26); - paredit.dragSexprForward(doc); + it('dragSexprForward: #f§(#b §[:f :b :z])§#x§#y§|a§#å#ä#ö => #f§(#b §[:f :b :z])§#x§#y§|a§#å#ä#ö', () => { + doc.selections = [new ModelEditSelection(26, 26)]; + void paredit.dragSexprForward(doc); expect(doc.model.getText(0, Infinity)).toBe('#f\n(#b \n[:f :b :z])\n#x\n#y\n1\n#å#ä#ö'); - expect(doc.selection).toEqual(new ModelEditSelection(26)); + expect(doc.selections).toEqual([new ModelEditSelection(26)]); }); }); }); @@ -648,44 +743,50 @@ describe('paredit', () => { const a = docFromTextNotation('(def foo [:foo :bar |<|:baz|<|])'); const selDoc = docFromTextNotation('(def foo [:foo |:bar| :baz])'); const b = docFromTextNotation('(def foo [:foo |<|:bar :baz|<|])'); - paredit.selectRangeBackward(a, [selDoc.selection.anchor, selDoc.selection.active]); + paredit.selectRangeBackward( + a, + selDoc.selections.map((s) => [s.anchor, s.active]) + ); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Contracts forward selection and extends backwards', () => { - const a = docFromTextNotation('(def foo [:foo :bar |>|:baz|>|])'); + const a = docFromTextNotation('(def foo [:foo :bar |:baz|])'); const selDoc = docFromTextNotation('(def foo [:foo |:bar| :baz])'); const b = docFromTextNotation('(def foo [:foo |<|:bar |<|:baz])'); - paredit.selectRangeBackward(a, [selDoc.selection.anchor, selDoc.selection.active]); + paredit.selectRangeBackward( + a, + selDoc.selections.map((s) => [s.anchor, s.active]) + ); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); describe('selectRangeForward', () => { it('(def foo [:foo >:bar> >|:baz>|]) => (def foo [:foo >:bar :baz>])', () => { - const barSelection = new ModelEditSelection(15, 19), - bazRange = [20, 24] as [number, number], - barBazSelection = new ModelEditSelection(15, 24); - doc.selection = barSelection; + const barSelection = [new ModelEditSelection(15, 19)], + bazRange = [[20, 24] as [number, number]], + barBazSelection = [new ModelEditSelection(15, 24)]; + doc.selections = barSelection; paredit.selectRangeForward(doc, bazRange); - expect(doc.selection).toEqual(barBazSelection); + expect(doc.selections).toEqual(barBazSelection); }); it('(def foo [<:foo :bar< >|:baz>|]) => (def foo [>:foo :bar :baz>])', () => { const [fooLeft, barRight] = [10, 19], - barFooSelection = new ModelEditSelection(barRight, fooLeft), - bazRange = [20, 24] as [number, number], - fooBazSelection = new ModelEditSelection(19, 24); - doc.selection = barFooSelection; + barFooSelection = [new ModelEditSelection(barRight, fooLeft)], + bazRange = [[20, 24] as [number, number]], + fooBazSelection = [new ModelEditSelection(19, 24)]; + doc.selections = barFooSelection; paredit.selectRangeForward(doc, bazRange); - expect(doc.selection).toEqual(fooBazSelection); + expect(doc.selections).toEqual(fooBazSelection); }); it('(def foo [<:foo :bar< <|:baz<|]) => (def foo [>:foo :bar :baz>])', () => { const [fooLeft, barRight] = [10, 19], - barFooSelection = new ModelEditSelection(barRight, fooLeft), - bazRange = [24, 20] as [number, number], - fooBazSelection = new ModelEditSelection(19, 24); - doc.selection = barFooSelection; + barFooSelection = [new ModelEditSelection(barRight, fooLeft)], + bazRange = [[24, 20] as [number, number]], + fooBazSelection = [new ModelEditSelection(19, 24)]; + doc.selections = barFooSelection; paredit.selectRangeForward(doc, bazRange); - expect(doc.selection).toEqual(fooBazSelection); + expect(doc.selections).toEqual(fooBazSelection); }); }); }); @@ -693,39 +794,41 @@ describe('paredit', () => { describe('selection stack', () => { const range = [15, 20] as [number, number]; it('should make grow selection the topmost element on the stack', () => { - paredit.growSelectionStack(doc, range); - expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual( - new ModelEditSelection(range[0], range[1]) - ); + paredit.growSelectionStack(doc, [range]); + expect(last(doc.selectionsStack)).toEqual([new ModelEditSelection(range[0], range[1])]); }); it('get us back to where we started if we just grow, then shrink', () => { - const selectionBefore = startSelection.clone(); - paredit.growSelectionStack(doc, range); + const selectionBefore = startSelections.map((s) => s.clone()); + paredit.growSelectionStack(doc, [range]); paredit.shrinkSelection(doc); - expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual(selectionBefore); + expect(last(doc.selectionsStack)).toEqual(selectionBefore); }); it('should not add selections identical to the topmost', () => { - const selectionBefore = doc.selection.clone(); - paredit.growSelectionStack(doc, range); - paredit.growSelectionStack(doc, range); + const selectionBefore = doc.selections.map((s) => s.clone()); + paredit.growSelectionStack(doc, [range]); + paredit.growSelectionStack(doc, [range]); paredit.shrinkSelection(doc); - expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual(selectionBefore); + expect(last(doc.selectionsStack)).toEqual(selectionBefore); }); it('should have A topmost after adding A, then B, then shrinking', () => { const a = range, b: [number, number] = [10, 24]; - paredit.growSelectionStack(doc, a); - paredit.growSelectionStack(doc, b); + paredit.growSelectionStack(doc, [a]); + paredit.growSelectionStack(doc, [b]); paredit.shrinkSelection(doc); - expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual( - new ModelEditSelection(a[0], a[1]) - ); + expect(last(doc.selectionsStack)).toEqual([new ModelEditSelection(a[0], a[1])]); + }); + it('selects the enclosing form when all the text in a list is selected', () => { + const a = docFromTextNotation('(|a|)'); + const b = docFromTextNotation('|(a)|'); // '(a)' [[0, 3]]; + paredit.growSelection(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); describe('dragSexpr', () => { describe('forwardAndBackwardSexpr', () => { - // (comment\n ['(0 1 2 "t" "f")• "b"• {:s "h"}• :f]• [:f '(0 "t") "b" :s]• [:f 0• "b" :s• 4 :b]• {:e '(e o ea)• 3 {:w? 'w}• :t '(t i o im)• :b 'b}) + // (comment\n ['(0 1 2 "t" "f")§ "b"§ {:s "h"}§ :f]§ [:f '(0 "t") "b" :s]§ [:f 0§ "b" :s§ 4 :b]§ {:e '(e o ea)§ 3 {:w? 'w}§ :t '(t i o im)§ :b 'b}) let doc: model.StringDocument; beforeEach(() => { @@ -733,87 +836,87 @@ describe('paredit', () => { }); it('drags forward in regular lists', () => { - const a = docFromTextNotation(`(c• [:|f '(0 "t")• "b" :s]•)`); - const b = docFromTextNotation(`(c• ['(0 "t") :|f• "b" :s]•)`); - paredit.dragSexprForward(a); + const a = docFromTextNotation(`(c§ [:|f '(0 "t")§ "b" :s]§)`); + const b = docFromTextNotation(`(c§ ['(0 "t") :|f§ "b" :s]§)`); + void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('drags backward in regular lists', () => { - const a = docFromTextNotation(`(c• [:f '(0 "t")• "b"| :s]•)`); - const b = docFromTextNotation(`(c• [:f "b"|• '(0 "t") :s]•)`); - paredit.dragSexprBackward(a); + const a = docFromTextNotation(`(c§ [:f '(0 "t")§ "b"| :s]§)`); + const b = docFromTextNotation(`(c§ [:f "b"|§ '(0 "t") :s]§)`); + void paredit.dragSexprBackward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('does not drag forward when sexpr is last in regular lists', () => { - const dotText = `(c• [:f '(0 "t")• "b" |:s ]•)`; + const dotText = `(c§ [:f '(0 "t")§ "b" |:s ]§)`; const a = docFromTextNotation(dotText); const b = docFromTextNotation(dotText); - paredit.dragSexprForward(a); + void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('does not drag backward when sexpr is last in regular lists', () => { - const dotText = `(c• [ :|f '(0 "t")• "b" :s ]•)`; + const dotText = `(c§ [ :|f '(0 "t")§ "b" :s ]§)`; const a = docFromTextNotation(dotText); const b = docFromTextNotation(dotText); - paredit.dragSexprBackward(a); + void paredit.dragSexprBackward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('drags pair forward in maps', () => { const a = docFromTextNotation( - `(c• {:|e '(e o ea)• 3 {:w? 'w}• :t '(t i o im)• :b 'b}•)` + `(c§ {:|e '(e o ea)§ 3 {:w? 'w}§ :t '(t i o im)§ :b 'b}§)` ); const b = docFromTextNotation( - `(c• {3 {:w? 'w}• :|e '(e o ea)• :t '(t i o im)• :b 'b}•)` + `(c§ {3 {:w? 'w}§ :|e '(e o ea)§ :t '(t i o im)§ :b 'b}§)` ); - paredit.dragSexprForward(a); + void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('drags pair backwards in maps', () => { const a = docFromTextNotation( - `(c• {:e '(e o ea)• 3 {:w? 'w}• :t '(t i o im)|• :b 'b}•)` + `(c§ {:e '(e o ea)§ 3 {:w? 'w}§ :t '(t i o im)|§ :b 'b}§)` ); const b = docFromTextNotation( - `(c• {:e '(e o ea)• :t '(t i o im)|• 3 {:w? 'w}• :b 'b}•)` + `(c§ {:e '(e o ea)§ :t '(t i o im)|§ 3 {:w? 'w}§ :b 'b}§)` ); - paredit.dragSexprBackward(a); + void paredit.dragSexprBackward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('drags pair backwards in meta-data maps', () => { const a = docFromTextNotation( - `(c• ^{:e '(e o ea)• 3 {:w? 'w}• :t '(t i o im)|• :b 'b}•)` + `(c§ ^{:e '(e o ea)§ 3 {:w? 'w}§ :t '(t i o im)|§ :b 'b}§)` ); const b = docFromTextNotation( - `(c• ^{:e '(e o ea)• :t '(t i o im)|• 3 {:w? 'w}• :b 'b}•)` + `(c§ ^{:e '(e o ea)§ :t '(t i o im)|§ 3 {:w? 'w}§ :b 'b}§)` ); - paredit.dragSexprBackward(a); + void paredit.dragSexprBackward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('drags single sexpr forward in sets', () => { const a = docFromTextNotation( - `(c• #{:|e '(e o ea)• 3 {:w? 'w}• :t '(t i o im)• :b 'b}•)` + `(c§ #{:|e '(e o ea)§ 3 {:w? 'w}§ :t '(t i o im)§ :b 'b}§)` ); const b = docFromTextNotation( - `(c• #{'(e o ea) :|e• 3 {:w? 'w}• :t '(t i o im)• :b 'b}•)` + `(c§ #{'(e o ea) :|e§ 3 {:w? 'w}§ :t '(t i o im)§ :b 'b}§)` ); - paredit.dragSexprForward(a); + void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('drags pair in binding box', () => { const b = docFromTextNotation( - `(c• [:e '(e o ea)• 3 {:w? 'w}• :t |'(t i o im)• :b 'b]•)` + `(c§ [:e '(e o ea)§ 3 {:w? 'w}§ :t |'(t i o im)§ :b 'b]§)` ); const a = docFromTextNotation( - `(c• [:e '(e o ea)• 3 {:w? 'w}• :b 'b• :t |'(t i o im)]•)` + `(c§ [:e '(e o ea)§ 3 {:w? 'w}§ :b 'b§ :t |'(t i o im)]§)` ); - paredit.dragSexprForward(b, ['c']); + void paredit.dragSexprForward(b, ['c']); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); }); @@ -822,63 +925,63 @@ describe('paredit', () => { it('Drags up from start of vector', () => { const b = docFromTextNotation(`(def foo [:|foo :bar :baz])`); const a = docFromTextNotation(`(def foo :|foo [:bar :baz])`); - paredit.dragSexprBackwardUp(b); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags up from middle of vector', () => { const b = docFromTextNotation(`(def foo [:foo |:bar :baz])`); const a = docFromTextNotation(`(def foo |:bar [:foo :baz])`); - paredit.dragSexprBackwardUp(b); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags up from end of vector', () => { const b = docFromTextNotation(`(def foo [:foo :bar :baz|])`); const a = docFromTextNotation(`(def foo :baz| [:foo :bar])`); - paredit.dragSexprBackwardUp(b); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags up from start of list', () => { const b = docFromTextNotation(`(d|e|f foo [:foo :bar :baz])`); const a = docFromTextNotation(`de|f (foo [:foo :bar :baz])`); - paredit.dragSexprBackwardUp(b); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags up without killing preceding line comments', () => { - const b = docFromTextNotation(`(;;foo•de|f foo [:foo :bar :baz])`); - const a = docFromTextNotation(`de|f•(;;foo• foo [:foo :bar :baz])`); - paredit.dragSexprBackwardUp(b); + const b = docFromTextNotation(`(;;foo§de|f foo [:foo :bar :baz])`); + const a = docFromTextNotation(`de|f§(;;foo§ foo [:foo :bar :baz])`); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags up without killing preceding line comments or trailing parens', () => { - const b = docFromTextNotation(`(def ;; foo• |:foo)`); - const a = docFromTextNotation(`|:foo•(def ;; foo•)`); - paredit.dragSexprBackwardUp(b); + const b = docFromTextNotation(`(def ;; foo§ |:foo)`); + const a = docFromTextNotation(`|:foo§(def ;; foo§)`); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); }); describe('backwardUp - multi-line', () => { it('Drags up from indented vector', () => { - const b = docFromTextNotation(`((fn foo• [x]• [|:foo• :bar• :baz])• 1)`); - const a = docFromTextNotation(`((fn foo• [x]• |:foo• [:bar• :baz])• 1)`); - paredit.dragSexprBackwardUp(b); + const b = docFromTextNotation(`((fn foo§ [x]§ [|:foo§ :bar§ :baz])§ 1)`); + const a = docFromTextNotation(`((fn foo§ [x]§ |:foo§ [:bar§ :baz])§ 1)`); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags up from indented list', () => { - const b = docFromTextNotation(`(|(fn foo• [x]• [:foo• :bar• :baz])• 1)`); - const a = docFromTextNotation(`|(fn foo• [x]• [:foo• :bar• :baz])•(1)`); - paredit.dragSexprBackwardUp(b); + const b = docFromTextNotation(`(|(fn foo§ [x]§ [:foo§ :bar§ :baz])§ 1)`); + const a = docFromTextNotation(`|(fn foo§ [x]§ [:foo§ :bar§ :baz])§(1)`); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags up from end of indented list', () => { - const b = docFromTextNotation(`((fn foo• [x]• [:foo• :bar• :baz])• |1)`); - const a = docFromTextNotation(`|1•((fn foo• [x]• [:foo• :bar• :baz]))`); - paredit.dragSexprBackwardUp(b); + const b = docFromTextNotation(`((fn foo§ [x]§ [:foo§ :bar§ :baz])§ |a)`); + const a = docFromTextNotation(`|a§((fn foo§ [x]§ [:foo§ :bar§ :baz]))`); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags up from indented vector w/o killing preceding comment', () => { - const b = docFromTextNotation(`((fn foo• [x]• [:foo• ;; foo• :b|ar• :baz])• 1)`); - const a = docFromTextNotation(`((fn foo• [x]• :b|ar• [:foo• ;; foo•• :baz])• 1)`); - paredit.dragSexprBackwardUp(b); + const b = docFromTextNotation(`((fn foo§ [x]§ [:foo§ ;; foo§ :b|ar§ :baz])§ 1)`); + const a = docFromTextNotation(`((fn foo§ [x]§ :b|ar§ [:foo§ ;; foo§§ :baz])§ 1)`); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); }); @@ -886,19 +989,19 @@ describe('paredit', () => { it('Drags down into vector', () => { const b = docFromTextNotation(`(def f|oo [:foo :bar :baz])`); const a = docFromTextNotation(`(def [f|oo :foo :bar :baz])`); - paredit.dragSexprForwardDown(b); + void paredit.dragSexprForwardDown(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags down into vector past sexpression on the same level', () => { const b = docFromTextNotation(`(d|ef| foo [:foo :bar :baz])`); const a = docFromTextNotation(`(foo [def| :foo :bar :baz])`); - paredit.dragSexprForwardDown(b); + void paredit.dragSexprForwardDown(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags down into vector w/o killing line comments on the way', () => { - const b = docFromTextNotation(`(d|ef ;; foo• [:foo :bar :baz])`); - const a = docFromTextNotation(`(;; foo• [d|ef :foo :bar :baz])`); - paredit.dragSexprForwardDown(b); + const b = docFromTextNotation(`(d|ef ;; foo§ [:foo :bar :baz])`); + const a = docFromTextNotation(`(;; foo§ [d|ef :foo :bar :baz])`); + void paredit.dragSexprForwardDown(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); }); @@ -906,13 +1009,13 @@ describe('paredit', () => { it('Drags forward out of vector', () => { const b = docFromTextNotation(`((fn foo [x] [:foo :b|ar])) :baz`); const a = docFromTextNotation(`((fn foo [x] [:foo] :b|ar)) :baz`); - paredit.dragSexprForwardUp(b); + void paredit.dragSexprForwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags forward out of vector w/o killing line comments on the way', () => { - const b = docFromTextNotation(`((fn foo [x] [:foo :b|ar ;; bar•])) :baz`); - const a = docFromTextNotation(`((fn foo [x] [:foo ;; bar•] :b|ar)) :baz`); - paredit.dragSexprForwardUp(b); + const b = docFromTextNotation(`((fn foo [x] [:foo :b|ar ;; bar§])) :baz`); + const a = docFromTextNotation(`((fn foo [x] [:foo ;; bar§] :b|ar)) :baz`); + void paredit.dragSexprForwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); }); @@ -920,35 +1023,36 @@ describe('paredit', () => { it('Drags backward down into list', () => { const b = docFromTextNotation(`((fn foo [x] [:foo :bar])) :b|az`); const a = docFromTextNotation(`((fn foo [x] [:foo :bar]) :b|az)`); - paredit.dragSexprBackwardDown(b); + void paredit.dragSexprBackwardDown(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags backward down into list w/o killing line comments on the way', () => { - const b = docFromTextNotation(`((fn foo [x] [:foo :bar])) ;; baz•:b|az`); + const b = docFromTextNotation(`((fn foo [x] [:foo :bar])) ;; baz§:b|az`); const a = docFromTextNotation(`((fn foo [x] [:foo :bar]) :b|az) ;; baz`); - paredit.dragSexprBackwardDown(b); + void paredit.dragSexprBackwardDown(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it("Does not drag when can't drag down", () => { const b = docFromTextNotation(`((fn foo [x] [:foo :b|ar])) :baz`); const a = docFromTextNotation(`((fn foo [x] [:foo :b|ar])) :baz`); - paredit.dragSexprBackwardDown(b); + void paredit.dragSexprBackwardDown(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); }); }); + describe('edits', () => { describe('Close lists', () => { it('Advances cursor if at end of list of the same type', () => { const a = docFromTextNotation('(str "foo"|)'); const b = docFromTextNotation('(str "foo")|'); - paredit.close(a, ')'); + void paredit.close(a, ')'); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Does not enter new closing parens in balanced doc', () => { const a = docFromTextNotation('(str |"foo")'); const b = docFromTextNotation('(str |"foo")'); - paredit.close(a, ')'); + void paredit.close(a, ')'); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); xit('Enter new closing parens in unbalanced doc', () => { @@ -956,13 +1060,13 @@ describe('paredit', () => { // (The extension actually behaves correctly.) const a = docFromTextNotation('(str |"foo"'); const b = docFromTextNotation('(str )|"foo"'); - paredit.close(a, ')'); + void paredit.close(a, ')'); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Enter new closing parens in string', () => { const a = docFromTextNotation('(str "|foo"'); const b = docFromTextNotation('(str ")|foo"'); - paredit.close(a, ')'); + void paredit.close(a, ')'); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); @@ -970,7 +1074,7 @@ describe('paredit', () => { it('Closes quote at end of string', () => { const a = docFromTextNotation('(str "foo|")'); const b = docFromTextNotation('(str "foo"|)'); - paredit.stringQuote(a); + void paredit.stringQuote(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); @@ -980,68 +1084,68 @@ describe('paredit', () => { it('slurps form after list', () => { const a = docFromTextNotation('(str|) "foo"'); const b = docFromTextNotation('(str| "foo")'); - paredit.forwardSlurpSexp(a); + void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps, in multiline document', () => { - const a = docFromTextNotation('(foo• (str| ) "foo")'); - const b = docFromTextNotation('(foo• (str| "foo"))'); - paredit.forwardSlurpSexp(a); + const a = docFromTextNotation('(foo§ (str| ) "foo")'); + const b = docFromTextNotation('(foo§ (str| "foo"))'); + void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps and adds leading space', () => { const a = docFromTextNotation('(s|tr)#(foo)'); const b = docFromTextNotation('(s|tr #(foo))'); - paredit.forwardSlurpSexp(a); + void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps without adding a space', () => { const a = docFromTextNotation('(s|tr )#(foo)'); const b = docFromTextNotation('(s|tr #(foo))'); - paredit.forwardSlurpSexp(a); + void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps, trimming inside whitespace', () => { const a = docFromTextNotation('(str| )"foo"'); const b = docFromTextNotation('(str| "foo")'); - paredit.forwardSlurpSexp(a); + void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps, trimming outside whitespace', () => { const a = docFromTextNotation('(str|) "foo"'); const b = docFromTextNotation('(str| "foo")'); - paredit.forwardSlurpSexp(a); + void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps, trimming inside and outside whitespace', () => { const a = docFromTextNotation('(str| ) "foo"'); const b = docFromTextNotation('(str| "foo")'); - paredit.forwardSlurpSexp(a); + void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps form after empty list', () => { const a = docFromTextNotation('(|) "foo"'); const b = docFromTextNotation('(| "foo")'); - paredit.forwardSlurpSexp(a); + void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('leaves newlines when slurp', () => { - const a = docFromTextNotation('(fo|o•) bar'); - const b = docFromTextNotation('(fo|o• bar)'); - paredit.forwardSlurpSexp(a); + const a = docFromTextNotation('(fo|o§) bar'); + const b = docFromTextNotation('(fo|o§ bar)'); + void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps properly when closing paren is on new line', () => { // https://github.com/BetterThanTomorrow/calva/issues/1171 - const a = docFromTextNotation('(def foo• (str|• )• 42)'); - const b = docFromTextNotation('(def foo• (str|• • 42))'); - paredit.forwardSlurpSexp(a); + const a = docFromTextNotation('(def foo§ (str|§ )§ 42)'); + const b = docFromTextNotation('(def foo§ (str|§ § 42))'); + void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps form including meta and readers', () => { const a = docFromTextNotation('(|) ^{:a b} #c ^d "foo"'); const b = docFromTextNotation('(| ^{:a b} #c ^d "foo")'); - paredit.forwardSlurpSexp(a); + void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); @@ -1052,13 +1156,13 @@ describe('paredit', () => { it.skip('slurps form before string', () => { const a = docFromTextNotation('(str) "fo|o"'); const b = docFromTextNotation('"(str) fo|o"'); - paredit.backwardSlurpSexp(a); + void paredit.backwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps form before list', () => { const a = docFromTextNotation('(str) (fo|o)'); const b = docFromTextNotation('((str) fo|o)'); - paredit.backwardSlurpSexp(a); + void paredit.backwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps form before list including meta and readers', () => { @@ -1066,7 +1170,7 @@ describe('paredit', () => { // TODO: Figure out how to test result after format // (Because that last space is then removed) const b = docFromTextNotation('(^{:a b} #c ^d "foo" |)'); - paredit.backwardSlurpSexp(a); + void paredit.backwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); @@ -1077,19 +1181,19 @@ describe('paredit', () => { it('barfs last form in list', () => { const a = docFromTextNotation('(str| "foo")'); const b = docFromTextNotation('(str|) "foo"'); - paredit.forwardBarfSexp(a); + void paredit.forwardBarfSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('leaves newlines when slurp', () => { - const a = docFromTextNotation('(fo|o• bar)'); - const b = docFromTextNotation('(fo|o)• bar'); - paredit.forwardBarfSexp(a); + const a = docFromTextNotation('(fo|o§ bar)'); + const b = docFromTextNotation('(fo|o)§ bar'); + void paredit.forwardBarfSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('barfs form including meta and readers', () => { const a = docFromTextNotation('(| ^{:a b} #c ^d "foo")'); const b = docFromTextNotation('(|) ^{:a b} #c ^d "foo"'); - paredit.forwardBarfSexp(a); + void paredit.forwardBarfSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('barfs form from balanced list, when inside unclosed list', () => { @@ -1097,7 +1201,22 @@ describe('paredit', () => { // https://github.com/BetterThanTomorrow/calva/issues/1585 const a = docFromTextNotation('(let [a| a)'); const b = docFromTextNotation('(let [a|) a'); - paredit.forwardBarfSexp(a); + void paredit.forwardBarfSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Barfs symbols from vectors at multiple cursors positions', () => { + const a = docFromTextNotation('[|a§ b]§[|1a§ b]'); + const b = docFromTextNotation('[|a]§ b§[|1a]§ b'); + void paredit.forwardBarfSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Barfs symbols from nested vectors at multiple cursors positions, needing post-format', () => { + // TODO: This behaves before formatting happens + // In the ”real” editor formatting happens after the barf + // and formatting is still a single-cursor thing... + const a = docFromTextNotation('([|a§ b])§([|1a§ b])'); + const b = docFromTextNotation('([|a]§ b)§([|1a]§ b)'); + void paredit.forwardBarfSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); @@ -1106,13 +1225,13 @@ describe('paredit', () => { it('barfs first form in list', () => { const a = docFromTextNotation('((str) fo|o)'); const b = docFromTextNotation('(str) (fo|o)'); - paredit.backwardBarfSexp(a); + void paredit.backwardBarfSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('barfs first form in list including meta and readers', () => { const a = docFromTextNotation('(^{:a b} #c ^d "foo"|)'); const b = docFromTextNotation('^{:a b} #c ^d "foo"(|)'); - paredit.backwardBarfSexp(a); + void paredit.backwardBarfSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); @@ -1120,286 +1239,368 @@ describe('paredit', () => { describe('Raise', () => { it('raises the current form when cursor is preceding', () => { - const a = docFromTextNotation('(comment• (str |#(foo)))'); - const b = docFromTextNotation('(comment• |#(foo))'); - paredit.raiseSexp(a); + const a = docFromTextNotation('(comment§ (str |#(foo)))'); + const b = docFromTextNotation('(comment§ |#(foo))'); + void paredit.raiseSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('raises the current form when cursor is trailing', () => { - const a = docFromTextNotation('(comment• (str #(foo)|))'); - const b = docFromTextNotation('(comment• #(foo)|)'); - paredit.raiseSexp(a); + const a = docFromTextNotation('(comment§ (str #(foo)|))'); + const b = docFromTextNotation('(comment§ #(foo)|)'); + void paredit.raiseSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('raises the current form when with two cursors ordered left->right', () => { + const a = docFromTextNotation('(a (b|)) (a (b|1)) (a (b))'); + const b = docFromTextNotation('(a b|) (a b|1) (a (b))'); + void paredit.raiseSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('raises the current form when with two cursors ordered right->left', () => { + const a = docFromTextNotation('(a (b|1)) (a (b|)) (a (b))'); + const b = docFromTextNotation('(a b|1) (a b|) (a (b))'); // "(a b) (a b) (a (b))", [[ 10, 10], [4, 4]] + void paredit.raiseSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); describe('Kill character backwards (backspace)', () => { - it('Leaves closing paren of empty list alone', () => { - const a = docFromTextNotation('{::foo ()|• ::bar :foo}'); - const b = docFromTextNotation('{::foo (|)• ::bar :foo}'); - void paredit.backspace(a); + it('Leaves closing paren of empty list alone', async () => { + const a = docFromTextNotation('{::foo ()|§ ::bar :foo}'); + const b = docFromTextNotation('{::foo (|)§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes closing paren if unbalance', () => { - const a = docFromTextNotation('{::foo )|• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.backspace(a); + it('Deletes closing paren if unbalance', async () => { + const a = docFromTextNotation('{::foo )|§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Leaves opening paren of non-empty list alone', () => { - const a = docFromTextNotation('{::foo (|a)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |(a)• ::bar :foo}'); - void paredit.backspace(a); + it('Leaves opening paren of non-empty list alone', async () => { + const a = docFromTextNotation('{::foo (|a)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |(a)§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Leaves opening quote of non-empty string alone', () => { - const a = docFromTextNotation('{::foo "|a"• ::bar :foo}'); - const b = docFromTextNotation('{::foo |"a"• ::bar :foo}'); - void paredit.backspace(a); + it('Leaves opening quote of non-empty string alone', async () => { + const a = docFromTextNotation('{::foo "|a"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |"a"§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Leaves closing quote of non-empty string alone', () => { - const a = docFromTextNotation('{::foo "a"|• ::bar :foo}'); - const b = docFromTextNotation('{::foo "a|"• ::bar :foo}'); - void paredit.backspace(a); + it('Leaves closing quote of non-empty string alone', async () => { + const a = docFromTextNotation('{::foo "a"|§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "a|"§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes contents in strings', () => { - const a = docFromTextNotation('{::foo "a|"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|"• ::bar :foo}'); - void paredit.backspace(a); + it('Deletes contents in strings', async () => { + const a = docFromTextNotation('{::foo "a|"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|"§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes contents in strings 2', () => { - const a = docFromTextNotation('{::foo "a|a"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|a"• ::bar :foo}'); - void paredit.backspace(a); + it('Deletes contents in strings 2', async () => { + const a = docFromTextNotation('{::foo "a|a"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|a"§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes contents in strings 3', () => { - const a = docFromTextNotation('{::foo "aa|"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "a|"• ::bar :foo}'); - void paredit.backspace(a); + it('Deletes contents in strings 3', async () => { + const a = docFromTextNotation('{::foo "aa|"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "a|"§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes quoted quote', () => { - const a = docFromTextNotation('{::foo \\"|• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.backspace(a); + it('Deletes quoted quote', async () => { + const a = docFromTextNotation('{::foo \\"|§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes quoted quote in string', () => { - const a = docFromTextNotation('{::foo "\\"|"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|"• ::bar :foo}'); - void paredit.backspace(a); + it('Deletes quoted quote in string', async () => { + const a = docFromTextNotation('{::foo "\\"|"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|"§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes contents in list', () => { - const a = docFromTextNotation('{::foo (a|)• ::bar :foo}'); - const b = docFromTextNotation('{::foo (|)• ::bar :foo}'); - void paredit.backspace(a); + it('Deletes contents in list', async () => { + const a = docFromTextNotation('{::foo (a|)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo (|)§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes empty list function', () => { - const a = docFromTextNotation('{::foo (|)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.backspace(a); + it('Deletes empty list function', async () => { + const a = docFromTextNotation('{::foo (|)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes empty set', () => { + it('Deletes empty set', async () => { const a = docFromTextNotation('#{|}'); const b = docFromTextNotation('|'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes empty literal function with trailing newline', () => { + it('Deletes empty literal function with trailing newline', async () => { // https://github.com/BetterThanTomorrow/calva/issues/1079 - const a = docFromTextNotation('{::foo #(|)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.backspace(a); + const a = docFromTextNotation('{::foo #(|)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes open paren prefix characters', () => { + it('Deletes open paren prefix characters', async () => { // https://github.com/BetterThanTomorrow/calva/issues/1122 const a = docFromTextNotation('#|(foo)'); const b = docFromTextNotation('|(foo)'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes open map curly prefix/ns characters', () => { + it('Deletes open map curly prefix/ns characters', async () => { const a = docFromTextNotation('#:same|{:thing :here}'); const b = docFromTextNotation('#:sam|{:thing :here}'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes open set hash characters', () => { + it('Deletes open set hash characters', async () => { // https://github.com/BetterThanTomorrow/calva/issues/1122 const a = docFromTextNotation('#|{:thing :here}'); const b = docFromTextNotation('|{:thing :here}'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Moves cursor past entire open paren, including prefix characters', () => { + it('Moves cursor past entire open paren, including prefix characters', async () => { const a = docFromTextNotation('#(|foo)'); const b = docFromTextNotation('|#(foo)'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes unbalanced bracket', () => { + it('Deletes unbalanced bracket', async () => { // This hangs the structural editing in the real editor // https://github.com/BetterThanTomorrow/calva/issues/1573 const a = docFromTextNotation('([{|)'); const b = docFromTextNotation('([|'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); describe('Kill character forwards (delete)', () => { - it('Leaves closing paren of empty list alone', () => { - const a = docFromTextNotation('{::foo |()• ::bar :foo}'); - const b = docFromTextNotation('{::foo (|)• ::bar :foo}'); - void paredit.deleteForward(a); + it('Leaves closing paren of empty list alone', async () => { + const a = docFromTextNotation('{::foo |()§ ::bar :foo}'); + const b = docFromTextNotation('{::foo (|)§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes closing paren if unbalance', () => { - const a = docFromTextNotation('{::foo |)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.deleteForward(a); + it('Deletes closing paren if unbalance', async () => { + const a = docFromTextNotation('{::foo |)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Leaves opening paren of non-empty list alone', () => { - const a = docFromTextNotation('{::foo |(a)• ::bar :foo}'); - const b = docFromTextNotation('{::foo (|a)• ::bar :foo}'); - void paredit.deleteForward(a); + it('Leaves opening paren of non-empty list alone', async () => { + const a = docFromTextNotation('{::foo |(a)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo (|a)§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Leaves opening quote of non-empty string alone', () => { - const a = docFromTextNotation('{::foo |"a"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|a"• ::bar :foo}'); - void paredit.deleteForward(a); + it('Leaves opening quote of non-empty string alone', async () => { + const a = docFromTextNotation('{::foo |"a"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|a"§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Leaves closing quote of non-empty string alone', () => { - const a = docFromTextNotation('{::foo "a|"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "a"|• ::bar :foo}'); - void paredit.deleteForward(a); + it('Leaves closing quote of non-empty string alone', async () => { + const a = docFromTextNotation('{::foo "a|"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "a"|§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes contents in strings', () => { - const a = docFromTextNotation('{::foo "|a"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|"• ::bar :foo}'); - void paredit.deleteForward(a); + it('Deletes contents in strings', async () => { + const a = docFromTextNotation('{::foo "|a"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|"§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes contents in strings 2', () => { - const a = docFromTextNotation('{::foo "|aa"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|a"• ::bar :foo}'); - void paredit.deleteForward(a); + it('Deletes contents in strings 2', async () => { + const a = docFromTextNotation('{::foo "|aa"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|a"§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes quoted quote', () => { - const a = docFromTextNotation('{::foo |\\"• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.deleteForward(a); + it('Deletes quoted quote', async () => { + const a = docFromTextNotation('{::foo |\\"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes quoted quote in string', () => { - const a = docFromTextNotation('{::foo "|\\""• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|"• ::bar :foo}'); - void paredit.deleteForward(a); + it('Deletes quoted quote in string', async () => { + const a = docFromTextNotation('{::foo "|\\""§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|"§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes contents in list', () => { - const a = docFromTextNotation('{::foo (|a)• ::bar :foo}'); - const b = docFromTextNotation('{::foo (|)• ::bar :foo}'); - void paredit.deleteForward(a); + it('Deletes contents in list', async () => { + const a = docFromTextNotation('{::foo (|a)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo (|)§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes empty list function', () => { - const a = docFromTextNotation('{::foo (|)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.deleteForward(a); + it('Deletes empty list function', async () => { + const a = docFromTextNotation('{::foo (|)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes empty set', () => { + it('Deletes empty set', async () => { const a = docFromTextNotation('#{|}'); const b = docFromTextNotation('|'); - void paredit.deleteForward(a); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes empty literal function with trailing newline', () => { + it('Deletes empty literal function with trailing newline', async () => { // https://github.com/BetterThanTomorrow/calva/issues/1079 - const a = docFromTextNotation('{::foo #(|)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.deleteForward(a); + const a = docFromTextNotation('{::foo #(|)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); + + describe('Kill/Delete forward to End of List', () => { + it('Multi: kills last symbol in each list after cursor', async () => { + const a = docFromTextNotation('(|2a)(|1a)(|a)'); + const b = docFromTextNotation('(|2)(|1)(|)'); + await paredit.killForwardList(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + }); + + describe('Kill/Delete backward to start of List', () => { + it('Multi: kills last symbol in list after cursor', async () => { + const a = docFromTextNotation('(a|)(a|1)(a|2)'); // "(a)(a)(a)" [[2,2], [5,5], [8,8]] + const b = docFromTextNotation('(|)(|1)(|2)'); // "()()()" [[1,1], [3,3], [5,5]], + await paredit.killBackwardList(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + }); + + describe('Kill/Delete Sexp', () => { + describe('Kill/Delete Sexp Forward', () => { + it('Multi: kills/deletes sexp forwards', () => { + const a = docFromTextNotation('(|2a) (|1a) (|a) (a)'); + const b = docFromTextNotation('(|2) (|1) (|) (a)'); + void paredit.killSexpForward(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + }); + describe('Kill/Delete Sexp Backwards', () => { + it('Multi: kills/deletes sexp Backwards', async () => { + const a = docFromTextNotation('(a|2) (a|1) (a|) (a)'); + const b = docFromTextNotation('(|2) (|1) (|) (a)'); + await paredit.killSexpBackward(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + }); + }); + describe('addRichComment', () => { it('Adds Rich Comment after Top Level form', () => { - const a = docFromTextNotation('(fo|o)••(bar)'); - const b = docFromTextNotation('(foo)••(comment• |• )••(bar)'); + const a = docFromTextNotation('(fo|o)§§(bar)'); + const b = docFromTextNotation('(foo)§§(comment§ |§ )§§(bar)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Inserts Rich Comment between Top Levels', () => { - const a = docFromTextNotation('(foo)•|•(bar)'); - const b = docFromTextNotation('(foo)••(comment• |• )••(bar)'); + const a = docFromTextNotation('(foo)§|§(bar)'); + const b = docFromTextNotation('(foo)§§(comment§ |§ )§§(bar)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Inserts Rich Comment between Top Levels, before Top Level form', () => { - const a = docFromTextNotation('(foo)••|(bar)'); - const b = docFromTextNotation('(foo)••(comment• |• )••(bar)'); + const a = docFromTextNotation('(foo)§§|(bar)'); + const b = docFromTextNotation('(foo)§§(comment§ |§ )§§(bar)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Inserts Rich Comment between Top Levels, after Top Level form', () => { - const a = docFromTextNotation('(foo)|••(bar)'); - const b = docFromTextNotation('(foo)••(comment• |• )••(bar)'); + const a = docFromTextNotation('(foo)|§§(bar)'); + const b = docFromTextNotation('(foo)§§(comment§ |§ )§§(bar)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Inserts Rich Comment between Top Levels, in comment', () => { - const a = docFromTextNotation('(foo)•;foo| bar•(bar)'); - const b = docFromTextNotation('(foo)•;foo bar••(comment• |• )••(bar)'); + const a = docFromTextNotation('(foo)§;foo| bar§(bar)'); + const b = docFromTextNotation('(foo)§;foo bar§§(comment§ |§ )§§(bar)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Moves to Rich Comment below, if any', () => { - const a = docFromTextNotation('(foo|)••(comment••bar••baz)'); - const b = docFromTextNotation('(foo)••(comment••|bar••baz)'); + const a = docFromTextNotation('(foo|)§§(comment§§bar§§baz)'); + const b = docFromTextNotation('(foo)§§(comment§§|bar§§baz)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Moves to Rich Comment below, if any, looking behind line comments', () => { - const a = docFromTextNotation('(foo|)••;;line comment••(comment••bar••baz)'); - const b = docFromTextNotation('(foo)••;;line comment••(comment••|bar••baz)'); + const a = docFromTextNotation('(foo|)§§;;line comment§§(comment§§bar§§baz)'); + const b = docFromTextNotation('(foo)§§;;line comment§§(comment§§|bar§§baz)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); describe('splice sexp', () => { - it('splice empty', () => { + it('does not splice empty', () => { const a = docFromTextNotation('|'); const b = docFromTextNotation('|'); void paredit.spliceSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('splice list', () => { - const a = docFromTextNotation('(a|a b c)'); - const b = docFromTextNotation('a|a b c'); + it('splices empty list', () => { + const a = docFromTextNotation('(|)'); + const b = docFromTextNotation('|'); + void paredit.spliceSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('splices two list (multi-cursor', () => { + const a = docFromTextNotation('(|a)§(|1b)'); + // TODO: This is quite strange. + // In the VS Code editor the result is: + // '|a§b|1' + // Running this test, the result is: + // '|a§(b|1' + // (Both are wrong) + const b = docFromTextNotation('|a§|1b'); + void paredit.spliceSexp(a); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('splices multiple list', () => { + const a = docFromTextNotation('(|a)§(|1b b)§(|2c c c)§(|3d d d d)'); + const b = docFromTextNotation('|a§|1b b§|2c c c§|3d d d d'); + // TODO: Same weirdness as with the two lists above + // What I get in VS Code: + // '|a§b |1b§c c |2c§d d d |3d' + // What this test produces: + // '|a§(b|1b)§c c|2c)§(dd d|3d)' void paredit.spliceSexp(a); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('splice list also when forms have meta and readers', () => { + it('splices list also when forms have meta and readers', () => { const a = docFromTextNotation('(^{:d e} #a|a b c)'); const b = docFromTextNotation('^{:d e} #a|a b c'); void paredit.spliceSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); + it.skip('splices list with meta and readers', () => { + // TODO: Probably the meta data attached to the list should be removed + const a = docFromTextNotation('^{:d e} #a (|a b c)'); + const b = docFromTextNotation('|a b c'); + void paredit.spliceSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); it('splice vector', () => { const a = docFromTextNotation('[a| b c]'); const b = docFromTextNotation('a| b c'); @@ -1433,7 +1634,58 @@ describe('paredit', () => { it.skip('splice string', () => { const a = docFromTextNotation('"h|ello"'); void paredit.spliceSexp(a); - expect(text(a)).toEqual('hello'); + expect(getText(a)).toEqual('hello'); + }); + }); + + describe('wrap sexp', () => { + it('wraps symbol', () => { + const a = docFromTextNotation('|a'); + const b = docFromTextNotation('(|a)'); + void paredit.wrapSexpr(a, '(', ')'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps multiple symbols (multi-cursor)', () => { + const a = docFromTextNotation('|a§|1b'); + const b = docFromTextNotation('(|a)§(|1b)'); + void paredit.wrapSexpr(a, '(', ')'); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps list', () => { + const a = docFromTextNotation('(a)|'); + const b = docFromTextNotation('[(a)|]'); + void paredit.wrapSexpr(a, '[', ']'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps multiple list (multi-cursor)', () => { + const a = docFromTextNotation('(a)|§(b)|1'); + const b = docFromTextNotation('[(a)|]§[(b)|1]'); + void paredit.wrapSexpr(a, '[', ']'); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps selection', () => { + // TODO: See if we can maintain the selection here + const a = docFromTextNotation('|a|'); + const b = docFromTextNotation('(|a)'); + void paredit.wrapSexpr(a, '(', ')'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps selection even at newlines', () => { + // TODO: See if we can maintain the selection here + const a = docFromTextNotation('a§|b|'); + const b = docFromTextNotation('a§(|b)'); + void paredit.wrapSexpr(a, '(', ')'); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps multiple selections (multi-cursor)', async () => { + const a = docFromTextNotation('|a|§|1b|1'); + const b = docFromTextNotation('(|a)§(|1b)'); + await paredit.wrapSexpr(a, '(', ')'); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); }); diff --git a/src/extension-test/unit/cursor-doc/token-cursor-test.ts b/src/extension-test/unit/cursor-doc/token-cursor-test.ts index 5d9006cbc..b7a2750a9 100644 --- a/src/extension-test/unit/cursor-doc/token-cursor-test.ts +++ b/src/extension-test/unit/cursor-doc/token-cursor-test.ts @@ -5,214 +5,214 @@ import { docFromTextNotation, textAndSelection } from '../common/text-notation'; describe('Token Cursor', () => { describe('backwardWhitespace', () => { it('it moves past whitespace', () => { - const a = docFromTextNotation('a •|c'); - const b = docFromTextNotation('a| •c'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('a §|c'); + const b = docFromTextNotation('a| §c'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardWhitespace(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('it moves past whitespace from inside symbol', () => { - const a = docFromTextNotation('a •c|c'); - const b = docFromTextNotation('a| •cc'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('a §c|c'); + const b = docFromTextNotation('a| §cc'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardWhitespace(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); }); describe('forwardSexp', () => { it('moves from beginning to end of symbol', () => { - const a = docFromTextNotation('(|c•#f)'); - const b = docFromTextNotation('(c|•#f)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(|c§#f)'); + const b = docFromTextNotation('(c|§#f)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('moves from beginning to end of nested list ', () => { - const a = docFromTextNotation('|(a(b(c•#f•(#b •[:f])•#z•1)))'); - const b = docFromTextNotation('(a(b(c•#f•(#b •[:f])•#z•1)))|'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('|(a(b(c§#f§(#b §[:f])§#z§1)))'); + const b = docFromTextNotation('(a(b(c§#f§(#b §[:f])§#z§1)))|'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Includes reader tag as part of a list form', () => { - const a = docFromTextNotation('(c|•#f•(#b •[:f :b :z])•#z•1)'); - const b = docFromTextNotation('(c•#f•(#b •[:f :b :z])|•#z•1)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(c|§#f§(#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(c§#f§(#b §[:f :b :z])|§#z§1)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Includes reader tag as part of a symbol', () => { - const a = docFromTextNotation('(c•#f•(#b •[:f :b :z])|•#z•1)'); - const b = docFromTextNotation('(c•#f•(#b •[:f :b :z])•#z•1|)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(c§#f§(#b §[:f :b :z])|§#z§1)'); + const b = docFromTextNotation('(c§#f§(#b §[:f :b :z])§#z§1|)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not move out of a list', () => { - const a = docFromTextNotation('(c•#f•(#b •[:f :b :z])•#z•1|)'); - const b = docFromTextNotation('(c•#f•(#b •[:f :b :z])•#z•1|)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(c§#f§(#b §[:f :b :z])§#z§1|)'); + const b = docFromTextNotation('(c§#f§(#b §[:f :b :z])§#z§1|)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skip metadata if skipMetadata is true', () => { const a = docFromTextNotation('(a |^{:a 1} (= 1 1))'); const b = docFromTextNotation('(a ^{:a 1} (= 1 1)|)'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skip metadata and reader if skipMetadata is true', () => { const a = docFromTextNotation('(a |^{:a 1} #a (= 1 1))'); const b = docFromTextNotation('(a ^{:a 1} #a (= 1 1)|)'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skip reader and metadata if skipMetadata is true', () => { const a = docFromTextNotation('(a |#a ^{:a 1} (= 1 1))'); const b = docFromTextNotation('(a #a ^{:a 1} (= 1 1)|)'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skips multiple metadata maps if skipMetadata is true', () => { const a = docFromTextNotation('(a |^{:a 1} ^{:b 2} (= 1 1))'); const b = docFromTextNotation('(a ^{:a 1} ^{:b 2} (= 1 1)|)'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skips symbol shorthand for metadata if skipMetadata is true', () => { const a = docFromTextNotation('(a| ^String (= 1 1))'); const b = docFromTextNotation('(a ^String (= 1 1)|)'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skips keyword shorthand for metadata if skipMetadata is true', () => { const a = docFromTextNotation('(a| ^:hello (= 1 1))'); const b = docFromTextNotation('(a ^:hello (= 1 1)|)'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skips multiple keyword shorthands for metadata if skipMetadata is true', () => { const a = docFromTextNotation('(a| ^:hello ^:world (= 1 1))'); const b = docFromTextNotation('(a ^:hello ^:world (= 1 1)|)'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not skip ignored forms if skipIgnoredForms is false', () => { const a = docFromTextNotation('(a| #_1 #_2 3)'); - const b = docFromTextNotation('(a #_|1 #_2 3)'); - const cursor = a.getTokenCursor(a.selection.anchor); + const b = docFromTextNotation('(a #_|a #_2 3)'); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skip ignored forms if skipIgnoredForms is true', () => { const a = docFromTextNotation('(a| #_1 #_2 3)'); const b = docFromTextNotation('(a #_1 #_2 3|)'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(true, true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('should skip stacked ignored forms if skipIgnoredForms is true', () => { const a = docFromTextNotation('(a| #_ #_ 1 2 3)'); const b = docFromTextNotation('(a #_ #_ 1 2 3|)'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(true, true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it.skip('Does not move past unbalanced top level form', () => { //TODO: Figure out why this doesn't work //TODO: Figure out why this breaks some tests run after this one const d = docFromTextNotation('|(foo "bar"'); - const cursor: LispTokenCursor = d.getTokenCursor(d.selection.anchor); + const cursor: LispTokenCursor = d.getTokenCursor(d.selections[0].anchor); cursor.forwardSexp(); - expect(cursor.offsetStart).toBe(d.selection.anchor); + expect(cursor.offsetStart).toBe(d.selections[0].anchor); }); }); describe('backwardSexp', () => { it('moves from end to beginning of symbol', () => { - const a = docFromTextNotation('(a(b(c|•#f•(#b •[:f :b :z])•#z•1)))'); - const b = docFromTextNotation('(a(b(|c•#f•(#b •[:f :b :z])•#z•1)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(a(b(c|§#f§(#b §[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(b(|c§#f§(#b §[:f :b :z])§#z§1)))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('moves from end to beginning of nested list ', () => { - const a = docFromTextNotation('(a(b(c•#f•(#b •[:f :b :z])•#z•1)))|'); - const b = docFromTextNotation('|(a(b(c•#f•(#b •[:f :b :z])•#z•1)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])§#z§1)))|'); + const b = docFromTextNotation('|(a(b(c§#f§(#b §[:f :b :z])§#z§1)))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Includes reader tag as part of a list form', () => { - const a = docFromTextNotation('(a(b(c•#f•(#b •[:f :b :z])|•#z•1)))'); - const b = docFromTextNotation('(a(b(c•|#f•(#b •[:f :b :z])•#z•1)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])|§#z§1)))'); + const b = docFromTextNotation('(a(b(c§|#f§(#b §[:f :b :z])§#z§1)))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Includes reader tag as part of a symbol', () => { - const a = docFromTextNotation('(a(b(c•#f•(#b •[:f :b :z])•#z•1|)))'); - const b = docFromTextNotation('(a(b(c•#f•(#b •[:f :b :z])•|#z•1)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])§#z§1|)))'); + const b = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])§|#z§1)))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not move out of a list', () => { - const a = docFromTextNotation('(a(|b(c•#f•(#b •[:f :b :z])•#z•1)))'); - const b = docFromTextNotation('(a(|b(c•#f•(#b •[:f :b :z])•#z•1)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(a(|b(c§#f§(#b §[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(|b(c§#f§(#b §[:f :b :z])§#z§1)))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skip metadata if skipMetadata is true', () => { const a = docFromTextNotation('(a ^{:a 1} (= 1 1)|)'); const b = docFromTextNotation('(a |^{:a 1} (= 1 1))'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Treats metadata as part of the sexp if skipMetadata is true', () => { const a = docFromTextNotation('(a ^{:a 1}| (= 1 1))'); const b = docFromTextNotation('(a |^{:a 1} (= 1 1))'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skips multiple metadata maps if skipMetadata is true', () => { const a = docFromTextNotation('(a ^{:a 1} ^{:b 2} (= 1 1)|)'); const b = docFromTextNotation('(a |^{:a 1} ^{:b 2} (= 1 1))'); - const cursor = a.getTokenCursor(a.selection.anchor); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Treats metadata and readers as part of the sexp if skipMetadata is true', () => { - const a = docFromTextNotation('#bar •^baz•|[:a :b :c]•x'); - const b = docFromTextNotation('|#bar •^baz•[:a :b :c]•x'); - const cursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('#bar §^baz§|[:a :b :c]§x'); + const b = docFromTextNotation('|#bar §^baz§[:a :b :c]§x'); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Treats reader and metadata as part of the sexp if skipMetadata is true', () => { - const a = docFromTextNotation('^bar •#baz•|[:a :b :c]•x'); - const b = docFromTextNotation('|^bar •#baz•[:a :b :c]•x'); - const cursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('^bar §#baz§|[:a :b :c]§x'); + const b = docFromTextNotation('|^bar §#baz§[:a :b :c]§x'); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Treats readers and metadata:s mixed as part of the sexp from behind the sexp if skipMetadata is true', () => { - const a = docFromTextNotation('^d #c ^b •#a•[:a :b :c]|•x'); - const b = docFromTextNotation('|^d #c ^b •#a•[:a :b :c]•x'); - const cursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('^d #c ^b §#a§[:a :b :c]|§x'); + const b = docFromTextNotation('|^d #c ^b §#a§[:a :b :c]§x'); + const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(true, true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); }); @@ -220,44 +220,44 @@ describe('Token Cursor', () => { it('Puts cursor to the right of the following open paren', () => { const a = docFromTextNotation('(a |(b 1))'); const b = docFromTextNotation('(a (|b 1))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.downList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Puts cursor to the right of the following open curly brace:', () => { const a = docFromTextNotation('(a |{:b 1}))'); const b = docFromTextNotation('(a {|:b 1}))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.downList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Puts cursor to the right of the following open bracket', () => { const a = docFromTextNotation('(a| [1 2]))'); - const b = docFromTextNotation('(a [|1 2]))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const b = docFromTextNotation('(a [|a 2]))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.downList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it(`Puts cursor to the right of the following opening quoted list`, () => { const a = docFromTextNotation(`(a| '(b 1))`); const b = docFromTextNotation(`(a '(|b 1))`); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.downList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skips whitespace', () => { - const a = docFromTextNotation('(a|• (b 1))'); - const b = docFromTextNotation('(a• (|b 1))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(a|§ (b 1))'); + const b = docFromTextNotation('(a§ (|b 1))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.downList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not skip metadata', () => { const a = docFromTextNotation('(a| ^{:x 1} (b 1))'); const b = docFromTextNotation('(a ^{|:x 1} (b 1))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.downList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); }); @@ -265,220 +265,220 @@ describe('Token Cursor', () => { it('Moves down, skipping metadata', () => { const a = docFromTextNotation('(|a #b ^{:x 1} (c 1))'); const b = docFromTextNotation('(a #b ^{:x 1} (|c 1))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.downListSkippingMeta(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Moves down when there is no metadata', () => { const a = docFromTextNotation('(|a #b (c 1))'); const b = docFromTextNotation('(a #b (|c 1))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.downListSkippingMeta(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); }); it('upList', () => { - const a = docFromTextNotation('(a(b(c•#f•(#b •[:f :b :z])•#z•1|)))'); - const b = docFromTextNotation('(a(b(c•#f•(#b •[:f :b :z])•#z•1)|))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])§#z§1|)))'); + const b = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])§#z§1)|))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.upList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); describe('forwardList', () => { it('Finds end of list', () => { const a = docFromTextNotation('(|foo (bar baz) [])'); const b = docFromTextNotation('(foo (bar baz) []|)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Finds end of list through readers and meta', () => { const a = docFromTextNotation('(|#a ^{:b c} #d (bar baz) [])'); const b = docFromTextNotation('(#a ^{:b c} #d (bar baz) []|)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not move at top level', () => { const a = docFromTextNotation('|foo (bar baz)'); const b = docFromTextNotation('|foo (bar baz)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not move at top level when unbalanced document from extra closings', () => { const a = docFromTextNotation('|foo (bar baz))'); const b = docFromTextNotation('|foo (bar baz))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not move at top level when unbalanced document from extra opens', () => { const a = docFromTextNotation('|foo ((bar baz)'); const b = docFromTextNotation('|foo ((bar baz)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not move when unbalanced from extra opens', () => { const a = docFromTextNotation('(|['); const b = docFromTextNotation('(|['); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not move when at end of list, returns true', () => { const a = docFromTextNotation('(|)'); const b = docFromTextNotation('(|)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.forwardList(); expect(result).toBe(true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Finds the list end when unbalanced from extra closes outside the current list', () => { const a = docFromTextNotation('(|a #b []))'); const b = docFromTextNotation('(a #b []|))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); }); describe('backwardList', () => { it('Finds start of list', () => { - const a = docFromTextNotation('(((c•(#b •[:f])•#z•|1)))'); - const b = docFromTextNotation('(((|c•(#b •[:f])•#z•1)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(((c§(#b §[:f])§#z§|a)))'); + const b = docFromTextNotation('(((|c§(#b §[:f])§#z§1)))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Finds start of list through readers', () => { - const a = docFromTextNotation('(((c•#a• #f•(#b •[:f])•#z•|1)))'); - const b = docFromTextNotation('(((|c•#a• #f•(#b •[:f])•#z•1)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(((c§#a§ #f§(#b §[:f])§#z§|a)))'); + const b = docFromTextNotation('(((|c§#a§ #f§(#b §[:f])§#z§1)))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Finds start of list through metadata', () => { - const a = docFromTextNotation('(((c•^{:a c} (#b •[:f])•#z•|1)))'); - const b = docFromTextNotation('(((|c•^{:a c} (#b •[:f])•#z•1)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(((c§^{:a c} (#b §[:f])§#z§|a)))'); + const b = docFromTextNotation('(((|c§^{:a c} (#b §[:f])§#z§1)))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not move at top level', () => { const a = docFromTextNotation('foo |(bar baz)'); const b = docFromTextNotation('foo |(bar baz)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not move when at start of unbalanced list', () => { // https://github.com/BetterThanTomorrow/calva/issues/1573 // https://github.com/BetterThanTomorrow/calva/commit/d77359fcea16bc052ab829853d5711434330a375 const a = docFromTextNotation('([|'); const b = docFromTextNotation('([|'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.backwardList(); expect(result).toBe(false); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Does not move to start of an unbalanced list when outer list is also unbalanced', () => { // NB: This is a bit arbitrary, this test documents the current behaviour const a = docFromTextNotation('(let [a| a'); const b = docFromTextNotation('(let [a| a'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.backwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(false); }); it('Moves to start of an unbalanced list when outer list is balanced', () => { // NB: This is a bit arbitrary, this test documents the current behaviour const a = docFromTextNotation('(let [a| a)'); const b = docFromTextNotation('(let [|a a)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.backwardList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(true); }); it('Finds the list start when unbalanced from extra closes outside the current list', () => { const a = docFromTextNotation('([]|))'); const b = docFromTextNotation('(|[]))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.backwardList(); expect(result).toBe(true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); }); describe('forwardListOfType', () => { it('Finds end of list', () => { - const a = docFromTextNotation('([#{|c•(#b •[:f])•#z•1}])'); - const b = docFromTextNotation('([#{c•(#b •[:f])•#z•1}]|)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('([#{|c§(#b §[:f])§#z§1}])'); + const b = docFromTextNotation('([#{c§(#b §[:f])§#z§1}]|)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.forwardListOfType(')'); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(true); }); it('Finds end of vector', () => { - const a = docFromTextNotation('([(c•(#b| •[:f])•#z•1)])'); - const b = docFromTextNotation('([(c•(#b •[:f])•#z•1)|])'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('([(c§(#b| §[:f])§#z§1)])'); + const b = docFromTextNotation('([(c§(#b §[:f])§#z§1)|])'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.forwardListOfType(']'); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(true); }); it('Finds end of map', () => { - const a = docFromTextNotation('({:a [(c•(#|b •[:f])•#z•|1)]})'); - const b = docFromTextNotation('({:a [(c•(#b •[:f])•#z•1)]|})'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('({:a [(c§(#|b §[:f])§#z§|a)]})'); + const b = docFromTextNotation('({:a [(c§(#b §[:f])§#z§1)]|})'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.forwardListOfType('}'); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(true); }); it('Does not move when list is unbalanced from missing open', () => { const a = docFromTextNotation('|])'); const b = docFromTextNotation('|])'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.forwardListOfType(')'); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(false); }); it('Does not move when list type is not found', () => { const a = docFromTextNotation('([|])'); const b = docFromTextNotation('([|])'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.forwardListOfType('}'); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(false); }); }); describe('backwardListOfType', () => { it('Finds start of list', () => { - const a = docFromTextNotation('([#{c•(#b •[:f])•#z•|1}])'); - const b = docFromTextNotation('(|[#{c•(#b •[:f])•#z•1}])'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('([#{c§(#b §[:f])§#z§|a}])'); + const b = docFromTextNotation('(|[#{c§(#b §[:f])§#z§1}])'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.backwardListOfType('('); expect(result).toBe(true); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Finds start of vector', () => { - const a = docFromTextNotation('([(c•(#b •[:f])•#z•|1)])'); - const b = docFromTextNotation('([|(c•(#b •[:f])•#z•1)])'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('([(c§(#b §[:f])§#z§|a)])'); + const b = docFromTextNotation('([|(c§(#b §[:f])§#z§1)])'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.backwardListOfType('['); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(true); }); it('Finds start of map', () => { - const a = docFromTextNotation('({:a [(c•(#b •[:f])•#z•|1)]})'); - const b = docFromTextNotation('({|:a [(c•(#b •[:f])•#z•1)]})'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('({:a [(c§(#b §[:f])§#z§|a)]})'); + const b = docFromTextNotation('({|:a [(c§(#b §[:f])§#z§1)]})'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.backwardListOfType('{'); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(true); }); it('Does not move when list type is unbalanced from missing close', () => { @@ -486,18 +486,18 @@ describe('Token Cursor', () => { // https://github.com/BetterThanTomorrow/calva/issues/1573 const a = docFromTextNotation('([|'); const b = docFromTextNotation('([|'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.backwardListOfType('('); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(false); }); it('Moves backward in unbalanced list when outer list is balanced', () => { // https://github.com/BetterThanTomorrow/calva/issues/1585 const a = docFromTextNotation('(let [a|)'); const b = docFromTextNotation('(let [|a)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.backwardListOfType('['); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(true); }); it('Moves backward in balanced list when inner list is unbalanced', () => { @@ -505,27 +505,27 @@ describe('Token Cursor', () => { // https://github.com/BetterThanTomorrow/calva/issues/1585 const a = docFromTextNotation('(let [a|)'); const b = docFromTextNotation('(|let [a)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.backwardListOfType('('); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(true); }); it('Does not move when list type is not found', () => { const a = docFromTextNotation('([|])'); const b = docFromTextNotation('([|])'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.backwardListOfType('{'); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(false); }); }); it('backwardUpList', () => { - const a = docFromTextNotation('(a(b(c•#f•(#b •|[:f :b :z])•#z•1)))'); - const b = docFromTextNotation('(a(b(c•#f•|(#b •[:f :b :z])•#z•1)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('(a(b(c§#f§(#b §|[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(b(c§#f§|(#b §[:f :b :z])§#z§1)))'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardUpList(); - expect(cursor.offsetStart).toBe(b.selection.anchor); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); // TODO: Figure out why adding these tests make other test break! @@ -533,331 +533,335 @@ describe('Token Cursor', () => { it('backwardList moves to start of string', () => { const a = docFromTextNotation('(str [] "", "foo" "f | b b" " f b b " "\\"" \\")'); const b = docFromTextNotation('(str [] "", "foo" "|f b b" " f b b " "\\"" \\")'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardList(); - expect(cursor.offsetStart).toEqual(b.selection.anchor); + expect(cursor.offsetStart).toEqual(b.selections[0].anchor); }); it('forwardList moves to end of string', () => { const a = docFromTextNotation('(str [] "", "foo" "f | b b" " f b b " "\\"" \\")'); const b = docFromTextNotation('(str [] "", "foo" "f b b|" " f b b " "\\"" \\")'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardList(); - expect(cursor.offsetStart).toEqual(b.selection.anchor); + expect(cursor.offsetStart).toEqual(b.selections[0].anchor); }); it('backwardSexpr inside string moves past quoted characters', () => { const a = docFromTextNotation('(str [] "foo \\"| bar")'); const b = docFromTextNotation('(str [] "foo |\\" bar")'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); - expect(cursor.offsetStart).toEqual(b.selection.anchor); + expect(cursor.offsetStart).toEqual(b.selections[0].anchor); }); }); describe('The REPL prompt', () => { it('Backward sexp bypasses prompt', () => { - const a = docFromTextNotation('foo•clj꞉foo꞉> |'); - const b = docFromTextNotation('|foo•clj꞉foo꞉> '); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('foo§clj꞉foo꞉> |'); + const b = docFromTextNotation('|foo§clj꞉foo꞉> '); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); - expect(cursor.offsetStart).toEqual(b.selection.active); + expect(cursor.offsetStart).toEqual(b.selections[0].active); }); it('Backward sexp not skipping comments bypasses prompt finding its start', () => { - const a = docFromTextNotation('foo•clj꞉foo꞉> |'); - const b = docFromTextNotation('foo•|clj꞉foo꞉> '); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const a = docFromTextNotation('foo§clj꞉foo꞉> |'); + const b = docFromTextNotation('foo§|clj꞉foo꞉> '); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(false); - expect(cursor.offsetStart).toEqual(b.selection.active); + expect(cursor.offsetStart).toEqual(b.selections[0].active); }); }); describe('Current Form', () => { it('0: selects from within non-list form', () => { - const a = docFromTextNotation('(a|aa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x'); - const b = docFromTextNotation('(|aaa| (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(a|aa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x'); + const b = docFromTextNotation('(|aaa| (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('0: selects from within non-list form including reader tag', () => { const a = docFromTextNotation('(#a a|aa (foo bar)))'); const b = docFromTextNotation('(|#a aaa| (foo bar)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('0: selects from within non-list form including multiple reader tags', () => { const a = docFromTextNotation('(#aa #a #b a|aa (foo bar)))'); const b = docFromTextNotation('(|#aa #a #b aaa| (foo bar)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('0: selects from within non-list form including metadata', () => { const a = docFromTextNotation('(^aa #a a|aa (foo bar)))'); const b = docFromTextNotation('(|^aa #a aaa| (foo bar)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('0: selects from within non-list form including readers and metadata', () => { const a = docFromTextNotation('(^aa #a a|aa (foo bar))'); const b = docFromTextNotation('(|^aa #a aaa| (foo bar))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('0: selects from within non-list form including metadata and readers', () => { const a = docFromTextNotation('(#a ^aa a|aa (foo bar))'); const b = docFromTextNotation('(|#a ^aa aaa| (foo bar))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('1: selects from adjacent when after form', () => { - const a = docFromTextNotation('(aaa •x•#(a b c)|)•#baz•yyy•)'); - const b = docFromTextNotation('(aaa •x•|#(a b c)|)•#baz•yyy•)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(aaa §x§#(a b c)|)§#baz§yyy§)'); + const b = docFromTextNotation('(aaa §x§|#(a b c)|)§#baz§yyy§)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('1: selects from adjacent when after form, including reader tags', () => { - const a = docFromTextNotation('(x• #a #b •#(a b c)|)•#baz•yyy•)'); - const b = docFromTextNotation('(x• |#a #b •#(a b c)|)•#baz•yyy•)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(x§ #a #b §#(a b c)|)§#baz§yyy§)'); + const b = docFromTextNotation('(x§ |#a #b §#(a b c)|)§#baz§yyy§)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('1: selects from adjacent when after form, including readers and meta data', () => { - const a = docFromTextNotation('(x• ^a #b •#(a b c)|)•#baz•yyy•)'); - const b = docFromTextNotation('(x• |^a #b •#(a b c)|)•#baz•yyy•)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(x§ ^a #b §#(a b c)|)§#baz§yyy§)'); + const b = docFromTextNotation('(x§ |^a #b §#(a b c)|)§#baz§yyy§)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('1: selects from adjacent when after form, including meta data and readers', () => { - const a = docFromTextNotation('(x• #a ^b •#(a b c)|)•#baz•yyy•)'); - const b = docFromTextNotation('(x• |#a ^b •#(a b c)|)•#baz•yyy•)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(x§ #a ^b §#(a b c)|)§#baz§yyy§)'); + const b = docFromTextNotation('(x§ |#a ^b §#(a b c)|)§#baz§yyy§)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form', () => { - const a = docFromTextNotation('#bar •#baz•[:a :b :c]•x•|#(a b c)'); - const b = docFromTextNotation('#bar •#baz•[:a :b :c]•x•|#(a b c)|'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('#bar §#baz§[:a :b :c]§x§|#(a b c)'); + const b = docFromTextNotation('#bar §#baz§[:a :b :c]§x§|#(a b c)|'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, including reader tags', () => { - const a = docFromTextNotation('|#bar •#baz•[:a :b :c]•x'); - const b = docFromTextNotation('|#bar •#baz•[:a :b :c]|•x'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('|#bar §#baz§[:a :b :c]§x'); + const b = docFromTextNotation('|#bar §#baz§[:a :b :c]|§x'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, including meta data', () => { - const a = docFromTextNotation('|^bar •[:a :b :c]•x'); - const b = docFromTextNotation('|^bar •[:a :b :c]|•x'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('|^bar §[:a :b :c]§x'); + const b = docFromTextNotation('|^bar §[:a :b :c]|§x'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, including meta data and reader', () => { - const a = docFromTextNotation('|^bar •#baz•[:a :b :c]•x'); - const b = docFromTextNotation('|^bar •#baz•[:a :b :c]|•x'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('|^bar §#baz§[:a :b :c]§x'); + const b = docFromTextNotation('|^bar §#baz§[:a :b :c]|§x'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, including preceding reader and meta data', () => { - const a = docFromTextNotation('^bar •#baz•|[:a :b :c]•x'); - const b = docFromTextNotation('|^bar •#baz•[:a :b :c]|•x'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('^bar §#baz§|[:a :b :c]§x'); + const b = docFromTextNotation('|^bar §#baz§[:a :b :c]|§x'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, including preceding meta data and reader', () => { - const a = docFromTextNotation('#bar •^baz•|[:a :b :c]•x'); - const b = docFromTextNotation('|#bar •^baz•[:a :b :c]|•x'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('#bar §^baz§|[:a :b :c]§x'); + const b = docFromTextNotation('|#bar §^baz§[:a :b :c]|§x'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, or in readers', () => { - const a = docFromTextNotation('ccc •#foo•|•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy'); - const b = docFromTextNotation('ccc •|#foo••(#bar •#baz•[:a :b :c]•x•#(a b c))|•#baz•yyy'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('ccc §#foo§|§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy'); + const b = docFromTextNotation('ccc §|#foo§§(#bar §#baz§[:a :b :c]§x§#(a b c))|§#baz§yyy'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before a form with reader tags', () => { - const a = docFromTextNotation('#bar |•#baz•[:a :b :c]•x'); - const b = docFromTextNotation('|#bar •#baz•[:a :b :c]|•x'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('#bar |§#baz§[:a :b :c]§x'); + const b = docFromTextNotation('|#bar §#baz§[:a :b :c]|§x'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('3: selects previous form, if on the same line', () => { - const a = docFromTextNotation('z z | •foo• • bar'); - const b = docFromTextNotation('z |z| •foo• • bar'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('z z | §foo§ § bar'); + const b = docFromTextNotation('z |z| §foo§ § bar'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('4: selects next form, if on the same line', () => { - const a = docFromTextNotation('yyy•| z z z •foo• • bar'); - const b = docFromTextNotation('yyy• |z| z z •foo• • bar'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('yyy§| z z z §foo§ § bar'); + const b = docFromTextNotation('yyy§ |z| z z §foo§ § bar'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('5: selects previous form, if any', () => { - const a = docFromTextNotation('yyy• z z z •foo• |• bar'); - const b = docFromTextNotation('yyy• z z z •|foo|• • bar'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('yyy§ z z z §foo§ |§ bar'); + const b = docFromTextNotation('yyy§ z z z §|foo|§ § bar'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('5: selects previous form, if any, when next form has metadata', () => { - const a = docFromTextNotation('z•foo•|•^{:a b}•bar'); - const b = docFromTextNotation('z•|foo|••^{:a b}•bar'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('z§foo§|§^{:a b}§bar'); + const b = docFromTextNotation('z§|foo|§§^{:a b}§bar'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('6: selects next form, if any', () => { - const a = docFromTextNotation(' | • (foo {:a b})•(c)'); - const b = docFromTextNotation(' • |(foo {:a b})|•(c)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation(' | § (foo {:a b})§(c)'); + const b = docFromTextNotation(' § |(foo {:a b})|§(c)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('7: selects enclosing form, if any', () => { - const a = docFromTextNotation('(|) • (foo {:a b})•(c)'); - const b = docFromTextNotation('|()| • (foo {:a b})•(c)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('(|) § (foo {:a b})§(c)'); + const b = docFromTextNotation('|()| § (foo {:a b})§(c)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects anonymous function when cursor is before #', () => { const a = docFromTextNotation('(map |#(println %) [1 2])'); const b = docFromTextNotation('(map |#(println %)| [1 2])'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects anonymous function when cursor is after # and before (', () => { const a = docFromTextNotation('(map #|(println %) [1 2])'); const b = docFromTextNotation('(map |#(println %)| [1 2])'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('8: does not croak on unbalance', () => { // This hangs the structural editing in the real editor // https://github.com/BetterThanTomorrow/calva/issues/1573 const a = docFromTextNotation('([|'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); - expect(cursor.rangeForCurrentForm(a.selection.anchor)).toBeUndefined(); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); + expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toBeUndefined(); }); }); describe('Top Level Form', () => { it('Finds range when nested down a some forms', () => { const a = docFromTextNotation( - 'aaa (bbb (ccc •#foo•(#bar •#baz•[:a :b| :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) (ddd eee)' + 'aaa (bbb (ccc §#foo§(#bar §#baz§[:a :b| :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) (ddd eee)' ); const b = docFromTextNotation( - 'aaa |(bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar))| (ddd eee)' + 'aaa |(bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar))| (ddd eee)' ); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Finds range when in current form is top level', () => { const a = docFromTextNotation( - 'aaa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) |(ddd eee)' + 'aaa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) |(ddd eee)' ); const b = docFromTextNotation( - 'aaa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) |(ddd eee)|' + 'aaa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) |(ddd eee)|' ); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Finds range when in ”solid” top level form', () => { const a = docFromTextNotation( - 'a|aa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) (ddd eee)' + 'a|aa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) (ddd eee)' ); const b = docFromTextNotation( - '|aaa| (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) (ddd eee)' + '|aaa| (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) (ddd eee)' ); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Finds range for a top level form inside a comment', () => { const a = docFromTextNotation('aaa (comment (comment [bbb cc|c] ddd))'); const b = docFromTextNotation('aaa (comment (comment |[bbb ccc]| ddd))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Finds top level comment range if comment special treatment is disabled', () => { const a = docFromTextNotation( - 'aaa (comment (ccc •#foo•(#bar •#baz•[:a :b| :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) (ddd eee)' + 'aaa (comment (ccc §#foo§(#bar §#baz§[:a :b| :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) (ddd eee)' ); const b = docFromTextNotation( - 'aaa |(comment (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar))| (ddd eee)' + 'aaa |(comment (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar))| (ddd eee)' + ); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active, false)).toEqual( + textAndSelection(b)[1][0] ); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active, false)).toEqual(textAndSelection(b)[1]); }); it('Finds comment range for empty comment form', () => { // Unimportant use case, just documenting how it behaves const a = docFromTextNotation('aaa (comment | ) bbb'); const b = docFromTextNotation('aaa (|comment| ) bbb'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Does not find comment range when comments are nested', () => { const a = docFromTextNotation('aaa (comment (comment [bbb ccc] | ddd))'); const b = docFromTextNotation('aaa (comment (comment |[bbb ccc]| ddd))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Finds comment range when current form is top level comment form', () => { const a = docFromTextNotation( - 'aaa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) |(comment eee)' + 'aaa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) |(comment eee)' ); const b = docFromTextNotation( - 'aaa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) |(comment eee)|' + 'aaa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) |(comment eee)|' ); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Includes reader tag', () => { const a = docFromTextNotation('aaa (comment #r [bbb ccc|] ddd)'); const b = docFromTextNotation('aaa (comment |#r [bbb ccc]| ddd)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Finds the preceding range when cursor is between to forms on the same line', () => { const a = docFromTextNotation('aaa (comment [bbb ccc] | ddd)'); const b = docFromTextNotation('aaa (comment |[bbb ccc]| ddd)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Finds the succeeding range when cursor is at the start of the line', () => { - const a = docFromTextNotation('aaa (comment [bbb ccc]• | ddd)'); - const b = docFromTextNotation('aaa (comment [bbb ccc]• |ddd|)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + const a = docFromTextNotation('aaa (comment [bbb ccc]§ | ddd)'); + const b = docFromTextNotation('aaa (comment [bbb ccc]§ |ddd|)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Finds the preceding comment symbol range when cursor is between that and something else on the same line', () => { // This is a bit funny, but is not an important use case const a = docFromTextNotation('aaa (comment | [bbb ccc] ddd)'); const b = docFromTextNotation('aaa (|comment| [bbb ccc] ddd)'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Can find the comment range for a top level form inside a comment', () => { const a = docFromTextNotation( - 'aaa (comment (ccc •#foo•(#bar •#baz•[:a :b| :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) (ddd eee)' + 'aaa (comment (ccc §#foo§(#bar §#baz§[:a :b| :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) (ddd eee)' ); const b = docFromTextNotation( - 'aaa |(comment (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar))| (ddd eee)' + 'aaa |(comment (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar))| (ddd eee)' ); const cursor: LispTokenCursor = a.getTokenCursor(0); - expect(cursor.rangeForDefun(a.selection.anchor, false)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selections[0].anchor, false)).toEqual( + textAndSelection(b)[1][0] + ); }); it('Finds closest form inside multiple nested comments', () => { const a = docFromTextNotation('aaa (comment (comment [bbb ccc] | ddd))'); const b = docFromTextNotation('aaa (comment (comment |[bbb ccc]| ddd))'); const cursor: LispTokenCursor = a.getTokenCursor(0); - expect(cursor.rangeForDefun(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('Finds the preceding range when cursor is between two forms on the same line', () => { const a = docFromTextNotation('aaa (comment [bbb ccc] | ddd)'); const b = docFromTextNotation('aaa (comment |[bbb ccc]| ddd)'); const cursor: LispTokenCursor = a.getTokenCursor(0); - expect(cursor.rangeForDefun(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('Finds top level form when deref in comment', () => { const a = docFromTextNotation('(comment @(foo [bar|]))'); const b = docFromTextNotation('(comment |@(foo [bar])|)'); const cursor: LispTokenCursor = a.getTokenCursor(0); - expect(cursor.rangeForDefun(a.selection.anchor)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); }); @@ -865,14 +869,14 @@ describe('Token Cursor', () => { describe('getFunctionName', () => { it('Finds function name in the current list', () => { const a = docFromTextNotation('(foo [|])'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.getFunctionName()).toEqual('foo'); }); it('Does not croak finding function name in unbalance', () => { // This hung the structural editing in the real editor // https://github.com/BetterThanTomorrow/calva/issues/1573 const a = docFromTextNotation('([|'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.getFunctionName()).toBeUndefined(); }); }); diff --git a/src/extension-test/unit/util/cursor-get-text-test.ts b/src/extension-test/unit/util/cursor-get-text-test.ts index 936d28742..d8664c395 100644 --- a/src/extension-test/unit/util/cursor-get-text-test.ts +++ b/src/extension-test/unit/util/cursor-get-text-test.ts @@ -1,24 +1,24 @@ import * as expect from 'expect'; -import { docFromTextNotation } from '../common/text-notation'; import * as getText from '../../../util/cursor-get-text'; +import { docFromTextNotation } from '../common/text-notation'; describe('get text', () => { describe('getTopLevelFunction', () => { it('Finds top level function at top', () => { - const a = docFromTextNotation('(foo bar)•(deftest a-test• (baz |gaz))'); - const b = docFromTextNotation('(foo bar)•(deftest |a-test|• (baz gaz))'); - const range: [number, number] = [b.selection.anchor, b.selection.active]; - expect(getText.currentTopLevelFunction(a, a.selection.active)).toEqual([ + const a = docFromTextNotation('(foo bar)§(deftest a-test§ (baz |gaz))'); + const b = docFromTextNotation('(foo bar)§(deftest |a-test|§ (baz gaz))'); + const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; + expect(getText.currentTopLevelFunction(a, a.selections[0].active)).toEqual([ range, b.model.getText(...range), ]); }); it('Finds top level function when nested', () => { - const a = docFromTextNotation('(foo bar)•(with-test• (deftest a-test• (baz |gaz)))'); - const b = docFromTextNotation('(foo bar)•(with-test• (deftest |a-test|• (baz gaz)))'); - const range: [number, number] = [b.selection.anchor, b.selection.active]; - expect(getText.currentTopLevelFunction(a, a.selection.active)).toEqual([ + const a = docFromTextNotation('(foo bar)§(with-test§ (deftest a-test§ (baz |gaz)))'); + const b = docFromTextNotation('(foo bar)§(with-test§ (deftest |a-test|§ (baz gaz)))'); + const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; + expect(getText.currentTopLevelFunction(a, a.selections[0].active)).toEqual([ range, b.model.getText(...range), ]); @@ -26,20 +26,20 @@ describe('get text', () => { it('Finds top level function when namespaced def-macro', () => { // https://github.com/BetterThanTomorrow/calva/issues/1086 - const a = docFromTextNotation('(foo bar)•(with-test• (t/deftest a-test• (baz |gaz)))'); - const b = docFromTextNotation('(foo bar)•(with-test• (t/deftest |a-test|• (baz gaz)))'); - const range: [number, number] = [b.selection.anchor, b.selection.active]; - expect(getText.currentTopLevelFunction(a, a.selection.active)).toEqual([ + const a = docFromTextNotation('(foo bar)§(with-test§ (t/deftest a-test§ (baz |gaz)))'); + const b = docFromTextNotation('(foo bar)§(with-test§ (t/deftest |a-test|§ (baz gaz)))'); + const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; + expect(getText.currentTopLevelFunction(a, a.selections[0].active)).toEqual([ range, b.model.getText(...range), ]); }); it('Finds top level function when function has metadata', () => { - const a = docFromTextNotation('(foo bar)•(deftest ^{:some :thing} a-test• (baz |gaz))'); - const b = docFromTextNotation('(foo bar)•(deftest ^{:some :thing} |a-test|• (baz gaz))'); - const range: [number, number] = [b.selection.anchor, b.selection.active]; - expect(getText.currentTopLevelFunction(a, a.selection.active)).toEqual([ + const a = docFromTextNotation('(foo bar)§(deftest ^{:some :thing} a-test§ (baz |gaz))'); + const b = docFromTextNotation('(foo bar)§(deftest ^{:some :thing} |a-test|§ (baz gaz))'); + const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; + expect(getText.currentTopLevelFunction(a, a.selections[0].active)).toEqual([ range, b.model.getText(...range), ]); @@ -48,18 +48,18 @@ describe('get text', () => { describe('getTopLevelForm', () => { it('Finds top level form', () => { - const a = docFromTextNotation('(foo bar)•(deftest a-test• (baz |gaz))'); - const b = docFromTextNotation('(foo bar)•|(deftest a-test• (baz gaz))|'); - const range: [number, number] = [b.selection.anchor, b.selection.active]; + const a = docFromTextNotation('(foo bar)§(deftest a-test§ (baz |gaz))'); + const b = docFromTextNotation('(foo bar)§|(deftest a-test§ (baz gaz))|'); + const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; expect(getText.currentTopLevelForm(a)).toEqual([range, b.model.getText(...range)]); }); }); describe('currentEnclosingFormToCursor', () => { it('Current enclosing form from start to cursor, then folded', () => { - const a = docFromTextNotation('(foo bar)•(deftest a-test• [baz ; f|oo• gaz])'); - const b = docFromTextNotation('(foo bar)•(deftest a-test• |[baz| ; foo• gaz])'); - const range: [number, number] = [b.selection.anchor, b.selection.active]; + const a = docFromTextNotation('(foo bar)§(deftest a-test§ [baz ; f|oo§ gaz])'); + const b = docFromTextNotation('(foo bar)§(deftest a-test§ |[baz| ; foo§ gaz])'); + const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; const trail = ']'; expect(getText.currentEnclosingFormToCursor(a)).toEqual([ range, @@ -70,9 +70,9 @@ describe('get text', () => { describe('topLevelFormToCursor', () => { it('Finds top level form from start to cursor', () => { - const a = docFromTextNotation('(foo bar)•(deftest a-test• [baz ; f|oo• gaz])'); - const b = docFromTextNotation('(foo bar)•|(deftest a-test• [baz| ; foo• gaz])'); - const range: [number, number] = [b.selection.anchor, b.selection.active]; + const a = docFromTextNotation('(foo bar)§(deftest a-test§ [baz ; f|oo§ gaz])'); + const b = docFromTextNotation('(foo bar)§|(deftest a-test§ [baz| ; foo§ gaz])'); + const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; const trail = '])'; expect(getText.currentTopLevelFormToCursor(a)).toEqual([ range, @@ -83,11 +83,11 @@ describe('get text', () => { describe('startOfFileToCursor', () => { it('Builds form from start of file to cursor, when cursor in line comment', () => { - const a = docFromTextNotation('(foo bar)•(deftest a-test• [baz ; f|oo• gaz])•(bar baz)'); + const a = docFromTextNotation('(foo bar)§(deftest a-test§ [baz ; f|oo§ gaz])§(bar baz)'); const b = docFromTextNotation( - '|(foo bar)•(deftest a-test• [baz| ; foo• gaz])•(bar baz)' + '|(foo bar)§(deftest a-test§ [baz| ; foo§ gaz])§(bar baz)' ); - const range: [number, number] = [b.selection.anchor, b.selection.active]; + const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; const trail = '])'; expect(getText.startOfFileToCursor(a)).toEqual([ range, @@ -96,12 +96,12 @@ describe('get text', () => { }); it('Builds form from start of file to cursor, when cursor in comment macro', () => { const a = docFromTextNotation( - '(foo bar)(comment• (deftest a-test• [baz ; f|oo• gaz])•(bar baz))' + '(foo bar)(comment§ (deftest a-test§ [baz ; f|oo§ gaz])§(bar baz))' ); const b = docFromTextNotation( - '|(foo bar)(comment• (deftest a-test• [baz| ; foo• gaz])•(bar baz))' + '|(foo bar)(comment§ (deftest a-test§ [baz| ; foo§ gaz])§(bar baz))' ); - const range: [number, number] = [b.selection.anchor, b.selection.active]; + const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; const trail = ']))'; expect(getText.startOfFileToCursor(a)).toEqual([ range, diff --git a/src/nrepl/index.ts b/src/nrepl/index.ts index 9054d53f8..d4b12c844 100644 --- a/src/nrepl/index.ts +++ b/src/nrepl/index.ts @@ -18,7 +18,7 @@ function hasStatus(res: any, status: string): boolean { return res.status && res.status.indexOf(status) > -1; } -// When a command fails becuase of an unknown-op (usually caused by missing +// When a command fails because of an unknown-op (usually caused by missing // middleware), we can mark the operation as failed, so that we can show a message // in the UI to the user. // https://nrepl.org/nrepl/design/handlers.html diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index e136c7fce..bcaabfa3f 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -7,14 +7,17 @@ import { Event, EventEmitter, ExtensionContext, + env, workspace, ConfigurationChangeEvent, } from 'vscode'; import * as paredit from '../cursor-doc/paredit'; import * as docMirror from '../doc-mirror/index'; -import { EditableDocument } from '../cursor-doc/model'; +import { EditableDocument, ModelEditResult } from '../cursor-doc/model'; import { assertIsDefined } from '../utilities'; - +import * as textNotation from '../extension-test/unit/common/text-notation'; +import * as calvaState from '../state'; +import _ = require('lodash'); const onPareditKeyMapChangedEmitter = new EventEmitter(); const languages = new Set(['clojure', 'lisp', 'scheme']); @@ -25,59 +28,93 @@ const enabled = true; * @param doc * @param range */ -function copyRangeToClipboard(doc: EditableDocument, [start, end]) { - const text = doc.model.getText(start, end); - void vscode.env.clipboard.writeText(text); +export function copyRangeToClipboard( + doc: EditableDocument, + ranges: Array<[number, number]> = _(doc.selections) + .map((s) => s.asRange) + .value() +) { + const texts: string[] = _(ranges) + // Why do we reorder the ranges? Because vscode does consider the cursor order when pasting, and considers the cursors sorted in the order they appear in the document (ascending) + .orderBy((r) => Math.min(...r), 'asc') + .map(([start, end]) => doc.model.getText(start, end)) + .value(); + // TODO: Evaluate if we need to use a different line ending per os for the clipboard multiline copy/paste + void env.clipboard.writeText(texts.join('\n')); } /** * Answers true when `calva.paredit.killAlsoCutsToClipboard` is enabled. * @returns boolean */ -function shouldKillAlsoCutToClipboard() { +export function shouldKillAlsoCutToClipboard(): boolean { return workspace.getConfiguration().get('calva.paredit.killAlsoCutsToClipboard'); } type PareditCommand = { command: string; - handler: (doc: EditableDocument) => void | Promise; + handler: (doc: EditableDocument) => void | Thenable | Thenable; }; const pareditCommands: PareditCommand[] = [ // NAVIGATING { command: 'paredit.forwardSexp', handler: (doc: EditableDocument) => { - paredit.moveToRangeRight(doc, paredit.forwardSexpRange(doc)); + paredit.moveToRangeRight( + doc, + paredit.forwardSexpRange( + doc, + doc.selections.map((s) => s.active) + ) + ); }, }, { command: 'paredit.backwardSexp', handler: (doc: EditableDocument) => { - paredit.moveToRangeLeft(doc, paredit.backwardSexpRange(doc)); + paredit.moveToRangeLeft( + doc, + paredit.backwardSexpRange( + doc, + doc.selections.map((s) => s.active) + ) + ); }, }, { command: 'paredit.forwardDownSexp', handler: (doc: EditableDocument) => { - paredit.moveToRangeRight(doc, paredit.rangeToForwardDownList(doc)); + paredit.moveToRangeRight( + doc, + doc.selections.map((s) => paredit.rangeToForwardDownList(doc, s.active)) + ); }, }, { command: 'paredit.backwardDownSexp', handler: (doc: EditableDocument) => { - paredit.moveToRangeLeft(doc, paredit.rangeToBackwardDownList(doc)); + paredit.moveToRangeLeft( + doc, + doc.selections.map((s) => paredit.rangeToBackwardDownList(doc, s.active)) + ); }, }, { command: 'paredit.forwardUpSexp', handler: (doc: EditableDocument) => { - paredit.moveToRangeRight(doc, paredit.rangeToForwardUpList(doc)); + paredit.moveToRangeRight( + doc, + doc.selections.map((s) => paredit.rangeToForwardUpList(doc, s.active)) + ); }, }, { command: 'paredit.backwardUpSexp', handler: (doc: EditableDocument) => { - paredit.moveToRangeLeft(doc, paredit.rangeToBackwardUpList(doc)); + paredit.moveToRangeLeft( + doc, + doc.selections.map((s) => paredit.rangeToBackwardUpList(doc, s.active)) + ); }, }, { @@ -95,13 +132,19 @@ const pareditCommands: PareditCommand[] = [ { command: 'paredit.closeList', handler: (doc: EditableDocument) => { - paredit.moveToRangeRight(doc, paredit.rangeToForwardList(doc)); + paredit.moveToRangeRight( + doc, + doc.selections.map((s) => paredit.rangeToForwardList(doc, s.active)) + ); }, }, { command: 'paredit.openList', handler: (doc: EditableDocument) => { - paredit.moveToRangeLeft(doc, paredit.rangeToBackwardList(doc)); + paredit.moveToRangeLeft( + doc, + doc.selections.map((s) => paredit.rangeToBackwardList(doc, s.active)) + ); }, }, @@ -109,7 +152,10 @@ const pareditCommands: PareditCommand[] = [ { command: 'paredit.rangeForDefun', handler: (doc: EditableDocument) => { - paredit.selectRange(doc, paredit.rangeForDefun(doc)); + paredit.selectRange( + doc, + doc.selections.map((selection) => paredit.rangeForDefun(doc, selection.active)) + ); }, }, { @@ -235,74 +281,69 @@ const pareditCommands: PareditCommand[] = [ { command: 'paredit.killRight', handler: (doc: EditableDocument) => { - const range = paredit.forwardHybridSexpRange(doc); + const ranges = paredit.forwardHybridSexpRange(doc); if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); + copyRangeToClipboard(doc, ranges); } - paredit.killRange(doc, range); + return paredit.killRange(doc, ranges); }, }, { command: 'paredit.killSexpForward', - handler: (doc: EditableDocument) => { - const range = paredit.forwardSexpRange(doc); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - paredit.killRange(doc, range); - }, + handler: (doc: EditableDocument) => + paredit.killSexpForward(doc, shouldKillAlsoCutToClipboard, copyRangeToClipboard), }, { command: 'paredit.killSexpBackward', - handler: (doc: EditableDocument) => { - const range = paredit.backwardSexpRange(doc); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - paredit.killRange(doc, range); - }, + handler: (doc: EditableDocument) => + paredit.killSexpBackward(doc, shouldKillAlsoCutToClipboard, copyRangeToClipboard), }, { command: 'paredit.killListForward', handler: (doc: EditableDocument) => { - const range = paredit.forwardListRange(doc); + const ranges = doc.selections.map((s) => paredit.forwardListRange(doc, s.active)); + if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); + copyRangeToClipboard(doc, ranges); } - return paredit.killForwardList(doc, range); + void paredit.killForwardList(doc, ranges); }, }, // TODO: Implement with killRange { command: 'paredit.killListBackward', handler: (doc: EditableDocument) => { - const range = paredit.backwardListRange(doc); + const ranges = doc.selections.map((s) => paredit.backwardListRange(doc, s.active)); if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); + copyRangeToClipboard(doc, ranges); } - return paredit.killBackwardList(doc, range); + return ranges; + void paredit.killBackwardList(doc, ranges); }, }, // TODO: Implement with killRange { command: 'paredit.spliceSexpKillForward', handler: (doc: EditableDocument) => { - const range = paredit.forwardListRange(doc); + const ranges = doc.selections.map((s) => paredit.forwardListRange(doc, s.active)); + if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); + copyRangeToClipboard(doc, ranges); } - void paredit.killForwardList(doc, range).then((isFulfilled) => { - return paredit.spliceSexp(doc, doc.selection.active, false); + return ranges; + void paredit.killForwardList(doc, ranges).then(() => { + return paredit.spliceSexp(doc, /* s.active, */ false); }); }, }, { command: 'paredit.spliceSexpKillBackward', handler: (doc: EditableDocument) => { - const range = paredit.backwardListRange(doc); + const ranges = doc.selections.map((s) => paredit.backwardListRange(doc, s.active)); + if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); + copyRangeToClipboard(doc, ranges); } - void paredit.killBackwardList(doc, range).then((isFulfilled) => { - return paredit.spliceSexp(doc, doc.selection.active, false); + void paredit.killBackwardList(doc, ranges).then(() => { + return paredit.spliceSexp(doc, /* s.active, */ false); }); }, }, @@ -428,6 +469,36 @@ export function activate(context: ExtensionContext) { .update('calva.paredit.defaultKeyMap', 'original', vscode.ConfigurationTarget.Global); } }), + commands.registerCommand('calva.diagnostics.printTextNotationFromDocument', () => { + const doc = vscode.window.activeTextEditor?.document; + if (doc && doc.languageId === 'clojure') { + const mirrorDoc = docMirror.getDocument(vscode.window.activeTextEditor?.document); + const notation = textNotation.textNotationFromDoc(mirrorDoc); + const chan = calvaState.outputChannel(); + const relPath = vscode.workspace.asRelativePath(doc.uri); + chan.appendLine(`Text notation for: ${relPath}:\n${notation}`); + } + }), + commands.registerCommand('calva.diagnostics.createDocumentFromTextNotation', async () => { + const tn = await vscode.window.showInputBox({ + placeHolder: 'Text-notation', + prompt: 'Type the text-notation for the document you want to create', + }); + const cursorDoc = textNotation.docFromTextNotation(tn); + await vscode.workspace + .openTextDocument({ language: 'clojure', content: textNotation.getText(cursorDoc) }) + .then(async (doc) => { + const editor = await vscode.window.showTextDocument(doc, { + preview: false, + preserveFocus: false, + }); + editor.selections = cursorDoc.selections.map((selection) => { + const anchor = doc.positionAt(selection.anchor), + active = doc.positionAt(selection.active); + return new vscode.Selection(anchor, active); + }); + }); + }), window.onDidChangeActiveTextEditor( (e) => e && e.document && languages.has(e.document.languageId) ), diff --git a/src/util/array.ts b/src/util/array.ts new file mode 100644 index 000000000..72a8b9c79 --- /dev/null +++ b/src/util/array.ts @@ -0,0 +1,26 @@ +import _ = require('lodash'); + +export const replaceAt = (array: A[], index: number, replacement: A): A[] => { + return array + .slice(0, index) + .concat([replacement]) + .concat(array.slice(index + 1)); +}; + +_.mixin({ + replaceAt, +}); + +/** + * Perhaps silly utility. + * Designed to help out with when you must sort an array and then resort it back to its original order. + * + * First you `.map(mapToItemAndOrder)` the array, then you sort or order it however you want, after which you can simply resort it by + * + * , specifically where `map(mapToItemAndOrder) is followed up with with a sort operation on the result, using the `idx` (2nd return array item) + */ +export const mapToItemAndOrder = (o: A, idx: number) => [o, idx] as [A, number]; + +export const mapToOriginalOrder = ([_o, idx]: [A, number]) => idx; + +export const mapToOriginalItem = ([o]: [A, number]) => o; diff --git a/src/util/cursor-get-text.ts b/src/util/cursor-get-text.ts index 6b565ed55..37712e794 100644 --- a/src/util/cursor-get-text.ts +++ b/src/util/cursor-get-text.ts @@ -8,7 +8,7 @@ export type RangeAndText = [[number, number], string] | [undefined, '']; export function currentTopLevelFunction( doc: EditableDocument, - active: number = doc.selection.active + active: number = doc.selections[0].active ): RangeAndText { const defunCursor = doc.getTokenCursor(active); const defunStart = defunCursor.rangeForDefun(active)[0]; @@ -34,8 +34,8 @@ export function currentTopLevelFunction( } export function currentTopLevelForm(doc: EditableDocument): RangeAndText { - const defunCursor = doc.getTokenCursor(doc.selection.active); - const defunRange = defunCursor.rangeForDefun(doc.selection.active); + const defunCursor = doc.getTokenCursor(doc.selections[0].active); + const defunRange = defunCursor.rangeForDefun(doc.selections[0].active); return defunRange ? [defunRange, doc.model.getText(...defunRange)] : [undefined, '']; } @@ -46,7 +46,7 @@ function rangeOrStartOfFileToCursor( ): RangeAndText { if (foldRange) { const closeBrackets: string[] = []; - const bracketCursor = doc.getTokenCursor(doc.selection.active); + const bracketCursor = doc.getTokenCursor(doc.selections[0].active); bracketCursor.backwardWhitespace(true); const rangeEnd = bracketCursor.offsetStart; while ( @@ -63,19 +63,19 @@ function rangeOrStartOfFileToCursor( } export function currentEnclosingFormToCursor(doc: EditableDocument): RangeAndText { - const cursor = doc.getTokenCursor(doc.selection.active); + const cursor = doc.getTokenCursor(doc.selections[0].active); const enclosingRange = cursor.rangeForList(1); return rangeOrStartOfFileToCursor(doc, enclosingRange, enclosingRange[0]); } export function currentTopLevelFormToCursor(doc: EditableDocument): RangeAndText { - const cursor = doc.getTokenCursor(doc.selection.active); - const defunRange = cursor.rangeForDefun(doc.selection.active); + const cursor = doc.getTokenCursor(doc.selections[0].active); + const defunRange = cursor.rangeForDefun(doc.selections[0].active); return rangeOrStartOfFileToCursor(doc, defunRange, defunRange[0]); } export function startOfFileToCursor(doc: EditableDocument): RangeAndText { - const cursor = doc.getTokenCursor(doc.selection.active); - const defunRange = cursor.rangeForDefun(doc.selection.active, false); + const cursor = doc.getTokenCursor(doc.selections[0].active); + const defunRange = cursor.rangeForDefun(doc.selections[0].active, false); return rangeOrStartOfFileToCursor(doc, defunRange, 0); } diff --git a/src/utilities.ts b/src/utilities.ts index d79a8750c..e9dfbe0b2 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -169,7 +169,7 @@ function getActualWord(document, position, selected, word) { } } -function getWordAtPosition(document, position) { +function getWordAtPosition(document: vscode.TextDocument, position) { const selected = document.getWordRangeAtPosition(position), selectedText = selected !== undefined diff --git a/src/when-contexts.ts b/src/when-contexts.ts index 3457663cc..655adf215 100644 --- a/src/when-contexts.ts +++ b/src/when-contexts.ts @@ -4,7 +4,7 @@ import * as docMirror from './doc-mirror'; import * as context from './cursor-doc/cursor-context'; import * as util from './utilities'; -let lastContexts: context.CursorContext[] = []; +let lastContexts: readonly context.CursorContext[] = context.allCursorContexts; export function setCursorContextIfChanged(editor: vscode.TextEditor) { if ( @@ -24,12 +24,12 @@ export function setCursorContextIfChanged(editor: vscode.TextEditor) { function determineCursorContexts( document: vscode.TextDocument, position: vscode.Position -): context.CursorContext[] { +): readonly context.CursorContext[] { const mirrorDoc = docMirror.getDocument(document); return context.determineContexts(mirrorDoc, document.offsetAt(position)); } -function setCursorContexts(currentContexts: context.CursorContext[]) { +function setCursorContexts(currentContexts: readonly context.CursorContext[]) { lastContexts = currentContexts; context.allCursorContexts.forEach((context) => { void vscode.commands.executeCommand( diff --git a/test-data/multi_cursor_sandbox.clj b/test-data/multi_cursor_sandbox.clj new file mode 100644 index 000000000..fb529b0fa --- /dev/null +++ b/test-data/multi_cursor_sandbox.clj @@ -0,0 +1,3 @@ +;(|a)§(|1) +(a) +() \ No newline at end of file diff --git a/test-data/paredit_sandbox.clj b/test-data/paredit_sandbox.clj index 3c2b00db9..2cca9af1a 100644 --- a/test-data/paredit_sandbox.clj +++ b/test-data/paredit_sandbox.clj @@ -9,6 +9,8 @@ ;; +(a| b (c + d) e) (a| b (c d) e) @@ -17,19 +19,31 @@ (aa| (c (e f)) g) +(aa| (c (e + f)) g) + ;; === Example 2 - comments ;; Comment killed (a| ;; comment e) +(a| ;; comment + e) + ;; Example 3 - newline ;; newline killed (a| e) +(a| + e) + ;; Example 4 - end of list ;; Don't kill past it +(a b (c |) + e) + (a b (c |) e) @@ -53,6 +67,15 @@ string. " d 19 e 31] (+ a b)) +(let [a 23 + b (+ 4 + 5 + 9) + m {:a 1} + c "hello" + d 19 e 31] + (+ a b)) + ;; Exmaple 8 -- map key value pairs ;; killing from :c includes the corresponding value {:a 1 @@ -60,19 +83,31 @@ string. " :c {:d 4 :e 5}} +{:a 1 + :b 2 + :c {:d 4 + :e 5}} + ;; Example 9 -- deleteing from #_ should delete whole expr [#_(comment (+ 2 3))] +[#_(comment + (+ 2 3))] + ;; Example 10 -- deleting from | should delete to eol ;; | (23 34 ;; ) - ;; Example 11 -- Deleting should delete whole expr to closing ] | 24 [1] +| 24 [1] + +43 [1 2 + 3] + 43 [1 2 3] @@ -84,9 +119,17 @@ string. " ;; Example 14 -- newline in string, deletes to end of string ["abc| def\n ghi" "this stays"] +["abc| def\n ghi" "this stays"] ;; Example 15 -- Heisenbug should delete up to and including g] #_|[a b (c d e f) g] + +#_|[a b (c d + e + f) g] + +:a + :a