From f53a13f15c28cdb163f5724899e9f617d4d5b14e Mon Sep 17 00:00:00 2001 From: Rayat Date: Mon, 28 Mar 2022 10:38:20 -0700 Subject: [PATCH] Make most multi cursor work, tons of tests changes --- .../src/providers/ontype_formatter.ts | 7 +- src/cursor-doc/cursor-context.ts | 7 +- src/cursor-doc/cursor-doc-utils.ts | 20 + src/cursor-doc/model.ts | 182 +- src/cursor-doc/paredit.ts | 1803 ++++++++++------- src/doc-mirror/index.ts | 66 +- .../unit/common/text-notation.ts | 75 +- .../unit/cursor-doc/cursor-context-test.ts | 2 +- .../unit/cursor-doc/paredit-test.ts | 279 +-- .../unit/cursor-doc/token-cursor-test.ts | 474 ++--- .../unit/util/cursor-get-text-test.ts | 26 +- src/paredit/extension.ts | 140 +- src/util/array.ts | 12 + src/util/cursor-get-text.ts | 18 +- src/utilities.ts | 2 +- 15 files changed, 1823 insertions(+), 1290 deletions(-) create mode 100644 src/cursor-doc/cursor-doc-utils.ts create mode 100644 src/util/array.ts 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..b2dad3872 100644 --- a/src/cursor-doc/cursor-context.ts +++ b/src/cursor-doc/cursor-context.ts @@ -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,9 +56,10 @@ 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 + offset = doc.selections[0].active ): CursorContext[] { const tokenCursor = doc.getTokenCursor(offset); const contexts: CursorContext[] = []; diff --git a/src/cursor-doc/cursor-doc-utils.ts b/src/cursor-doc/cursor-doc-utils.ts new file mode 100644 index 000000000..066450b14 --- /dev/null +++ b/src/cursor-doc/cursor-doc-utils.ts @@ -0,0 +1,20 @@ +import { EditableDocument, ModelEditSelection } from './model'; + +export function selectionToRange( + selection: ModelEditSelection, + assumeDirection: 'ltr' | 'rtl' = undefined +) { + const { anchor, active } = selection; + switch (assumeDirection) { + case 'ltr': + return [anchor, active]; + case 'rtl': + return [active, anchor]; + case undefined: + default: { + const start = 'start' in selection ? selection.start : Math.min(anchor, active); + const end = 'end' in selection ? selection.end : Math.max(anchor, active); + return [start, end]; + } + } +} diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index f104cfcc6..ed757d997 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -1,7 +1,8 @@ -import { isUndefined, max, min } from 'lodash'; +import { isUndefined, max, min, isNumber } from 'lodash'; import { deepEqual as equal } from '../util/object'; import { Scanner, ScannerState, Token } from './clojure-lexer'; import { LispTokenCursor } from './token-cursor'; +import type { Selection, TextDocument } from 'vscode'; let scanner: Scanner; @@ -45,26 +46,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,6 +107,67 @@ 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 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; + } } clone() { @@ -88,6 +183,11 @@ export type ModelEditOptions = { selections?: ModelEditSelection[]; }; +export type ModelEditResult = { + edits: ModelEdit[]; + selections: ModelEditSelection[]; + success: boolean; +}; export interface EditableModel { readonly lineEndingLength: number; @@ -96,7 +196,7 @@ export interface EditableModel { * 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; @@ -105,10 +205,8 @@ 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 @@ -124,10 +222,10 @@ export interface EditableDocument { selectionsStack: ModelEditSelection[][]; getTokenCursor: (offset?: number, previous?: boolean) => LispTokenCursor; insertString: (text: string) => void; - getSelectionText: () => string; getSelectionTexts: () => string[]; - delete: () => Thenable; - backspace: () => Thenable; + getSelectionText: (index: number) => string; + delete: (index?: number) => Thenable; + backspace: (index?: number) => Thenable; } /** The underlying model for the REPL readline. */ @@ -372,7 +470,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) { @@ -396,9 +494,9 @@ export class LineInputModel implements EditableModel { } } if (this.document && options.selections) { - this.document.selections = [options.selections[0]]; + this.document.selections = options.selections; } - resolve(true); + resolve({ edits, selections: options.selections, success: true }); }); } @@ -548,7 +646,6 @@ export class StringDocument implements EditableDocument { } } - selection: ModelEditSelection; selections: ModelEditSelection[]; model: LineInputModel = new LineInputModel(1, this); @@ -563,30 +660,43 @@ export class StringDocument implements EditableDocument { return this.model.getTokenCursor(offset); } - getSelectionsText: () => string[]; insertString(text: string) { this.model.insertString(0, text); } getSelectionTexts: () => string[]; - getSelectionText: () => string; - - delete() { - return this.model.edit( - [this.selection].map(({ anchor: p }) => new ModelEdit('deleteRange', [p, 1])), - { - selections: this.selections.map(({ anchor: p }) => new ModelEditSelection(p)), - } - ); + getSelectionText: (index: number) => string; + + delete(index?: number) { + if (isUndefined(index)) { + return this.model.edit( + this.selections.map(({ anchor: p }) => new ModelEdit('deleteRange', [p, 1])), + { + selections: this.selections.map(({ anchor: p }) => new ModelEditSelection(p)), + } + ); + } else { + return this.model.edit([new ModelEdit('deleteRange', [(this.selections[index].anchor, 1)])], { + selections: [new ModelEditSelection(this.selections[index].anchor)], + }); + } } - getSelectionText: () => string; - backspace() { - return this.model.edit( - [this.selection].map(({ anchor: p }) => new ModelEdit('deleteRange', [p - 1, 1])), - { - selections: [this.selection].map(({ anchor: p }) => new ModelEditSelection(p - 1)), - } - ); + backspace(index?: number) { + if (isUndefined(index)) { + 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)), + } + ); + } else { + return this.model.edit( + [new ModelEdit('deleteRange', [this.selections[index].anchor - 1, 1])], + { + selections: [new ModelEditSelection(this.selections[index].anchor - 1)], + } + ); + } } } diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 92b16349c..36b0c351f 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -1,8 +1,9 @@ -import { isArray, isEqual, isNumber, last, pick, property } from 'lodash'; +import { isEqual, isNumber, last, pick, property, clone } from 'lodash'; import { validPair } from './clojure-lexer'; -import { EditableDocument, ModelEdit, ModelEditSelection } from './model'; +import { EditableDocument, ModelEdit, ModelEditSelection, ModelEditResult } from './model'; import { LispTokenCursor } from './token-cursor'; -import { last } from 'lodash'; +import { replaceAt } from '../util/array'; +import { ShowDocumentRequest } from 'vscode-languageclient'; // 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. @@ -17,8 +18,8 @@ import { last } from 'lodash'; export function killRange( doc: EditableDocument, range: [number, number], - start = doc.selection.anchor, - end = doc.selection.active + start = doc.selections[0].anchor, + end = doc.selections[0].active ) { const [left, right] = [Math.min(...range), Math.max(...range)]; void doc.model.edit([new ModelEdit('deleteRange', [left, right - left, [start, end]])], { @@ -26,99 +27,128 @@ export function killRange( }); } -export function moveToRangeLeft(doc: EditableDocument, range: [number, number]) { - doc.selections = [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.selections = [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] | Array) { - if (isArray(range[0])) { - growSelectionStack(doc, range as Array); - } else if (range.length === 2 && isNumber(range[0])) { - growSelectionStack(doc, [range as [number, number]]); - } +export function selectRange(doc: EditableDocument, ranges: Array<[number, number]>) { + growSelectionStack(doc, ranges); } export function selectRangeForward( - doc: EditableDocument, - range: [number, number] + 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]) ) { - const selectionLeft = doc.selection.anchor; - const rangeRight = Math.max(range[0], range[1]); - growSelectionStack(doc, [[selectionLeft, rangeRight]]); + 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 ranges = doc.selections.map((selection) => { const rangeFn = - doc.selection.active >= doc.selection.anchor - ? forwardSexpRange - : (doc: EditableDocument) => - forwardSexpRange(doc, doc.selection.active, true); - selectRangeForward(doc, rangeFn(doc)); + selection.active >= selection.anchor + ? forwardSexpRange + : (doc: EditableDocument) => forwardSexpRange(doc, selection.active, true); + return rangeFn(doc, selection.start); + }); + selectRangeForward(doc, ranges); } +// TODO: could prob use ModelEditSelection semantics for `end` versus checking for active >= anchor export function selectRight(doc: EditableDocument) { + const ranges = doc.selections.map((selection) => { const rangeFn = - doc.selection.active >= doc.selection.anchor - ? forwardHybridSexpRange - : (doc: EditableDocument) => - forwardHybridSexpRange(doc, doc.selection.active, true); - selectRangeForward(doc, rangeFn(doc)); + selection.active >= selection.anchor + ? doc => forwardHybridSexpRange(doc, selection.end) + : (doc: EditableDocument) => forwardHybridSexpRange(doc, selection.active, true); + return rangeFn(doc); + }); + selectRangeForward(doc, ranges); } export function selectBackwardSexp(doc: EditableDocument) { + const ranges = doc.selections.map((selection) => { const rangeFn = - doc.selection.active <= doc.selection.anchor - ? backwardSexpRange - : (doc: EditableDocument) => - backwardSexpRange(doc, doc.selection.active, false); - selectRangeBackward(doc, rangeFn(doc)); + selection.active <= selection.anchor + ? backwardSexpRange + : (doc: EditableDocument) => backwardSexpRange(doc, selection.active, false); + return rangeFn(doc, selection.start); + }); + selectRangeBackward(doc, ranges); } export function selectForwardDownSexp(doc: EditableDocument) { + const ranges = doc.selections.map((selection) => { 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)); + 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 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 +157,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); @@ -136,46 +166,95 @@ export function rangeForDefun( export function forwardSexpRange( doc: EditableDocument, - offset = Math.max(doc.selection.anchor, doc.selection.active), + offsets?: number[], + goPastWhitespace?: boolean +): Array<[number, number]>; +export function forwardSexpRange( + doc: EditableDocument, + offset?: number, + goPastWhitespace?: boolean +): [number, number]; +export function forwardSexpRange( + doc: EditableDocument, + // offset = Math.max(doc.selections.anchor, doc.selections.active), + // offset: number /* = doc.selections[0].end */, + // selections: ModelEditSelection[] = doc.selections, + offsets: number | number[] = doc.selections.map((s) => s.end), goPastWhitespace = false -): [number, number] { - const cursor = doc.getTokenCursor(offset); - cursor.forwardWhitespace(); - if (cursor.forwardSexp(true, true)) { - if (goPastWhitespace) { - cursor.forwardWhitespace(); +): [number, number] | Array<[number, number]> { + if (isNumber(offsets)) { + offsets = [offsets]; + } + + const ranges = offsets.map<[number, number]>((offset) => { + const cursor = doc.getTokenCursor(offset); + cursor.forwardWhitespace(); + if (cursor.forwardSexp(true, true)) { + if (goPastWhitespace) { + cursor.forwardWhitespace(); + } + return [offset, cursor.offsetStart]; + } else { + return [offset, offset]; } - return [offset, cursor.offsetStart]; + }); + + if(isNumber(offsets)) { + return ranges[0]; } else { - return [offset, offset]; + return ranges; } } export function backwardSexpRange( doc: EditableDocument, - offset: number = Math.min(doc.selection.anchor, doc.selection.active), + offsets?: number[], + goPastWhitespace?: boolean +): Array<[number, number]>; +export function backwardSexpRange( + doc: EditableDocument, + offset?: number, + goPastWhitespace?: boolean +): [number, number]; +export function backwardSexpRange( + doc: EditableDocument, + // offset: number = Math.min(doc.selections.anchor, doc.selections.active), + offsets: number | number[] = doc.selections.map((s) => s.start), goPastWhitespace = false -): [number, number] { - const cursor = doc.getTokenCursor(offset); - 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(); +): Array<[number, number]> | [number, number] { + + if (isNumber(offsets)) { + offsets = [offsets]; } - cursor.backwardWhitespace(); - if (cursor.backwardSexp(true, true)) { - if (goPastWhitespace) { - cursor.backwardWhitespace(); + + const ranges = offsets.map<[number, number]>((offset) => { + const cursor = doc.getTokenCursor(offset); + 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(); } - return [cursor.offsetStart, offset]; + cursor.backwardWhitespace(); + if (cursor.backwardSexp(true, true)) { + if (goPastWhitespace) { + cursor.backwardWhitespace(); + } + return [cursor.offsetStart, offset]; + } else { + return [offset, offset]; + } + }); + + if (isNumber(offsets)) { + return ranges[0]; } else { - return [offset, offset]; + return ranges; } } 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(); @@ -184,7 +263,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(); @@ -207,74 +286,104 @@ export function backwardListRange( */ export function forwardHybridSexpRange( doc: EditableDocument, - offset = Math.max(doc.selection.anchor, doc.selection.active), + offsets?: number[], + goPastWhitespace?: boolean +): Array<[number, number]>; +export function forwardHybridSexpRange( + doc: EditableDocument, + offset?: number, + goPastWhitespace?: boolean +): [number, number]; +export function forwardHybridSexpRange( + doc: EditableDocument, + // offset = Math.max(doc.selections.anchor, doc.selections.active), + // offset?: number = doc.selections[0].end, + // selections: ModelEditSelection[] = doc.selections, + offsets: number | 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]; - } +): [number, number] | Array<[number, number]> { - 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(isNumber(offsets)) { + offsets = [offsets]; } - 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); - } + const ranges = 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]; } - 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 + 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 (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]; + }); + + if (isNumber(offsets)) { + return ranges[0]; + } else { + return ranges; } - 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] { const cursor = doc.getTokenCursor(offset); @@ -291,7 +400,8 @@ export function rangeToForwardUpList( 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] { const cursor = doc.getTokenCursor(offset); @@ -310,7 +420,8 @@ export function rangeToBackwardUpList( 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); @@ -326,7 +437,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); @@ -348,7 +460,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()) { @@ -360,7 +473,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()) { @@ -370,32 +484,49 @@ export function rangeToBackwardList( } } +// TODO: test export function wrapSexpr( doc: EditableDocument, open: string, close: string, - start: number = doc.selection.anchor, - end: number = doc.selection.active, + // _start: number, // = doc.selections.anchor, + // _end: number, // = doc.selections.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( +): void { + return doc.selections.forEach((sel) => { + 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; + void doc.model.edit( + [ + new ModelEdit('insertString', [range[1], close]), + new ModelEdit('insertString', [ + range[0], + open, + [end, end], + [start + open.length, start + open.length], + ]), + ], + { + selections: [new ModelEditSelection(start + open.length)], + skipFormat: options.skipFormat, + } + ); + } + } else { + // there is a selection + const range = [Math.min(start, end), Math.max(start, end)]; + void doc.model.edit( [ new ModelEdit('insertString', [range[1], close]), - new ModelEdit('insertString', [ - range[0], - open, - [end, end], - [start + open.length, start + open.length], - ]), + new ModelEdit('insertString', [range[0], open]), ], { selections: [new ModelEditSelection(start + open.length)], @@ -403,62 +534,61 @@ export function wrapSexpr( } ); } - } 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]), - ], - { - selections: [new ModelEditSelection(start + open.length)], - skipFormat: options.skipFormat, - } - ); - } + }); } +// TODO: test 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]), - ], - { selections: [new ModelEditSelection(end)] } - ); + close: string + // _start: number, // = doc.selections.anchor, + // _end: number // = doc.selections.active +) { + 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}`])], { - selections: [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, + }); } /** @@ -466,66 +596,74 @@ 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], - ]), - ], - { selections: [new ModelEditSelection(prevEnd)], formatDepth: 2 } - ); + ]) + ); + 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. - - 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( - [ +): Thenable { + const edits = [], + selections = clone(doc.selections); + doc.selections.forEach((selection, index) => { + 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, '']), - ], - { undoStopBefore, selections: [new ModelEditSelection(start - 1)] } - ); + new ModelEdit('changeRange', [beginning - open.raw.length, beginning, '']) + ); + selections[index] = new ModelEditSelection(start - 1); + } } - } + }); + + return doc.model.edit(edits, { undoStopBefore, selections }); } export function killBackwardList( doc: EditableDocument, [start, end]: [number, number] -): Thenable { +): Thenable { return doc.model.edit( [new ModelEdit('changeRange', [start, end, '', [end, end], [start, start]])], { @@ -537,7 +675,7 @@ export function killBackwardList( export function killForwardList( doc: EditableDocument, [start, end]: [number, number] -): Thenable { +): Thenable { const cursor = doc.getTokenCursor(start); const inComment = (cursor.getToken().type == 'comment' && start > cursor.offsetStart) || @@ -556,9 +694,19 @@ export function killForwardList( ); } -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); @@ -571,6 +719,7 @@ 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; @@ -593,19 +742,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(); @@ -630,78 +784,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 - ? { - selections: [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 - ? { - selections: [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 @@ -710,102 +883,162 @@ 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])], { - selections: [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])], { - selections: [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])], - { - selections: [new ModelEditSelection(p - prevToken.raw.length)], - } - ); - } else { - if (['open', 'close'].includes(prevToken.type) && docIsBalanced(doc)) { - doc.selection = new ModelEditSelection(p - prevToken.raw.length); - return new Promise((resolve) => resolve(true)); + 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; } else { - return doc.backspace(); + 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; + } else if (nextToken.type == 'prompt') { + // return new Promise((resolve) => resolve(true)); + return true; + // 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(selections, index, sel), + }); + // return sel; + selections[index] = res.selections[index]; + } else if (prevToken.type === 'open' && nextToken.type === 'close') { + // return doc.model.edit( + const sel = new ModelEditSelection(p - prevToken.raw.length); + selections[index] = sel; + return 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(selections, index, sel), + } + ); + // return sel; + } 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)); + } else { + const res = await doc.backspace(); + const { selections: sels } = res; + selections[index] = res.selections[0]; + return res.success; + } + } } - } - } + }) + ).then((succeeded) => { + doc.selections = selections; + return succeeded; + }); } -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])], { - selections: [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])], - { - selections: [new ModelEditSelection(p - prevToken.raw.length)], - } - ); + doc.selections = await Promise.all(doc.selections.map(async (selection, index) => { + const { start, end } = selection; + if (start != end) { + await doc.delete(); + return selection; } else { - if (['open', 'close'].includes(nextToken.type) && docIsBalanced(doc)) { - doc.selection = new ModelEditSelection(p + 1); - return new Promise((resolve) => resolve(true)); + const cursor = doc.getTokenCursor(start); + const prevToken = cursor.getPrevToken(); + const nextToken = cursor.getToken(); + const p = start; + if (doc.model.getText(p, p + 2, true) == '\\"') { + await doc.model.edit([new ModelEdit('deleteRange', [p, 2])], { + selections: replaceAt(doc.selections, index, new ModelEditSelection(p)), + }); + return new ModelEditSelection(p); + } else if (prevToken.type === 'open' && nextToken.type === 'close') { + 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 new ModelEditSelection(p - prevToken.raw.length); } else { - return doc.delete(); + 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 new ModelEditSelection(p + 1); + } else { + // return doc.delete(); + return selection; + } } } - } + })); } +// 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('"'); @@ -819,7 +1052,9 @@ export function stringQuote( 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('\\')) { @@ -853,66 +1088,62 @@ export function stringQuote( * built-in Expand Selection/Shrink Selection commands) */ export function growSelection( - doc: EditableDocument, - 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 newRanges = doc.selections.map(({ 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] as const; + 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.getPrevToken().type == 'open' && endC.getToken().type == 'close') { + startC.backwardList(); + startC.backwardUpList(); + endC.forwardList(); + // growSelectionStack(doc, [startC.offsetStart, endC.offsetEnd]); + return [startC.offsetStart, startC.offsetEnd]; + } else { + if (startC.backwardList()) { + // we are in an sexpr. + endC.forwardList(); + endC.previous(); } else { - if ( - startC.getPrevToken().type == 'open' && - endC.getToken().type == 'close' - ) { - startC.backwardList(); - startC.backwardUpList(); - endC.forwardList(); - // growSelectionStack(doc, [startC.offsetStart, endC.offsetEnd]); - return [startC.offsetStart, startC.offsetEnd] as const; - } else { - 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(); - } - } - // growSelectionStack(doc, [startC.offsetStart, endC.offsetEnd]); - return [startC.offsetStart, endC.offsetEnd] as const; + 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(); + } } - }) - growSelectionStack(doc, newRanges); + // growSelectionStack(doc, [startC.offsetStart, endC.offsetEnd]); + return [startC.offsetStart, endC.offsetEnd]; + } + } + }); + growSelectionStack(doc, newRanges); } /** @@ -934,171 +1165,184 @@ export function growSelection( * @param ranges the new ranges to grow the selection into * @returns */ -export function growSelectionStack( - doc: EditableDocument, - ranges: Array<(readonly [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); - - // 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 { - // start a "fresh" selection set expansion history - // FIXME(multi-cursor): why doesn't this use `setSelectionStack(doc)` from below? - doc.selectionsStack = [doc.selections]; +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); + + // 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; } - doc.selections = ranges.map((range) => new ModelEditSelection(...range)); - doc.selectionsStack.push(doc.selections); + } else { + // start a "fresh" selection set expansion history + // FIXME(multi-cursor): why doesn't this use `setSelectionStack(doc)` from below? + doc.selectionsStack = [doc.selections]; + } + 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.selectionsStack.length) { - const latest = doc.selectionsStack.pop(); - if ( - doc.selectionsStack.length && - latest - .every((selection, index) => isEqual( - pick(selection, ['anchor, active']), - pick(doc.selections[index], ['anchor, active']) - )) - ) { - doc.selections = last(doc.selectionsStack); - } + if (doc.selectionsStack.length) { + const latest = doc.selectionsStack.pop(); + if ( + doc.selectionsStack.length && + latest.every((selection, index) => + isEqual( + pick(selection, ['anchor, active']), + pick(doc.selections[index], ['anchor, active']) + ) + ) + ) { + doc.selections = last(doc.selectionsStack); } - + } } export function setSelectionStack( - doc: EditableDocument, - selections: ModelEditSelection[] = doc.selections + doc: EditableDocument, + selections: ModelEditSelection[][] = [doc.selections] ) { - doc.selectionsStack = [selections]; + doc.selectionsStack = selections; } export function raiseSexp( - doc: EditableDocument, - start = doc.selection.anchor, - end = doc.selection.active + doc: EditableDocument + // start = doc.selections.anchor, + // end = doc.selections.active ) { - 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])], - { - selections: [new ModelEditSelection( - isCaretTrailing ? startCursor.offsetStart + raised.length : startCursor.offsetStart - )], - } - ); + const edits = [], + selections = clone(doc.selections); + doc.selections.forEach((selection, 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]) + ); + selections[index] = new ModelEditSelection( + isCaretTrailing ? startCursor.offsetStart + raised.length : startCursor.offsetStart + ); + } } } - } + }); + return doc.model.edit(edits, { + selections, + }); } 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, @@ -1106,13 +1350,14 @@ export function transpose( rightText, [left, left], [newCursorPos, newCursorPos], - ]), - ], - { selections: [new ModelEditSelection(newCursorPos)] } - ); + ]) + ); + selections[index] = new ModelEditSelection(newCursorPos); + } } } - } + }); + return doc.model.edit(edits, { selections }); } export const bindingForms = [ @@ -1177,60 +1422,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]), - ], - { selections: [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]), - ], - { - selections: [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 = { @@ -1251,7 +1507,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); @@ -1279,151 +1535,191 @@ 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]]), - ], - { - selections: [new ModelEditSelection(newCursorPos)], - skipFormat: false, - undoStopBefore: true, +// 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 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, [p, p], [newCursorPos, newCursorPos]]) + ); + selections[index] = new ModelEditSelection(newCursorPos); + } + }); + void doc.model.edit(edits, { + selections, + 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') { - 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( - [ +// 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 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, [p, p], [newCursorPos, newCursorPos], ]), - new ModelEdit('deleteRange', [currentRange[0], deleteLength]), - ], - { - selections: [new ModelEditSelection(newCursorPos)], - skipFormat: false, - undoStopBefore: true, - } - ); - break; + new ModelEdit('deleteRange', [currentRange[0], deleteLength]) + ); + selections[index] = new ModelEditSelection(newCursorPos); + break; + } } - } + }); + 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; - 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]), - ], - { - selections: [new ModelEditSelection(newCursorPos)], - skipFormat: false, - undoStopBefore: true, +// 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, [p, p], [newCursorPos, newCursorPos]]), + new ModelEdit('deleteRange', [deleteStart, deleteLength]) + ); + selections[index] = new ModelEditSelection(newCursorPos); + } + }); + 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(); - const listEnd = cursor.offsetStart; +// 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], - ]), - ], - { - selections: [new ModelEditSelection(newCursorPos)], - skipFormat: false, - undoStopBefore: true, - } - ); - break; + ]) + ); + selections[index] = new ModelEditSelection(newCursorPos); + break; + } } - } + }); + void doc.model.edit(edits, { + selections, + skipFormat: false, + undoStopBefore: true, + }); } function adaptContentsToRichComment(contents: string): string { @@ -1434,7 +1730,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); diff --git a/src/doc-mirror/index.ts b/src/doc-mirror/index.ts index 7c52662c3..8586c621f 100644 --- a/src/doc-mirror/index.ts +++ b/src/doc-mirror/index.ts @@ -9,6 +9,7 @@ import { ModelEdit, ModelEditOptions, ModelEditSelection, + ModelEditResult, } from '../cursor-doc/model'; import { LispTokenCursor } from '../cursor-doc/token-cursor'; import * as utilities from '../utilities'; @@ -24,7 +25,7 @@ export class DocumentModel implements EditableModel { this.lineInputModel = new LineInputModel(this.lineEndingLength); } - edit(modelEdits: ModelEdit[], options: ModelEditOptions): Thenable { + edit(modelEdits: ModelEdit[], options: ModelEditOptions): Thenable { const editor = utilities.getActiveTextEditor(), undoStopBefore = !!options.undoStopBefore; return editor @@ -48,18 +49,18 @@ export class DocumentModel implements EditableModel { }, { undoStopBefore, undoStopAfter: false } ) - .then((isFulfilled) => { - if (isFulfilled) { + .then(async(success) => { + if (success) { if (options.selections) { this.document.selections = options.selections; } if (!options.skipFormat) { - return formatter.formatPosition(editor, false, { + 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 }; }); } @@ -122,23 +123,6 @@ export class DocumentModel implements EditableModel { export class MirroredDocument implements EditableDocument { constructor(public document: vscode.TextDocument) {} - get selections() { - return utilities - .tryToGetActiveTextEditor() - .selections.map( - ({ anchor, active }) => - new ModelEditSelection(this.document.offsetAt(anchor), this.document.offsetAt(active)) - ); - } - - get selectionLeft(): number { - return this.document.offsetAt(utilities.tryToGetActiveTextEditor().selection.anchor); - } - - get selectionRight(): number { - return this.document.offsetAt(utilities.getActiveTextEditor().selection.active); - } - model = new DocumentModel(this); selectionsStack: ModelEditSelection[][] = []; @@ -152,15 +136,15 @@ export class MirroredDocument implements EditableDocument { public insertString(text: string) { const editor = utilities.tryToGetActiveTextEditor(), - selection = editor.selection, + selections = editor.selections, wsEdit = new vscode.WorkspaceEdit(), - // TODO: prob prefer selection.active or .start + // TODO: prob prefer selection.active or .start 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.selections = [selections[0]]; + editor.selections = selections; }); } @@ -172,6 +156,16 @@ export class MirroredDocument implements EditableDocument { this.selections = [sel]; } + get selections(): ModelEditSelection[] { + const editor = utilities.getActiveTextEditor(), + 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; @@ -186,16 +180,6 @@ export class MirroredDocument implements EditableDocument { editor.revealRange(new vscode.Range(active, active)); } - get selections(): ModelEditSelection[] { - const editor = utilities.getActiveTextEditor(), - document = editor.document; - return editor.selections.map((sel) => { - const anchor = document.offsetAt(sel.anchor), - active = document.offsetAt(sel.active); - return new ModelEditSelection(anchor, active); - }); - } - public getSelectionTexts() { const editor = utilities.getActiveTextEditor(), selections = editor.selections; @@ -208,17 +192,11 @@ export class MirroredDocument implements EditableDocument { return this.document.getText(selection); } - public getSelectionsText() { - const editor = utilities.tryToGetActiveTextEditor(), - selections = editor.selections; - return selections.map((s) => this.document.getText(s)); - } - - public delete(): Thenable { + public delete(): Thenable { return vscode.commands.executeCommand('deleteRight'); } - public backspace(): Thenable { + public backspace(): Thenable { return vscode.commands.executeCommand('deleteLeft'); } } diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index 9304ad773..d826cda03 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -1,4 +1,5 @@ import * as model from '../../../cursor-doc/model'; +import { clone, entries, cond, toInteger, last, first, cloneDeep } from 'lodash'; /** * Text Notation for expressing states of a document, including @@ -6,13 +7,13 @@ import * as model from '../../../cursor-doc/model'; * * Since JavasScript makes it clumsy with multiline strings, * newlines are denoted with a middle dot 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 + * TODO: make it clearer that single | is just a shorthand for |>| + * * Single position selections 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. + * * Selections w/o direction are denoted with `|` (plus multi-cursor numbered variations) at the range's boundaries. + * * Selections with direction left->right are denoted with `|>|`, `|>|1`, `|>|2`, ... `|>|9` etc at the range boundaries + * * Selections with direction right->left are denoted with `|<|`, `|<|1`, `|<|2`, ... `|<|9` etc at the range boundaries */ - -function textNotationToTextAndSelection(s: string): [string, { anchor: number; active: number }] { +function _textNotationToTextAndSelection(s: string): [string, { anchor: number; active: number }] { const text = s.replace(/•/g, '\n').replace(/\|?[<>]?\|/g, ''); let anchor = undefined; let active = undefined; @@ -39,13 +40,66 @@ function textNotationToTextAndSelection(s: string): [string, { anchor: number; a return [text, { anchor, active }]; } +function textNotationToTextAndSelection(content: string): [string, model.ModelEditSelection[]] { + const text = clone(content) + .replace(/•/g, '\n') + .replace(/\|?[<>]?\|\d?/g, ''); + + // 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( + /(?(?:\|(?<|>)\|)|(?:\|))(?\d)?/g + )); + + // 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 = Array.from(matches).reduce((acc, curr, index) => { + const nextAcc = { ...acc }; + // const currRepositioned = cloneDeep(curr); + + 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 matchesForCursor = nextAcc[cursorMatchStr] ?? []; + nextAcc[cursorMatchStr] = [...matchesForCursor, curr]; + return nextAcc; + }, {} as { [key: string]: RegExpMatchArray[] }); + + return [ + text, + entries(cursorMatchInstances).map(([cursorMatchStr, 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); + }), + ]; +} + /** * 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 [text, selections] = _textNotationToTextAndSelection(s); const doc = new model.StringDocument(text); - doc.selection = new model.ModelEditSelection(selection.anchor, selection.active); + doc.selections = selections; + // doc.selections = [selections]; + // doc.selections = [new model.ModelEditSelection(selections.anchor, selections.active)]; return doc; } @@ -62,6 +116,7 @@ export function text(doc: model.StringDocument): string { * 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.StringDocument): [string, [number, number][]] { + // return [text(doc), [doc.selection.anchor, doc.selection.active]]; + return [text(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..946f73329 100644 --- a/src/extension-test/unit/cursor-doc/cursor-context-test.ts +++ b/src/extension-test/unit/cursor-doc/cursor-context-test.ts @@ -90,7 +90,7 @@ describe('Cursor Contexts', () => { ); expect(contexts.includes('calva:cursorBeforeComment')).toBe(true); }); - it('is false adjacent before comment on line with leading witespace and preceding comment line', () => { + 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); }); diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index 9e31e0450..7823b681a 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -3,7 +3,7 @@ import * as paredit from '../../../cursor-doc/paredit'; import * as model from '../../../cursor-doc/model'; import { docFromTextNotation, textAndSelection, text } from '../common/text-notation'; import { ModelEditSelection } from '../../../cursor-doc/model'; -import { last } from 'lodash'; +import { last, method } from 'lodash'; model.initScanner(20000); @@ -14,17 +14,18 @@ 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 b = docFromTextNotation('|(def foo [vec])|'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); @@ -176,8 +177,8 @@ 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]); }); @@ -193,8 +194,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]); }); @@ -389,12 +390,12 @@ describe('paredit', () => { 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]); + 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]); + expect(paredit.rangeToForwardList(a)).toEqual(textAndSelection(b)[1][0]); }); }); @@ -402,12 +403,12 @@ describe('paredit', () => { 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]); + 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]); + expect(paredit.rangeToBackwardList(a)).toEqual(textAndSelection(b)[1][0]); }); }); @@ -415,32 +416,32 @@ describe('paredit', () => { 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]); + 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]); + 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]); + 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]); + 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]); + 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]); + expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); }); @@ -448,28 +449,28 @@ describe('paredit', () => { 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]); + 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]); + 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]); + 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]); + 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]); + expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1][0]); }); }); }); @@ -478,13 +479,13 @@ describe('paredit', () => { 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); + 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); + void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); describe('Stacked readers', () => { @@ -494,16 +495,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); + void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); @@ -515,22 +516,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); + 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)]); }); }); }); @@ -542,44 +543,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 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); }); }); }); @@ -591,17 +598,17 @@ describe('paredit', () => { 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(); + const selectionBefore = startSelections.map((s) => s.clone()); paredit.growSelectionStack(doc, [range]); paredit.shrinkSelection(doc); - expect(last(doc.selectionsStack)).toEqual([selectionBefore]); + expect(last(doc.selectionsStack)).toEqual(selectionBefore); }); it('should not add selections identical to the topmost', () => { - const selectionBefore = doc.selection.clone(); + const selectionBefore = doc.selections.map((s) => s.clone()); paredit.growSelectionStack(doc, [range]); paredit.growSelectionStack(doc, [range]); paredit.shrinkSelection(doc); - expect(last(doc.selectionsStack)).toEqual([selectionBefore]); + expect(last(doc.selectionsStack)).toEqual(selectionBefore); }); it('should have A topmost after adding A, then B, then shrinking', () => { const a = range, @@ -625,14 +632,14 @@ 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); + 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); + void paredit.dragSexprBackward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); @@ -640,7 +647,7 @@ describe('paredit', () => { 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)); }); @@ -648,7 +655,7 @@ describe('paredit', () => { 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)); }); @@ -659,7 +666,7 @@ describe('paredit', () => { const b = docFromTextNotation( `(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)); }); @@ -670,7 +677,7 @@ describe('paredit', () => { const b = docFromTextNotation( `(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)); }); @@ -681,7 +688,7 @@ describe('paredit', () => { const b = docFromTextNotation( `(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)); }); @@ -692,7 +699,7 @@ describe('paredit', () => { const b = docFromTextNotation( `(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)); }); @@ -703,7 +710,7 @@ describe('paredit', () => { const a = docFromTextNotation( `(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)); }); }); @@ -712,37 +719,37 @@ 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); + 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); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); }); @@ -750,25 +757,25 @@ describe('paredit', () => { 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); + 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); + 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); + void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); }); @@ -776,19 +783,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); + void paredit.dragSexprForwardDown(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); }); @@ -796,13 +803,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); + void paredit.dragSexprForwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); }); @@ -810,19 +817,19 @@ 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 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)); }); }); @@ -832,13 +839,13 @@ describe('paredit', () => { 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', () => { @@ -846,13 +853,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)); }); }); @@ -860,7 +867,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)); }); }); @@ -870,68 +877,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); + 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); + 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); + 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)); }); }); @@ -942,13 +949,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', () => { @@ -956,7 +963,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)); }); }); @@ -967,19 +974,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); + 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', () => { @@ -987,7 +994,7 @@ 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)); }); }); @@ -996,13 +1003,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)); }); }); @@ -1012,46 +1019,46 @@ describe('paredit', () => { it('raises the current form when cursor is preceding', () => { const a = docFromTextNotation('(comment• (str |#(foo)))'); const b = docFromTextNotation('(comment• |#(foo))'); - paredit.raiseSexp(a); + 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); + void paredit.raiseSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); describe('Kill character backwards (backspace)', () => { - it('Leaves closing paren of empty list alone', () => { + it('Leaves closing paren of empty list alone', async () => { const a = docFromTextNotation('{::foo ()|• ::bar :foo}'); const b = docFromTextNotation('{::foo (|)• ::bar :foo}'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes closing paren if unbalance', () => { + it('Deletes closing paren if unbalance', async () => { const a = docFromTextNotation('{::foo )|• ::bar :foo}'); const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Leaves opening paren of non-empty list alone', () => { + it('Leaves opening paren of non-empty list alone', async () => { const a = docFromTextNotation('{::foo (|a)• ::bar :foo}'); const b = docFromTextNotation('{::foo |(a)• ::bar :foo}'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Leaves opening quote of non-empty string alone', () => { + it('Leaves opening quote of non-empty string alone', async () => { const a = docFromTextNotation('{::foo "|a"• ::bar :foo}'); const b = docFromTextNotation('{::foo |"a"• ::bar :foo}'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Leaves closing quote of non-empty string alone', () => { + it('Leaves closing quote of non-empty string alone', async () => { const a = docFromTextNotation('{::foo "a"|• ::bar :foo}'); const b = docFromTextNotation('{::foo "a|"• ::bar :foo}'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes contents in strings', () => { @@ -1129,10 +1136,10 @@ describe('paredit', () => { void 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', () => { @@ -1152,10 +1159,10 @@ describe('paredit', () => { void paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes closing paren if unbalance', () => { + it('Deletes closing paren if unbalance', async () => { const a = docFromTextNotation('{::foo |)• ::bar :foo}'); const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.deleteForward(a); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Leaves opening paren of non-empty list alone', () => { @@ -1176,16 +1183,16 @@ describe('paredit', () => { void paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes contents in strings', () => { + it('Deletes contents in strings', async () => { const a = docFromTextNotation('{::foo "|a"• ::bar :foo}'); const b = docFromTextNotation('{::foo "|"• ::bar :foo}'); - void paredit.deleteForward(a); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes contents in strings 2', () => { + it('Deletes contents in strings 2', async () => { const a = docFromTextNotation('{::foo "|aa"• ::bar :foo}'); const b = docFromTextNotation('{::foo "|a"• ::bar :foo}'); - void paredit.deleteForward(a); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes quoted quote', () => { @@ -1200,10 +1207,10 @@ describe('paredit', () => { void paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes contents in list', () => { + it('Deletes contents in list', async () => { const a = docFromTextNotation('{::foo (|a)• ::bar :foo}'); const b = docFromTextNotation('{::foo (|)• ::bar :foo}'); - void paredit.deleteForward(a); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes empty list function', () => { 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..b1ad3c486 100644 --- a/src/extension-test/unit/cursor-doc/token-cursor-test.ts +++ b/src/extension-test/unit/cursor-doc/token-cursor-test.ts @@ -7,16 +7,16 @@ describe('Token Cursor', () => { it('it moves past whitespace', () => { const a = docFromTextNotation('a •|c'); const b = docFromTextNotation('a| •c'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + 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 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); }); }); @@ -24,115 +24,115 @@ describe('Token Cursor', () => { 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 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 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 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 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 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); }); }); @@ -140,79 +140,79 @@ describe('Token Cursor', () => { 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 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 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 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 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 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 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 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 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 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,151 +265,151 @@ 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 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 a = docFromTextNotation('(((c•(#b •[:f])•#z•|a)))'); const b = docFromTextNotation('(((|c•(#b •[:f])•#z•1)))'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + 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 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.selection.anchor); + 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 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.selection.anchor); + 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); }); }); @@ -417,68 +417,68 @@ describe('Token Cursor', () => { 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 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 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 a = docFromTextNotation('({:a [(c•(#|b •[:f])•#z•|a)]})'); const b = docFromTextNotation('({:a [(c•(#b •[:f])•#z•1)]|})'); - 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(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 a = docFromTextNotation('([#{c•(#b •[:f])•#z•|a}])'); const b = docFromTextNotation('(|[#{c•(#b •[:f])•#z•1}])'); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.anchor); + 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 a = docFromTextNotation('([(c•(#b •[:f])•#z•|a)])'); const b = docFromTextNotation('([|(c•(#b •[:f])•#z•1)])'); - 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('Finds start of map', () => { - const a = docFromTextNotation('({:a [(c•(#b •[:f])•#z•|1)]})'); + const a = docFromTextNotation('({:a [(c•(#b •[:f])•#z•|a)]})'); const b = docFromTextNotation('({|:a [(c•(#b •[:f])•#z•1)]})'); - 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 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,17 +505,17 @@ 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); }); }); @@ -523,9 +523,9 @@ describe('Token Cursor', () => { 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 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,23 +533,23 @@ 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); }); }); @@ -557,16 +557,16 @@ describe('Token Cursor', () => { 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 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 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); }); }); @@ -574,165 +574,165 @@ describe('Token Cursor', () => { 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(); }); }); @@ -744,8 +744,8 @@ describe('Token Cursor', () => { 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)' ); - 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( @@ -754,8 +754,8 @@ describe('Token Cursor', () => { 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)|' ); - 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( @@ -764,14 +764,14 @@ describe('Token Cursor', () => { 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)' ); - 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( @@ -780,21 +780,21 @@ describe('Token Cursor', () => { 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)' ); - const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun(a.selection.active, false)).toEqual(textAndSelection(b)[1]); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active, false)).toEqual(textAndSelection(b)[1][0]); }); 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( @@ -803,33 +803,33 @@ describe('Token Cursor', () => { 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)|' ); - 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 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( @@ -839,25 +839,25 @@ describe('Token Cursor', () => { '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 +865,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 3565c3246..220940d9d 100644 --- a/src/extension-test/unit/util/cursor-get-text-test.ts +++ b/src/extension-test/unit/util/cursor-get-text-test.ts @@ -7,8 +7,8 @@ describe('get text', () => { 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 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), ]); @@ -17,8 +17,8 @@ describe('get text', () => { 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 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), ]); @@ -28,8 +28,8 @@ describe('get text', () => { // 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 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), ]); @@ -38,8 +38,8 @@ describe('get text', () => { 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 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), ]); @@ -50,7 +50,7 @@ describe('get text', () => { 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 range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; expect(getText.currentTopLevelForm(a)).toEqual([range, b.model.getText(...range)]); }); }); @@ -59,7 +59,7 @@ describe('get text', () => { 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 range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; const trail = ']'; expect(getText.currentEnclosingFormToCursor(a)).toEqual([ range, @@ -72,7 +72,7 @@ describe('get text', () => { 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 range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; const trail = '])'; expect(getText.currentTopLevelFormToCursor(a)).toEqual([ range, @@ -87,7 +87,7 @@ describe('get text', () => { const b = docFromTextNotation( '|(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, @@ -101,7 +101,7 @@ describe('get text', () => { const b = docFromTextNotation( '|(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/paredit/extension.ts b/src/paredit/extension.ts index db543ea2e..7adf77d7e 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -47,49 +47,73 @@ const pareditCommands: PareditCommand[] = [ { command: 'paredit.forwardSexp', handler: (doc: EditableDocument) => { - paredit.moveToRangeRight(doc, paredit.forwardSexpRange(doc)); + paredit.moveToRangeRight( + doc, + doc.selections.map((s) => paredit.forwardSexpRange(doc, s.active)) + ); }, }, { command: 'paredit.backwardSexp', handler: (doc: EditableDocument) => { - paredit.moveToRangeLeft(doc, paredit.backwardSexpRange(doc)); + paredit.moveToRangeLeft( + doc, + doc.selections.map((s) => paredit.backwardSexpRange(doc, 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)) + ); }, }, { 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)) + ); }, }, @@ -97,7 +121,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)) + ); }, }, { @@ -215,74 +242,95 @@ const pareditCommands: PareditCommand[] = [ { command: 'paredit.killRight', handler: (doc: EditableDocument) => { - const range = paredit.forwardHybridSexpRange(doc); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - paredit.killRange(doc, range); + // doc.selections.forEach((s) => { + // const range = paredit.forwardHybridSexpRange(doc, s.active); + paredit.forwardHybridSexpRange(doc).forEach((range) => { + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, range); + } + paredit.killRange(doc, range); + }); }, }, { command: 'paredit.killSexpForward', handler: (doc: EditableDocument) => { - const range = paredit.forwardSexpRange(doc); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - paredit.killRange(doc, range); + // doc.selections.forEach(s => { + // const range = paredit.forwardSexpRange(doc, s.active); + paredit.forwardSexpRange(doc).forEach((range) => { + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, range); + } + paredit.killRange(doc, range); + }); }, }, { command: 'paredit.killSexpBackward', handler: (doc: EditableDocument) => { - const range = paredit.backwardSexpRange(doc); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - paredit.killRange(doc, range); + doc.selections.forEach((s) => { + const range = paredit.backwardSexpRange(doc, s.active); + + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, range); + } + paredit.killRange(doc, range); + }); }, }, { command: 'paredit.killListForward', handler: (doc: EditableDocument) => { - const range = paredit.forwardListRange(doc); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - return paredit.killForwardList(doc, range); + doc.selections.forEach((s) => { + const range = paredit.forwardListRange(doc, s.active); + + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, range); + } + void paredit.killForwardList(doc, range); + }); }, }, // TODO: Implement with killRange { command: 'paredit.killListBackward', handler: (doc: EditableDocument) => { - const range = paredit.backwardListRange(doc); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - return paredit.killBackwardList(doc, range); + doc.selections.forEach((s) => { + const range = paredit.backwardListRange(doc, s.active); + + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, range); + } + void paredit.killBackwardList(doc, range); + }); }, }, // TODO: Implement with killRange { command: 'paredit.spliceSexpKillForward', handler: (doc: EditableDocument) => { - const range = paredit.forwardListRange(doc); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - void paredit.killForwardList(doc, range).then((isFulfilled) => { - return paredit.spliceSexp(doc, doc.selection.active, false); + doc.selections.forEach((s) => { + const range = paredit.forwardListRange(doc, s.active); + + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, range); + } + void paredit.killForwardList(doc, range).then((isFulfilled) => { + return paredit.spliceSexp(doc, /* s.active, */ false); + }); }); }, }, { command: 'paredit.spliceSexpKillBackward', handler: (doc: EditableDocument) => { - const range = paredit.backwardListRange(doc); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - void paredit.killBackwardList(doc, range).then((isFulfilled) => { - return paredit.spliceSexp(doc, doc.selection.active, false); + doc.selections.forEach((s) => { + const range = paredit.backwardListRange(doc, s.active); + + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, range); + } + void paredit.killBackwardList(doc, range).then((isFulfilled) => { + return paredit.spliceSexp(doc, /* s.active, */ false); + }); }); }, }, diff --git a/src/util/array.ts b/src/util/array.ts new file mode 100644 index 000000000..0480a8cbd --- /dev/null +++ b/src/util/array.ts @@ -0,0 +1,12 @@ +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, +}); 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 1fdaff4f5..c1df31289 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