From a4823514bd61ed48d1c5af85e1ad9474e134734e Mon Sep 17 00:00:00 2001 From: Rayat Date: Sat, 19 Mar 2022 19:42:52 -0700 Subject: [PATCH 01/49] fixes #1607 selection api cleanup --- src/cursor-doc/model.ts | 6 +++--- src/cursor-doc/paredit.ts | 2 +- src/doc-mirror/index.ts | 10 +++++----- src/extension-test/unit/util/cursor-get-text-test.ts | 2 +- src/paredit/extension.ts | 8 ++++---- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index fcca51ff1..97699423b 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -1,7 +1,7 @@ -import { Scanner, Token, ScannerState } from './clojure-lexer'; -import { LispTokenCursor } from './token-cursor'; -import { deepEqual as equal } from '../util/object'; import { isUndefined } from 'lodash'; +import { deepEqual as equal } from '../util/object'; +import { Scanner, ScannerState, Token } from './clojure-lexer'; +import { LispTokenCursor } from './token-cursor'; let scanner: Scanner; diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index cba3ac35e..1175092b7 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -1,5 +1,5 @@ import { validPair } from './clojure-lexer'; -import { ModelEdit, EditableDocument, ModelEditSelection } from './model'; +import { EditableDocument, ModelEdit, ModelEditSelection } from './model'; import { LispTokenCursor } from './token-cursor'; // NB: doc.model.edit returns a Thenable, so that the vscode Editor can compose commands. diff --git a/src/doc-mirror/index.ts b/src/doc-mirror/index.ts index 4d6ec962f..f8402f650 100644 --- a/src/doc-mirror/index.ts +++ b/src/doc-mirror/index.ts @@ -1,17 +1,17 @@ export { getIndent } from '../cursor-doc/indent'; +import { isUndefined } from 'lodash'; import * as vscode from 'vscode'; -import * as utilities from '../utilities'; import * as formatter from '../calva-fmt/src/format'; -import { LispTokenCursor } from '../cursor-doc/token-cursor'; import { - ModelEdit, EditableDocument, EditableModel, - ModelEditOptions, LineInputModel, + ModelEdit, + ModelEditOptions, ModelEditSelection, } from '../cursor-doc/model'; -import { isUndefined } from 'lodash'; +import { LispTokenCursor } from '../cursor-doc/token-cursor'; +import * as utilities from '../utilities'; const documents = new Map(); diff --git a/src/extension-test/unit/util/cursor-get-text-test.ts b/src/extension-test/unit/util/cursor-get-text-test.ts index 936d28742..3565c3246 100644 --- a/src/extension-test/unit/util/cursor-get-text-test.ts +++ b/src/extension-test/unit/util/cursor-get-text-test.ts @@ -1,6 +1,6 @@ import * as expect from 'expect'; -import { docFromTextNotation } from '../common/text-notation'; import * as getText from '../../../util/cursor-get-text'; +import { docFromTextNotation } from '../common/text-notation'; describe('get text', () => { describe('getTopLevelFunction', () => { diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 57e85031c..db7547e25 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -1,19 +1,19 @@ 'use strict'; -import { StatusBar } from './statusbar'; import * as vscode from 'vscode'; import { commands, - window, + ConfigurationChangeEvent, Event, EventEmitter, ExtensionContext, + window, workspace, - ConfigurationChangeEvent, } from 'vscode'; +import { EditableDocument } from '../cursor-doc/model'; import * as paredit from '../cursor-doc/paredit'; import * as docMirror from '../doc-mirror/index'; -import { EditableDocument } from '../cursor-doc/model'; import { assertIsDefined } from '../utilities'; +import { StatusBar } from './statusbar'; const onPareditKeyMapChangedEmitter = new EventEmitter(); From 815e4bf008c84eb261fd5d1fec10f5c4d973908a Mon Sep 17 00:00:00 2001 From: Rayat Date: Sat, 26 Mar 2022 16:21:51 -0700 Subject: [PATCH 02/49] reformat + undo auto import organize --- src/paredit/extension.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index db7547e25..57e85031c 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -1,19 +1,19 @@ 'use strict'; +import { StatusBar } from './statusbar'; import * as vscode from 'vscode'; import { commands, - ConfigurationChangeEvent, + window, Event, EventEmitter, ExtensionContext, - window, workspace, + ConfigurationChangeEvent, } from 'vscode'; -import { EditableDocument } from '../cursor-doc/model'; import * as paredit from '../cursor-doc/paredit'; import * as docMirror from '../doc-mirror/index'; +import { EditableDocument } from '../cursor-doc/model'; import { assertIsDefined } from '../utilities'; -import { StatusBar } from './statusbar'; const onPareditKeyMapChangedEmitter = new EventEmitter(); From 87c6d4910c6503ddd28702ebd4611c08e29e90b8 Mon Sep 17 00:00:00 2001 From: Rayat Date: Sat, 19 Mar 2022 15:36:25 -0700 Subject: [PATCH 03/49] Multi Cursor for paredit Expand selection and shrink selection Fixes #610 --- .vscode/settings.json | 6 +- src/cursor-doc/model.ts | 38 ++- src/cursor-doc/paredit.ts | 280 ++++++++++++------ src/doc-mirror/index.ts | 40 ++- .../unit/cursor-doc/paredit-test.ts | 12 +- 5 files changed, 258 insertions(+), 118 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a27c1afc..899767fe8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -203,7 +203,11 @@ ], "cSpell.languageSettings": [ { - "languageId": ["clojure", "json", "typescript"], + "languageId": [ + "clojure", + "json", + "typescript" + ], "allowCompoundWords": false } ], diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index 97699423b..8c8340ca2 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -1,4 +1,4 @@ -import { isUndefined } from 'lodash'; +import { isUndefined, max, min } from 'lodash'; import { deepEqual as equal } from '../util/object'; import { Scanner, ScannerState, Token } from './clojure-lexer'; import { LispTokenCursor } from './token-cursor'; @@ -84,6 +84,7 @@ export type ModelEditOptions = { undoStopBefore?: boolean; formatDepth?: number; skipFormat?: boolean; + selections?: ModelEditSelection[]; selection?: ModelEditSelection; }; @@ -105,11 +106,14 @@ export interface EditableModel { export interface EditableDocument { selection: ModelEditSelection; + selections: ModelEditSelection[]; model: EditableModel; + selectionsStack: ModelEditSelection[][]; selectionStack: ModelEditSelection[]; getTokenCursor: (offset?: number, previous?: boolean) => LispTokenCursor; insertString: (text: string) => void; - getSelectionText: () => string; + getSelectionText: (index?: number) => string; + getSelectionsText: () => string[]; delete: () => Thenable; backspace: () => Thenable; } @@ -536,6 +540,8 @@ export class StringDocument implements EditableDocument { model: LineInputModel = new LineInputModel(1, this); + selectionsStack: ModelEditSelection[][] = []; + selections: ModelEditSelection[] = []; selectionStack: ModelEditSelection[] = []; getTokenCursor(offset?: number, previous?: boolean): LispTokenCursor { @@ -546,23 +552,35 @@ export class StringDocument implements EditableDocument { return this.model.getTokenCursor(offset); } + getSelectionsText: () => string[]; insertString(text: string) { this.model.insertString(0, text); } - getSelectionText: () => string; - delete() { - const p = this.selection.anchor; - return this.model.edit([new ModelEdit('deleteRange', [p, 1])], { - selection: new ModelEditSelection(p), + const edits = []; + const selections = []; + this.selections.forEach(({ anchor: p }) => { + edits.push(new ModelEdit('deleteRange', [p, 1])); + selections.push(new ModelEditSelection(p)); + }); + + return this.model.edit(edits, { + selections, }); } + getSelectionText: () => string; backspace() { - const p = this.selection.anchor; - return this.model.edit([new ModelEdit('deleteRange', [p - 1, 1])], { - selection: new ModelEditSelection(p - 1), + const edits = []; + const selections = []; + this.selections.forEach(({ anchor: p }) => { + edits.push(new ModelEdit('deleteRange', [p - 1, 1])); + selections.push(new ModelEditSelection(p - 1)); + }); + + return this.model.edit(edits, { + selections, }); } } diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 1175092b7..96bf4424f 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -1,6 +1,8 @@ +import { isArray, isEqual, isNumber, last, pick, property } from 'lodash'; import { validPair } from './clojure-lexer'; import { EditableDocument, ModelEdit, ModelEditSelection } from './model'; import { LispTokenCursor } from './token-cursor'; +import _ = require('lodash'); // 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. @@ -32,36 +34,48 @@ export function moveToRangeRight(doc: EditableDocument, range: [number, number]) doc.selection = new ModelEditSelection(Math.max(range[0], range[1])); } -export function selectRange(doc: EditableDocument, range: [number, number]) { - growSelectionStack(doc, range); +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 selectRangeForward(doc: EditableDocument, range: [number, number]) { - const selectionLeft = doc.selection.anchor; - const rangeRight = Math.max(range[0], range[1]); - growSelectionStack(doc, [selectionLeft, rangeRight]); +export function selectRangeForward( + doc: EditableDocument, + range: [number, number] +) { + const selectionLeft = doc.selection.anchor; + const rangeRight = Math.max(range[0], range[1]); + growSelectionStack(doc, [[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, + range: [number, number] +) { + const selectionRight = doc.selection.anchor; + const rangeLeft = Math.min(range[0], range[1]); + growSelectionStack(doc, [[selectionRight, rangeLeft]]); } export function selectForwardSexp(doc: EditableDocument) { - const rangeFn = - doc.selection.active >= doc.selection.anchor - ? forwardSexpRange - : (doc: EditableDocument) => forwardSexpRange(doc, doc.selection.active, true); - selectRangeForward(doc, rangeFn(doc)); + const rangeFn = + doc.selection.active >= doc.selection.anchor + ? forwardSexpRange + : (doc: EditableDocument) => + forwardSexpRange(doc, doc.selection.active, true); + selectRangeForward(doc, rangeFn(doc)); } export function selectRight(doc: EditableDocument) { - const rangeFn = - doc.selection.active >= doc.selection.anchor - ? forwardHybridSexpRange - : (doc: EditableDocument) => forwardHybridSexpRange(doc, doc.selection.active, true); - selectRangeForward(doc, rangeFn(doc)); + const rangeFn = + doc.selection.active >= doc.selection.anchor + ? forwardHybridSexpRange + : (doc: EditableDocument) => + forwardHybridSexpRange(doc, doc.selection.active, true); + selectRangeForward(doc, rangeFn(doc)); } export function selectForwardSexpOrUp(doc: EditableDocument) { @@ -74,19 +88,22 @@ export function selectForwardSexpOrUp(doc: EditableDocument) { } export function selectBackwardSexp(doc: EditableDocument) { - const rangeFn = - doc.selection.active <= doc.selection.anchor - ? backwardSexpRange - : (doc: EditableDocument) => backwardSexpRange(doc, doc.selection.active, false); - selectRangeBackward(doc, rangeFn(doc)); + const rangeFn = + doc.selection.active <= doc.selection.anchor + ? backwardSexpRange + : (doc: EditableDocument) => + backwardSexpRange(doc, doc.selection.active, false); + selectRangeBackward(doc, rangeFn(doc)); } export function selectForwardDownSexp(doc: EditableDocument) { - const rangeFn = - doc.selection.active >= doc.selection.anchor - ? (doc: EditableDocument) => rangeToForwardDownList(doc, doc.selection.active, true) - : (doc: EditableDocument) => rangeToForwardDownList(doc, doc.selection.active, true); - selectRangeForward(doc, rangeFn(doc)); + const 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)); } export function selectBackwardDownSexp(doc: EditableDocument) { @@ -901,85 +918,160 @@ export function stringQuote( } } +/** + * Given the set of selections in the given document, + * edit the set of selections therein such that for each selection: + * the selection expands to encompass just the contents of the form + * (where this selection or its cursor lies), the entire form itself + * (including open/close symbols) or the full contents of the form + * immediately enclosing this one, repeating each time the function is + * called, for each selection in the doc. + * + * (Or in other words, the S-expression powered equivalent to vs-code's + * built-in Expand Selection/Shrink Selection commands) + */ export function growSelection( + doc: EditableDocument, doc: EditableDocument, start: number = doc.selection.anchor, end: number = doc.selection.active ) { - const startC = doc.getTokenCursor(start), - endC = doc.getTokenCursor(end), - emptySelection = startC.equals(endC); - - if (emptySelection) { - const currentFormRange = startC.rangeForCurrentForm(start); - if (currentFormRange) { - growSelectionStack(doc, currentFormRange); - } - } else { - if (startC.getPrevToken().type == 'open' && endC.getToken().type == 'close') { - startC.backwardList(); - startC.backwardUpList(); - endC.forwardList(); - growSelectionStack(doc, [startC.offsetStart, endC.offsetEnd]); - } 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(); + 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; + } 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; + } } - } - growSelectionStack(doc, [startC.offsetStart, endC.offsetEnd]); - } - } + }) + growSelectionStack(doc, newRanges); } -export function growSelectionStack(doc: EditableDocument, range: [number, number]) { - const [start, end] = range; - if (doc.selectionStack.length > 0) { - const prev = doc.selectionStack[doc.selectionStack.length - 1]; - if (!(doc.selection.anchor == prev.anchor && doc.selection.active == prev.active)) { - setSelectionStack(doc); - } else if (prev.anchor === range[0] && prev.active === range[1]) { - return; +/** + * When growing a stack, we want a temporary selection undo/redo history, + * so to speak, such that each time the growSelection (labelled "Expand Selection") + * command is invoked, the selection keeps expanding by s-expression level, without + * "losing" prior selections. + * + * Recall that one cannot arbitrarily "Shrink Selection" _without_ such a history stack + * or outside of the context of a series of "Expand Selection" operations, + * as there is no way to know to which symbol(s) a user could hypothetically intend on + * "zooming in" on via selection shrinking. + * + * Thus to implement the above, we store a stack of selections (each item in the stack + * is an array of selection items, one per cursor) and grow or traverse this stack + * as the user expands or shrinks their selection(s). + * + * @param doc EditableDocument + * @param ranges the new ranges to grow the selection into + * @returns + */ +export function growSelectionStack( + doc: EditableDocument, + ranges: Array<(readonly [number, number])>, +) { + // const [start, end] = range; + // 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]; } - } else { - doc.selectionStack = [doc.selection]; - } - doc.selection = new ModelEditSelection(start, end); - doc.selectionStack.push(doc.selection); + doc.selections = ranges.map((range) => new ModelEditSelection(...range)); + doc.selectionsStack.push(doc.selections); } +// FIXME(multi-cursor): prob needs rethinking export function shrinkSelection(doc: EditableDocument) { - if (doc.selectionStack.length) { - const latest = doc.selectionStack.pop(); - if ( - doc.selectionStack.length && - latest.anchor == doc.selection.anchor && - latest.active == doc.selection.active - ) { - doc.selection = doc.selectionStack[doc.selectionStack.length - 1]; + 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, selection = doc.selection) { - doc.selectionStack = [selection]; +export function setSelectionStack( + doc: EditableDocument, + selections: ModelEditSelection[] = doc.selections +) { + doc.selectionsStack = [selections]; } export function raiseSexp( diff --git a/src/doc-mirror/index.ts b/src/doc-mirror/index.ts index f8402f650..4c9c08329 100644 --- a/src/doc-mirror/index.ts +++ b/src/doc-mirror/index.ts @@ -122,24 +122,44 @@ 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[][] = []; selectionStack: ModelEditSelection[] = []; public getTokenCursor( - offset: number = this.selection.active, + offset: number = this.selections[0].active, previous: boolean = false ): LispTokenCursor { return this.model.getTokenCursor(offset, previous); } public insertString(text: string) { - const editor = utilities.getActiveTextEditor(), + const editor = utilities.tryToGetActiveTextEditor(), selection = editor.selection, wsEdit = new vscode.WorkspaceEdit(), // TODO: prob prefer selection.active or .start - edit = vscode.TextEdit.insert(this.document.positionAt(this.selection.anchor), text); - wsEdit.set(this.document.uri, [edit]); + edits = this.selections.map(({ anchor: left }) => + vscode.TextEdit.insert(this.document.positionAt(left), text) + ); + wsEdit.set(this.document.uri, edits); void vscode.workspace.applyEdit(wsEdit).then((_v) => { editor.selection = selection; }); @@ -162,12 +182,18 @@ export class MirroredDocument implements EditableDocument { return new ModelEditSelection(anchor, active); } - public getSelectionText() { - const editor = utilities.getActiveTextEditor(), - selection = editor.selection; + public getSelectionText(index: number = 0) { + const editor = utilities.tryToGetActiveTextEditor(), + selection = editor.selections[index]; 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 { return vscode.commands.executeCommand('deleteRight'); } diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index eff0299dc..9ff824669 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -693,29 +693,29 @@ describe('paredit', () => { describe('selection stack', () => { const range = [15, 20] as [number, number]; it('should make grow selection the topmost element on the stack', () => { - paredit.growSelectionStack(doc, range); + paredit.growSelectionStack(doc, [range]); expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual( new ModelEditSelection(range[0], range[1]) ); }); it('get us back to where we started if we just grow, then shrink', () => { const selectionBefore = startSelection.clone(); - paredit.growSelectionStack(doc, range); + paredit.growSelectionStack(doc, [range]); paredit.shrinkSelection(doc); expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual(selectionBefore); }); it('should not add selections identical to the topmost', () => { const selectionBefore = doc.selection.clone(); - paredit.growSelectionStack(doc, range); - paredit.growSelectionStack(doc, range); + paredit.growSelectionStack(doc, [range]); + paredit.growSelectionStack(doc, [range]); paredit.shrinkSelection(doc); expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual(selectionBefore); }); it('should have A topmost after adding A, then B, then shrinking', () => { const a = range, b: [number, number] = [10, 24]; - paredit.growSelectionStack(doc, a); - paredit.growSelectionStack(doc, b); + paredit.growSelectionStack(doc, [a]); + paredit.growSelectionStack(doc, [b]); paredit.shrinkSelection(doc); expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual( new ModelEditSelection(a[0], a[1]) From 461640503c3c02605578ae62ff3c900f8f84fbac Mon Sep 17 00:00:00 2001 From: Rayat Date: Sat, 19 Mar 2022 22:17:49 -0700 Subject: [PATCH 04/49] 2nd attempt multi cursor paredit start --- src/cursor-doc/model.ts | 62 ++++++++-------- src/cursor-doc/paredit.ts | 71 +++++++++---------- src/doc-mirror/index.ts | 54 +++++++++----- .../unit/cursor-doc/paredit-test.ts | 13 ++-- 4 files changed, 111 insertions(+), 89 deletions(-) diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index 8c8340ca2..f104cfcc6 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -84,8 +84,8 @@ export type ModelEditOptions = { undoStopBefore?: boolean; formatDepth?: number; skipFormat?: boolean; + // selection?: ModelEditSelection; selections?: ModelEditSelection[]; - selection?: ModelEditSelection; }; export interface EditableModel { @@ -108,12 +108,24 @@ export interface EditableDocument { selection: ModelEditSelection; selections: ModelEditSelection[]; model: EditableModel; + // selectionStack: ModelEditSelection[]; + /** + * A stack of selections - that is, a 2d array, where the outer array index is a point in "selection/form nesting order" and the inner array index is which cursor that ModelEditSelection belongs to. That "selection/form nesting order" axis can be thought of as the axis for time, or something close to that. That is, .selectionStacks + * is only used when the user invokes the "Expand Selection" or "Shrink Selection" Paredit commands, such that each time the user invokes "Expand", it pushes an item onto the stack. Similarly, when "Shrink" is invoked, the last item + * is popped. In essence, it's sort of an undo/history/time stack for the selection of forms/text in the current document. + * + * Each item in the stack however, is not a single selection, but rather an array of selections, one for each active cursor. Recall that vscode users may have as many cursors as they like, and will expect that selection expansion/shrinking commands should work equally well for each cursor, with respect to their particular locations, as they do outside of Paredit, eg with vscode's native selection expansion/shrinking commands. + * + * A further detail is that, along the "selection/form nesting order" axis, the selections are not an undo history of + * the user's arbitrary selections anywhere in the document, but specifically of the order in which selection expansion operates. That is, if we for simplicity pretend there can only be one cursor, each selection stack is a stack whose items hold forms/s-exps, such that each item is the form immediately enclosing the previous one. As such, we can imagine traversing forward (towards the top/right) of the stack + * as representing expanding the selection of forms by each nesting level, and backwards as shrinking the selection back down to the starting form/cursor position. + * + */ selectionsStack: ModelEditSelection[][]; - selectionStack: ModelEditSelection[]; getTokenCursor: (offset?: number, previous?: boolean) => LispTokenCursor; insertString: (text: string) => void; - getSelectionText: (index?: number) => string; - getSelectionsText: () => string[]; + getSelectionText: () => string; + getSelectionTexts: () => string[]; delete: () => Thenable; backspace: () => Thenable; } @@ -383,8 +395,8 @@ export class LineInputModel implements EditableModel { break; } } - if (this.document && options.selection) { - this.document.selection = options.selection; + if (this.document && options.selections) { + this.document.selections = [options.selections[0]]; } resolve(true); }); @@ -537,12 +549,11 @@ export class StringDocument implements EditableDocument { } selection: ModelEditSelection; + selections: ModelEditSelection[]; model: LineInputModel = new LineInputModel(1, this); selectionsStack: ModelEditSelection[][] = []; - selections: ModelEditSelection[] = []; - selectionStack: ModelEditSelection[] = []; getTokenCursor(offset?: number, previous?: boolean): LispTokenCursor { if (isUndefined(offset)) { @@ -557,30 +568,25 @@ export class StringDocument implements EditableDocument { this.model.insertString(0, text); } - delete() { - const edits = []; - const selections = []; - this.selections.forEach(({ anchor: p }) => { - edits.push(new ModelEdit('deleteRange', [p, 1])); - selections.push(new ModelEditSelection(p)); - }); + getSelectionTexts: () => string[]; + getSelectionText: () => string; - return this.model.edit(edits, { - selections, - }); + 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: () => string; backspace() { - const edits = []; - const selections = []; - this.selections.forEach(({ anchor: p }) => { - edits.push(new ModelEdit('deleteRange', [p - 1, 1])); - selections.push(new ModelEditSelection(p - 1)); - }); - - return this.model.edit(edits, { - selections, - }); + 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)), + } + ); } } diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 96bf4424f..6d53ad3d1 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -2,7 +2,7 @@ import { isArray, isEqual, isNumber, last, pick, property } from 'lodash'; import { validPair } from './clojure-lexer'; import { EditableDocument, ModelEdit, ModelEditSelection } from './model'; import { LispTokenCursor } from './token-cursor'; -import _ = require('lodash'); +import { last } from 'lodash'; // 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. @@ -22,16 +22,16 @@ export function killRange( ) { const [left, right] = [Math.min(...range), Math.max(...range)]; void doc.model.edit([new ModelEdit('deleteRange', [left, right - left, [start, end]])], { - selection: new ModelEditSelection(left), + selections: [new ModelEditSelection(left)], }); } export function moveToRangeLeft(doc: EditableDocument, range: [number, number]) { - doc.selection = new ModelEditSelection(Math.min(range[0], range[1])); + doc.selections = [new ModelEditSelection(Math.min(range[0], range[1]))]; } export function moveToRangeRight(doc: EditableDocument, range: [number, number]) { - doc.selection = new ModelEditSelection(Math.max(range[0], range[1])); + doc.selections = [new ModelEditSelection(Math.max(range[0], range[1]))]; } export function selectRange(doc: EditableDocument, range: [number, number] | Array) { @@ -476,7 +476,7 @@ export function wrapSexpr( ]), ], { - selection: new ModelEditSelection(start + open.length), + selections: [new ModelEditSelection(start + open.length)], skipFormat: options.skipFormat, } ); @@ -490,7 +490,7 @@ export function wrapSexpr( new ModelEdit('insertString', [range[0], open]), ], { - selection: new ModelEditSelection(start + open.length), + selections: [new ModelEditSelection(start + open.length)], skipFormat: options.skipFormat, } ); @@ -516,7 +516,7 @@ export function rewrapSexpr( new ModelEdit('changeRange', [closeStart, closeEnd, close]), new ModelEdit('changeRange', [openStart, openEnd, open]), ], - { selection: new ModelEditSelection(end) } + { selections: [new ModelEditSelection(end)] } ); } } @@ -533,7 +533,7 @@ export function splitSexp(doc: EditableDocument, start: number = doc.selection.a if (cursor.forwardList()) { const close = cursor.getToken().raw; void doc.model.edit([new ModelEdit('changeRange', [splitPos, splitPos, `${close}${open}`])], { - selection: new ModelEditSelection(splitPos + 1), + selections: [new ModelEditSelection(splitPos + 1)], }); } } @@ -567,7 +567,7 @@ export function joinSexp( [prevEnd, prevEnd], ]), ], - { selection: new ModelEditSelection(prevEnd), formatDepth: 2 } + { selections: [new ModelEditSelection(prevEnd)], formatDepth: 2 } ); } } @@ -594,7 +594,7 @@ export function spliceSexp( new ModelEdit('changeRange', [end, end + close.raw.length, '']), new ModelEdit('changeRange', [beginning - open.raw.length, beginning, '']), ], - { undoStopBefore, selection: new ModelEditSelection(start - 1) } + { undoStopBefore, selections: [new ModelEditSelection(start - 1)] } ); } } @@ -607,7 +607,7 @@ export function killBackwardList( return doc.model.edit( [new ModelEdit('changeRange', [start, end, '', [end, end], [start, start]])], { - selection: new ModelEditSelection(start), + selections: [new ModelEditSelection(start)], } ); } @@ -630,7 +630,7 @@ export function killForwardList( [start, start], ]), ], - { selection: new ModelEditSelection(start) } + { selections: [new ModelEditSelection(start)] } ); } @@ -730,7 +730,7 @@ export function forwardBarfSexp(doc: EditableDocument, start: number = doc.selec ], start >= cursor.offsetStart ? { - selection: new ModelEditSelection(cursor.offsetStart), + selections: [new ModelEditSelection(cursor.offsetStart)], formatDepth: 2, } : { formatDepth: 2 } @@ -756,7 +756,7 @@ export function backwardBarfSexp(doc: EditableDocument, start: number = doc.sele ], start <= cursor.offsetStart ? { - selection: new ModelEditSelection(cursor.offsetStart), + selections: [new ModelEditSelection(cursor.offsetStart)], formatDepth: 2, } : { formatDepth: 2 } @@ -799,7 +799,7 @@ export function close(doc: EditableDocument, close: string, start: number = doc. // Do nothing when there is balance } else { void doc.model.edit([new ModelEdit('insertString', [start, close])], { - selection: new ModelEditSelection(start + close.length), + selections: [new ModelEditSelection(start + close.length)], }); } } @@ -826,13 +826,13 @@ export function backspace( return new Promise((resolve) => resolve(true)); } else if (doc.model.getText(p - 2, p, true) == '\\"') { return doc.model.edit([new ModelEdit('deleteRange', [p - 2, 2])], { - selection: new ModelEditSelection(p - 2), + 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])], { - selection: new ModelEditSelection(p - prevToken.raw.length), + selections: [new ModelEditSelection(p - prevToken.raw.length)], } ); } else { @@ -860,13 +860,13 @@ export function deleteForward( const p = start; if (doc.model.getText(p, p + 2, true) == '\\"') { return doc.model.edit([new ModelEdit('deleteRange', [p, 2])], { - selection: new ModelEditSelection(p), + 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])], { - selection: new ModelEditSelection(p - prevToken.raw.length), + selections: [new ModelEditSelection(p - prevToken.raw.length)], } ); } else { @@ -894,7 +894,7 @@ export function stringQuote( if (cursor.getToken().type == 'close') { if (doc.model.getText(0, start).endsWith('\\')) { void doc.model.edit([new ModelEdit('changeRange', [start, start, '"'])], { - selection: new ModelEditSelection(start + 1), + selections: [new ModelEditSelection(start + 1)], }); } else { close(doc, '"', start); @@ -902,17 +902,17 @@ export function stringQuote( } else { if (doc.model.getText(0, start).endsWith('\\')) { void doc.model.edit([new ModelEdit('changeRange', [start, start, '"'])], { - selection: new ModelEditSelection(start + 1), + selections: [new ModelEditSelection(start + 1)], }); } else { void doc.model.edit([new ModelEdit('changeRange', [start, start, '\\"'])], { - selection: new ModelEditSelection(start + 2), + selections: [new ModelEditSelection(start + 2)], }); } } } else { void doc.model.edit([new ModelEdit('changeRange', [start, start, '""'])], { - selection: new ModelEditSelection(start + 1), + selections: [new ModelEditSelection(start + 1)], }); } } @@ -1016,7 +1016,6 @@ export function growSelectionStack( doc: EditableDocument, ranges: Array<(readonly [number, number])>, ) { - // const [start, end] = range; // 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. @@ -1094,9 +1093,9 @@ export function raiseSexp( void doc.model.edit( [new ModelEdit('changeRange', [startCursor.offsetStart, endCursor.offsetEnd, raised])], { - selection: new ModelEditSelection( + selections: [new ModelEditSelection( isCaretTrailing ? startCursor.offsetStart + raised.length : startCursor.offsetStart - ), + )], } ); } @@ -1187,7 +1186,7 @@ export function transpose( [newCursorPos, newCursorPos], ]), ], - { selection: new ModelEditSelection(newCursorPos) } + { selections: [new ModelEditSelection(newCursorPos)] } ); } } @@ -1276,7 +1275,7 @@ export function dragSexprBackward( new ModelEdit('changeRange', [currentRange[0], currentRange[1], leftText]), new ModelEdit('changeRange', [backRange[0], backRange[1], currentText]), ], - { selection: new ModelEditSelection(backRange[0] + newPosOffset) } + { selections: [new ModelEditSelection(backRange[0] + newPosOffset)] } ); } } @@ -1304,9 +1303,9 @@ export function dragSexprForward( new ModelEdit('changeRange', [currentRange[0], currentRange[1], rightText]), ], { - selection: new ModelEditSelection( + selections: [new ModelEditSelection( currentRange[1] + (forwardRange[1] - currentRange[1]) - newPosOffset - ), + )], } ); } @@ -1391,7 +1390,7 @@ export function dragSexprBackwardUp(doc: EditableDocument, p = doc.selection.act new ModelEdit('insertString', [listStart, dragText, [p, p], [newCursorPos, newCursorPos]]), ], { - selection: new ModelEditSelection(newCursorPos), + selections: [new ModelEditSelection(newCursorPos)], skipFormat: false, undoStopBefore: true, } @@ -1425,7 +1424,7 @@ export function dragSexprForwardDown(doc: EditableDocument, p = doc.selection.ac new ModelEdit('deleteRange', [currentRange[0], deleteLength]), ], { - selection: new ModelEditSelection(newCursorPos), + selections: [new ModelEditSelection(newCursorPos)], skipFormat: false, undoStopBefore: true, } @@ -1458,7 +1457,7 @@ export function dragSexprForwardUp(doc: EditableDocument, p = doc.selection.acti new ModelEdit('deleteRange', [deleteStart, deleteLength]), ], { - selection: new ModelEditSelection(newCursorPos), + selections: [new ModelEditSelection(newCursorPos)], skipFormat: false, undoStopBefore: true, } @@ -1495,7 +1494,7 @@ export function dragSexprBackwardDown(doc: EditableDocument, p = doc.selection.a ]), ], { - selection: new ModelEditSelection(newCursorPos), + selections: [new ModelEditSelection(newCursorPos)], skipFormat: false, undoStopBefore: true, } @@ -1550,7 +1549,7 @@ export function addRichComment(doc: EditableDocument, p = doc.selection.active, ]), ], { - selection: new ModelEditSelection(newCursorPos), + selections: [new ModelEditSelection(newCursorPos)], skipFormat: true, undoStopBefore: false, } @@ -1578,7 +1577,7 @@ export function addRichComment(doc: EditableDocument, p = doc.selection.active, ]), ], { - selection: new ModelEditSelection(newCursorPos), + selections: [new ModelEditSelection(newCursorPos)], skipFormat: false, undoStopBefore: true, } diff --git a/src/doc-mirror/index.ts b/src/doc-mirror/index.ts index 4c9c08329..7c52662c3 100644 --- a/src/doc-mirror/index.ts +++ b/src/doc-mirror/index.ts @@ -50,8 +50,8 @@ export class DocumentModel implements EditableModel { ) .then((isFulfilled) => { if (isFulfilled) { - if (options.selection) { - this.document.selection = options.selection; + if (options.selections) { + this.document.selections = options.selections; } if (!options.skipFormat) { return formatter.formatPosition(editor, false, { @@ -142,7 +142,6 @@ export class MirroredDocument implements EditableDocument { model = new DocumentModel(this); selectionsStack: ModelEditSelection[][] = []; - selectionStack: ModelEditSelection[] = []; public getTokenCursor( offset: number = this.selections[0].active, @@ -155,35 +154,56 @@ export class MirroredDocument implements EditableDocument { const editor = utilities.tryToGetActiveTextEditor(), selection = editor.selection, 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.selection = selection; + editor.selections = [selections[0]]; }); } - set selection(selection: ModelEditSelection) { + get selection() { + return this.selections[0]; + } + + set selection(sel: ModelEditSelection) { + this.selections = [sel]; + } + + set selections(selections: ModelEditSelection[]) { const editor = utilities.getActiveTextEditor(), - document = editor.document, - anchor = document.positionAt(selection.anchor), - active = document.positionAt(selection.active); - editor.selection = new vscode.Selection(anchor, active); + document = editor.document; + editor.selections = selections.map((selection) => { + const anchor = document.positionAt(selection.anchor), + active = document.positionAt(selection.active); + return new vscode.Selection(anchor, active); + }); + + const primarySelection = selections[0]; + const active = document.positionAt(primarySelection.active); editor.revealRange(new vscode.Range(active, active)); } - get selection(): ModelEditSelection { + get selections(): ModelEditSelection[] { const editor = utilities.getActiveTextEditor(), - document = editor.document, - anchor = document.offsetAt(editor.selection.anchor), - active = document.offsetAt(editor.selection.active); - return new ModelEditSelection(anchor, active); + document = editor.document; + return editor.selections.map((sel) => { + const anchor = document.offsetAt(sel.anchor), + active = document.offsetAt(sel.active); + return new ModelEditSelection(anchor, active); + }); + } + + public getSelectionTexts() { + const editor = utilities.getActiveTextEditor(), + selections = editor.selections; + return selections.map((selection) => this.document.getText(selection)); } public getSelectionText(index: number = 0) { - const editor = utilities.tryToGetActiveTextEditor(), + const editor = utilities.getActiveTextEditor(), selection = editor.selections[index]; return this.document.getText(selection); } @@ -235,7 +255,7 @@ export function tryToGetDocument(doc: vscode.TextDocument) { return documents.get(doc); } -export function getDocument(doc: vscode.TextDocument) { +export function getDocument(doc: vscode.TextDocument): MirroredDocument { const mirrorDoc = tryToGetDocument(doc); if (isUndefined(mirrorDoc)) { diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index 9ff824669..7f126d738 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -3,6 +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'; model.initScanner(20000); @@ -694,22 +695,20 @@ describe('paredit', () => { const range = [15, 20] as [number, number]; it('should make grow selection the topmost element on the stack', () => { paredit.growSelectionStack(doc, [range]); - expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual( - new ModelEditSelection(range[0], range[1]) - ); + expect(last(doc.selectionsStack)).toEqual([new ModelEditSelection(range[0], range[1])]); }); it('get us back to where we started if we just grow, then shrink', () => { const selectionBefore = startSelection.clone(); paredit.growSelectionStack(doc, [range]); paredit.shrinkSelection(doc); - expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual(selectionBefore); + expect(last(doc.selectionsStack)).toEqual([selectionBefore]); }); it('should not add selections identical to the topmost', () => { const selectionBefore = doc.selection.clone(); paredit.growSelectionStack(doc, [range]); paredit.growSelectionStack(doc, [range]); paredit.shrinkSelection(doc); - expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual(selectionBefore); + expect(last(doc.selectionsStack)).toEqual([selectionBefore]); }); it('should have A topmost after adding A, then B, then shrinking', () => { const a = range, @@ -717,9 +716,7 @@ describe('paredit', () => { paredit.growSelectionStack(doc, [a]); paredit.growSelectionStack(doc, [b]); paredit.shrinkSelection(doc); - expect(doc.selectionStack[doc.selectionStack.length - 1]).toEqual( - new ModelEditSelection(a[0], a[1]) - ); + expect(last(doc.selectionsStack)).toEqual([new ModelEditSelection(a[0], a[1])]); }); }); From b0909216963dd0e5ff217d41cb4d4b5d9271f700 Mon Sep 17 00:00:00 2001 From: Rayat Date: Mon, 28 Mar 2022 10:38:20 -0700 Subject: [PATCH 05/49] 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 | 1710 ++++++++++------- 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, 1752 insertions(+), 1268 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 6d53ad3d1..e459579ab 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,56 +27,67 @@ 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 selectForwardSexpOrUp(doc: EditableDocument) { @@ -88,38 +100,50 @@ export function selectForwardSexpOrUp(doc: EditableDocument) { } 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 selectBackwardSexpOrUp(doc: EditableDocument) { @@ -131,11 +155,17 @@ export function selectBackwardSexpOrUp(doc: EditableDocument) { } 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)) + ); } /** @@ -144,7 +174,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); @@ -257,7 +287,7 @@ export function backwardSexpRange( 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(); @@ -266,7 +296,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(); @@ -289,74 +319,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] { return _forwardSexpRange(doc, offset, GoUpSexpOption.Required, goPastWhitespace); @@ -364,7 +424,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] { return _backwardSexpRange(doc, offset, GoUpSexpOption.Required, goPastWhitespace); @@ -388,7 +449,8 @@ export function backwardSexpOrUpRange( 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); @@ -404,7 +466,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); @@ -426,7 +489,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()) { @@ -438,7 +502,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()) { @@ -448,32 +513,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)], @@ -481,62 +563,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, + }); } /** @@ -544,66 +625,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]])], { @@ -615,7 +704,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) || @@ -634,9 +723,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); @@ -649,6 +748,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; @@ -671,19 +771,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(); @@ -708,78 +813,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 @@ -788,102 +912,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('"'); @@ -897,7 +1081,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('\\')) { @@ -931,66 +1117,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); } /** @@ -1012,171 +1194,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, @@ -1184,13 +1379,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 = [ @@ -1255,60 +1451,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 = { @@ -1329,7 +1536,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); @@ -1357,151 +1564,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 { @@ -1512,7 +1759,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 7f126d738..5d10292d1 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]); }); @@ -496,12 +497,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]); }); }); @@ -509,12 +510,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]); }); }); @@ -522,32 +523,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]); }); }); @@ -555,28 +556,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]); }); }); }); @@ -585,13 +586,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', () => { @@ -601,16 +602,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)); }); }); @@ -622,22 +623,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)]); }); }); }); @@ -649,44 +650,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); }); }); }); @@ -698,17 +705,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, @@ -732,14 +739,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)); }); @@ -747,7 +754,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)); }); @@ -755,7 +762,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)); }); @@ -766,7 +773,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)); }); @@ -777,7 +784,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)); }); @@ -788,7 +795,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)); }); @@ -799,7 +806,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)); }); @@ -810,7 +817,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)); }); }); @@ -819,37 +826,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)); }); }); @@ -857,25 +864,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)); }); }); @@ -883,19 +890,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)); }); }); @@ -903,13 +910,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)); }); }); @@ -917,19 +924,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)); }); }); @@ -939,13 +946,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', () => { @@ -953,13 +960,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)); }); }); @@ -967,7 +974,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)); }); }); @@ -977,68 +984,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)); }); }); @@ -1049,13 +1056,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', () => { @@ -1063,7 +1070,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)); }); }); @@ -1074,19 +1081,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', () => { @@ -1094,7 +1101,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)); }); }); @@ -1103,13 +1110,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)); }); }); @@ -1119,46 +1126,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', () => { @@ -1236,10 +1243,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', () => { @@ -1259,10 +1266,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', () => { @@ -1283,16 +1290,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', () => { @@ -1307,10 +1314,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 57e85031c..0d2f2ded5 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -47,37 +47,55 @@ 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)) + ); }, }, { @@ -95,13 +113,19 @@ const pareditCommands: PareditCommand[] = [ { command: 'paredit.closeList', handler: (doc: EditableDocument) => { - paredit.moveToRangeRight(doc, paredit.rangeToForwardList(doc)); + paredit.moveToRangeRight( + doc, + doc.selections.map((s) => paredit.rangeToForwardList(doc, s.active)) + ); }, }, { command: 'paredit.openList', handler: (doc: EditableDocument) => { - paredit.moveToRangeLeft(doc, paredit.rangeToBackwardList(doc)); + paredit.moveToRangeLeft( + doc, + doc.selections.map((s) => paredit.rangeToBackwardList(doc, s.active)) + ); }, }, @@ -109,7 +133,10 @@ const pareditCommands: PareditCommand[] = [ { command: 'paredit.rangeForDefun', handler: (doc: EditableDocument) => { - paredit.selectRange(doc, paredit.rangeForDefun(doc)); + paredit.selectRange( + doc, + doc.selections.map((selection) => paredit.rangeForDefun(doc, selection.active)) + ); }, }, { @@ -235,74 +262,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 From 5f12aeaaa154282901f9baa717377228afe6472d Mon Sep 17 00:00:00 2001 From: Rayat Date: Fri, 1 Apr 2022 12:03:58 -0700 Subject: [PATCH 06/49] Experiment with mixing native delete/backspace with paredit per-cursor --- src/cursor-doc/paredit.ts | 164 ++++++++++-------- .../unit/cursor-doc/paredit-test.ts | 88 +++++----- 2 files changed, 137 insertions(+), 115 deletions(-) diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index e459579ab..961385507 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -1,4 +1,4 @@ -import { isEqual, isNumber, last, pick, property, clone } from 'lodash'; +import { isEqual, isNumber, last, pick, property, clone, isBoolean } from 'lodash'; import { validPair } from './clojure-lexer'; import { EditableDocument, ModelEdit, ModelEditSelection, ModelEditResult } from './model'; import { LispTokenCursor } from './token-cursor'; @@ -946,74 +946,88 @@ export function backspace( // _end: number // = doc.selections.active // ): Thenable { ): Thenable { - const selections = clone(doc.selections); + // const selections = clone(doc.selections); return Promise.all( - doc.selections.map(async (selection, index) => { - const { start, end } = selection; + 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 { - 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; + if (start != end) { + // const res = await doc.backspace(); + + // return res.selections[0]; + // return [res.success, res.selections[0]]; + return ['backspace', selection, index] as const; } else { - 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)); + const cursor = doc.getTokenCursor(start); + const nextToken = cursor.getToken(); + const p = start; + const prevToken = + p > cursor.offsetStart && !['open', 'close'].includes(nextToken.type) + ? nextToken + : cursor.getPrevToken(); + if (prevToken.type == 'prompt') { + // return new Promise((resolve) => resolve(true)); + // return selection; + return [true, selection] as const; + } else if (nextToken.type == 'prompt') { + // return new Promise((resolve) => resolve(true)); + return [true, selection] as const; + // return selection; + } else if (doc.model.getText(p - 2, p, true) == '\\"') { + // return doc.model.edit([new ModelEdit('deleteRange', [p - 2, 2])], { + const sel = new ModelEditSelection(p - 2); + // selections[index] = sel; + const res = await doc.model.edit([new ModelEdit('deleteRange', [p - 2, 2])], { + // selections: [new ModelEditSelection(p - 2)], + // selections: Object.assign([...selections], {[index]: new ModelEditSelection(p - 2)}) + selections: replaceAt(doc.selections, index, sel), + }); + // return sel; + // selections[index] = res.selections[index]; + return [res.success, res.selections[index]] as const; + } else if (prevToken.type === 'open' && nextToken.type === 'close') { + // return doc.model.edit( + const sel = new ModelEditSelection(p - prevToken.raw.length); + // selections[index] = sel; + const res = await doc.model.edit( + [new ModelEdit('deleteRange', [p - prevToken.raw.length, prevToken.raw.length + 1])], + { + // selections: [new ModelEditSelection(p - prevToken.raw.length)], + // selections: Object.assign([...selections], {[index]: new ModelEditSelection(p - prevToken.raw.length)}, + selections: replaceAt(doc.selections, index, sel), + } + ); + // selections[index] = res.selections[index]; + return [res.success, res.selections[index]] as const; } else { - const res = await doc.backspace(); - const { selections: sels } = res; - selections[index] = res.selections[0]; - return res.success; + if (['open', 'close'].includes(prevToken.type) && docIsBalanced(doc)) { + // doc.selection = new ModelEditSelection(p - prevToken.raw.length); + // return new ModelEditSelection(p - prevToken.raw.length); + // selections[index] = new ModelEditSelection(p - prevToken.raw.length); + // return new Promise((resolve) => resolve(true)); + return [true, new ModelEditSelection(p - prevToken.raw.length)] as const; + } else { + // const res = await doc.backspace(); + // selections[index] = res.selections[0]; + // return [res.success, res.selections[0]] as const; + return ['backspace', selection, index] as const; + } } } } - }) - ).then((succeeded) => { - doc.selections = selections; - return succeeded; + ) + ).then>(async (results) => { + // run native backspace on non edited cursors + const nativeBackspaceSelections = results + .filter((result) => result[0] === 'backspace') + .map((result) => result[1]); + doc.selections = nativeBackspaceSelections; + await doc.backspace(); + // set edited selections + doc.selections.push(...results.filter((r) => r[0] === true).map(([_, sel]) => sel)); + return results.map(([success]) => (success === 'backspace' ? true : success)); }); } @@ -1022,23 +1036,24 @@ export async function deleteForward( // _start: number = doc.selections.anchor, // _end: number = doc.selections.active ) { - doc.selections = await Promise.all(doc.selections.map(async (selection, index) => { + const results = await Promise.all(doc.selections.map(async (selection, index) => { const { start, end } = selection; if (start != end) { - await doc.delete(); - return selection; + // await doc.delete(); + // return selection; + return ['delete', selection, index] as const; } 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) == '\\"') { - await doc.model.edit([new ModelEdit('deleteRange', [p, 2])], { + const res = await doc.model.edit([new ModelEdit('deleteRange', [p, 2])], { selections: replaceAt(doc.selections, index, new ModelEditSelection(p)), }); - return new ModelEditSelection(p); + return [res.success, res.selections[index], index] as const; } else if (prevToken.type === 'open' && nextToken.type === 'close') { - await doc.model.edit( + const res = await doc.model.edit( [new ModelEdit('deleteRange', [p - prevToken.raw.length, prevToken.raw.length + 1])], { selections: replaceAt( @@ -1048,19 +1063,26 @@ export async function deleteForward( ), } ); - return new ModelEditSelection(p - prevToken.raw.length); + return [res.success, res.selections[index], index] as const; } else { if (['open', 'close'].includes(nextToken.type) && docIsBalanced(doc)) { - doc.selections = replaceAt(doc.selections, index, new ModelEditSelection(p + 1)); + // doc.selections = replaceAt(doc.selections, index, new ModelEditSelection(p + 1)); // return new Promise((resolve) => resolve(true)); - return new ModelEditSelection(p + 1); + return [true, new ModelEditSelection(p + 1), index] as const; } else { // return doc.delete(); - return selection; + // return selection; + return ['delete', selection, index] as const; } } } })); + const postCalvaEditSelections = results.filter(r => isBoolean(r[0])) + const cursorsNeedingNativeDeletion = results.filter(r => r[0] === "delete"); + doc.selections = cursorsNeedingNativeDeletion.map(r => r[1]) + await doc.delete(); + doc.selections.push(...postCalvaEditSelections.map(s => s[1])) + return results.map(r => r[0] === "delete" ? true : r[0]); } // FIXME: stringQuote() is defined and tested but is never used or referenced? diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index 5d10292d1..83ea0726c 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -1168,79 +1168,79 @@ describe('paredit', () => { await paredit.backspace(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.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes contents in strings 2', () => { + it('Deletes contents in strings 2', async () => { const a = docFromTextNotation('{::foo "a|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 3', () => { + it('Deletes contents in strings 3', async () => { const a = docFromTextNotation('{::foo "aa|"• ::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 quoted quote', () => { + it('Deletes quoted quote', 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 quoted quote in string', () => { + it('Deletes quoted quote in string', 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 contents in list', () => { + it('Deletes contents in list', async () => { const a = docFromTextNotation('{::foo (a|)• ::bar :foo}'); const b = docFromTextNotation('{::foo (|)• ::bar :foo}'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes empty list function', () => { + it('Deletes empty list function', 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 empty set', () => { + it('Deletes empty set', async () => { const a = docFromTextNotation('#{|}'); const b = docFromTextNotation('|'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes empty literal function with trailing newline', () => { + it('Deletes empty literal function with trailing newline', async () => { // https://github.com/BetterThanTomorrow/calva/issues/1079 const a = docFromTextNotation('{::foo #(|)• ::bar :foo}'); const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes open paren prefix characters', () => { + it('Deletes open paren prefix characters', async () => { // https://github.com/BetterThanTomorrow/calva/issues/1122 const a = docFromTextNotation('#|(foo)'); const b = docFromTextNotation('|(foo)'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes open map curly prefix/ns characters', () => { + it('Deletes open map curly prefix/ns characters', async () => { const a = docFromTextNotation('#:same|{:thing :here}'); const b = docFromTextNotation('#:sam|{:thing :here}'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes open set hash characters', () => { + it('Deletes open set hash characters', async () => { // https://github.com/BetterThanTomorrow/calva/issues/1122 const a = docFromTextNotation('#|{:thing :here}'); const b = docFromTextNotation('|{:thing :here}'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Moves cursor past entire open paren, including prefix characters', async () => { @@ -1249,21 +1249,21 @@ describe('paredit', () => { await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes unbalanced bracket', () => { + it('Deletes unbalanced bracket', async () => { // This hangs the structural editing in the real editor // https://github.com/BetterThanTomorrow/calva/issues/1573 const a = docFromTextNotation('([{|)'); const b = docFromTextNotation('([|'); - void paredit.backspace(a); + await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); describe('Kill character forwards (delete)', () => { - it('Leaves closing paren of empty list alone', () => { + it('Leaves closing paren of empty list alone', 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('Deletes closing paren if unbalance', async () => { @@ -1272,22 +1272,22 @@ describe('paredit', () => { await paredit.deleteForward(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.deleteForward(a); + await paredit.deleteForward(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.deleteForward(a); + await paredit.deleteForward(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.deleteForward(a); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes contents in strings', async () => { @@ -1302,16 +1302,16 @@ describe('paredit', () => { await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes quoted quote', () => { + it('Deletes quoted quote', 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('Deletes quoted quote in string', () => { + it('Deletes quoted quote in string', 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('Deletes contents in list', async () => { @@ -1320,23 +1320,23 @@ describe('paredit', () => { await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes empty list function', () => { + it('Deletes empty list function', 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('Deletes empty set', () => { + it('Deletes empty set', async () => { const a = docFromTextNotation('#{|}'); const b = docFromTextNotation('|'); - void paredit.deleteForward(a); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('Deletes empty literal function with trailing newline', () => { + it('Deletes empty literal function with trailing newline', async () => { // https://github.com/BetterThanTomorrow/calva/issues/1079 const a = docFromTextNotation('{::foo #(|)• ::bar :foo}'); const b = docFromTextNotation('{::foo |• ::bar :foo}'); - void paredit.deleteForward(a); + await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); From 0b377a8d9ede47b2a13292574ed7b252973113dd Mon Sep 17 00:00:00 2001 From: Rayat Date: Fri, 1 Apr 2022 12:40:31 -0700 Subject: [PATCH 07/49] Fix spelling mistake --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dced160d9..9ec6aef35 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ }, "printEngine": { "type": "string", - "description": "The print engine to use. 'calva' means that the nREPL server will first plain print it and then Calva will prettify. The other options will make the server use the chosen printer-function to print the result, and Calva will not reformat it. To use some other function (on the server), configure `printFn` instead. To use the `nREPL` default (the equivalent of `clojure.core/pr`), set neither this nore `printFn`.", + "description": "The print engine to use. 'calva' means that the nREPL server will first plain print it and then Calva will prettify. The other options will make the server use the chosen printer-function to print the result, and Calva will not reformat it. To use some other function (on the server), configure `printFn` instead. To use the `nREPL` default (the equivalent of `clojure.core/pr`), set neither this nor `printFn`.", "enum": [ "calva", "pprint", From d00a3c22bab5e7a281a5a1bcc779b68f3d1ab362 Mon Sep 17 00:00:00 2001 From: Rayat Date: Fri, 1 Apr 2022 12:41:25 -0700 Subject: [PATCH 08/49] prettier format files --- src/cursor-doc/paredit.ts | 97 ++++++++++--------- src/doc-mirror/index.ts | 12 ++- .../unit/common/text-notation.ts | 8 +- .../unit/cursor-doc/token-cursor-test.ts | 8 +- 4 files changed, 69 insertions(+), 56 deletions(-) diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 961385507..94f55c803 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -83,7 +83,7 @@ export function selectRight(doc: EditableDocument) { const ranges = doc.selections.map((selection) => { const rangeFn = selection.active >= selection.anchor - ? doc => forwardHybridSexpRange(doc, selection.end) + ? (doc) => forwardHybridSexpRange(doc, selection.end) : (doc: EditableDocument) => forwardHybridSexpRange(doc, selection.active, true); return rangeFn(doc); }); @@ -335,12 +335,11 @@ export function forwardHybridSexpRange( offsets: number | number[] = doc.selections.map((s) => s.end), goPastWhitespace = false ): [number, number] | Array<[number, number]> { - - if(isNumber(offsets)) { + if (isNumber(offsets)) { offsets = [offsets]; } - const ranges = offsets.map<[number,number]>((offset) => { + const ranges = offsets.map<[number, number]>((offset) => { // const { end: offset } = selection; let cursor = doc.getTokenCursor(offset); @@ -1036,53 +1035,57 @@ export async function deleteForward( // _start: number = doc.selections.anchor, // _end: number = doc.selections.active ) { - const results = await Promise.all(doc.selections.map(async (selection, index) => { - const { start, end } = selection; - if (start != end) { - // await doc.delete(); - // return selection; - return ['delete', selection, index] as const; - } else { - const cursor = doc.getTokenCursor(start); - const prevToken = cursor.getPrevToken(); - const nextToken = cursor.getToken(); - const p = start; - if (doc.model.getText(p, p + 2, true) == '\\"') { - const res = await doc.model.edit([new ModelEdit('deleteRange', [p, 2])], { - selections: replaceAt(doc.selections, index, new ModelEditSelection(p)), - }); - return [res.success, res.selections[index], index] as const; - } else if (prevToken.type === 'open' && nextToken.type === 'close') { - const res = await doc.model.edit( - [new ModelEdit('deleteRange', [p - prevToken.raw.length, prevToken.raw.length + 1])], - { - selections: replaceAt( - doc.selections, - index, - new ModelEditSelection(p - prevToken.raw.length) - ), - } - ); - return [res.success, res.selections[index], index] as const; + const results = await Promise.all( + doc.selections.map(async (selection, index) => { + const { start, end } = selection; + if (start != end) { + // await doc.delete(); + // return selection; + return ['delete', selection, index] as const; } else { - if (['open', 'close'].includes(nextToken.type) && docIsBalanced(doc)) { - // doc.selections = replaceAt(doc.selections, index, new ModelEditSelection(p + 1)); - // return new Promise((resolve) => resolve(true)); - return [true, new ModelEditSelection(p + 1), index] as const; + const cursor = doc.getTokenCursor(start); + const prevToken = cursor.getPrevToken(); + const nextToken = cursor.getToken(); + const p = start; + if (doc.model.getText(p, p + 2, true) == '\\"') { + const res = await doc.model.edit([new ModelEdit('deleteRange', [p, 2])], { + selections: replaceAt(doc.selections, index, new ModelEditSelection(p)), + }); + return [res.success, res.selections[index], index] as const; + } else if (prevToken.type === 'open' && nextToken.type === 'close') { + const res = await doc.model.edit( + [new ModelEdit('deleteRange', [p - prevToken.raw.length, prevToken.raw.length + 1])], + { + selections: replaceAt( + doc.selections, + index, + new ModelEditSelection(p - prevToken.raw.length) + ), + } + ); + return [res.success, res.selections[index], index] as const; } else { - // return doc.delete(); - // return selection; - return ['delete', selection, index] as const; + if (['open', 'close'].includes(nextToken.type) && docIsBalanced(doc)) { + // doc.selections = replaceAt(doc.selections, index, new ModelEditSelection(p + 1)); + // return new Promise((resolve) => resolve(true)); + return [true, new ModelEditSelection(p + 1), index] as const; + } else { + // return doc.delete(); + // return selection; + return ['delete', selection, index] as const; + } } } - } - })); - const postCalvaEditSelections = results.filter(r => isBoolean(r[0])) - const cursorsNeedingNativeDeletion = results.filter(r => r[0] === "delete"); - doc.selections = cursorsNeedingNativeDeletion.map(r => r[1]) - await doc.delete(); - doc.selections.push(...postCalvaEditSelections.map(s => s[1])) - return results.map(r => r[0] === "delete" ? true : r[0]); + }) + ); + const postCalvaEditSelections = results.filter((r) => isBoolean(r[0])); + const cursorsNeedingNativeDeletion = results.filter((r) => r[0] === 'delete'); + doc.selections = cursorsNeedingNativeDeletion.map((r) => r[1]); + await doc.delete() + ; + + doc.selections.push(...postCalvaEditSelections.map((s) => s[1])); + return results.map((r) => (r[0] === 'delete' ? true : r[0])); } // FIXME: stringQuote() is defined and tested but is never used or referenced? diff --git a/src/doc-mirror/index.ts b/src/doc-mirror/index.ts index 8586c621f..d15cd464b 100644 --- a/src/doc-mirror/index.ts +++ b/src/doc-mirror/index.ts @@ -49,15 +49,19 @@ export class DocumentModel implements EditableModel { }, { undoStopBefore, undoStopAfter: false } ) - .then(async(success) => { + .then(async (success) => { if (success) { if (options.selections) { this.document.selections = options.selections; } if (!options.skipFormat) { - return {edits: modelEdits, selections: options.selections, success: await formatter.formatPosition(editor, false, { - 'format-depth': options.formatDepth ? options.formatDepth : 1, - })}; + return { + edits: modelEdits, + selections: options.selections, + success: await formatter.formatPosition(editor, false, { + 'format-depth': options.formatDepth ? options.formatDepth : 1, + }), + }; } } return { edits: modelEdits, selections: options.selections, success }; diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index d826cda03..a55dd071a 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -46,9 +46,11 @@ function textNotationToTextAndSelection(content: string): [string, model.ModelEd .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 - )); + 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 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 b1ad3c486..d65f2b0a5 100644 --- a/src/extension-test/unit/cursor-doc/token-cursor-test.ts +++ b/src/extension-test/unit/cursor-doc/token-cursor-test.ts @@ -781,7 +781,9 @@ 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(a.selections[0].active); - expect(cursor.rangeForDefun(a.selections[0].active, false)).toEqual(textAndSelection(b)[1][0]); + 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 @@ -839,7 +841,9 @@ 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.selections[0].anchor, false)).toEqual(textAndSelection(b)[1][0]); + 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))'); From 5594bc4431d128f7f570f298778b048fae8d4322 Mon Sep 17 00:00:00 2001 From: Rayat Date: Fri, 1 Apr 2022 19:38:43 -0700 Subject: [PATCH 09/49] prettier format (again lol) --- src/cursor-doc/paredit.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 94f55c803..549537ef2 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -1081,8 +1081,7 @@ export async function deleteForward( const postCalvaEditSelections = results.filter((r) => isBoolean(r[0])); const cursorsNeedingNativeDeletion = results.filter((r) => r[0] === 'delete'); doc.selections = cursorsNeedingNativeDeletion.map((r) => r[1]); - await doc.delete() - ; + await doc.delete(); doc.selections.push(...postCalvaEditSelections.map((s) => s[1])); return results.map((r) => (r[0] === 'delete' ? true : r[0])); From 5c8a40836ae13fedc4faeee9cdb4cd99c014839f Mon Sep 17 00:00:00 2001 From: Rayat Date: Sat, 2 Apr 2022 14:50:15 -0700 Subject: [PATCH 10/49] Post forward or up commands rebase fixes; multi cursor seems to work! --- src/cursor-doc/paredit.ts | 197 +++++++++++------- .../unit/cursor-doc/indent-test.ts | 46 ++-- src/paredit/extension.ts | 6 +- 3 files changed, 153 insertions(+), 96 deletions(-) diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 549537ef2..4171ee747 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -91,12 +91,14 @@ export function selectRight(doc: EditableDocument) { } export function selectForwardSexpOrUp(doc: EditableDocument) { - const rangeFn = - doc.selection.active >= doc.selection.anchor - ? forwardSexpOrUpRange - : (doc: EditableDocument) => forwardSexpOrUpRange(doc, doc.selection.active, true); - - selectRangeForward(doc, rangeFn(doc)); + const ranges = doc.selections.map((selection) => { + const rangeFn = + selection.active >= selection.anchor + ? (doc) => forwardSexpOrUpRange(doc, selection.end) + : (doc: EditableDocument) => forwardSexpOrUpRange(doc, selection.active, true); + return rangeFn(doc); + }); + selectRangeForward(doc, ranges); } export function selectBackwardSexp(doc: EditableDocument) { @@ -147,11 +149,14 @@ export function selectBackwardUpSexp(doc: EditableDocument) { } export function selectBackwardSexpOrUp(doc: EditableDocument) { - const rangeFn = - doc.selection.active <= doc.selection.anchor - ? (doc: EditableDocument) => backwardSexpOrUpRange(doc, doc.selection.active, false) - : (doc: EditableDocument) => backwardSexpOrUpRange(doc, doc.selection.active, false); - selectRangeBackward(doc, rangeFn(doc)); + const ranges = doc.selections.map((selection) => { + const rangeFn = + selection.active <= selection.anchor + ? (doc: EditableDocument) => backwardSexpOrUpRange(doc, selection.active, false) + : (doc: EditableDocument) => backwardSexpOrUpRange(doc, selection.active, false); + return rangeFn(doc); + }); + selectRangeBackward(doc, ranges); } export function selectCloseList(doc: EditableDocument) { @@ -197,34 +202,36 @@ enum GoUpSexpOption { */ function _forwardSexpRange( doc: EditableDocument, - offset = Math.max(doc.selection.anchor, doc.selection.active), + offsets = doc.selections.map((s) => s.end), goUpSexp: GoUpSexpOption, goPastWhitespace = false -): [number, number] { - const cursor = doc.getTokenCursor(offset); +): Array<[number, number]> { + return offsets.map((offset) => { + const cursor = doc.getTokenCursor(offset); - if (goUpSexp == GoUpSexpOption.Never || goUpSexp == GoUpSexpOption.WhenAtLimit) { - // Normalize our position by scooting to the beginning of the closest sexp - cursor.forwardWhitespace(); + if (goUpSexp == GoUpSexpOption.Never || goUpSexp == GoUpSexpOption.WhenAtLimit) { + // Normalize our position by scooting to the beginning of the closest sexp + cursor.forwardWhitespace(); - if (cursor.forwardSexp(true, true)) { - if (goPastWhitespace) { - cursor.forwardWhitespace(); + if (cursor.forwardSexp(true, true)) { + if (goPastWhitespace) { + cursor.forwardWhitespace(); + } + return [offset, cursor.offsetStart]; } - return [offset, cursor.offsetStart]; } - } - if (goUpSexp == GoUpSexpOption.Required || goUpSexp == GoUpSexpOption.WhenAtLimit) { - cursor.forwardList(); - if (cursor.upList()) { - if (goPastWhitespace) { - cursor.forwardWhitespace(); + if (goUpSexp == GoUpSexpOption.Required || goUpSexp == GoUpSexpOption.WhenAtLimit) { + cursor.forwardList(); + if (cursor.upList()) { + if (goPastWhitespace) { + cursor.forwardWhitespace(); + } + return [offset, cursor.offsetStart]; } - return [offset, cursor.offsetStart]; } - } - return [offset, offset]; + return [offset, offset]; + }); } /** @@ -232,57 +239,83 @@ function _forwardSexpRange( */ function _backwardSexpRange( doc: EditableDocument, - offset: number = Math.min(doc.selection.anchor, doc.selection.active), + offsets: number[] = doc.selections.map((s) => s.start), goUpSexp: GoUpSexpOption, goPastWhitespace = false -): [number, number] { - const cursor = doc.getTokenCursor(offset); - - if (goUpSexp == GoUpSexpOption.Never || goUpSexp == GoUpSexpOption.WhenAtLimit) { - if (!cursor.isWhiteSpace() && cursor.offsetStart < offset) { - // This is because cursor.backwardSexp() can't move backwards when "on" the first sexp inside a list - // TODO: Try to fix this in LispTokenCursor instead. - cursor.forwardSexp(); - } - cursor.backwardWhitespace(); +): Array<[number, number]> { + return offsets.map((offset) => { + const cursor = doc.getTokenCursor(offset); + + if (goUpSexp == GoUpSexpOption.Never || goUpSexp == GoUpSexpOption.WhenAtLimit) { + if (!cursor.isWhiteSpace() && cursor.offsetStart < offset) { + // This is because cursor.backwardSexp() can't move backwards when "on" the first sexp inside a list + // TODO: Try to fix this in LispTokenCursor instead. + cursor.forwardSexp(); + } + cursor.backwardWhitespace(); - if (cursor.backwardSexp(true, true)) { - if (goPastWhitespace) { - cursor.backwardWhitespace(); + if (cursor.backwardSexp(true, true)) { + if (goPastWhitespace) { + cursor.backwardWhitespace(); + } + return [cursor.offsetStart, offset]; } - return [cursor.offsetStart, offset]; } - } - if (goUpSexp == GoUpSexpOption.Required || goUpSexp == GoUpSexpOption.WhenAtLimit) { - cursor.backwardList(); - if (cursor.backwardUpList()) { - cursor.forwardSexp(true, true); - cursor.backwardSexp(true, true); - if (goPastWhitespace) { - cursor.backwardWhitespace(); + if (goUpSexp == GoUpSexpOption.Required || goUpSexp == GoUpSexpOption.WhenAtLimit) { + cursor.backwardList(); + if (cursor.backwardUpList()) { + cursor.forwardSexp(true, true); + cursor.backwardSexp(true, true); + if (goPastWhitespace) { + cursor.backwardWhitespace(); + } + return [cursor.offsetStart, offset]; } - return [cursor.offsetStart, offset]; } - } - return [offset, offset]; + return [offset, offset]; + }); } export function forwardSexpRange( doc: EditableDocument, - offset = Math.max(doc.selection.anchor, doc.selection.active), + offsets?: number[], + goPastWhitespace?: boolean +): Array<[number, number]>; +export function forwardSexpRange( + doc: EditableDocument, + offset?: number, + goPastWhitespace?: boolean +): [number, number]; +export function forwardSexpRange( + doc: EditableDocument, + oneOrMoreOffsets: number[]|number = doc.selections.map((s) => s.end), goPastWhitespace = false -): [number, number] { - return _forwardSexpRange(doc, offset, GoUpSexpOption.Never, goPastWhitespace); +): Array<[number, number]> | [number, number] { + const offsets = Array.isArray(oneOrMoreOffsets) ? oneOrMoreOffsets : [oneOrMoreOffsets]; + const ranges = _forwardSexpRange(doc, offsets, GoUpSexpOption.Never, goPastWhitespace); + return Array.isArray(oneOrMoreOffsets) ? ranges : ranges[0]; } 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, + oneOrMoreOffsets: number[] | number = doc.selections.map((s) => s.start), goPastWhitespace = false -): [number, number] { - return _backwardSexpRange(doc, offset, GoUpSexpOption.Never, goPastWhitespace); +): Array<[number, number]> | [number, number] { + const offsets = Array.isArray(oneOrMoreOffsets) ? oneOrMoreOffsets : [oneOrMoreOffsets]; + const ranges = _backwardSexpRange(doc, offsets, GoUpSexpOption.Never, goPastWhitespace); + return Array.isArray(oneOrMoreOffsets) ? ranges : ranges[0]; } export function forwardListRange( @@ -418,7 +451,7 @@ export function rangeToForwardUpList( offset: number = doc.selections[0].end, goPastWhitespace = false ): [number, number] { - return _forwardSexpRange(doc, offset, GoUpSexpOption.Required, goPastWhitespace); + return _forwardSexpRange(doc, [offset], GoUpSexpOption.Required, goPastWhitespace)[0]; } export function rangeToBackwardUpList( @@ -427,23 +460,47 @@ export function rangeToBackwardUpList( offset: number = doc.selections[0].start, goPastWhitespace = false ): [number, number] { - return _backwardSexpRange(doc, offset, GoUpSexpOption.Required, goPastWhitespace); + return _backwardSexpRange(doc, [offset], GoUpSexpOption.Required, goPastWhitespace)[0]; } export function forwardSexpOrUpRange( doc: EditableDocument, - offset = Math.max(doc.selection.anchor, doc.selection.active), + offsets?: number[], + goPastWhitespace?: boolean +): Array<[number, number]>; +export function forwardSexpOrUpRange( + doc: EditableDocument, + offset?: number, + goPastWhitespace?: boolean +): [number, number]; +export function forwardSexpOrUpRange( + doc: EditableDocument, + oneOrMoreOffsets: number[] | number = doc.selections.map((s) => s.end), goPastWhitespace = false -): [number, number] { - return _forwardSexpRange(doc, offset, GoUpSexpOption.WhenAtLimit, goPastWhitespace); +): Array<[number, number]> | [number, number] { + const offsets = isNumber(oneOrMoreOffsets) ? [oneOrMoreOffsets] : oneOrMoreOffsets; + const ranges = _forwardSexpRange(doc, offsets, GoUpSexpOption.WhenAtLimit, goPastWhitespace); + return isNumber(oneOrMoreOffsets) ? ranges[0] : ranges; } export function backwardSexpOrUpRange( doc: EditableDocument, - offset: number = Math.min(doc.selection.anchor, doc.selection.active), + offsets?: number[], + goPastWhitespace?: boolean +): Array<[number, number]>; +export function backwardSexpOrUpRange( + doc: EditableDocument, + offset?: number, + goPastWhitespace?: boolean +): [number, number]; +export function backwardSexpOrUpRange( + doc: EditableDocument, + oneOrMoreOffsets: number[] | number = doc.selections.map((s) => s.start), goPastWhitespace = false -): [number, number] { - return _backwardSexpRange(doc, offset, GoUpSexpOption.WhenAtLimit, goPastWhitespace); +): Array<[number, number]> | [number, number] { + const offsets = isNumber(oneOrMoreOffsets) ? [oneOrMoreOffsets] : oneOrMoreOffsets; + const ranges = _backwardSexpRange(doc, offsets, GoUpSexpOption.WhenAtLimit, goPastWhitespace); + return isNumber(oneOrMoreOffsets) ? ranges[0] : ranges; } export function rangeToForwardDownList( diff --git a/src/extension-test/unit/cursor-doc/indent-test.ts b/src/extension-test/unit/cursor-doc/indent-test.ts index 1b75a0580..954545f02 100644 --- a/src/extension-test/unit/cursor-doc/indent-test.ts +++ b/src/extension-test/unit/cursor-doc/indent-test.ts @@ -11,22 +11,22 @@ describe('indent', () => { describe('lists', () => { it('calculates indents for cursor in empty list', () => { const doc = docFromTextNotation('(|)'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(1); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(1); }); it('calculates indents for cursor in empty list prepended by text', () => { const doc = docFromTextNotation(' a b (|)'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(7); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(7); }); it('calculates indents for empty list inside vector', () => { const doc = docFromTextNotation('[(|)]'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(2); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(2); }); it("calculates indents for cursor in at arg 0 in `[['inner' 0]]`", () => { const doc = docFromTextNotation('(foo|)'); expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['inner', 0]], }) @@ -38,7 +38,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['inner', 0]], }) @@ -50,7 +50,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['block', 1]], }) @@ -62,7 +62,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['block', 1]], }) @@ -74,11 +74,11 @@ describe('indent', () => { describe('vectors', () => { it('calculates indents for cursor in empty vector', () => { const doc = docFromTextNotation('[|]'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(1); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(1); }); it('calculates indents for cursor in empty vector inside list', () => { const doc = docFromTextNotation('([|])'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(2); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(2); }); it('does not use indent rules for vectors with symbols at ”call” position', () => { // https://github.com/BetterThanTomorrow/calva/issues/1622 @@ -86,7 +86,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['inner', 0]], }) @@ -98,11 +98,11 @@ describe('indent', () => { describe('maps', () => { it('calculates indents for cursor in empty map', () => { const doc = docFromTextNotation('{|}'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(1); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(1); }); it('calculates indents for cursor in empty map inside list inside a vector', () => { const doc = docFromTextNotation('([{|}])'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(3); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(3); }); it('does not use indent rules for maps with symbols at ”call” position', () => { // https://github.com/BetterThanTomorrow/calva/issues/1622 @@ -110,7 +110,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['inner', 0]], }) @@ -122,11 +122,11 @@ describe('indent', () => { describe('sets', () => { it('calculates indents for cursor in empty set', () => { const doc = docFromTextNotation('#{|}'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(2); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(2); }); it('calculates indents for cursor in empty set inside list inside a vector', () => { const doc = docFromTextNotation('([#{|}])'); - expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0])).toEqual(4); + expect(indent.getIndent(doc.model, textAndSelection(doc)[1][0][0])).toEqual(4); }); it('does not use indent rules for maps with symbols at ”call” position', () => { // https://github.com/BetterThanTomorrow/calva/issues/1622 @@ -134,7 +134,7 @@ describe('indent', () => { expect( indent.getIndent( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig({ '#"^\\w"': [['inner', 0]], }) @@ -153,7 +153,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); @@ -166,7 +166,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); @@ -183,7 +183,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); @@ -196,7 +196,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); @@ -210,7 +210,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); @@ -228,7 +228,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); @@ -246,7 +246,7 @@ describe('indent', () => { }; const state: indent.IndentInformation[] = indent.collectIndents( doc.model, - textAndSelection(doc)[1][0], + textAndSelection(doc)[1][0][0], mkConfig(rules) ); expect(state.length).toEqual(1); diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 0d2f2ded5..3b6f45293 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -12,7 +12,7 @@ import { } from 'vscode'; import * as paredit from '../cursor-doc/paredit'; import * as docMirror from '../doc-mirror/index'; -import { EditableDocument } from '../cursor-doc/model'; +import { EditableDocument, ModelEditResult, ModelEditSelection } from '../cursor-doc/model'; import { assertIsDefined } from '../utilities'; const onPareditKeyMapChangedEmitter = new EventEmitter(); @@ -40,7 +40,7 @@ function shouldKillAlsoCutToClipboard() { type PareditCommand = { command: string; - handler: (doc: EditableDocument) => void; + handler: (doc: EditableDocument) => void | Thenable | Thenable; }; const pareditCommands: PareditCommand[] = [ // NAVIGATING @@ -439,7 +439,7 @@ function wrapPareditCommand(command: PareditCommand) { if (!enabled || !languages.has(textEditor.document.languageId)) { return; } - command.handler(mDoc); + void command.handler(mDoc); } catch (e) { console.error(e.message); } From 57a1d9a6d1f1296e63666fd89088bd9243b0b9f1 Mon Sep 17 00:00:00 2001 From: Rayat Date: Sat, 2 Apr 2022 14:50:22 -0700 Subject: [PATCH 11/49] Add more cspell exceptions --- .vscode/settings.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 899767fe8..de404e510 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "ahlbrecht", "alnum", "Alsos", + "analyse", "analysing", "arglist", "arglists", @@ -24,11 +25,14 @@ "calva", "Calva's", "calvapretty", + "ccls", "chmod", "cibuilds", "circleci", + "clangd", "classpath", "cljc", + "cljd", "cljfmt", "cljfx", "cljify", @@ -44,7 +48,9 @@ "Cognitect", "Configurability", "cospaia", + "cpcache", "Dallo", + "darcs", "datafication", "debugable", "debugadapter", @@ -68,10 +74,12 @@ "Elisp", "enablement", "enablements", + "ensime", "entrypoint", "errored", "ESPACEIALLY", "être", + "eunit", "eval", "evals", "falsesomething", @@ -82,6 +90,7 @@ "filipe", "fipp", "foob", + "fslckout", "FUBAR", "gifs", "Girardi", @@ -139,6 +148,7 @@ "parinfer", "pidfile", "piggieback", + "pijul", "polyrepos", "postrelease", "pprint", @@ -189,6 +199,7 @@ "unpromote", "unpromoted", "unsets", + "uuidv", "visibles", "vsce", "vscodevim", From 2bcc6e7494fd68308faf2fcae9ee4c74976dee82 Mon Sep 17 00:00:00 2001 From: Rayat Date: Sat, 2 Apr 2022 15:11:48 -0700 Subject: [PATCH 12/49] Double each example in test-date/paredit-sandbox for multi cursor tests --- test-data/paredit_sandbox.clj | 45 ++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/test-data/paredit_sandbox.clj b/test-data/paredit_sandbox.clj index 3c2b00db9..2cca9af1a 100644 --- a/test-data/paredit_sandbox.clj +++ b/test-data/paredit_sandbox.clj @@ -9,6 +9,8 @@ ;; +(a| b (c + d) e) (a| b (c d) e) @@ -17,19 +19,31 @@ (aa| (c (e f)) g) +(aa| (c (e + f)) g) + ;; === Example 2 - comments ;; Comment killed (a| ;; comment e) +(a| ;; comment + e) + ;; Example 3 - newline ;; newline killed (a| e) +(a| + e) + ;; Example 4 - end of list ;; Don't kill past it +(a b (c |) + e) + (a b (c |) e) @@ -53,6 +67,15 @@ string. " d 19 e 31] (+ a b)) +(let [a 23 + b (+ 4 + 5 + 9) + m {:a 1} + c "hello" + d 19 e 31] + (+ a b)) + ;; Exmaple 8 -- map key value pairs ;; killing from :c includes the corresponding value {:a 1 @@ -60,19 +83,31 @@ string. " :c {:d 4 :e 5}} +{:a 1 + :b 2 + :c {:d 4 + :e 5}} + ;; Example 9 -- deleteing from #_ should delete whole expr [#_(comment (+ 2 3))] +[#_(comment + (+ 2 3))] + ;; Example 10 -- deleting from | should delete to eol ;; | (23 34 ;; ) - ;; Example 11 -- Deleting should delete whole expr to closing ] | 24 [1] +| 24 [1] + +43 [1 2 + 3] + 43 [1 2 3] @@ -84,9 +119,17 @@ string. " ;; Example 14 -- newline in string, deletes to end of string ["abc| def\n ghi" "this stays"] +["abc| def\n ghi" "this stays"] ;; Example 15 -- Heisenbug should delete up to and including g] #_|[a b (c d e f) g] + +#_|[a b (c d + e + f) g] + +:a + :a From 40f26129cdd7443a6e707e99a9fdcc5b3acfd86b Mon Sep 17 00:00:00 2001 From: Rayat Date: Sat, 2 Apr 2022 15:42:15 -0700 Subject: [PATCH 13/49] Format --- .vscode/settings.json | 6 +----- src/cursor-doc/paredit.ts | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index de404e510..6183d1eca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -214,11 +214,7 @@ ], "cSpell.languageSettings": [ { - "languageId": [ - "clojure", - "json", - "typescript" - ], + "languageId": ["clojure", "json", "typescript"], "allowCompoundWords": false } ], diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 4171ee747..fab2a4393 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -290,7 +290,7 @@ export function forwardSexpRange( ): [number, number]; export function forwardSexpRange( doc: EditableDocument, - oneOrMoreOffsets: number[]|number = doc.selections.map((s) => s.end), + oneOrMoreOffsets: number[] | number = doc.selections.map((s) => s.end), goPastWhitespace = false ): Array<[number, number]> | [number, number] { const offsets = Array.isArray(oneOrMoreOffsets) ? oneOrMoreOffsets : [oneOrMoreOffsets]; From 46d4fce55c16a7feb02542ad01ce8c2bf52dae1a Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 7 Apr 2022 11:43:00 -0700 Subject: [PATCH 14/49] Fix unit test watcher not capturing changes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ec6aef35..b0a3d3e8a 100644 --- a/package.json +++ b/package.json @@ -2723,7 +2723,7 @@ "calva-lib-test": "node ./out/cljs-lib/test/cljs-lib-tests.js", "integration-test": "node ./out/extension-test/integration/runTests.js", "unit-test": "npx mocha --require ts-node/register 'src/extension-test/unit/**/*-test.ts'", - "unit-test-watch": "npx mocha --watch --require ts-node/register 'src/extension-test/unit/**/*-test.ts'", + "unit-test-watch": "npx mocha --watch --require ts-node/register --watch-extensions ts --watch-files src 'src/extension-test/unit/**/*-test.ts'", "publish": "bb publish.clj", "prettier-format": "npx prettier --write './**/*.{ts,js,json}'", "prettier-check": "npx prettier --check './**/*.{ts,js,json}'", From 6f8a758cfe28552a2b3fe019fdd69d875e9c1b56 Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 7 Apr 2022 11:43:22 -0700 Subject: [PATCH 15/49] Add whole-project format step before compile --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0a3d3e8a..4105f1eaf 100644 --- a/package.json +++ b/package.json @@ -2706,7 +2706,7 @@ "watch-docs": "mkdocs serve", "clean": "rimraf ./out && rimraf ./tsconfig.tsbuildinfo && rimraf ./cljs-out", "update-grammar": "node ./src/calva-fmt/update-grammar.js ./src/calva-fmt/atom-language-clojure/grammars/clojure.cson clojure.tmLanguage.json", - "precompile": "npm i && npm run clean && npm run update-grammar", + "precompile": "npm i && npm run clean && npm run update-grammar && npm run prettier-format", "compile-cljs": "npx shadow-cljs compile :calva-lib :test", "compile-ts": "npx tsc --project ./tsconfig.json", "compile": "npm run compile-cljs && npm run compile-ts", From 8fd9345888ee6ef830a4619334b5121d3f6950ed Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 7 Apr 2022 11:45:56 -0700 Subject: [PATCH 16/49] Update text notations utils for new syntax; cleanup --- .../unit/common/text-notation.ts | 92 +++++++------------ .../unit/cursor-doc/paredit-test.ts | 39 ++++---- 2 files changed, 55 insertions(+), 76 deletions(-) diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index a55dd071a..e75365a39 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -1,5 +1,5 @@ import * as model from '../../../cursor-doc/model'; -import { clone, entries, cond, toInteger, last, first, cloneDeep } from 'lodash'; +import { clone, entries, cond, toInteger, last, first, cloneDeep, orderBy } from 'lodash'; /** * Text Notation for expressing states of a document, including @@ -7,56 +7,34 @@ import { clone, entries, cond, toInteger, last, first, cloneDeep } from 'lodash' * * Since JavasScript makes it clumsy with multiline strings, * newlines are denoted with a middle dot character: `•` * * Selections are denoted like so - * 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 + * * Cursors, (which are actually selections with identical start and end) are denoted with a single `|`, with <= 10 multiple cursors defined by `|1`, `|2`, ... `|9`, etc, or in regex: /\|\d/. 0-indexed, so `|` is 0, `|1` is 1, etc. + * * This is however actually an alternative for the following `>\d?` notation, it has the same left->right semantics, but looks more like a cursor/caret lol, and more importantly, drops the 0 for the first cursor. + * * Selections with direction left->right are denoted with `>0`, `>1`, `>2`, ... `>9` etc at the range boundaries + * * Selections with direction right->left are denoted with `<0`, `<1`, `<2`, ... `<9` etc at the range boundaries */ -function _textNotationToTextAndSelection(s: string): [string, { anchor: number; active: number }] { - const text = s.replace(/•/g, '\n').replace(/\|?[<>]?\|/g, ''); - let anchor = undefined; - let active = undefined; - anchor = s.indexOf('|>|'); - if (anchor >= 0) { - active = s.lastIndexOf('|>|') - 3; - } else { - anchor = s.lastIndexOf('|<|'); - if (anchor >= 0) { - anchor -= 3; - active = s.indexOf('|<|'); - } else { - anchor = s.indexOf('|'); - if (anchor >= 0) { - active = s.lastIndexOf('|'); - if (active !== anchor) { - active -= 1; - } else { - active = anchor; - } - } - } - } - return [text, { anchor, active }]; -} - 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) + /** + * 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 + /(?(?:(?<|>(?=\d{1})))|(?:\|))(?\d{1})?/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 + // 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 cursorMatchInstances = matches.reduce((acc, curr, index) => { const nextAcc = { ...acc }; - // const currRepositioned = cloneDeep(curr); const sumOfPreviousCursorOffsets = Array.from(matches) .slice(0, index) @@ -64,32 +42,35 @@ function textNotationToTextAndSelection(content: string): [string, model.ModelEd curr.index = curr.index - sumOfPreviousCursorOffsets; - const cursorMatchStr = curr.groups['cursorType'] ?? curr[0]; + // const cursorMatchStr = curr.groups['cursorType'] ?? curr[0]; + const cursorMatchStr = curr[0]; const matchesForCursor = nextAcc[cursorMatchStr] ?? []; nextAcc[cursorMatchStr] = [...matchesForCursor, curr]; return nextAcc; }, {} as { [key: string]: RegExpMatchArray[] }); - return [ - text, - entries(cursorMatchInstances).map(([cursorMatchStr, matches]) => { - const firstMatch = first(matches); - const secondMatch = last(matches) ?? firstMatch; + const selections = [].fill(0, matches.length, undefined); + + entries(cursorMatchInstances).forEach(([_, matches]) => { + const firstMatch = first(matches); + const secondMatch = last(matches) ?? firstMatch; + + const isReversed = + (firstMatch.groups['selectionDirection'] ?? firstMatch[2] ?? '') === '<' ? true : false; - const isReversed = - (firstMatch.groups['selectionDirection'] ?? firstMatch[2] ?? '') === '<' ? true : false; + const start = firstMatch.index; + const end = secondMatch.index === firstMatch.index ? secondMatch.index : secondMatch.index; - 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 anchor = isReversed ? end : start; - const active = isReversed ? start : end; + const cursorNumber = toInteger(firstMatch.groups['cursorNumber'] ?? firstMatch[3] ?? '0'); - // const cursorNumber = toInteger(firstMatch.groups['cursorNumber'] ?? firstMatch[3] ?? '0'); + // return new model.ModelEditSelection(anchor, active, start, end, isReversed); + selections[cursorNumber] = new model.ModelEditSelection(anchor, active, start, end, isReversed); + }); - return new model.ModelEditSelection(anchor, active, start, end, isReversed); - }), - ]; + return [text, selections]; } /** @@ -97,11 +78,8 @@ function textNotationToTextAndSelection(content: string): [string, model.ModelEd */ export function docFromTextNotation(s: string): model.StringDocument { const [text, selections] = textNotationToTextAndSelection(s); - // const [text, selections] = _textNotationToTextAndSelection(s); const doc = new model.StringDocument(text); doc.selections = selections; - // doc.selections = [selections]; - // doc.selections = [new model.ModelEditSelection(selections.anchor, selections.active)]; return doc; } diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index 83ea0726c..c0c25a966 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -80,18 +80,18 @@ describe('paredit', () => { expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds next symbol, including leading space', () => { - const a = docFromTextNotation('(|>|def|>| foo [vec])'); - const b = docFromTextNotation('(def|>| foo|>| [vec])'); + const a = docFromTextNotation('(|def| foo [vec])'); + const b = docFromTextNotation('(def| foo| [vec])'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds following vector including leading space', () => { - const a = docFromTextNotation('(|>|def foo|>| [vec])'); - const b = docFromTextNotation('(def foo|>| [vec]|>|)'); + const a = docFromTextNotation('(|def foo| [vec])'); + const b = docFromTextNotation('(def foo| [vec]|)'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Reverses direction of selection and finds next sexp', () => { const a = docFromTextNotation('(|<|def foo|<| [vec])'); - const b = docFromTextNotation('(def foo|>| [vec]|>|)'); + const b = docFromTextNotation('(def foo| [vec]|)'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); }); @@ -120,7 +120,7 @@ describe('paredit', () => { it('Finds previous form, including space, and reverses direction', () => { // TODO: Should we really be reversing the direction here? const a = docFromTextNotation('(def |<|foo [vec]|<|)'); - const b = docFromTextNotation('(|>|def |>|foo [vec])'); + const b = docFromTextNotation('(|def |foo [vec])'); expect(paredit.backwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); }); @@ -399,18 +399,18 @@ describe('paredit', () => { expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds next symbol, including leading space', () => { - const a = docFromTextNotation('(|>|def|>| foo [vec])'); - const b = docFromTextNotation('(def|>| foo|>| [vec])'); + const a = docFromTextNotation('(|def| foo [vec])'); + const b = docFromTextNotation('(def| foo| [vec])'); expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds following vector including leading space', () => { - const a = docFromTextNotation('(|>|def foo|>| [vec])'); - const b = docFromTextNotation('(def foo|>| [vec]|>|)'); + const a = docFromTextNotation('(|def foo| [vec])'); + const b = docFromTextNotation('(def foo| [vec]|)'); expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Reverses direction of selection and finds next sexp', () => { const a = docFromTextNotation('(|<|def foo|<| [vec])'); - const b = docFromTextNotation('(def foo|>| [vec]|>|)'); + const b = docFromTextNotation('(def foo| [vec]|)'); expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); }); @@ -439,7 +439,7 @@ describe('paredit', () => { it('Finds previous form, including space, and reverses direction', () => { // TODO: Should we really be reversing the direction here? const a = docFromTextNotation('(def |<|foo [vec]|<|)'); - const b = docFromTextNotation('(|>|def |>|foo [vec])'); + const b = docFromTextNotation('(|def |foo [vec])'); expect(paredit.backwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Goes up when at front bounds', () => { @@ -451,20 +451,20 @@ describe('paredit', () => { describe('moveToRangeRight', () => { it('Places cursor at the right end of the selection', () => { - const a = docFromTextNotation('(def |>|foo|>| [vec])'); + const a = docFromTextNotation('(def |foo| [vec])'); const b = docFromTextNotation('(def foo| [vec])'); paredit.moveToRangeRight(a, textAndSelection(a)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Places cursor at the right end of the selection 2', () => { - const a = docFromTextNotation('(|>|def foo|>| [vec])'); + const a = docFromTextNotation('(|def foo| [vec])'); const b = docFromTextNotation('(def foo| [vec])'); paredit.moveToRangeRight(a, textAndSelection(a)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Move to right of given range, regardless of previous selection', () => { const a = docFromTextNotation('(|<|def|<| foo [vec])'); - const b = docFromTextNotation('(def foo |>|[vec]|>|)'); + const b = docFromTextNotation('(def foo |[vec]|)'); const c = docFromTextNotation('(def foo [vec]|)'); paredit.moveToRangeRight(a, textAndSelection(b)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(c)); @@ -473,20 +473,20 @@ describe('paredit', () => { describe('moveToRangeLeft', () => { it('Places cursor at the left end of the selection', () => { - const a = docFromTextNotation('(def |>|foo|>| [vec])'); + const a = docFromTextNotation('(def |foo| [vec])'); const b = docFromTextNotation('(def |foo [vec])'); paredit.moveToRangeLeft(a, textAndSelection(a)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Places cursor at the left end of the selection 2', () => { - const a = docFromTextNotation('(|>|def foo|>| [vec])'); + const a = docFromTextNotation('(|def foo| [vec])'); const b = docFromTextNotation('(|def foo [vec])'); paredit.moveToRangeLeft(a, textAndSelection(a)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Move to left of given range, regardless of previous selection', () => { const a = docFromTextNotation('(|<|def|<| foo [vec])'); - const b = docFromTextNotation('(def foo |>|[vec]|>|)'); + const b = docFromTextNotation('(def foo |[vec]|)'); const c = docFromTextNotation('(def foo |[vec])'); paredit.moveToRangeLeft(a, textAndSelection(b)[1]); expect(textAndSelection(a)).toEqual(textAndSelection(c)); @@ -657,7 +657,7 @@ describe('paredit', () => { expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Contracts forward selection and extends backwards', () => { - const a = docFromTextNotation('(def foo [:foo :bar |>|:baz|>|])'); + const a = docFromTextNotation('(def foo [:foo :bar |:baz|])'); const selDoc = docFromTextNotation('(def foo [:foo |:bar| :baz])'); const b = docFromTextNotation('(def foo [:foo |<|:bar |<|:baz])'); paredit.selectRangeBackward( @@ -941,6 +941,7 @@ describe('paredit', () => { }); }); }); + describe('edits', () => { describe('Close lists', () => { it('Advances cursor if at end of list of the same type', () => { From 49f77e84fed0a5750b14cc52460386c5594bbca0 Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 7 Apr 2022 11:46:31 -0700 Subject: [PATCH 17/49] Add textNotationFromDoc debug unit test util + test for it --- .../unit/common/text-notation-test.ts | 13 ++++++ .../unit/common/text-notation.ts | 43 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/extension-test/unit/common/text-notation-test.ts diff --git a/src/extension-test/unit/common/text-notation-test.ts b/src/extension-test/unit/common/text-notation-test.ts new file mode 100644 index 000000000..9f3a65b70 --- /dev/null +++ b/src/extension-test/unit/common/text-notation-test.ts @@ -0,0 +1,13 @@ +import * as expect from 'expect'; +import { docFromTextNotation, textNotationFromDoc } from '../common/text-notation'; +import _ = require('lodash'); + +describe('text-notation test utils', () => { + describe('textNotationFromDoc', () => { + it('should return the same input text to textNotationFromDoc', () => { + const inputText = '(a b|1) (a b|) (a (b))'; + const doc = docFromTextNotation(inputText); + expect(textNotationFromDoc(doc)).toEqual(inputText); + }); + }); +}); diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index e75365a39..ffdab6c7b 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -83,6 +83,49 @@ export function docFromTextNotation(s: string): model.StringDocument { return doc; } +export function textNotationFromDoc(doc: model.StringDocument): string { + const selections = doc.selections ?? []; + let cursorSymbols: [number, string][] = []; + selections.forEach((s, cursorNumber) => { + const cursorType = s.isReversed ? '<' : '|'; + cursorSymbols.push([s.start, `${cursorType}${cursorNumber || ''}`]); + if (s.isSelection) { + cursorSymbols.push([s.end, `${cursorType}${cursorNumber || ''}`]); + } + }); + + cursorSymbols = orderBy(cursorSymbols, (c) => c[0]); + + const text = doc.model.lines.map((l) => l.text).join('•'); + + // basically split up the text into chunks separated by where they'd have had cursor symbols, and append cursor symbols after each chunk, before joining back together + // this way we can insert the cursor symbols in the right place without having to worry about the cumulative offsets created by appending the cursor symbols + const textSegments = cursorSymbols + .reduce( + (acc, [offset, symbol], index) => { + const lastSection = last(acc)[1]; + const sections = acc.slice(0, -1); + + const lastSectionOffset = + offset - sections.filter((s) => s[0]).reduce((sum, sec) => sum + sec[1].length, 0); + const newSectionOfText = [true, lastSection.slice(0, lastSectionOffset)]; + const newSectionWithCursor = [false, symbol]; + const restOfText = lastSection.slice(lastSectionOffset); + + return [...sections, newSectionOfText, newSectionWithCursor, [true, restOfText]]; + }, + [ + [true, text] as [ + boolean /* is an actual text segment instead of cursor symbol? */, + string /* text segment or cursor symbol */ + ], + ] + ) + .map((s) => s[1]); + + return textSegments.join(''); +} + /** * Utility function to get the text from a document. * @param doc From 230dff698b114bca3cea8020cafac33a3e5c7aba Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 7 Apr 2022 11:48:14 -0700 Subject: [PATCH 18/49] Add distance/isSelection/isCursor utility props to ModelEditSelection --- src/cursor-doc/model.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index ed757d997..3fa289681 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -154,6 +154,14 @@ export class ModelEditSelection { } } */ + get isCursor() { + return this.anchor === this.active; + } + + get isSelection() { + return this.anchor !== this.active; + } + get isReversed() { this._updateDirection(); return this._isReversed; @@ -170,6 +178,10 @@ export class ModelEditSelection { } } + get distance() { + return this._end - this._start; + } + clone() { return new ModelEditSelection(this._anchor, this._active); } From 2242e08fc21b571a979c44b4c697045eafb32bf3 Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 7 Apr 2022 11:49:33 -0700 Subject: [PATCH 19/49] Remove naive EditableDocument .delete() .backspace() single cursor args --- src/cursor-doc/model.ts | 47 ++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index 3fa289681..5e57a1519 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -236,8 +236,8 @@ export interface EditableDocument { insertString: (text: string) => void; getSelectionTexts: () => string[]; getSelectionText: (index: number) => string; - delete: (index?: number) => Thenable; - backspace: (index?: number) => Thenable; + delete: () => Thenable; + backspace: () => Thenable; } /** The underlying model for the REPL readline. */ @@ -679,36 +679,21 @@ export class StringDocument implements EditableDocument { getSelectionTexts: () => string[]; 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)], - }); - } + delete() { + return this.model.edit( + this.selections.map(({ anchor: p }) => new ModelEdit('deleteRange', [p, 1])), + { + selections: this.selections.map(({ anchor: p }) => new ModelEditSelection(p)), + } + ); } - 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)], - } - ); - } + backspace() { + 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)), + } + ); } } From 4f77f77ccc9762fa4ca151aab9e63836ceaf5fc8 Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 7 Apr 2022 11:56:11 -0700 Subject: [PATCH 20/49] Make ModelEdit generic with edit fn as type param + utils, fix dead code --- src/cursor-doc/model.ts | 36 +++++++--------- src/cursor-doc/paredit.ts | 90 ++++++++++++--------------------------- 2 files changed, 44 insertions(+), 82 deletions(-) diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index 5e57a1519..0cc684180 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -28,9 +28,14 @@ export class TextLine { } export type ModelEditFunction = 'insertString' | 'changeRange' | 'deleteRange'; - -export class ModelEdit { - constructor(public editFn: ModelEditFunction, public args: any[]) {} +export type ModelEditFunctionArgs = T extends 'insertString' + ? Parameters + : T extends 'changeRange' + ? Parameters + : Parameters; + +export class ModelEdit { + constructor(public editFn: T, public args: ModelEditFunctionArgs) {} } /** @@ -191,7 +196,6 @@ export type ModelEditOptions = { undoStopBefore?: boolean; formatDepth?: number; skipFormat?: boolean; - // selection?: ModelEditSelection; selections?: ModelEditSelection[]; }; @@ -208,7 +212,10 @@ 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; @@ -523,13 +530,7 @@ export class LineInputModel implements EditableModel { * @param oldSelection the old selection * @param newSelection the new selection */ - private changeRange( - start: number, - end: number, - text: string, - oldSelection?: [number, number], - newSelection?: [number, number] - ) { + private changeRange(start: number, end: number, text: string) { const t1 = new Date(); const startPos = Math.min(start, end); @@ -595,13 +596,8 @@ export class LineInputModel implements EditableModel { * @param text the text to insert * @param oldCursor the [row,col] of the cursor at the start of the operation */ - insertString( - offset: number, - text: string, - oldSelection?: [number, number], - newSelection?: [number, number] - ): number { - this.changeRange(offset, offset, text, oldSelection, newSelection); + insertString(offset: number, text: string): number { + this.changeRange(offset, offset, text); return text.length; } @@ -620,7 +616,7 @@ export class LineInputModel implements EditableModel { oldSelection?: [number, number], newSelection?: [number, number] ) { - this.changeRange(offset, offset + count, '', oldSelection, newSelection); + this.changeRange(offset, offset + count, ''); } /** Return the offset of the last character in this model. */ diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index fab2a4393..1e5c21a6b 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -1,9 +1,14 @@ -import { isEqual, isNumber, last, pick, property, clone, isBoolean } from 'lodash'; +import { isEqual, last, pick, property, clone, isBoolean, orderBy } from 'lodash'; import { validPair } from './clojure-lexer'; -import { EditableDocument, ModelEdit, ModelEditSelection, ModelEditResult } from './model'; +import { + EditableDocument, + ModelEdit, + ModelEditSelection, + ModelEditResult, + ModelEditFunctionArgs, +} from './model'; import { LispTokenCursor } from './token-cursor'; 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. @@ -595,8 +600,8 @@ export function wrapSexpr( new ModelEdit('insertString', [ range[0], open, - [end, end], - [start + open.length, start + open.length], + // [end, end], + // [start + open.length, start + open.length], ]), ], { @@ -703,8 +708,8 @@ export function joinSexp(doc: EditableDocument): Thenable { prevEnd - 1, nextStart + 1, prevToken.type === 'close' ? ' ' : '', - [start, start], - [prevEnd, prevEnd], + // [start, start], + // [prevEnd, prevEnd], ]) ); selections[index] = new ModelEditSelection(prevEnd); @@ -810,7 +815,7 @@ export function _forwardSlurpSexpSingle( const wsEndOffset = wsOutSideCursor.offsetStart; const newCloseOffset = cursor.offsetStart; const replacedText = doc.model.getText(wsStartOffset, wsEndOffset); - const changeArgs = + const changeArgs: ModelEditFunctionArgs<'changeRange'> = replacedText.indexOf('\n') >= 0 ? [currentCloseOffset, currentCloseOffset + close.length, ''] : [wsStartOffset, wsEndOffset, ' ']; @@ -1454,13 +1459,7 @@ export function transpose( } edits.push( new ModelEdit('changeRange', [rightStart, rightEnd, leftText]), - new ModelEdit('changeRange', [ - leftStart, - leftEnd, - rightText, - [left, left], - [newCursorPos, newCursorPos], - ]) + new ModelEdit('changeRange', [leftStart, leftEnd, rightText]) ); selections[index] = new ModelEditSelection(newCursorPos); } @@ -1681,10 +1680,7 @@ export function dragSexprBackwardUp( wsInfo.rightWsRange[1] - currentRange[0], ]); } - edits.push( - deleteEdit, - new ModelEdit('insertString', [listStart, dragText, [p, p], [newCursorPos, newCursorPos]]) - ); + edits.push(deleteEdit, new ModelEdit('insertString', [listStart, dragText])); selections[index] = new ModelEditSelection(newCursorPos); } }); @@ -1722,12 +1718,7 @@ export function dragSexprForwardDown( const insertText = doc.model.getText(...currentRange) + (wsInfo.rightWsHasNewline ? '\n' : ' '); edits.push( - new ModelEdit('insertString', [ - insertStart, - insertText, - [p, p], - [newCursorPos, newCursorPos], - ]), + new ModelEdit('insertString', [insertStart, insertText]), new ModelEdit('deleteRange', [currentRange[0], deleteLength]) ); selections[index] = new ModelEditSelection(newCursorPos); @@ -1770,7 +1761,7 @@ export function dragSexprForwardUp( } const newCursorPos = listEnd + newPosOffset + 1 - deleteLength; edits.push( - new ModelEdit('insertString', [listEnd, dragText, [p, p], [newCursorPos, newCursorPos]]), + new ModelEdit('insertString', [listEnd, dragText]), new ModelEdit('deleteRange', [deleteStart, deleteLength]) ); selections[index] = new ModelEditSelection(newCursorPos); @@ -1813,12 +1804,7 @@ export function dragSexprBackwardDown( insertText = (siblingWsInfo.leftWsHasNewline ? '\n' : ' ') + insertText; edits.push( new ModelEdit('deleteRange', [wsInfo.leftWsRange[0], deleteLength]), - new ModelEdit('insertString', [ - insertStart, - insertText, - [p, p], - [newCursorPos, newCursorPos], - ]) + new ModelEdit('insertString', [insertStart, insertText]) ); selections[index] = new ModelEditSelection(newCursorPos); break; @@ -1872,21 +1858,11 @@ export function addRichComment( checkIfRichCommentExistsCursor.forwardWhitespace(false); // insert nothing, just place cursor const newCursorPos = checkIfRichCommentExistsCursor.offsetStart; - void doc.model.edit( - [ - new ModelEdit('insertString', [ - newCursorPos, - '', - [newCursorPos, newCursorPos], - [newCursorPos, newCursorPos], - ]), - ], - { - selections: [new ModelEditSelection(newCursorPos)], - skipFormat: true, - undoStopBefore: false, - } - ); + void doc.model.edit([new ModelEdit('insertString', [newCursorPos, ''])], { + selections: [new ModelEditSelection(newCursorPos)], + skipFormat: true, + undoStopBefore: false, + }); return; } } @@ -1900,19 +1876,9 @@ export function addRichComment( const append = '\n'.repeat(numAppendNls); const insertText = `${prepend}${richComment}${append}`; const newCursorPos = insertStart + 11 + numPrependNls * doc.model.lineEndingLength; - void doc.model.edit( - [ - new ModelEdit('insertString', [ - insertStart, - insertText, - [insertStart, insertStart], - [newCursorPos, newCursorPos], - ]), - ], - { - selections: [new ModelEditSelection(newCursorPos)], - skipFormat: false, - undoStopBefore: true, - } - ); + void doc.model.edit([new ModelEdit('insertString', [insertStart, insertText])], { + selections: [new ModelEditSelection(newCursorPos)], + skipFormat: false, + undoStopBefore: true, + }); } From 071b4d2abaebace4085cb5c0c8667ec705d04103 Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 7 Apr 2022 11:59:27 -0700 Subject: [PATCH 21/49] Simplify overloaded paredit funcs --- src/cursor-doc/paredit.ts | 121 +++++++++----------------------------- src/paredit/extension.ts | 25 ++++---- 2 files changed, 41 insertions(+), 105 deletions(-) diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 1e5c21a6b..06cda81c6 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -77,8 +77,8 @@ export function selectForwardSexp(doc: EditableDocument) { const rangeFn = selection.active >= selection.anchor ? forwardSexpRange - : (doc: EditableDocument) => forwardSexpRange(doc, selection.active, true); - return rangeFn(doc, selection.start); + : (doc: EditableDocument) => forwardSexpRange(doc, [selection.active], true); + return rangeFn(doc, [selection.start])[0]; }); selectRangeForward(doc, ranges); } @@ -88,8 +88,8 @@ export function selectRight(doc: EditableDocument) { const ranges = doc.selections.map((selection) => { const rangeFn = selection.active >= selection.anchor - ? (doc) => forwardHybridSexpRange(doc, selection.end) - : (doc: EditableDocument) => forwardHybridSexpRange(doc, selection.active, true); + ? (doc) => forwardHybridSexpRange(doc, [selection.end])[0] + : (doc: EditableDocument) => forwardHybridSexpRange(doc, [selection.active], true)[0]; return rangeFn(doc); }); selectRangeForward(doc, ranges); @@ -99,8 +99,8 @@ export function selectForwardSexpOrUp(doc: EditableDocument) { const ranges = doc.selections.map((selection) => { const rangeFn = selection.active >= selection.anchor - ? (doc) => forwardSexpOrUpRange(doc, selection.end) - : (doc: EditableDocument) => forwardSexpOrUpRange(doc, selection.active, true); + ? (doc) => forwardSexpOrUpRange(doc, [selection.end])[0] + : (doc: EditableDocument) => forwardSexpOrUpRange(doc, [selection.active], true)[0]; return rangeFn(doc); }); selectRangeForward(doc, ranges); @@ -111,8 +111,8 @@ export function selectBackwardSexp(doc: EditableDocument) { const rangeFn = selection.active <= selection.anchor ? backwardSexpRange - : (doc: EditableDocument) => backwardSexpRange(doc, selection.active, false); - return rangeFn(doc, selection.start); + : (doc: EditableDocument) => backwardSexpRange(doc, [selection.active], false); + return rangeFn(doc, [selection.start])[0]; }); selectRangeBackward(doc, ranges); } @@ -157,9 +157,9 @@ export function selectBackwardSexpOrUp(doc: EditableDocument) { const ranges = doc.selections.map((selection) => { const rangeFn = selection.active <= selection.anchor - ? (doc: EditableDocument) => backwardSexpOrUpRange(doc, selection.active, false) - : (doc: EditableDocument) => backwardSexpOrUpRange(doc, selection.active, false); - return rangeFn(doc); + ? (doc: EditableDocument) => backwardSexpOrUpRange(doc, [selection.active], false) + : (doc: EditableDocument) => backwardSexpOrUpRange(doc, [selection.active], false); + return rangeFn(doc)[0]; }); selectRangeBackward(doc, ranges); } @@ -285,42 +285,18 @@ function _backwardSexpRange( export function forwardSexpRange( doc: EditableDocument, - offsets?: number[], - goPastWhitespace?: boolean -): Array<[number, number]>; -export function forwardSexpRange( - doc: EditableDocument, - offset?: number, - goPastWhitespace?: boolean -): [number, number]; -export function forwardSexpRange( - doc: EditableDocument, - oneOrMoreOffsets: number[] | number = doc.selections.map((s) => s.end), + offsets: number[] = doc.selections.map((s) => s.end), goPastWhitespace = false -): Array<[number, number]> | [number, number] { - const offsets = Array.isArray(oneOrMoreOffsets) ? oneOrMoreOffsets : [oneOrMoreOffsets]; - const ranges = _forwardSexpRange(doc, offsets, GoUpSexpOption.Never, goPastWhitespace); - return Array.isArray(oneOrMoreOffsets) ? ranges : ranges[0]; +): Array<[number, number]> { + return _forwardSexpRange(doc, offsets, GoUpSexpOption.Never, goPastWhitespace); } export function backwardSexpRange( doc: EditableDocument, - offsets?: number[], - goPastWhitespace?: boolean -): Array<[number, number]>; -export function backwardSexpRange( - doc: EditableDocument, - offset?: number, - goPastWhitespace?: boolean -): [number, number]; -export function backwardSexpRange( - doc: EditableDocument, - oneOrMoreOffsets: number[] | number = doc.selections.map((s) => s.start), + offsets: number[] = doc.selections.map((s) => s.start), goPastWhitespace = false -): Array<[number, number]> | [number, number] { - const offsets = Array.isArray(oneOrMoreOffsets) ? oneOrMoreOffsets : [oneOrMoreOffsets]; - const ranges = _backwardSexpRange(doc, offsets, GoUpSexpOption.Never, goPastWhitespace); - return Array.isArray(oneOrMoreOffsets) ? ranges : ranges[0]; +): Array<[number, number]> { + return _backwardSexpRange(doc, offsets, GoUpSexpOption.Never, goPastWhitespace); } export function forwardListRange( @@ -355,29 +331,16 @@ export function backwardListRange( * @param goPastWhitespace * @returns [number, number] */ -export function forwardHybridSexpRange( - doc: EditableDocument, - 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), + offsets: number[] = doc.selections.map((s) => s.end), goPastWhitespace = false -): [number, number] | Array<[number, number]> { - if (isNumber(offsets)) { - offsets = [offsets]; - } - - const ranges = offsets.map<[number, number]>((offset) => { +): Array<[number, number]> { + return offsets.map<[number, number]>((offset) => { // const { end: offset } = selection; let cursor = doc.getTokenCursor(offset); @@ -442,12 +405,6 @@ export function forwardHybridSexpRange( } return [offset, end]; }); - - if (isNumber(offsets)) { - return ranges[0]; - } else { - return ranges; - } } export function rangeToForwardUpList( @@ -470,42 +427,18 @@ export function rangeToBackwardUpList( export function forwardSexpOrUpRange( doc: EditableDocument, - offsets?: number[], - goPastWhitespace?: boolean -): Array<[number, number]>; -export function forwardSexpOrUpRange( - doc: EditableDocument, - offset?: number, - goPastWhitespace?: boolean -): [number, number]; -export function forwardSexpOrUpRange( - doc: EditableDocument, - oneOrMoreOffsets: number[] | number = doc.selections.map((s) => s.end), + offsets: number[] = doc.selections.map((s) => s.end), goPastWhitespace = false -): Array<[number, number]> | [number, number] { - const offsets = isNumber(oneOrMoreOffsets) ? [oneOrMoreOffsets] : oneOrMoreOffsets; - const ranges = _forwardSexpRange(doc, offsets, GoUpSexpOption.WhenAtLimit, goPastWhitespace); - return isNumber(oneOrMoreOffsets) ? ranges[0] : ranges; +): Array<[number, number]> { + return _forwardSexpRange(doc, offsets, GoUpSexpOption.WhenAtLimit, goPastWhitespace); } export function backwardSexpOrUpRange( doc: EditableDocument, - offsets?: number[], - goPastWhitespace?: boolean -): Array<[number, number]>; -export function backwardSexpOrUpRange( - doc: EditableDocument, - offset?: number, - goPastWhitespace?: boolean -): [number, number]; -export function backwardSexpOrUpRange( - doc: EditableDocument, - oneOrMoreOffsets: number[] | number = doc.selections.map((s) => s.start), + offsets: number[] = doc.selections.map((s) => s.start), goPastWhitespace = false -): Array<[number, number]> | [number, number] { - const offsets = isNumber(oneOrMoreOffsets) ? [oneOrMoreOffsets] : oneOrMoreOffsets; - const ranges = _backwardSexpRange(doc, offsets, GoUpSexpOption.WhenAtLimit, goPastWhitespace); - return isNumber(oneOrMoreOffsets) ? ranges[0] : ranges; +): Array<[number, number]> { + return _backwardSexpRange(doc, offsets, GoUpSexpOption.WhenAtLimit, goPastWhitespace); } export function rangeToForwardDownList( diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 3b6f45293..20f190a66 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -12,7 +12,7 @@ import { } from 'vscode'; import * as paredit from '../cursor-doc/paredit'; import * as docMirror from '../doc-mirror/index'; -import { EditableDocument, ModelEditResult, ModelEditSelection } from '../cursor-doc/model'; +import { EditableDocument, ModelEditResult } from '../cursor-doc/model'; import { assertIsDefined } from '../utilities'; const onPareditKeyMapChangedEmitter = new EventEmitter(); @@ -49,7 +49,10 @@ const pareditCommands: PareditCommand[] = [ handler: (doc: EditableDocument) => { paredit.moveToRangeRight( doc, - doc.selections.map((s) => paredit.forwardSexpRange(doc, s.active)) + paredit.forwardSexpRange( + doc, + doc.selections.map((s) => s.active) + ) ); }, }, @@ -58,7 +61,10 @@ const pareditCommands: PareditCommand[] = [ handler: (doc: EditableDocument) => { paredit.moveToRangeLeft( doc, - doc.selections.map((s) => paredit.backwardSexpRange(doc, s.active)) + paredit.backwardSexpRange( + doc, + doc.selections.map((s) => s.active) + ) ); }, }, @@ -262,14 +268,11 @@ const pareditCommands: PareditCommand[] = [ { command: 'paredit.killRight', handler: (doc: EditableDocument) => { - // 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); - }); + const ranges = paredit.forwardHybridSexpRange(doc); + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, ranges); + } + return paredit.killRange(doc, ranges); }, }, { From 6c5a74a3c9c58bab168726cd1f792111e930cc4a Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 7 Apr 2022 12:00:42 -0700 Subject: [PATCH 22/49] Create exported functions for killForward/BackwardList/Sexp, fix multi --- src/cursor-doc/paredit.ts | 290 ++++++++++++++---- .../unit/cursor-doc/paredit-test.ts | 50 +++ src/paredit/extension.ts | 91 +++--- 3 files changed, 314 insertions(+), 117 deletions(-) diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 06cda81c6..b4b63e750 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -1,4 +1,5 @@ import { isEqual, last, pick, property, clone, isBoolean, orderBy } from 'lodash'; +import _ = require('lodash'); import { validPair } from './clojure-lexer'; import { EditableDocument, @@ -20,15 +21,60 @@ import { replaceAt } from '../util/array'; // Example: paredit.moveToRangeRight(this.readline, paredit.forwardSexpRange(this.readline)) // => paredit.moveForwardSexp(this.readline) -export function killRange( - doc: EditableDocument, - range: [number, number], - 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]])], { - selections: [new ModelEditSelection(left)], +export function killRange(doc: EditableDocument, ranges: Array<[number, number]>) { + const edits = [], + // use tuple to track + // [selection, original selection/cursor order before sorting by location, and amount of text deleted] + selections: [ModelEditSelection, number, number][] = []; + + /** + * Sorting by location backwards simplifies the underlying "deleteRange" operation, + * as the operation logic doesn't have to translate the range by the sum of each prior deletion. + * + * We still however have to do the aforementioned translation/relocation for the post-delete cursor replacement, + * but that happens ONCE, at the end of this whole range killing series, + * and also seems to not be subject to as many strange bugs as + * when we do the translation/relocation for deletion operations. + * + * For example, it appears that sometimes a series of deletions DOESN'T take into account + * prior (ascending order of location) deletions, + * so in order to prevent incorrect text being deleted, we might want to precalculate the updated offsets + * on behalf of the delete operations, as we suggested NOT doing above. + * + * However, sometimes, it DOES take into account the prior deletions (or at least some of them) + * + * Of course, if the latter occurs, yet we thought the former would, our preventative + * precalculated offset adjustments would then cause + * incorrect text to be deleted anyways. + */ + + _(ranges) + .map((r, idx) => [r, idx] as const) + .orderBy(([r]) => Math.min(...r), 'desc') + .forEach(([range, idx]) => { + // we assume the ranges passed above are some transformation of the current selections + // therefore doc.selections[index] should be the range before the transformation... maybe + const [left, right] = [Math.min(...range), Math.max(...range)]; + const length = right - left; + edits.push(new ModelEdit('deleteRange', [left, length])); + selections.push([new ModelEditSelection(left), idx, length]); + }); + + return doc.model.edit(edits, { + selections: _(selections) + // return to original selection/cursor order + .orderBy(([_, idx]) => idx) + // pull each cursor backwards by the amount of text deleted by every prior (by location) cursor's delete operation + .map( + ([selection], _index, others) => + new ModelEditSelection( + selection.start - + _(others) + .filter(([s]) => s.start < selection.start) + .reduce((sum, [_, __, length]) => sum + length, 0) + ) + ) + .value(), }); } @@ -683,38 +729,132 @@ export function spliceSexp( return doc.model.edit(edits, { undoStopBefore, selections }); } +export function killSexpBackward( + doc: EditableDocument, + shouldKillAlsoCutToClipboard = () => false, + copyRangeToClipboard = (doc: EditableDocument, range: Array<[number, number]>) => undefined +) { + const ranges = backwardSexpRange( + doc, + doc.selections.map((s) => s.active) + ); + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, ranges); + } + return killRange(doc, ranges); +} + +export function killSexpForward( + doc: EditableDocument, + shouldKillAlsoCutToClipboard = () => false, + copyRangeToClipboard = (doc: EditableDocument, range: Array<[number, number]>) => undefined +) { + const ranges = forwardSexpRange(doc); + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, ranges); + } + return killRange(doc, ranges); +} + +/** + * In making this compatible with multi-cursor, + * we had to complicate the logic somewhat to make sure + * deletions by prior cursors are taken into account by + * later ones, and are relocated accordingly. + * + * See comments in paredit.killRange() for more details. + */ export function killBackwardList( doc: EditableDocument, - [start, end]: [number, number] + ranges: Array<[number, number]> = doc.selections.map((s) => [s.start, s.end]) ): Thenable { - return doc.model.edit( - [new ModelEdit('changeRange', [start, end, '', [end, end], [start, start]])], - { - selections: [new ModelEditSelection(start)], - } - ); + const edits: ModelEdit<'deleteRange'>[] = [], + selections: [ModelEditSelection, number, number][] = []; + + _(ranges) + .map((r, idx) => [r, idx] as const) + .orderBy(([r]) => Math.min(...r), 'desc') + .forEach(([r, originalIndex]) => { + const [left, right] = r; + const cursor = doc.getTokenCursor(left); + cursor.backwardList(); + // const offset = selections + // .filter((s) => s.start < left) + // .reduce((sum, s) => sum + s.distance, 0); + // const start = cursor.offsetStart - offset; + // const end = right - offset; + // edits.push(new ModelEdit('deleteRange', [start, Math.abs(end - start)])); + const start = cursor.offsetStart; + const end = right; + const length = Math.abs(end - start); + edits.push(new ModelEdit('deleteRange', [start, length])); + // selections.push([new ModelEditSelection(start, end), originalIndex, length]); + selections.push([new ModelEditSelection(start, end), originalIndex, length]); + }); + + return doc.model.edit(edits, { + selections: _(selections) + .orderBy(([_, originalIndex]) => originalIndex) + .map( + ([selection], _idx, others) => + new ModelEditSelection( + selection.start - + _(others) + .filter(([s]) => s.start < selection.start) + .reduce((sum, [_, __, lengthDeleted]) => sum + lengthDeleted, 0) + ) + ) + .value(), + }); } +/** + * In making this compatible with multi-cursor, + * we had to complicate the logic somewhat to make sure + * deletions by prior cursors are taken into account by + * later ones, and are relocated accordingly. + * + * See comments in paredit.killRange() for more details. + */ export function killForwardList( doc: EditableDocument, - [start, end]: [number, number] + ranges: Array<[number, number]> = doc.selections.map((s) => [s.start, s.end]) ): Thenable { - const cursor = doc.getTokenCursor(start); - const inComment = - (cursor.getToken().type == 'comment' && start > cursor.offsetStart) || - cursor.getPrevToken().type == 'comment'; - return doc.model.edit( - [ - new ModelEdit('changeRange', [ - start, - end, - inComment ? '\n' : '', - [start, start], - [start, start], - ]), - ], - { selections: [new ModelEditSelection(start)] } - ); + const edits: ModelEdit<'changeRange'>[] = [], + selections: [ModelEditSelection, number, number][] = []; + + _(ranges) + .map((r, idx) => [r, idx] as const) + .orderBy(([r]) => Math.min(...r), 'desc') + .forEach(([r, originalIndex]) => { + const [left, right] = r; + const cursor = doc.getTokenCursor(left); + cursor.forwardList(); + const inComment = + (cursor.getToken().type == 'comment' && left > cursor.offsetStart) || + cursor.getPrevToken().type == 'comment'; + + const start = cursor.offsetStart; + const end = right; + const length = Math.abs(end - start); + edits.push(new ModelEdit('changeRange', [start, end, inComment ? '\n' : ''])); + selections.push([new ModelEditSelection(start, end), originalIndex, length]); + }); + + return doc.model.edit(edits, { + selections: _(selections) + .orderBy(([_, originalIndex]) => originalIndex) + .map( + ([selection], _idx, others) => + new ModelEditSelection( + selection.start - + _(others) + .filter(([s]) => s.start < selection.start) + .reduce((sum, [_, __, lengthDeleted]) => sum + lengthDeleted, 0) + ) + ) + .value(), + }); } // FIXME: check if this forEach solution works vs map into modelEdit batch @@ -1272,40 +1412,64 @@ export function setSelectionStack( doc.selectionsStack = selections; } -export function raiseSexp( - doc: EditableDocument - // start = doc.selections.anchor, - // end = doc.selections.active -) { - const edits = [], - selections = clone(doc.selections); - doc.selections.forEach((selection, index) => { - const { start, end } = selection; +/** + * In making this compatible with multi-cursor, + * we had to complicate the logic somewhat to make sure + * deletions by prior cursors are taken into account by + * later ones, and are relocated accordingly. + * + * See comments in paredit.killRange() for more details. + */ +export function raiseSexp(doc: EditableDocument) { + const edits: ModelEdit<'changeRange'>[] = [], + selections = doc.selections.map((s) => [0, s.clone()] as [number, ModelEditSelection]); + + _(doc.selections) + .map((s, index) => [s, index] as const) + .orderBy(([s]) => s.start, 'desc') + .forEach(([selection, originalIndex], index) => { + const { start, end } = selection; - const cursor = doc.getTokenCursor(end); - const [formStart, formEnd] = cursor.rangeForCurrentForm(start); - const isCaretTrailing = formEnd - start < start - formStart; - const startCursor = doc.getTokenCursor(formStart); - const endCursor = startCursor.clone(); - if (endCursor.forwardSexp()) { - const raised = doc.model.getText(startCursor.offsetStart, endCursor.offsetStart); - startCursor.backwardList(); - endCursor.forwardList(); - if (startCursor.getPrevToken().type == 'open') { - startCursor.previous(); - if (endCursor.getToken().type == 'close') { - edits.push( - new ModelEdit('changeRange', [startCursor.offsetStart, endCursor.offsetEnd, raised]) - ); - selections[index] = new ModelEditSelection( - isCaretTrailing ? startCursor.offsetStart + raised.length : startCursor.offsetStart - ); + const cursor = doc.getTokenCursor(end); + const [formStart, formEnd] = cursor.rangeForCurrentForm(start); + const isCaretTrailing = formEnd - start < start - formStart; + const startCursor = doc.getTokenCursor(formStart); + const endCursor = startCursor.clone(); + if (endCursor.forwardSexp()) { + const raised = doc.model.getText(startCursor.offsetStart, endCursor.offsetStart); + startCursor.backwardList(); + endCursor.forwardList(); + if (startCursor.getPrevToken().type == 'open') { + startCursor.previous(); + if (endCursor.getToken().type == 'close') { + edits.push( + new ModelEdit('changeRange', [startCursor.offsetStart, endCursor.offsetEnd, raised]) + ); + const cursorPos = isCaretTrailing + ? startCursor.offsetStart + raised.length + : startCursor.offsetStart; + + selections[originalIndex] = [ + endCursor.offsetEnd - startCursor.offsetStart - raised.length, + new ModelEditSelection(cursorPos), + ]; + } } } - } - }); + }); return doc.model.edit(edits, { - selections, + selections: selections.map(([_, selection], __, others) => { + const s = selection.clone(); + + const offsetSum = others + .filter(([_, otherSel]) => otherSel.start < selection.start) + .reduce((sum, o) => sum + o[0], 0); + + s.anchor -= offsetSum; + s.active -= offsetSum; + + return s; + }), }); } diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index c0c25a966..58cecfc8f 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -1136,6 +1136,18 @@ describe('paredit', () => { void paredit.raiseSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); + it('raises the current form when with two cursors ordered left->right', () => { + const a = docFromTextNotation('(a (b|)) (a (b|1)) (a (b))'); + const b = docFromTextNotation('(a b|) (a b|1) (a (b))'); + void paredit.raiseSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('raises the current form when with two cursors ordered right->left', () => { + const a = docFromTextNotation('(a (b|1)) (a (b|)) (a (b))'); + const b = docFromTextNotation('(a b|1) (a b|) (a (b))'); // "(a b) (a b) (a (b))", [[ 10, 10], [4, 4]] + void paredit.raiseSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); }); describe('Kill character backwards (backspace)', () => { @@ -1341,6 +1353,44 @@ describe('paredit', () => { expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); + + describe('Kill/Delete forward to End of List', () => { + it('Multi: kills last symbol in each list after cursor', async () => { + const a = docFromTextNotation('(|2a)(|1a)(|a)'); + const b = docFromTextNotation('(|2)(|1)(|)'); + await paredit.killForwardList(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + }); + + describe('Kill/Delete backward to start of List', () => { + it('Multi: kills last symbol in list after cursor', async () => { + const a = docFromTextNotation('(a|)(a|1)(a|2)'); // "(a)(a)(a)" [[2,2], [5,5], [8,8]] + const b = docFromTextNotation('(|)(|1)(|2)'); // "()()()" [[1,1], [3,3], [5,5]], + await paredit.killBackwardList(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + }); + + describe('Kill/Delete Sexp', () => { + describe('Kill/Delete Sexp Forward', () => { + it('Multi: kills/deletes sexp forwards', () => { + const a = docFromTextNotation('(|2a) (|1a) (|a) (a)'); + const b = docFromTextNotation('(|2) (|1) (|) (a)'); + void paredit.killSexpForward(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + }); + describe('Kill/Delete Sexp Backwards', () => { + it('Multi: kills/deletes sexp Backwards', async () => { + const a = docFromTextNotation('(a|2) (a|1) (a|) (a)'); + const b = docFromTextNotation('(|2) (|1) (|) (a)'); + await paredit.killSexpBackward(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + }); + }); + describe('addRichComment', () => { it('Adds Rich Comment after Top Level form', () => { const a = docFromTextNotation('(fo|o)••(bar)'); diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 20f190a66..9d21a85bc 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -7,6 +7,7 @@ import { Event, EventEmitter, ExtensionContext, + env, workspace, ConfigurationChangeEvent, } from 'vscode'; @@ -25,16 +26,21 @@ const enabled = true; * @param doc * @param range */ -function copyRangeToClipboard(doc: EditableDocument, [start, end]) { +export function copyRangeToClipboard(doc: EditableDocument, ranges: Array<[number, number]>) { + // FIXME: This is tricky. Somehow, vsc natively support cut/copy & pasting for multiple selections. + // But, how it does so is not known to me at this time. + // So, I am using the native copy command for now with only the first range (presumably the primary selection). + const range = ranges[0]; + const [start, end] = range; const text = doc.model.getText(start, end); - void vscode.env.clipboard.writeText(text); + void env.clipboard.writeText(text); } /** * Answers true when `calva.paredit.killAlsoCutsToClipboard` is enabled. * @returns boolean */ -function shouldKillAlsoCutToClipboard() { +export function shouldKillAlsoCutToClipboard(): boolean { return workspace.getConfiguration().get('calva.paredit.killAlsoCutsToClipboard'); } @@ -277,83 +283,60 @@ const pareditCommands: PareditCommand[] = [ }, { command: 'paredit.killSexpForward', - handler: (doc: EditableDocument) => { - // 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); - }); - }, + handler: (doc: EditableDocument) => + paredit.killSexpForward(doc, shouldKillAlsoCutToClipboard, copyRangeToClipboard), }, { command: 'paredit.killSexpBackward', - handler: (doc: EditableDocument) => { - doc.selections.forEach((s) => { - const range = paredit.backwardSexpRange(doc, s.active); - - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - paredit.killRange(doc, range); - }); - }, + handler: (doc: EditableDocument) => + paredit.killSexpBackward(doc, shouldKillAlsoCutToClipboard, copyRangeToClipboard), }, { command: 'paredit.killListForward', handler: (doc: EditableDocument) => { - doc.selections.forEach((s) => { - const range = paredit.forwardListRange(doc, s.active); + const ranges = doc.selections.map((s) => paredit.forwardListRange(doc, s.active)); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - void paredit.killForwardList(doc, range); - }); + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, ranges); + } + void paredit.killForwardList(doc, ranges); }, }, // TODO: Implement with killRange { command: 'paredit.killListBackward', handler: (doc: EditableDocument) => { - doc.selections.forEach((s) => { - const range = paredit.backwardListRange(doc, s.active); - - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - void paredit.killBackwardList(doc, range); - }); + const ranges = doc.selections.map((s) => paredit.backwardListRange(doc, s.active)); + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, ranges); + } + return ranges; + void paredit.killBackwardList(doc, ranges); }, }, // TODO: Implement with killRange { command: 'paredit.spliceSexpKillForward', handler: (doc: EditableDocument) => { - doc.selections.forEach((s) => { - const range = paredit.forwardListRange(doc, s.active); + const ranges = doc.selections.map((s) => paredit.forwardListRange(doc, s.active)); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - void paredit.killForwardList(doc, range).then((isFulfilled) => { - return paredit.spliceSexp(doc, /* s.active, */ false); - }); + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, ranges); + } + return ranges; + void paredit.killForwardList(doc, ranges).then(() => { + return paredit.spliceSexp(doc, /* s.active, */ false); }); }, }, { command: 'paredit.spliceSexpKillBackward', handler: (doc: EditableDocument) => { - doc.selections.forEach((s) => { - const range = paredit.backwardListRange(doc, s.active); + const ranges = doc.selections.map((s) => paredit.backwardListRange(doc, s.active)); - if (shouldKillAlsoCutToClipboard()) { - copyRangeToClipboard(doc, range); - } - void paredit.killBackwardList(doc, range).then((isFulfilled) => { - return paredit.spliceSexp(doc, /* s.active, */ false); - }); + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, ranges); + } + void paredit.killBackwardList(doc, ranges).then(() => { + return paredit.spliceSexp(doc, /* s.active, */ false); }); }, }, From c79490c84da77ab1fe87614f346f64ee9a6cd9fa Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 7 Apr 2022 12:05:11 -0700 Subject: [PATCH 23/49] Update changelog. Fixes #610 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52787b539..a20376456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes to Calva. ## [Unreleased] +- [Multi cursor](https://github.com/BetterThanTomorrow/calva/issues/610) + ## [2.0.263] - 2022-04-06 - [Improve kondo configuration documentation](https://github.com/BetterThanTomorrow/calva/issues/1282) - [Require VS Code 1.66+ (and update project node version to 16+)](https://github.com/BetterThanTomorrow/calva/issues/1638#issuecomment-1086726236) From e991540167aad149e8ed582c571aed0ee8fa1de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Fri, 8 Apr 2022 09:01:49 +0200 Subject: [PATCH 24/49] Add command for getting textnotation from doc --- package.json | 5 +++++ src/extension-test/unit/common/text-notation.ts | 10 ++++++---- src/extension-test/unit/cursor-doc/indent-test.ts | 2 +- src/extension-test/unit/cursor-doc/paredit-test.ts | 4 ++-- src/paredit/extension.ts | 13 ++++++++++++- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 6c0578c48..1c203bf69 100644 --- a/package.json +++ b/package.json @@ -917,6 +917,11 @@ "title": "Toggle nREPL Logging Enabled", "category": "Calva Diagnostics" }, + { + "command": "calva.diagnostics.printTextNotationFromDocument", + "title": "Print TextNotation from the current document to Calva says", + "category": "Calva Diagnostics" + }, { "command": "calva.linting.resolveMacroAs", "title": "Resolve Macro As", diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index ffdab6c7b..7968dc2c3 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -83,7 +83,7 @@ export function docFromTextNotation(s: string): model.StringDocument { return doc; } -export function textNotationFromDoc(doc: model.StringDocument): string { +export function textNotationFromDoc(doc: model.EditableDocument): string { const selections = doc.selections ?? []; let cursorSymbols: [number, string][] = []; selections.forEach((s, cursorNumber) => { @@ -96,7 +96,9 @@ export function textNotationFromDoc(doc: model.StringDocument): string { cursorSymbols = orderBy(cursorSymbols, (c) => c[0]); - const text = doc.model.lines.map((l) => l.text).join('•'); + const text = getText(doc) + .split(doc.model.lineEndingLength === 1 ? '\n' : '\r\n') + .join('•'); // basically split up the text into chunks separated by where they'd have had cursor symbols, and append cursor symbols after each chunk, before joining back together // this way we can insert the cursor symbols in the right place without having to worry about the cumulative offsets created by appending the cursor symbols @@ -131,7 +133,7 @@ export function textNotationFromDoc(doc: model.StringDocument): string { * @param doc * @returns string */ -export function text(doc: model.StringDocument): string { +export function getText(doc: model.EditableDocument): string { return doc.model.getText(0, Infinity); } @@ -141,5 +143,5 @@ export function text(doc: model.StringDocument): string { */ 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])]; + return [getText(doc), doc.selections.map((s) => [s.anchor, s.active])]; } diff --git a/src/extension-test/unit/cursor-doc/indent-test.ts b/src/extension-test/unit/cursor-doc/indent-test.ts index 954545f02..853176a87 100644 --- a/src/extension-test/unit/cursor-doc/indent-test.ts +++ b/src/extension-test/unit/cursor-doc/indent-test.ts @@ -1,7 +1,7 @@ import * as expect from 'expect'; import * as model from '../../../cursor-doc/model'; import * as indent from '../../../cursor-doc/indent'; -import { docFromTextNotation, textAndSelection, text } from '../common/text-notation'; +import { docFromTextNotation, textAndSelection, getText } from '../common/text-notation'; import { ModelEditSelection } from '../../../cursor-doc/model'; model.initScanner(20000); diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index 58cecfc8f..fe3b81a5c 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -1,7 +1,7 @@ import * as expect from 'expect'; import * as paredit from '../../../cursor-doc/paredit'; import * as model from '../../../cursor-doc/model'; -import { docFromTextNotation, textAndSelection, text } from '../common/text-notation'; +import { docFromTextNotation, textAndSelection, getText } from '../common/text-notation'; import { ModelEditSelection } from '../../../cursor-doc/model'; import { last, method } from 'lodash'; @@ -1488,7 +1488,7 @@ describe('paredit', () => { it.skip('splice string', () => { const a = docFromTextNotation('"h|ello"'); void paredit.spliceSexp(a); - expect(text(a)).toEqual('hello'); + expect(getText(a)).toEqual('hello'); }); }); }); diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 9d21a85bc..e51a39877 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -15,7 +15,8 @@ import * as paredit from '../cursor-doc/paredit'; import * as docMirror from '../doc-mirror/index'; import { EditableDocument, ModelEditResult } from '../cursor-doc/model'; import { assertIsDefined } from '../utilities'; - +import { textNotationFromDoc } from '../extension-test/unit/common/text-notation'; +import * as calvaState from '../state'; const onPareditKeyMapChangedEmitter = new EventEmitter(); const languages = new Set(['clojure', 'lisp', 'scheme']); @@ -462,6 +463,16 @@ export function activate(context: ExtensionContext) { .update('calva.paredit.defaultKeyMap', 'original', vscode.ConfigurationTarget.Global); } }), + commands.registerCommand('calva.diagnostics.printTextNotationFromDocument', () => { + const doc = vscode.window.activeTextEditor?.document; + if (doc && doc.languageId === 'clojure') { + const mirrorDoc = docMirror.getDocument(vscode.window.activeTextEditor?.document); + const notation = textNotationFromDoc(mirrorDoc); + const chan = calvaState.outputChannel(); + const relPath = vscode.workspace.asRelativePath(doc.uri); + chan.appendLine(`Text notation for: ${relPath}:\n${notation}`); + } + }), window.onDidChangeActiveTextEditor( (e) => e && e.document && languages.has(e.document.languageId) ), From 61087b52d7558316d1518205209c270e730b6d5f Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 7 Apr 2022 12:21:11 -0700 Subject: [PATCH 25/49] Remove cursor when-contexts. Let's hope no one's mad ! --- CHANGELOG.md | 1 + docs/site/customizing.md | 16 ++--- package.json | 24 +++---- src/cursor-doc/cursor-context.ts | 29 ++++---- .../unit/cursor-doc/cursor-context-test.ts | 72 +++++++++---------- src/when-contexts.ts | 6 +- 6 files changed, 75 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bf823637..0307bd51f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changes to Calva. ## [Unreleased] - [Multi cursor](https://github.com/BetterThanTomorrow/calva/issues/610) +- [Remove all cursor when-context logic - this might affect the behavior of those who make substantial use of them. Worth testing](https://github.com/BetterThanTomorrow/calva/pull/1606#issuecomment-1086567905) ## [2.0.264] - 2022-04-07 - Fix: [Shadowcljs shows error when running calva.loadFile command](https://github.com/BetterThanTomorrow/calva/issues/1670) diff --git a/docs/site/customizing.md b/docs/site/customizing.md index 4f45bf2e2..6e78d4b71 100644 --- a/docs/site/customizing.md +++ b/docs/site/customizing.md @@ -75,7 +75,7 @@ The versions used are configurable via the VS Code settings `calva.jackInDepende ## Key bindings -Most of Calva's commands have default keybindings. They are only defaults, though, and you can change keybindings as you wish. To facilitate precision in binding keys Calva keeps some [when clause contexts](https://code.visualstudio.com/api/references/when-clause-contexts) updated. +Most of Calva's commands have default keybindings. They are only defaults, though, and you can change keybindings as you wish. To facilitate precision in binding keys Calva keeps some [when clause contexts](https://code.visualstudio.com/api/references/when-clause-contexts) updated. ### When Clause Contexts @@ -87,15 +87,15 @@ The following contexts are available with Calva: * `calva:outputWindowActive`: `true` when the [Output/REPL window](output.md) has input focus * `calva:replHistoryCommandsActive`: `true` when the cursor is in the Output/REPL window at the top level after the last prompt * `calva:outputWindowSubmitOnEnter`: `true` when the cursor is adjacent after the last top level form in the Output/REPL window -* `calva:cursorInString`: `true` when the cursor/caret is in a string or a regexp -* `calva:cursorInComment`: `true` when the cursor is in, or adjacent to a line comment -* `calva:cursorBeforeComment`: `true` when the cursor is adjacent before a line comment -* `calva:cursorAfterComment`: `true` when the cursor is adjacent after a line comment -* `calva:cursorAtStartOfLine`: `true` when the cursor is at the start of a line including any leading whitespace -* `calva:cursorAtEndOfLine`: `true` when the cursor is at the end of a line including any trailing whitespace + + + + + + * `calva:showReplUi`: `false` when Calva's REPL UI is disabled through the corresponding setting -### Some Custom Bindings +### Some Custom Bindings Here is a collection of custom keybindings from here and there. diff --git a/package.json b/package.json index 1c203bf69..8a1f69de2 100644 --- a/package.json +++ b/package.json @@ -1894,28 +1894,28 @@ "mac": "ctrl+left", "win": "alt+left", "linux": "alt+left", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorBeforeComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.backwardSexp", "mac": "alt+left", "win": "ctrl+left", "linux": "ctrl+left", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorBeforeComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.forwardSexp", "mac": "ctrl+right", "win": "alt+right", "linux": "alt+right", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorAfterComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.forwardSexp", "mac": "alt+right", "win": "ctrl+right", "linux": "ctrl+right", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorAfterComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.forwardDownSexp", @@ -2008,14 +2008,14 @@ "mac": "ctrl+w", "win": "shift+alt+right", "linux": "shift+alt+right", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" }, { "command": "paredit.sexpRangeContraction", "mac": "ctrl+shift+w", "win": "shift+alt+left", "linux": "shift+alt+left", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" }, { "command": "paredit.slurpSexpForward", @@ -2067,22 +2067,22 @@ { "command": "paredit.dragSexprBackward", "key": "ctrl+shift+alt+b", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" }, { "command": "paredit.dragSexprForward", "key": "ctrl+shift+alt+f", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" }, { "command": "paredit.dragSexprBackward", "key": "alt+up", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /strict/ && config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.dragSexprForward", "key": "alt+down", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /strict/ && config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.dragSexprBackwardUp", @@ -2189,12 +2189,12 @@ { "command": "paredit.deleteForward", "key": "delete", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap == strict && !editorReadOnly && !editorHasMultipleSelections && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap == strict && !editorReadOnly && !editorHasMultipleSelections" }, { "command": "paredit.deleteBackward", "key": "backspace", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap == strict && !editorReadOnly && !editorHasMultipleSelections && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap == strict && !editorReadOnly && !editorHasMultipleSelections" }, { "command": "paredit.forceDeleteForward", diff --git a/src/cursor-doc/cursor-context.ts b/src/cursor-doc/cursor-context.ts index b2dad3872..63b1beffc 100644 --- a/src/cursor-doc/cursor-context.ts +++ b/src/cursor-doc/cursor-context.ts @@ -1,12 +1,12 @@ import { EditableDocument } from './model'; export const allCursorContexts = [ - 'calva:cursorInString', - 'calva:cursorInComment', - 'calva:cursorAtStartOfLine', - 'calva:cursorAtEndOfLine', - 'calva:cursorBeforeComment', - 'calva:cursorAfterComment', + // 'calva:cursorInString', + // 'calva:cursorInComment', + // 'calva:cursorAtStartOfLine', + // 'calva:cursorAtEndOfLine', + // 'calva:cursorBeforeComment', + // 'calva:cursorAfterComment', ] as const; export type CursorContext = typeof allCursorContexts[number]; @@ -60,24 +60,25 @@ export function isAtLineEndInclWS(doc: EditableDocument, offset = doc.selections export function determineContexts( doc: EditableDocument, offset = doc.selections[0].active -): CursorContext[] { +): readonly CursorContext[] { const tokenCursor = doc.getTokenCursor(offset); - const contexts: CursorContext[] = []; + // const contexts: CursorContext[] = []; + const contexts: readonly CursorContext[] = allCursorContexts; if (isAtLineStartInclWS(doc)) { - contexts.push('calva:cursorAtStartOfLine'); + // contexts.push('calva:cursorAtStartOfLine'); } else if (isAtLineEndInclWS(doc)) { - contexts.push('calva:cursorAtEndOfLine'); + // contexts.push('calva:cursorAtEndOfLine'); } if (tokenCursor.withinString()) { - contexts.push('calva:cursorInString'); + // contexts.push('calva:cursorInString'); } else if (tokenCursor.withinComment()) { - contexts.push('calva:cursorInComment'); + // contexts.push('calva:cursorInComment'); } // Compound contexts - if (contexts.includes('calva:cursorInComment')) { + /* if (contexts.includes('calva:cursorInComment')) { if (contexts.includes('calva:cursorAtEndOfLine')) { tokenCursor.forwardWhitespace(false); if (tokenCursor.getToken().type != 'comment') { @@ -89,7 +90,7 @@ export function determineContexts( contexts.push('calva:cursorBeforeComment'); } } - } + } */ return contexts; } 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 946f73329..1c6a3c8ae 100644 --- a/src/extension-test/unit/cursor-doc/cursor-context-test.ts +++ b/src/extension-test/unit/cursor-doc/cursor-context-test.ts @@ -3,24 +3,24 @@ import * as context from '../../../cursor-doc/cursor-context'; import { docFromTextNotation, textAndSelection } from '../common/text-notation'; describe('Cursor Contexts', () => { - describe('cursorInString', () => { + xdescribe('cursorInString', () => { it('is true in string', () => { const contexts = context.determineContexts(docFromTextNotation('foo• "bar• |baz"•gaz')); - expect(contexts.includes('calva:cursorInString')).toBe(true); + // expect(contexts.includes('calva:cursorInString')).toBe(true); }); it('is false outside after string', () => { const contexts = context.determineContexts(docFromTextNotation('foo• "bar• baz"|•gaz')); - expect(contexts.includes('calva:cursorInString')).toBe(false); + // expect(contexts.includes('calva:cursorInString')).toBe(false); }); it('is true in regexp', () => { const contexts = context.determineContexts(docFromTextNotation('foo• #"bar• ba|z"•gaz')); - expect(contexts.includes('calva:cursorInString')).toBe(true); + // expect(contexts.includes('calva:cursorInString')).toBe(true); }); it('is false in regexp open token', () => { const contexts = context.determineContexts( docFromTextNotation('foo• #|"bat bar• baz"•gaz') ); - expect(contexts.includes('calva:cursorInString')).toBe(false); + // expect(contexts.includes('calva:cursorInString')).toBe(false); }); }); describe('cursorInComment', () => { @@ -28,53 +28,53 @@ describe('Cursor Contexts', () => { const contexts = context.determineContexts( docFromTextNotation(';; f|oo• ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorInComment')).toBe(true); + // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is true adjacent before comment', () => { const contexts = context.determineContexts( docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorInComment')).toBe(true); + // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is true in whitespace between SOL and comment', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo•| ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorInComment')).toBe(true); + // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is true adjacent after comment', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo |• ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorInComment')).toBe(true); + // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is false in symbol', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •g|az ;; ') ); - expect(contexts.includes('calva:cursorInComment')).toBe(false); + // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); it('is false adjacent after symbol', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •gaz| ;; ') ); - expect(contexts.includes('calva:cursorInComment')).toBe(false); + // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); it('is false in whitespace after symbol', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •gaz | ;; ') ); - expect(contexts.includes('calva:cursorInComment')).toBe(false); + // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); it('is true after symbol adjacent before comment', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •gaz |;; ') ); - expect(contexts.includes('calva:cursorInComment')).toBe(true); + // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is false in leading ws on line after comment', () => { const contexts = context.determineContexts(docFromTextNotation('(+• ;foo• | 2)')); - expect(contexts.includes('calva:cursorInComment')).toBe(false); + // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); }); describe('cursorBeforeComment', () => { @@ -82,71 +82,71 @@ describe('Cursor Contexts', () => { const contexts = context.determineContexts( docFromTextNotation(';; fo|o• ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is true adjacent before comment', () => { const contexts = context.determineContexts( docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(true); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(true); }); 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); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is true after symbol in whitespace between SOL and comment', () => { const contexts = context.determineContexts( docFromTextNotation(' foo•| ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(true); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(true); }); it('is false at SOL on a comment line with more comment lines following', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo •| ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false at empty line squeezed in along comments lines', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • •|• • ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false after comment lines before symbol', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz |•gaz ;; ') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false in symbol', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •g|az ;; ') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false adjacent after symbol after comment lines', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •gaz| ;; ') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false in whitespace after symbol', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •gaz | ;; ') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is true adjacent before comment after symbol', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •gaz |;; ') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false at EOT after comment', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •gaz ;; |') ); - expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); + // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); }); describe('cursorAfterComment', () => { @@ -154,61 +154,61 @@ describe('Cursor Contexts', () => { const contexts = context.determineContexts( docFromTextNotation(';; fo|o• ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false adjacent before comment', () => { const contexts = context.determineContexts( docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false in whitespace between SOL and comment', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo•| ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false at EOL on a comment line with more comment lines following', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo |• ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false at empty line squeezed in along comments lines', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • •|• • ;; bar• ;; baz •gaz') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is true after comment lines before symbol', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz |•gaz ;; ') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(true); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(true); }); it('is false in symbol', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •g|az ;; ') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false adjacent after symbol after comment lines', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •gaz| ;; ') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false in whitespace after symbol', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •gaz | ;; ') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(false); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is true at EOT after comment', () => { const contexts = context.determineContexts( docFromTextNotation(';; foo • ;; bar• ;; baz •gaz ;; |') ); - expect(contexts.includes('calva:cursorAfterComment')).toBe(true); + // expect(contexts.includes('calva:cursorAfterComment')).toBe(true); }); }); describe('isAtLineStartInclWS', () => { diff --git a/src/when-contexts.ts b/src/when-contexts.ts index 3457663cc..655adf215 100644 --- a/src/when-contexts.ts +++ b/src/when-contexts.ts @@ -4,7 +4,7 @@ import * as docMirror from './doc-mirror'; import * as context from './cursor-doc/cursor-context'; import * as util from './utilities'; -let lastContexts: context.CursorContext[] = []; +let lastContexts: readonly context.CursorContext[] = context.allCursorContexts; export function setCursorContextIfChanged(editor: vscode.TextEditor) { if ( @@ -24,12 +24,12 @@ export function setCursorContextIfChanged(editor: vscode.TextEditor) { function determineCursorContexts( document: vscode.TextDocument, position: vscode.Position -): context.CursorContext[] { +): readonly context.CursorContext[] { const mirrorDoc = docMirror.getDocument(document); return context.determineContexts(mirrorDoc, document.offsetAt(position)); } -function setCursorContexts(currentContexts: context.CursorContext[]) { +function setCursorContexts(currentContexts: readonly context.CursorContext[]) { lastContexts = currentContexts; context.allCursorContexts.forEach((context) => { void vscode.commands.executeCommand( From 1e44247329c0906be0e71968071cf85a4be14277 Mon Sep 17 00:00:00 2001 From: Rayat Date: Fri, 8 Apr 2022 15:58:45 -0700 Subject: [PATCH 26/49] Fix Expand Selection selects only open parens bug --- src/cursor-doc/paredit.ts | 2 +- src/extension-test/unit/cursor-doc/paredit-test.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index b4b63e750..426e6b23d 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -1302,7 +1302,7 @@ export function growSelection( startC.backwardUpList(); endC.forwardList(); // growSelectionStack(doc, [startC.offsetStart, endC.offsetEnd]); - return [startC.offsetStart, startC.offsetEnd]; + return [startC.offsetStart, endC.offsetEnd]; } else { if (startC.backwardList()) { // we are in an sexpr. diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index fe3b81a5c..f4b3229b9 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -725,6 +725,12 @@ describe('paredit', () => { paredit.shrinkSelection(doc); expect(last(doc.selectionsStack)).toEqual([new ModelEditSelection(a[0], a[1])]); }); + it('selects the enclosing form when all the text in a list is selected', () => { + const a = docFromTextNotation('(|a|)'); + const b = docFromTextNotation('|(a)|'); // '(a)' [[0, 3]]; + paredit.growSelection(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); }); describe('dragSexpr', () => { From 43f67f352f669c33f2ad20fc2f3f3c605b0058c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sat, 9 Apr 2022 14:44:51 +0200 Subject: [PATCH 27/49] =?UTF-8?q?Change=20newline=20text-notation=20`?= =?UTF-8?q?=E2=80=A2`=20->=20`=C2=A7`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/common/text-notation.ts | 6 +- .../unit/cursor-doc/cursor-context-test.ts | 130 ++++---- .../unit/cursor-doc/indent-test.ts | 2 +- .../unit/cursor-doc/paredit-test.ts | 306 +++++++++--------- .../unit/cursor-doc/token-cursor-test.ts | 220 ++++++------- .../unit/util/cursor-get-text-test.ts | 36 +-- 6 files changed, 350 insertions(+), 350 deletions(-) diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index 7968dc2c3..cd4e02803 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -5,7 +5,7 @@ import { clone, entries, cond, toInteger, last, first, cloneDeep, orderBy } from * Text Notation for expressing states of a document, including * current text and selection. * * Since JavasScript makes it clumsy with multiline strings, - * newlines are denoted with a middle dot character: `•` + * newlines are denoted with the paragraph character: `§` * * Selections are denoted like so * * Cursors, (which are actually selections with identical start and end) are denoted with a single `|`, with <= 10 multiple cursors defined by `|1`, `|2`, ... `|9`, etc, or in regex: /\|\d/. 0-indexed, so `|` is 0, `|1` is 1, etc. * * This is however actually an alternative for the following `>\d?` notation, it has the same left->right semantics, but looks more like a cursor/caret lol, and more importantly, drops the 0 for the first cursor. @@ -14,7 +14,7 @@ import { clone, entries, cond, toInteger, last, first, cloneDeep, orderBy } from */ function textNotationToTextAndSelection(content: string): [string, model.ModelEditSelection[]] { const text = clone(content) - .replace(/•/g, '\n') + .replace(/§/g, '\n') .replace(/\|?[<>]?\|\d?/g, ''); /** @@ -98,7 +98,7 @@ export function textNotationFromDoc(doc: model.EditableDocument): string { const text = getText(doc) .split(doc.model.lineEndingLength === 1 ? '\n' : '\r\n') - .join('•'); + .join('§'); // basically split up the text into chunks separated by where they'd have had cursor symbols, and append cursor symbols after each chunk, before joining back together // this way we can insert the cursor symbols in the right place without having to worry about the cumulative offsets created by appending the cursor symbols 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 1c6a3c8ae..ba854b957 100644 --- a/src/extension-test/unit/cursor-doc/cursor-context-test.ts +++ b/src/extension-test/unit/cursor-doc/cursor-context-test.ts @@ -5,20 +5,20 @@ import { docFromTextNotation, textAndSelection } from '../common/text-notation'; describe('Cursor Contexts', () => { xdescribe('cursorInString', () => { it('is true in string', () => { - const contexts = context.determineContexts(docFromTextNotation('foo• "bar• |baz"•gaz')); + const contexts = context.determineContexts(docFromTextNotation('foo§ "bar§ |baz"§gaz')); // expect(contexts.includes('calva:cursorInString')).toBe(true); }); it('is false outside after string', () => { - const contexts = context.determineContexts(docFromTextNotation('foo• "bar• baz"|•gaz')); + const contexts = context.determineContexts(docFromTextNotation('foo§ "bar§ baz"|§gaz')); // expect(contexts.includes('calva:cursorInString')).toBe(false); }); it('is true in regexp', () => { - const contexts = context.determineContexts(docFromTextNotation('foo• #"bar• ba|z"•gaz')); + const contexts = context.determineContexts(docFromTextNotation('foo§ #"bar§ ba|z"§gaz')); // expect(contexts.includes('calva:cursorInString')).toBe(true); }); it('is false in regexp open token', () => { const contexts = context.determineContexts( - docFromTextNotation('foo• #|"bat bar• baz"•gaz') + docFromTextNotation('foo§ #|"bat bar§ baz"§gaz') ); // expect(contexts.includes('calva:cursorInString')).toBe(false); }); @@ -26,125 +26,125 @@ describe('Cursor Contexts', () => { describe('cursorInComment', () => { it('is true in comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; f|oo• ;; bar• ;; baz •gaz') + docFromTextNotation(';; f|oo§ ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is true adjacent before comment', () => { const contexts = context.determineContexts( - docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz') + docFromTextNotation('|;; foo§ ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is true in whitespace between SOL and comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo•| ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo§| ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is true adjacent after comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo |• ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo |§ ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is false in symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •g|az ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §g|az ;; ') ); // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); it('is false adjacent after symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz| ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz| ;; ') ); // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); it('is false in whitespace after symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz | ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz | ;; ') ); // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); it('is true after symbol adjacent before comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz |;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz |;; ') ); // expect(contexts.includes('calva:cursorInComment')).toBe(true); }); it('is false in leading ws on line after comment', () => { - const contexts = context.determineContexts(docFromTextNotation('(+• ;foo• | 2)')); + const contexts = context.determineContexts(docFromTextNotation('(+§ ;foo§ | 2)')); // expect(contexts.includes('calva:cursorInComment')).toBe(false); }); }); describe('cursorBeforeComment', () => { it('is false in comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; fo|o• ;; bar• ;; baz •gaz') + docFromTextNotation(';; fo|o§ ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is true adjacent before comment', () => { const contexts = context.determineContexts( - docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz') + docFromTextNotation('|;; foo§ ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(true); }); it('is false adjacent before comment on line with leading whitespace and preceding comment line', () => { - const contexts = context.determineContexts(docFromTextNotation(' ;; foo• |;; bar')); + const contexts = context.determineContexts(docFromTextNotation(' ;; foo§ |;; bar')); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is true after symbol in whitespace between SOL and comment', () => { const contexts = context.determineContexts( - docFromTextNotation(' foo•| ;; bar• ;; baz •gaz') + docFromTextNotation(' foo§| ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(true); }); it('is false at SOL on a comment line with more comment lines following', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo •| ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo §| ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false at empty line squeezed in along comments lines', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • •|• • ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo § §|§ § ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false after comment lines before symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz |•gaz ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz |§gaz ;; ') ); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false in symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •g|az ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §g|az ;; ') ); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false adjacent after symbol after comment lines', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz| ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz| ;; ') ); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false in whitespace after symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz | ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz | ;; ') ); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is true adjacent before comment after symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz |;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz |;; ') ); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); it('is false at EOT after comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz ;; |') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz ;; |') ); // expect(contexts.includes('calva:cursorBeforeComment')).toBe(false); }); @@ -152,61 +152,61 @@ describe('Cursor Contexts', () => { describe('cursorAfterComment', () => { it('is false in comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; fo|o• ;; bar• ;; baz •gaz') + docFromTextNotation(';; fo|o§ ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false adjacent before comment', () => { const contexts = context.determineContexts( - docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz') + docFromTextNotation('|;; foo§ ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false in whitespace between SOL and comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo•| ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo§| ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false at EOL on a comment line with more comment lines following', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo |• ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo |§ ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false at empty line squeezed in along comments lines', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • •|• • ;; bar• ;; baz •gaz') + docFromTextNotation(';; foo § §|§ § ;; bar§ ;; baz §gaz') ); // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is true after comment lines before symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz |•gaz ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz |§gaz ;; ') ); // expect(contexts.includes('calva:cursorAfterComment')).toBe(true); }); it('is false in symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •g|az ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §g|az ;; ') ); // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false adjacent after symbol after comment lines', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz| ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz| ;; ') ); // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is false in whitespace after symbol', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz | ;; ') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz | ;; ') ); // expect(contexts.includes('calva:cursorAfterComment')).toBe(false); }); it('is true at EOT after comment', () => { const contexts = context.determineContexts( - docFromTextNotation(';; foo • ;; bar• ;; baz •gaz ;; |') + docFromTextNotation(';; foo § ;; bar§ ;; baz §gaz ;; |') ); // expect(contexts.includes('calva:cursorAfterComment')).toBe(true); }); @@ -214,136 +214,136 @@ describe('Cursor Contexts', () => { describe('isAtLineStartInclWS', () => { it('returns true at the start of a line', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation('|;; foo§ ;; bar§ ;; baz §gaz')) ).toBe(true); expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• ;; bar• ;; baz •|gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ ;; bar§ ;; baz §|gaz')) ).toBe(true); }); it('returns true at line start with leading whitespace', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• |;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ |;; bar§ ;; baz §gaz')) ).toBe(true); expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• | ;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ | ;; bar§ ;; baz §gaz')) ).toBe(true); expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo•| ;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§| ;; bar§ ;; baz §gaz')) ).toBe(true); }); it('returns true at end of line with only whitespace', () => { - expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo• |• ;; baz •gaz'))).toBe( + expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo§ |§ ;; baz §gaz'))).toBe( true ); }); it('returns true at start of line with only whitespace', () => { - expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo•| • ;; baz •gaz'))).toBe( + expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo§| § ;; baz §gaz'))).toBe( true ); }); it('returns true in middle of line with only whitespace', () => { - expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo• | • ;; baz •gaz'))).toBe( + expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo§ | § ;; baz §gaz'))).toBe( true ); }); it('returns true at empty line', () => { - expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo•|• ;; baz •gaz'))).toBe( + expect(context.isAtLineStartInclWS(docFromTextNotation(';; foo§|§ ;; baz §gaz'))).toBe( true ); }); it('returns true at line start with leading & trailing whitespace', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• ;; bar• | ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ ;; bar§ | ;; baz §gaz')) ).toBe(true); }); it('returns false within a line (non-whitespace)', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';|; foo• ;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';|; foo§ ;; bar§ ;; baz §gaz')) ).toBe(false); }); it('returns false within a line with leading whitespace', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• ;; |bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ ;; |bar§ ;; baz §gaz')) ).toBe(false); }); it('returns false at the end of a line', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo|• ;; bar• ;; baz •gaz')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo|§ ;; bar§ ;; baz §gaz')) ).toBe(false); }); it('returns false at the end of document', () => { expect( - context.isAtLineStartInclWS(docFromTextNotation(';; foo• ;; bar• ;; baz •gaz|')) + context.isAtLineStartInclWS(docFromTextNotation(';; foo§ ;; bar§ ;; baz §gaz|')) ).toBe(false); }); }); describe('isAtLineEndInclWS', () => { it('returns true at the end of a line', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar • ;; baz •gaz|')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar § ;; baz §gaz|')) ).toBe(true); expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo|• ;; bar • ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo|§ ;; bar § ;; baz §gaz')) ).toBe(true); }); it('returns true at line end with trailing whitespace', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar| • ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar| § ;; baz §gaz')) ).toBe(true); expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar |• ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar |§ ;; baz §gaz')) ).toBe(true); }); it('returns true at line end with leading & trailing whitespace', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar | • ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar | § ;; baz §gaz')) ).toBe(true); }); it('returns true at end of line with only whitespace', () => { - expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo• |• ;; baz •gaz'))).toBe( + expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo§ |§ ;; baz §gaz'))).toBe( true ); }); it('returns true at start of line with only whitespace', () => { - expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo•| • ;; baz •gaz'))).toBe( + expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo§| § ;; baz §gaz'))).toBe( true ); }); it('returns true in middle of line with only whitespace', () => { - expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo• | • ;; baz •gaz'))).toBe( + expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo§ | § ;; baz §gaz'))).toBe( true ); }); it('returns true at empty line', () => { - expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo•|• ;; baz •gaz'))).toBe(true); + expect(context.isAtLineEndInclWS(docFromTextNotation(';; foo§|§ ;; baz §gaz'))).toBe(true); }); it('returns true at line start with only leading & trailing whitespace', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar | • ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar | § ;; baz §gaz')) ).toBe(true); }); it('returns false within a line (non-whitespace)', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar• ;; ba|z •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar§ ;; ba|z §gaz')) ).toBe(false); expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar• ;; ba|z •gaz ;|;')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar§ ;; ba|z §gaz ;|;')) ).toBe(false); }); it('returns false within a line with trailing whitespace', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar• ;|; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar§ ;|; baz §gaz')) ).toBe(false); expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar• ;; b|az •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar§ ;; b|az §gaz')) ).toBe(false); }); it('returns false at the start of a line', () => { expect( - context.isAtLineEndInclWS(docFromTextNotation(';; foo• ;; bar•| ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation(';; foo§ ;; bar§| ;; baz §gaz')) ).toBe(false); expect( - context.isAtLineEndInclWS(docFromTextNotation('|;; foo• ;; bar• ;; baz •gaz')) + context.isAtLineEndInclWS(docFromTextNotation('|;; foo§ ;; bar§ ;; baz §gaz')) ).toBe(false); }); }); diff --git a/src/extension-test/unit/cursor-doc/indent-test.ts b/src/extension-test/unit/cursor-doc/indent-test.ts index 853176a87..d1c93303a 100644 --- a/src/extension-test/unit/cursor-doc/indent-test.ts +++ b/src/extension-test/unit/cursor-doc/indent-test.ts @@ -173,7 +173,7 @@ describe('indent', () => { expect(state[0].rules).toEqual([]); }); it('collects indents for cursor in nested structure', () => { - const doc = docFromTextNotation('[]•(aa []•(bb•(cc :dd|)))•[]'); + const doc = docFromTextNotation('[]§(aa []§(bb§(cc :dd|)))§[]'); const rule1: indent.IndentRule[] = [ ['inner', 0], ['block', 1], diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index f4b3229b9..6e1e9e2a3 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -331,10 +331,10 @@ describe('paredit', () => { it('Finds the full form after an ignore marker', () => { // https://github.com/BetterThanTomorrow/calva/pull/1293#issuecomment-927123696 const a = docFromTextNotation( - '(comment• #_|[a b (c d• e• f) g]• :a•)' + '(comment§ #_|[a b (c d§ e§ f) g]§ :a§)' ); const b = docFromTextNotation( - '(comment• #_|[a b (c d• e• f) g]|• :a•)' + '(comment§ #_|[a b (c d§ e§ f) g]|§ :a§)' ); const expected = textAndSelection(b)[1]; const actual = paredit.forwardHybridSexpRange(a); @@ -495,88 +495,88 @@ describe('paredit', () => { describe('Forward to end of list', () => { it('rangeToForwardList', () => { - const a = docFromTextNotation('(|c•(#b •[:f :b :z])•#z•1)'); - const b = docFromTextNotation('(|c•(#b •[:f :b :z])•#z•1|)'); + const a = docFromTextNotation('(|c§(#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(|c§(#b §[:f :b :z])§#z§1|)'); expect(paredit.rangeToForwardList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardList through readers and meta', () => { - const a = docFromTextNotation('(|^e #a ^{:c d}•#b•[:f]•#z•1)'); - const b = docFromTextNotation('(|^e #a ^{:c d}•#b•[:f]•#z•1|)'); + const a = docFromTextNotation('(|^e #a ^{:c d}§#b§[:f]§#z§1)'); + const b = docFromTextNotation('(|^e #a ^{:c d}§#b§[:f]§#z§1|)'); expect(paredit.rangeToForwardList(a)).toEqual(textAndSelection(b)[1][0]); }); }); describe('Backward to start of list', () => { it('rangeToBackwardList', () => { - const a = docFromTextNotation('(c•(#b •[:f :b :z])•#z•1|)'); - const b = docFromTextNotation('(|c•(#b •[:f :b :z])•#z•1|)'); + const a = docFromTextNotation('(c§(#b §[:f :b :z])§#z§1|)'); + const b = docFromTextNotation('(|c§(#b §[:f :b :z])§#z§1|)'); expect(paredit.rangeToBackwardList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToBackwardList through readers and meta', () => { - const a = docFromTextNotation('(^e #a ^{:c d}•#b•[:f]•#z•1|)'); - const b = docFromTextNotation('(|^e #a ^{:c d}•#b•[:f]•#z•1|)'); + const a = docFromTextNotation('(^e #a ^{:c d}§#b§[:f]§#z§1|)'); + const b = docFromTextNotation('(|^e #a ^{:c d}§#b§[:f]§#z§1|)'); expect(paredit.rangeToBackwardList(a)).toEqual(textAndSelection(b)[1][0]); }); }); describe('Down list', () => { it('rangeToForwardDownList', () => { - const a = docFromTextNotation('(|c•(#b •[:f :b :z])•#z•1)'); - const b = docFromTextNotation('(|c•(|#b •[:f :b :z])•#z•1)'); + const a = docFromTextNotation('(|c§(#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(|c§(|#b §[:f :b :z])§#z§1)'); expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardDownList through readers', () => { - const a = docFromTextNotation('(|c•#f•(#b •[:f :b :z])•#z•1)'); - const b = docFromTextNotation('(|c•#f•(|#b •[:f :b :z])•#z•1)'); + const a = docFromTextNotation('(|c§#f§(#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(|c§#f§(|#b §[:f :b :z])§#z§1)'); expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardDownList through metadata', () => { - const a = docFromTextNotation('(|c•^f•(#b •[:f :b]))'); - const b = docFromTextNotation('(|c•^f•(|#b •[:f :b]))'); + const a = docFromTextNotation('(|c§^f§(#b §[:f :b]))'); + const b = docFromTextNotation('(|c§^f§(|#b §[:f :b]))'); expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardDownList through metadata collection', () => { - const a = docFromTextNotation('(|c•^{:f 1}•(#b •[:f :b]))'); - const b = docFromTextNotation('(|c•^{:f 1}•(|#b •[:f :b]))'); + const a = docFromTextNotation('(|c§^{:f 1}§(#b §[:f :b]))'); + const b = docFromTextNotation('(|c§^{:f 1}§(|#b §[:f :b]))'); expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardDownList through metadata and readers', () => { - const a = docFromTextNotation('(|c•^:a #f•(#b •[:f :b]))'); - const b = docFromTextNotation('(|c•^:a #f•(|#b •[:f :b]))'); + const a = docFromTextNotation('(|c§^:a #f§(#b §[:f :b]))'); + const b = docFromTextNotation('(|c§^:a #f§(|#b §[:f :b]))'); expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToForwardDownList through metadata collection and reader', () => { - const a = docFromTextNotation('(|c•^{:f 1}•#a •(#b •[:f :b]))'); - const b = docFromTextNotation('(|c•^{:f 1}•#a •(|#b •[:f :b]))'); + const a = docFromTextNotation('(|c§^{:f 1}§#a §(#b §[:f :b]))'); + const b = docFromTextNotation('(|c§^{:f 1}§#a §(|#b §[:f :b]))'); expect(paredit.rangeToForwardDownList(a)).toEqual(textAndSelection(b)[1][0]); }); }); describe('Backward Up list', () => { it('rangeToBackwardUpList', () => { - const a = docFromTextNotation('(c•(|#b •[:f :b :z])•#z•1)'); - const b = docFromTextNotation('(c•|(|#b •[:f :b :z])•#z•1)'); + const a = docFromTextNotation('(c§(|#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(c§|(|#b §[:f :b :z])§#z§1)'); expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToBackwardUpList through readers', () => { - const a = docFromTextNotation('(c•#f•(|#b •[:f :b :z])•#z•1)'); - const b = docFromTextNotation('(c•|#f•(|#b •[:f :b :z])•#z•1)'); + const a = docFromTextNotation('(c§#f§(|#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(c§|#f§(|#b §[:f :b :z])§#z§1)'); expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToBackwardUpList through metadata', () => { - const a = docFromTextNotation('(c•^f•(|#b •[:f :b]))'); - const b = docFromTextNotation('(c•|^f•(|#b •[:f :b]))'); + const a = docFromTextNotation('(c§^f§(|#b §[:f :b]))'); + const b = docFromTextNotation('(c§|^f§(|#b §[:f :b]))'); expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToBackwardUpList through metadata and readers', () => { - const a = docFromTextNotation('(c•^:a #f•(|#b •[:f :b]))'); - const b = docFromTextNotation('(c•|^:a #f•(|#b •[:f :b]))'); + const a = docFromTextNotation('(c§^:a #f§(|#b §[:f :b]))'); + const b = docFromTextNotation('(c§|^:a #f§(|#b §[:f :b]))'); expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1][0]); }); it('rangeToBackwardUpList 2', () => { // TODO: This is wrong! But real Paredit behaves as it should... - const a = docFromTextNotation('(a(b(c•#f•(#b •|[:f :b :z])•#z•1)))'); - const b = docFromTextNotation('(a(b|(c•#f•(#b •|[:f :b :z])•#z•1)))'); + const a = docFromTextNotation('(a(b(c§#f§(#b §|[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(b|(c§#f§(#b §|[:f :b :z])§#z§1)))'); expect(paredit.rangeToBackwardUpList(a)).toEqual(textAndSelection(b)[1][0]); }); }); @@ -584,14 +584,14 @@ describe('paredit', () => { describe('Reader tags', () => { it('dragSexprBackward', () => { - const a = docFromTextNotation('(a(b(c•#f•|(#b •[:f :b :z])•#z•1)))'); - const b = docFromTextNotation('(a(b(#f•|(#b •[:f :b :z])•c•#z•1)))'); + const a = docFromTextNotation('(a(b(c§#f§|(#b §[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(b(#f§|(#b §[:f :b :z])§c§#z§1)))'); void paredit.dragSexprBackward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('dragSexprForward', () => { - const a = docFromTextNotation('(a(b(c•#f•|(#b •[:f :b :z])•#z•1)))'); - const b = docFromTextNotation('(a(b(c•#z•1•#f•|(#b •[:f :b :z]))))'); + const a = docFromTextNotation('(a(b(c§#f§|(#b §[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(b(c§#z§1§#f§|(#b §[:f :b :z]))))'); void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); @@ -603,14 +603,14 @@ describe('paredit', () => { doc = new model.StringDocument(docText); }); 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]))'); + 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]))'); + const a = docFromTextNotation('(c§#f§|(#b §[:f :b :z])§#x§#y§1)'); + const b = docFromTextNotation('(c§#x§#y§1§#f§|(#b §[:f :b :z]))'); void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); @@ -623,18 +623,18 @@ describe('paredit', () => { beforeEach(() => { doc = new model.StringDocument(docText); }); - it('dragSexprBackward: #f•(#b •[:f :b :z])•#x•#y•|a•#å#ä#ö => #x•#y•1•#f•(#b •[:f :b :z])•#å#ä#ö', () => { + 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])•#å#ä#ö', () => { + it('dragSexprForward: #f§|(#b §[:f :b :z])§#x§#y§1#å#ä#ö => #x§#y§1§#f§|(#b §[:f :b :z])§#å#ä#ö', () => { doc.selections = [new ModelEditSelection(3, 3)]; void paredit.dragSexprForward(doc); expect(doc.model.getText(0, Infinity)).toBe('#x\n#y\n1\n#f\n(#b \n[:f :b :z])\n#å#ä#ö'); expect(doc.selections).toEqual([new ModelEditSelection(11)]); }); - it('dragSexprForward: #f•(#b •[:f :b :z])•#x•#y•|a•#å#ä#ö => #f•(#b •[:f :b :z])•#x•#y•|a•#å#ä#ö', () => { + 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#å#ä#ö'); @@ -735,7 +735,7 @@ describe('paredit', () => { describe('dragSexpr', () => { describe('forwardAndBackwardSexpr', () => { - // (comment\n ['(0 1 2 "t" "f")• "b"• {:s "h"}• :f]• [:f '(0 "t") "b" :s]• [:f 0• "b" :s• 4 :b]• {:e '(e o ea)• 3 {:w? 'w}• :t '(t i o im)• :b 'b}) + // (comment\n ['(0 1 2 "t" "f")§ "b"§ {:s "h"}§ :f]§ [:f '(0 "t") "b" :s]§ [:f 0§ "b" :s§ 4 :b]§ {:e '(e o ea)§ 3 {:w? 'w}§ :t '(t i o im)§ :b 'b}) let doc: model.StringDocument; beforeEach(() => { @@ -743,21 +743,21 @@ 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]•)`); + const a = docFromTextNotation(`(c§ [:|f '(0 "t")§ "b" :s]§)`); + const b = docFromTextNotation(`(c§ ['(0 "t") :|f§ "b" :s]§)`); void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('drags backward in regular lists', () => { - const a = docFromTextNotation(`(c• [:f '(0 "t")• "b"| :s]•)`); - const b = docFromTextNotation(`(c• [:f "b"|• '(0 "t") :s]•)`); + const a = docFromTextNotation(`(c§ [:f '(0 "t")§ "b"| :s]§)`); + const b = docFromTextNotation(`(c§ [:f "b"|§ '(0 "t") :s]§)`); void paredit.dragSexprBackward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('does not drag forward when sexpr is last in regular lists', () => { - const dotText = `(c• [:f '(0 "t")• "b" |:s ]•)`; + const dotText = `(c§ [:f '(0 "t")§ "b" |:s ]§)`; const a = docFromTextNotation(dotText); const b = docFromTextNotation(dotText); void paredit.dragSexprForward(a); @@ -765,7 +765,7 @@ describe('paredit', () => { }); it('does not drag backward when sexpr is last in regular lists', () => { - const dotText = `(c• [ :|f '(0 "t")• "b" :s ]•)`; + const dotText = `(c§ [ :|f '(0 "t")§ "b" :s ]§)`; const a = docFromTextNotation(dotText); const b = docFromTextNotation(dotText); void paredit.dragSexprBackward(a); @@ -774,10 +774,10 @@ describe('paredit', () => { it('drags pair forward in maps', () => { const a = docFromTextNotation( - `(c• {:|e '(e o ea)• 3 {:w? 'w}• :t '(t i o im)• :b 'b}•)` + `(c§ {:|e '(e o ea)§ 3 {:w? 'w}§ :t '(t i o im)§ :b 'b}§)` ); const b = docFromTextNotation( - `(c• {3 {:w? 'w}• :|e '(e o ea)• :t '(t i o im)• :b 'b}•)` + `(c§ {3 {:w? 'w}§ :|e '(e o ea)§ :t '(t i o im)§ :b 'b}§)` ); void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); @@ -785,10 +785,10 @@ describe('paredit', () => { it('drags pair backwards in maps', () => { const a = docFromTextNotation( - `(c• {:e '(e o ea)• 3 {:w? 'w}• :t '(t i o im)|• :b 'b}•)` + `(c§ {:e '(e o ea)§ 3 {:w? 'w}§ :t '(t i o im)|§ :b 'b}§)` ); const b = docFromTextNotation( - `(c• {:e '(e o ea)• :t '(t i o im)|• 3 {:w? 'w}• :b 'b}•)` + `(c§ {:e '(e o ea)§ :t '(t i o im)|§ 3 {:w? 'w}§ :b 'b}§)` ); void paredit.dragSexprBackward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); @@ -796,10 +796,10 @@ describe('paredit', () => { it('drags pair backwards in meta-data maps', () => { const a = docFromTextNotation( - `(c• ^{:e '(e o ea)• 3 {:w? 'w}• :t '(t i o im)|• :b 'b}•)` + `(c§ ^{:e '(e o ea)§ 3 {:w? 'w}§ :t '(t i o im)|§ :b 'b}§)` ); const b = docFromTextNotation( - `(c• ^{:e '(e o ea)• :t '(t i o im)|• 3 {:w? 'w}• :b 'b}•)` + `(c§ ^{:e '(e o ea)§ :t '(t i o im)|§ 3 {:w? 'w}§ :b 'b}§)` ); void paredit.dragSexprBackward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); @@ -807,10 +807,10 @@ describe('paredit', () => { it('drags single sexpr forward in sets', () => { const a = docFromTextNotation( - `(c• #{:|e '(e o ea)• 3 {:w? 'w}• :t '(t i o im)• :b 'b}•)` + `(c§ #{:|e '(e o ea)§ 3 {:w? 'w}§ :t '(t i o im)§ :b 'b}§)` ); const b = docFromTextNotation( - `(c• #{'(e o ea) :|e• 3 {:w? 'w}• :t '(t i o im)• :b 'b}•)` + `(c§ #{'(e o ea) :|e§ 3 {:w? 'w}§ :t '(t i o im)§ :b 'b}§)` ); void paredit.dragSexprForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); @@ -818,10 +818,10 @@ describe('paredit', () => { it('drags pair in binding box', () => { const b = docFromTextNotation( - `(c• [:e '(e o ea)• 3 {:w? 'w}• :t |'(t i o im)• :b 'b]•)` + `(c§ [:e '(e o ea)§ 3 {:w? 'w}§ :t |'(t i o im)§ :b 'b]§)` ); const a = docFromTextNotation( - `(c• [:e '(e o ea)• 3 {:w? 'w}• :b 'b• :t |'(t i o im)]•)` + `(c§ [:e '(e o ea)§ 3 {:w? 'w}§ :b 'b§ :t |'(t i o im)]§)` ); void paredit.dragSexprForward(b, ['c']); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); @@ -854,40 +854,40 @@ describe('paredit', () => { 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])`); + const b = docFromTextNotation(`(;;foo§de|f foo [:foo :bar :baz])`); + const a = docFromTextNotation(`de|f§(;;foo§ foo [:foo :bar :baz])`); void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags up without killing preceding line comments or trailing parens', () => { - const b = docFromTextNotation(`(def ;; foo• |:foo)`); - const a = docFromTextNotation(`|:foo•(def ;; foo•)`); + const b = docFromTextNotation(`(def ;; foo§ |:foo)`); + const a = docFromTextNotation(`|:foo§(def ;; foo§)`); void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); }); describe('backwardUp - multi-line', () => { it('Drags up from indented vector', () => { - const b = docFromTextNotation(`((fn foo• [x]• [|:foo• :bar• :baz])• 1)`); - const a = docFromTextNotation(`((fn foo• [x]• |:foo• [:bar• :baz])• 1)`); + const b = docFromTextNotation(`((fn foo§ [x]§ [|:foo§ :bar§ :baz])§ 1)`); + const a = docFromTextNotation(`((fn foo§ [x]§ |:foo§ [:bar§ :baz])§ 1)`); void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags up from indented list', () => { - const b = docFromTextNotation(`(|(fn foo• [x]• [:foo• :bar• :baz])• 1)`); - const a = docFromTextNotation(`|(fn foo• [x]• [:foo• :bar• :baz])•(1)`); + const b = docFromTextNotation(`(|(fn foo§ [x]§ [:foo§ :bar§ :baz])§ 1)`); + const a = docFromTextNotation(`|(fn foo§ [x]§ [:foo§ :bar§ :baz])§(1)`); void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags up from end of indented list', () => { - const b = docFromTextNotation(`((fn foo• [x]• [:foo• :bar• :baz])• |a)`); - const a = docFromTextNotation(`|a•((fn foo• [x]• [:foo• :bar• :baz]))`); + 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)`); + const b = docFromTextNotation(`((fn foo§ [x]§ [:foo§ ;; foo§ :b|ar§ :baz])§ 1)`); + const a = docFromTextNotation(`((fn foo§ [x]§ :b|ar§ [:foo§ ;; foo§§ :baz])§ 1)`); void paredit.dragSexprBackwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); @@ -906,8 +906,8 @@ describe('paredit', () => { 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])`); + const b = docFromTextNotation(`(d|ef ;; foo§ [:foo :bar :baz])`); + const a = docFromTextNotation(`(;; foo§ [d|ef :foo :bar :baz])`); void paredit.dragSexprForwardDown(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); @@ -920,8 +920,8 @@ describe('paredit', () => { 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`); + const b = docFromTextNotation(`((fn foo [x] [:foo :b|ar ;; bar§])) :baz`); + const a = docFromTextNotation(`((fn foo [x] [:foo ;; bar§] :b|ar)) :baz`); void paredit.dragSexprForwardUp(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); @@ -934,7 +934,7 @@ describe('paredit', () => { expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); }); it('Drags backward down into list w/o killing line comments on the way', () => { - const b = docFromTextNotation(`((fn foo [x] [:foo :bar])) ;; baz•:b|az`); + const b = docFromTextNotation(`((fn foo [x] [:foo :bar])) ;; baz§:b|az`); const a = docFromTextNotation(`((fn foo [x] [:foo :bar]) :b|az) ;; baz`); void paredit.dragSexprBackwardDown(b); expect(textAndSelection(b)).toStrictEqual(textAndSelection(a)); @@ -995,8 +995,8 @@ describe('paredit', () => { expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps, in multiline document', () => { - const a = docFromTextNotation('(foo• (str| ) "foo")'); - const b = docFromTextNotation('(foo• (str| "foo"))'); + const a = docFromTextNotation('(foo§ (str| ) "foo")'); + const b = docFromTextNotation('(foo§ (str| "foo"))'); void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); @@ -1037,15 +1037,15 @@ describe('paredit', () => { expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('leaves newlines when slurp', () => { - const a = docFromTextNotation('(fo|o•) bar'); - const b = docFromTextNotation('(fo|o• bar)'); + const a = docFromTextNotation('(fo|o§) bar'); + const b = docFromTextNotation('(fo|o§ bar)'); void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('slurps properly when closing paren is on new line', () => { // https://github.com/BetterThanTomorrow/calva/issues/1171 - const a = docFromTextNotation('(def foo• (str|• )• 42)'); - const b = docFromTextNotation('(def foo• (str|• • 42))'); + const a = docFromTextNotation('(def foo§ (str|§ )§ 42)'); + const b = docFromTextNotation('(def foo§ (str|§ § 42))'); void paredit.forwardSlurpSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); @@ -1092,8 +1092,8 @@ describe('paredit', () => { expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('leaves newlines when slurp', () => { - const a = docFromTextNotation('(fo|o• bar)'); - const b = docFromTextNotation('(fo|o)• bar'); + const a = docFromTextNotation('(fo|o§ bar)'); + const b = docFromTextNotation('(fo|o)§ bar'); void paredit.forwardBarfSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); @@ -1131,14 +1131,14 @@ describe('paredit', () => { describe('Raise', () => { it('raises the current form when cursor is preceding', () => { - const a = docFromTextNotation('(comment• (str |#(foo)))'); - const b = docFromTextNotation('(comment• |#(foo))'); + const a = docFromTextNotation('(comment§ (str |#(foo)))'); + const b = docFromTextNotation('(comment§ |#(foo))'); void paredit.raiseSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('raises the current form when cursor is trailing', () => { - const a = docFromTextNotation('(comment• (str #(foo)|))'); - const b = docFromTextNotation('(comment• #(foo)|)'); + const a = docFromTextNotation('(comment§ (str #(foo)|))'); + const b = docFromTextNotation('(comment§ #(foo)|)'); void paredit.raiseSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); @@ -1158,74 +1158,74 @@ describe('paredit', () => { describe('Kill character backwards (backspace)', () => { it('Leaves closing paren of empty list alone', async () => { - const a = docFromTextNotation('{::foo ()|• ::bar :foo}'); - const b = docFromTextNotation('{::foo (|)• ::bar :foo}'); + const a = docFromTextNotation('{::foo ()|§ ::bar :foo}'); + const b = docFromTextNotation('{::foo (|)§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes closing paren if unbalance', async () => { - const a = docFromTextNotation('{::foo )|• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); + const a = docFromTextNotation('{::foo )|§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Leaves opening paren of non-empty list alone', async () => { - const a = docFromTextNotation('{::foo (|a)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |(a)• ::bar :foo}'); + const a = docFromTextNotation('{::foo (|a)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |(a)§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Leaves opening quote of non-empty string alone', async () => { - const a = docFromTextNotation('{::foo "|a"• ::bar :foo}'); - const b = docFromTextNotation('{::foo |"a"• ::bar :foo}'); + const a = docFromTextNotation('{::foo "|a"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |"a"§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Leaves closing quote of non-empty string alone', async () => { - const a = docFromTextNotation('{::foo "a"|• ::bar :foo}'); - const b = docFromTextNotation('{::foo "a|"• ::bar :foo}'); + const a = docFromTextNotation('{::foo "a"|§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "a|"§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes contents in strings', async () => { - const a = docFromTextNotation('{::foo "a|"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|"• ::bar :foo}'); + const a = docFromTextNotation('{::foo "a|"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|"§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes contents in strings 2', async () => { - const a = docFromTextNotation('{::foo "a|a"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|a"• ::bar :foo}'); + const a = docFromTextNotation('{::foo "a|a"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|a"§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes contents in strings 3', async () => { - const a = docFromTextNotation('{::foo "aa|"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "a|"• ::bar :foo}'); + const a = docFromTextNotation('{::foo "aa|"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "a|"§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes quoted quote', async () => { - const a = docFromTextNotation('{::foo \\"|• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); + const a = docFromTextNotation('{::foo \\"|§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes quoted quote in string', async () => { - const a = docFromTextNotation('{::foo "\\"|"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|"• ::bar :foo}'); + const a = docFromTextNotation('{::foo "\\"|"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|"§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes contents in list', async () => { - const a = docFromTextNotation('{::foo (a|)• ::bar :foo}'); - const b = docFromTextNotation('{::foo (|)• ::bar :foo}'); + const a = docFromTextNotation('{::foo (a|)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo (|)§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes empty list function', async () => { - const a = docFromTextNotation('{::foo (|)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); + const a = docFromTextNotation('{::foo (|)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); @@ -1237,8 +1237,8 @@ describe('paredit', () => { }); it('Deletes empty literal function with trailing newline', async () => { // https://github.com/BetterThanTomorrow/calva/issues/1079 - const a = docFromTextNotation('{::foo #(|)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); + const a = docFromTextNotation('{::foo #(|)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); await paredit.backspace(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); @@ -1280,68 +1280,68 @@ describe('paredit', () => { describe('Kill character forwards (delete)', () => { it('Leaves closing paren of empty list alone', async () => { - const a = docFromTextNotation('{::foo |()• ::bar :foo}'); - const b = docFromTextNotation('{::foo (|)• ::bar :foo}'); + const a = docFromTextNotation('{::foo |()§ ::bar :foo}'); + const b = docFromTextNotation('{::foo (|)§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes closing paren if unbalance', async () => { - const a = docFromTextNotation('{::foo |)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); + const a = docFromTextNotation('{::foo |)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Leaves opening paren of non-empty list alone', async () => { - const a = docFromTextNotation('{::foo |(a)• ::bar :foo}'); - const b = docFromTextNotation('{::foo (|a)• ::bar :foo}'); + const a = docFromTextNotation('{::foo |(a)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo (|a)§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Leaves opening quote of non-empty string alone', async () => { - const a = docFromTextNotation('{::foo |"a"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|a"• ::bar :foo}'); + const a = docFromTextNotation('{::foo |"a"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|a"§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Leaves closing quote of non-empty string alone', async () => { - const a = docFromTextNotation('{::foo "a|"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "a"|• ::bar :foo}'); + const a = docFromTextNotation('{::foo "a|"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "a"|§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes contents in strings', async () => { - const a = docFromTextNotation('{::foo "|a"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|"• ::bar :foo}'); + const a = docFromTextNotation('{::foo "|a"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|"§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes contents in strings 2', async () => { - const a = docFromTextNotation('{::foo "|aa"• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|a"• ::bar :foo}'); + const a = docFromTextNotation('{::foo "|aa"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|a"§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes quoted quote', async () => { - const a = docFromTextNotation('{::foo |\\"• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); + const a = docFromTextNotation('{::foo |\\"§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes quoted quote in string', async () => { - const a = docFromTextNotation('{::foo "|\\""• ::bar :foo}'); - const b = docFromTextNotation('{::foo "|"• ::bar :foo}'); + const a = docFromTextNotation('{::foo "|\\""§ ::bar :foo}'); + const b = docFromTextNotation('{::foo "|"§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes contents in list', async () => { - const a = docFromTextNotation('{::foo (|a)• ::bar :foo}'); - const b = docFromTextNotation('{::foo (|)• ::bar :foo}'); + const a = docFromTextNotation('{::foo (|a)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo (|)§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Deletes empty list function', async () => { - const a = docFromTextNotation('{::foo (|)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); + const a = docFromTextNotation('{::foo (|)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); @@ -1353,8 +1353,8 @@ describe('paredit', () => { }); it('Deletes empty literal function with trailing newline', async () => { // https://github.com/BetterThanTomorrow/calva/issues/1079 - const a = docFromTextNotation('{::foo #(|)• ::bar :foo}'); - const b = docFromTextNotation('{::foo |• ::bar :foo}'); + const a = docFromTextNotation('{::foo #(|)§ ::bar :foo}'); + const b = docFromTextNotation('{::foo |§ ::bar :foo}'); await paredit.deleteForward(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); @@ -1399,44 +1399,44 @@ describe('paredit', () => { describe('addRichComment', () => { it('Adds Rich Comment after Top Level form', () => { - const a = docFromTextNotation('(fo|o)••(bar)'); - const b = docFromTextNotation('(foo)••(comment• |• )••(bar)'); + const a = docFromTextNotation('(fo|o)§§(bar)'); + const b = docFromTextNotation('(foo)§§(comment§ |§ )§§(bar)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Inserts Rich Comment between Top Levels', () => { - const a = docFromTextNotation('(foo)•|•(bar)'); - const b = docFromTextNotation('(foo)••(comment• |• )••(bar)'); + const a = docFromTextNotation('(foo)§|§(bar)'); + const b = docFromTextNotation('(foo)§§(comment§ |§ )§§(bar)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Inserts Rich Comment between Top Levels, before Top Level form', () => { - const a = docFromTextNotation('(foo)••|(bar)'); - const b = docFromTextNotation('(foo)••(comment• |• )••(bar)'); + const a = docFromTextNotation('(foo)§§|(bar)'); + const b = docFromTextNotation('(foo)§§(comment§ |§ )§§(bar)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Inserts Rich Comment between Top Levels, after Top Level form', () => { - const a = docFromTextNotation('(foo)|••(bar)'); - const b = docFromTextNotation('(foo)••(comment• |• )••(bar)'); + const a = docFromTextNotation('(foo)|§§(bar)'); + const b = docFromTextNotation('(foo)§§(comment§ |§ )§§(bar)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Inserts Rich Comment between Top Levels, in comment', () => { - const a = docFromTextNotation('(foo)•;foo| bar•(bar)'); - const b = docFromTextNotation('(foo)•;foo bar••(comment• |• )••(bar)'); + const a = docFromTextNotation('(foo)§;foo| bar§(bar)'); + const b = docFromTextNotation('(foo)§;foo bar§§(comment§ |§ )§§(bar)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Moves to Rich Comment below, if any', () => { - const a = docFromTextNotation('(foo|)••(comment••bar••baz)'); - const b = docFromTextNotation('(foo)••(comment••|bar••baz)'); + const a = docFromTextNotation('(foo|)§§(comment§§bar§§baz)'); + const b = docFromTextNotation('(foo)§§(comment§§|bar§§baz)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('Moves to Rich Comment below, if any, looking behind line comments', () => { - const a = docFromTextNotation('(foo|)••;;line comment••(comment••bar••baz)'); - const b = docFromTextNotation('(foo)••;;line comment••(comment••|bar••baz)'); + const a = docFromTextNotation('(foo|)§§;;line comment§§(comment§§bar§§baz)'); + const b = docFromTextNotation('(foo)§§;;line comment§§(comment§§|bar§§baz)'); paredit.addRichComment(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); 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 d65f2b0a5..b7a2750a9 100644 --- a/src/extension-test/unit/cursor-doc/token-cursor-test.ts +++ b/src/extension-test/unit/cursor-doc/token-cursor-test.ts @@ -5,15 +5,15 @@ import { docFromTextNotation, textAndSelection } from '../common/text-notation'; describe('Token Cursor', () => { describe('backwardWhitespace', () => { it('it moves past whitespace', () => { - const a = docFromTextNotation('a •|c'); - const b = docFromTextNotation('a| •c'); + const a = docFromTextNotation('a §|c'); + const b = docFromTextNotation('a| §c'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardWhitespace(); expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('it moves past whitespace from inside symbol', () => { - const a = docFromTextNotation('a •c|c'); - const b = docFromTextNotation('a| •cc'); + const a = docFromTextNotation('a §c|c'); + const b = docFromTextNotation('a| §cc'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardWhitespace(); expect(cursor.offsetStart).toBe(b.selections[0].anchor); @@ -22,36 +22,36 @@ describe('Token Cursor', () => { describe('forwardSexp', () => { it('moves from beginning to end of symbol', () => { - const a = docFromTextNotation('(|c•#f)'); - const b = docFromTextNotation('(c|•#f)'); + const a = docFromTextNotation('(|c§#f)'); + const b = docFromTextNotation('(c|§#f)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(); expect(cursor.offsetStart).toBe(b.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 a = docFromTextNotation('|(a(b(c§#f§(#b §[:f])§#z§1)))'); + const b = docFromTextNotation('(a(b(c§#f§(#b §[:f])§#z§1)))|'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(); expect(cursor.offsetStart).toBe(b.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 a = docFromTextNotation('(c|§#f§(#b §[:f :b :z])§#z§1)'); + const b = docFromTextNotation('(c§#f§(#b §[:f :b :z])|§#z§1)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(); expect(cursor.offsetStart).toBe(b.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 a = docFromTextNotation('(c§#f§(#b §[:f :b :z])|§#z§1)'); + const b = docFromTextNotation('(c§#f§(#b §[:f :b :z])§#z§1|)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(); expect(cursor.offsetStart).toBe(b.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 a = docFromTextNotation('(c§#f§(#b §[:f :b :z])§#z§1|)'); + const b = docFromTextNotation('(c§#f§(#b §[:f :b :z])§#z§1|)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.forwardSexp(); expect(cursor.offsetStart).toBe(b.selections[0].anchor); @@ -138,36 +138,36 @@ describe('Token Cursor', () => { describe('backwardSexp', () => { it('moves from end to beginning of symbol', () => { - const a = docFromTextNotation('(a(b(c|•#f•(#b •[:f :b :z])•#z•1)))'); - const b = docFromTextNotation('(a(b(|c•#f•(#b •[:f :b :z])•#z•1)))'); + const a = docFromTextNotation('(a(b(c|§#f§(#b §[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(b(|c§#f§(#b §[:f :b :z])§#z§1)))'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); expect(cursor.offsetStart).toBe(b.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 a = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])§#z§1)))|'); + const b = docFromTextNotation('|(a(b(c§#f§(#b §[:f :b :z])§#z§1)))'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); expect(cursor.offsetStart).toBe(b.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 a = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])|§#z§1)))'); + const b = docFromTextNotation('(a(b(c§|#f§(#b §[:f :b :z])§#z§1)))'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); expect(cursor.offsetStart).toBe(b.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 a = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])§#z§1|)))'); + const b = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])§|#z§1)))'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); expect(cursor.offsetStart).toBe(b.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 a = docFromTextNotation('(a(|b(c§#f§(#b §[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(|b(c§#f§(#b §[:f :b :z])§#z§1)))'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); expect(cursor.offsetStart).toBe(b.selections[0].anchor); @@ -194,22 +194,22 @@ describe('Token Cursor', () => { 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 a = docFromTextNotation('#bar §^baz§|[:a :b :c]§x'); + const b = docFromTextNotation('|#bar §^baz§[:a :b :c]§x'); const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(true, true); expect(cursor.offsetStart).toBe(b.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 a = docFromTextNotation('^bar §#baz§|[:a :b :c]§x'); + const b = docFromTextNotation('|^bar §#baz§[:a :b :c]§x'); const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(true, true); expect(cursor.offsetStart).toBe(b.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 a = docFromTextNotation('^d #c ^b §#a§[:a :b :c]|§x'); + const b = docFromTextNotation('|^d #c ^b §#a§[:a :b :c]§x'); const cursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(true, true); expect(cursor.offsetStart).toBe(b.selections[0].anchor); @@ -246,8 +246,8 @@ describe('Token Cursor', () => { expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Skips whitespace', () => { - const a = docFromTextNotation('(a|• (b 1))'); - const b = docFromTextNotation('(a• (|b 1))'); + const a = docFromTextNotation('(a|§ (b 1))'); + const b = docFromTextNotation('(a§ (|b 1))'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.downList(); expect(cursor.offsetStart).toBe(b.selections[0].anchor); @@ -279,8 +279,8 @@ describe('Token Cursor', () => { }); 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 a = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])§#z§1|)))'); + const b = docFromTextNotation('(a(b(c§#f§(#b §[:f :b :z])§#z§1)|))'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.upList(); expect(cursor.offsetStart).toBe(b.selections[0].anchor); @@ -348,22 +348,22 @@ describe('Token Cursor', () => { describe('backwardList', () => { it('Finds start of list', () => { - const a = docFromTextNotation('(((c•(#b •[:f])•#z•|a)))'); - const b = 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.selections[0].anchor); cursor.backwardList(); expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Finds start of list through readers', () => { - const a = docFromTextNotation('(((c•#a• #f•(#b •[:f])•#z•|a)))'); - const b = 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.selections[0].anchor); cursor.backwardList(); expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Finds start of list through metadata', () => { - const a = docFromTextNotation('(((c•^{:a c} (#b •[:f])•#z•|a)))'); - const b = 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.selections[0].anchor); cursor.backwardList(); expect(cursor.offsetStart).toBe(b.selections[0].anchor); @@ -415,24 +415,24 @@ describe('Token Cursor', () => { describe('forwardListOfType', () => { it('Finds end of list', () => { - const a = docFromTextNotation('([#{|c•(#b •[:f])•#z•1}])'); - const b = docFromTextNotation('([#{c•(#b •[:f])•#z•1}]|)'); + const a = docFromTextNotation('([#{|c§(#b §[:f])§#z§1}])'); + const b = docFromTextNotation('([#{c§(#b §[:f])§#z§1}]|)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.forwardListOfType(')'); expect(cursor.offsetStart).toBe(b.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 a = docFromTextNotation('([(c§(#b| §[:f])§#z§1)])'); + const b = docFromTextNotation('([(c§(#b §[:f])§#z§1)|])'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); const result = cursor.forwardListOfType(']'); expect(cursor.offsetStart).toBe(b.selections[0].anchor); expect(result).toBe(true); }); it('Finds end of map', () => { - const a = docFromTextNotation('({:a [(c•(#|b •[:f])•#z•|a)]})'); - const b = 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.selections[0].anchor); const result = cursor.forwardListOfType('}'); expect(cursor.offsetStart).toBe(b.selections[0].anchor); @@ -458,24 +458,24 @@ describe('Token Cursor', () => { describe('backwardListOfType', () => { it('Finds start of list', () => { - const a = docFromTextNotation('([#{c•(#b •[:f])•#z•|a}])'); - const b = 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.selections[0].anchor); const result = cursor.backwardListOfType('('); expect(result).toBe(true); expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); it('Finds start of vector', () => { - const a = docFromTextNotation('([(c•(#b •[:f])•#z•|a)])'); - const b = 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.selections[0].anchor); const result = cursor.backwardListOfType('['); 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•|a)]})'); - const b = 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.selections[0].anchor); const result = cursor.backwardListOfType('{'); expect(cursor.offsetStart).toBe(b.selections[0].anchor); @@ -521,8 +521,8 @@ 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 a = docFromTextNotation('(a(b(c§#f§(#b §|[:f :b :z])§#z§1)))'); + const b = docFromTextNotation('(a(b(c§#f§|(#b §[:f :b :z])§#z§1)))'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardUpList(); expect(cursor.offsetStart).toBe(b.selections[0].anchor); @@ -555,15 +555,15 @@ describe('Token Cursor', () => { describe('The REPL prompt', () => { it('Backward sexp bypasses prompt', () => { - const a = docFromTextNotation('foo•clj꞉foo꞉> |'); - const b = docFromTextNotation('|foo•clj꞉foo꞉> '); + const a = docFromTextNotation('foo§clj꞉foo꞉> |'); + const b = docFromTextNotation('|foo§clj꞉foo꞉> '); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(); expect(cursor.offsetStart).toEqual(b.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 a = docFromTextNotation('foo§clj꞉foo꞉> |'); + const b = docFromTextNotation('foo§|clj꞉foo꞉> '); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); cursor.backwardSexp(false); expect(cursor.offsetStart).toEqual(b.selections[0].active); @@ -572,8 +572,8 @@ describe('Token Cursor', () => { describe('Current Form', () => { it('0: selects from within non-list form', () => { - const a = docFromTextNotation('(a|aa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x'); - const b = docFromTextNotation('(|aaa| (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x'); + const a = docFromTextNotation('(a|aa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x'); + const b = docFromTextNotation('(|aaa| (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); @@ -608,110 +608,110 @@ describe('Token Cursor', () => { 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 a = docFromTextNotation('(aaa §x§#(a b c)|)§#baz§yyy§)'); + const b = docFromTextNotation('(aaa §x§|#(a b c)|)§#baz§yyy§)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('1: selects from adjacent when after form, including reader tags', () => { - const a = docFromTextNotation('(x• #a #b •#(a b c)|)•#baz•yyy•)'); - const b = docFromTextNotation('(x• |#a #b •#(a b c)|)•#baz•yyy•)'); + const a = docFromTextNotation('(x§ #a #b §#(a b c)|)§#baz§yyy§)'); + const b = docFromTextNotation('(x§ |#a #b §#(a b c)|)§#baz§yyy§)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('1: selects from adjacent when after form, including readers and meta data', () => { - const a = docFromTextNotation('(x• ^a #b •#(a b c)|)•#baz•yyy•)'); - const b = docFromTextNotation('(x• |^a #b •#(a b c)|)•#baz•yyy•)'); + const a = docFromTextNotation('(x§ ^a #b §#(a b c)|)§#baz§yyy§)'); + const b = docFromTextNotation('(x§ |^a #b §#(a b c)|)§#baz§yyy§)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('1: selects from adjacent when after form, including meta data and readers', () => { - const a = docFromTextNotation('(x• #a ^b •#(a b c)|)•#baz•yyy•)'); - const b = docFromTextNotation('(x• |#a ^b •#(a b c)|)•#baz•yyy•)'); + const a = docFromTextNotation('(x§ #a ^b §#(a b c)|)§#baz§yyy§)'); + const b = docFromTextNotation('(x§ |#a ^b §#(a b c)|)§#baz§yyy§)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form', () => { - const a = docFromTextNotation('#bar •#baz•[:a :b :c]•x•|#(a b c)'); - const b = docFromTextNotation('#bar •#baz•[:a :b :c]•x•|#(a b c)|'); + const a = docFromTextNotation('#bar §#baz§[:a :b :c]§x§|#(a b c)'); + const b = docFromTextNotation('#bar §#baz§[:a :b :c]§x§|#(a b c)|'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, including reader tags', () => { - const a = docFromTextNotation('|#bar •#baz•[:a :b :c]•x'); - const b = docFromTextNotation('|#bar •#baz•[:a :b :c]|•x'); + const a = docFromTextNotation('|#bar §#baz§[:a :b :c]§x'); + const b = docFromTextNotation('|#bar §#baz§[:a :b :c]|§x'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, including meta data', () => { - const a = docFromTextNotation('|^bar •[:a :b :c]•x'); - const b = docFromTextNotation('|^bar •[:a :b :c]|•x'); + const a = docFromTextNotation('|^bar §[:a :b :c]§x'); + const b = docFromTextNotation('|^bar §[:a :b :c]|§x'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, including meta data and reader', () => { - const a = docFromTextNotation('|^bar •#baz•[:a :b :c]•x'); - const b = docFromTextNotation('|^bar •#baz•[:a :b :c]|•x'); + const a = docFromTextNotation('|^bar §#baz§[:a :b :c]§x'); + const b = docFromTextNotation('|^bar §#baz§[:a :b :c]|§x'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, including preceding reader and meta data', () => { - const a = docFromTextNotation('^bar •#baz•|[:a :b :c]•x'); - const b = docFromTextNotation('|^bar •#baz•[:a :b :c]|•x'); + const a = docFromTextNotation('^bar §#baz§|[:a :b :c]§x'); + const b = docFromTextNotation('|^bar §#baz§[:a :b :c]|§x'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, including preceding meta data and reader', () => { - const a = docFromTextNotation('#bar •^baz•|[:a :b :c]•x'); - const b = docFromTextNotation('|#bar •^baz•[:a :b :c]|•x'); + const a = docFromTextNotation('#bar §^baz§|[:a :b :c]§x'); + const b = docFromTextNotation('|#bar §^baz§[:a :b :c]|§x'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before form, or in readers', () => { - const a = docFromTextNotation('ccc •#foo•|•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy'); - const b = docFromTextNotation('ccc •|#foo••(#bar •#baz•[:a :b :c]•x•#(a b c))|•#baz•yyy'); + const a = docFromTextNotation('ccc §#foo§|§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy'); + const b = docFromTextNotation('ccc §|#foo§§(#bar §#baz§[:a :b :c]§x§#(a b c))|§#baz§yyy'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('2: selects from adjacent before a form with reader tags', () => { - const a = docFromTextNotation('#bar |•#baz•[:a :b :c]•x'); - const b = docFromTextNotation('|#bar •#baz•[:a :b :c]|•x'); + const a = docFromTextNotation('#bar |§#baz§[:a :b :c]§x'); + const b = docFromTextNotation('|#bar §#baz§[:a :b :c]|§x'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('3: selects previous form, if on the same line', () => { - const a = docFromTextNotation('z z | •foo• • bar'); - const b = docFromTextNotation('z |z| •foo• • bar'); + const a = docFromTextNotation('z z | §foo§ § bar'); + const b = docFromTextNotation('z |z| §foo§ § bar'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('4: selects next form, if on the same line', () => { - const a = docFromTextNotation('yyy•| z z z •foo• • bar'); - const b = docFromTextNotation('yyy• |z| z z •foo• • bar'); + const a = docFromTextNotation('yyy§| z z z §foo§ § bar'); + const b = docFromTextNotation('yyy§ |z| z z §foo§ § bar'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('5: selects previous form, if any', () => { - const a = docFromTextNotation('yyy• z z z •foo• |• bar'); - const b = docFromTextNotation('yyy• z z z •|foo|• • bar'); + const a = docFromTextNotation('yyy§ z z z §foo§ |§ bar'); + const b = docFromTextNotation('yyy§ z z z §|foo|§ § bar'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('5: selects previous form, if any, when next form has metadata', () => { - const a = docFromTextNotation('z•foo•|•^{:a b}•bar'); - const b = docFromTextNotation('z•|foo|••^{:a b}•bar'); + const a = docFromTextNotation('z§foo§|§^{:a b}§bar'); + const b = docFromTextNotation('z§|foo|§§^{:a b}§bar'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('6: selects next form, if any', () => { - const a = docFromTextNotation(' | • (foo {:a b})•(c)'); - const b = docFromTextNotation(' • |(foo {:a b})|•(c)'); + const a = docFromTextNotation(' | § (foo {:a b})§(c)'); + const b = docFromTextNotation(' § |(foo {:a b})|§(c)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); it('7: selects enclosing form, if any', () => { - const a = docFromTextNotation('(|) • (foo {:a b})•(c)'); - const b = docFromTextNotation('|()| • (foo {:a b})•(c)'); + const a = docFromTextNotation('(|) § (foo {:a b})§(c)'); + const b = docFromTextNotation('|()| § (foo {:a b})§(c)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].anchor); expect(cursor.rangeForCurrentForm(a.selections[0].anchor)).toEqual(textAndSelection(b)[1][0]); }); @@ -739,30 +739,30 @@ describe('Token Cursor', () => { describe('Top Level Form', () => { it('Finds range when nested down a some forms', () => { const a = docFromTextNotation( - 'aaa (bbb (ccc •#foo•(#bar •#baz•[:a :b| :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) (ddd eee)' + 'aaa (bbb (ccc §#foo§(#bar §#baz§[:a :b| :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) (ddd eee)' ); const b = docFromTextNotation( - 'aaa |(bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar))| (ddd eee)' + 'aaa |(bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar))| (ddd eee)' ); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Finds range when in current form is top level', () => { const a = docFromTextNotation( - 'aaa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) |(ddd eee)' + 'aaa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) |(ddd eee)' ); const b = docFromTextNotation( - 'aaa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) |(ddd eee)|' + 'aaa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) |(ddd eee)|' ); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); it('Finds range when in ”solid” top level form', () => { const a = docFromTextNotation( - 'a|aa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) (ddd eee)' + 'a|aa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) (ddd eee)' ); const b = docFromTextNotation( - '|aaa| (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) (ddd eee)' + '|aaa| (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) (ddd eee)' ); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); @@ -775,10 +775,10 @@ describe('Token Cursor', () => { }); it('Finds top level comment range if comment special treatment is disabled', () => { const a = docFromTextNotation( - 'aaa (comment (ccc •#foo•(#bar •#baz•[:a :b| :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) (ddd eee)' + 'aaa (comment (ccc §#foo§(#bar §#baz§[:a :b| :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) (ddd eee)' ); const b = docFromTextNotation( - 'aaa |(comment (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar))| (ddd eee)' + 'aaa |(comment (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar))| (ddd eee)' ); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); expect(cursor.rangeForDefun(a.selections[0].active, false)).toEqual( @@ -800,10 +800,10 @@ describe('Token Cursor', () => { }); it('Finds comment range when current form is top level comment form', () => { const a = docFromTextNotation( - 'aaa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) |(comment eee)' + 'aaa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) |(comment eee)' ); const b = docFromTextNotation( - 'aaa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) |(comment eee)|' + 'aaa (bbb (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) |(comment eee)|' ); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); @@ -821,8 +821,8 @@ describe('Token Cursor', () => { 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 a = docFromTextNotation('aaa (comment [bbb ccc]§ | ddd)'); + const b = docFromTextNotation('aaa (comment [bbb ccc]§ |ddd|)'); const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1][0]); }); @@ -835,10 +835,10 @@ describe('Token Cursor', () => { }); it('Can find the comment range for a top level form inside a comment', () => { const a = docFromTextNotation( - 'aaa (comment (ccc •#foo•(#bar •#baz•[:a :b| :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)) (ddd eee)' + 'aaa (comment (ccc §#foo§(#bar §#baz§[:a :b| :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar)) (ddd eee)' ); const b = docFromTextNotation( - 'aaa |(comment (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar))| (ddd eee)' + 'aaa |(comment (ccc §#foo§(#bar §#baz§[:a :b :c]§x§#(a b c))§#baz§yyy§ z z z §foo§ § bar))| (ddd eee)' ); const cursor: LispTokenCursor = a.getTokenCursor(0); expect(cursor.rangeForDefun(a.selections[0].anchor, false)).toEqual( 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 220940d9d..d8664c395 100644 --- a/src/extension-test/unit/util/cursor-get-text-test.ts +++ b/src/extension-test/unit/util/cursor-get-text-test.ts @@ -5,8 +5,8 @@ import { docFromTextNotation } from '../common/text-notation'; describe('get text', () => { describe('getTopLevelFunction', () => { it('Finds top level function at top', () => { - const a = docFromTextNotation('(foo bar)•(deftest a-test• (baz |gaz))'); - const b = docFromTextNotation('(foo bar)•(deftest |a-test|• (baz gaz))'); + const a = docFromTextNotation('(foo bar)§(deftest a-test§ (baz |gaz))'); + const b = docFromTextNotation('(foo bar)§(deftest |a-test|§ (baz gaz))'); const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; expect(getText.currentTopLevelFunction(a, a.selections[0].active)).toEqual([ range, @@ -15,8 +15,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 a = docFromTextNotation('(foo bar)§(with-test§ (deftest a-test§ (baz |gaz)))'); + const b = docFromTextNotation('(foo bar)§(with-test§ (deftest |a-test|§ (baz gaz)))'); const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; expect(getText.currentTopLevelFunction(a, a.selections[0].active)).toEqual([ range, @@ -26,8 +26,8 @@ describe('get text', () => { it('Finds top level function when namespaced def-macro', () => { // https://github.com/BetterThanTomorrow/calva/issues/1086 - const a = docFromTextNotation('(foo bar)•(with-test• (t/deftest a-test• (baz |gaz)))'); - const b = docFromTextNotation('(foo bar)•(with-test• (t/deftest |a-test|• (baz gaz)))'); + const a = docFromTextNotation('(foo bar)§(with-test§ (t/deftest a-test§ (baz |gaz)))'); + const b = docFromTextNotation('(foo bar)§(with-test§ (t/deftest |a-test|§ (baz gaz)))'); const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; expect(getText.currentTopLevelFunction(a, a.selections[0].active)).toEqual([ range, @@ -36,8 +36,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 a = docFromTextNotation('(foo bar)§(deftest ^{:some :thing} a-test§ (baz |gaz))'); + const b = docFromTextNotation('(foo bar)§(deftest ^{:some :thing} |a-test|§ (baz gaz))'); const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; expect(getText.currentTopLevelFunction(a, a.selections[0].active)).toEqual([ range, @@ -48,8 +48,8 @@ describe('get text', () => { describe('getTopLevelForm', () => { it('Finds top level form', () => { - const a = docFromTextNotation('(foo bar)•(deftest a-test• (baz |gaz))'); - const b = docFromTextNotation('(foo bar)•|(deftest a-test• (baz gaz))|'); + const a = docFromTextNotation('(foo bar)§(deftest a-test§ (baz |gaz))'); + const b = docFromTextNotation('(foo bar)§|(deftest a-test§ (baz gaz))|'); const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; expect(getText.currentTopLevelForm(a)).toEqual([range, b.model.getText(...range)]); }); @@ -57,8 +57,8 @@ describe('get text', () => { describe('currentEnclosingFormToCursor', () => { it('Current enclosing form from start to cursor, then folded', () => { - const a = docFromTextNotation('(foo bar)•(deftest a-test• [baz ; f|oo• gaz])'); - const b = docFromTextNotation('(foo bar)•(deftest a-test• |[baz| ; foo• gaz])'); + const a = docFromTextNotation('(foo bar)§(deftest a-test§ [baz ; f|oo§ gaz])'); + const b = docFromTextNotation('(foo bar)§(deftest a-test§ |[baz| ; foo§ gaz])'); const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; const trail = ']'; expect(getText.currentEnclosingFormToCursor(a)).toEqual([ @@ -70,8 +70,8 @@ describe('get text', () => { describe('topLevelFormToCursor', () => { it('Finds top level form from start to cursor', () => { - const a = docFromTextNotation('(foo bar)•(deftest a-test• [baz ; f|oo• gaz])'); - const b = docFromTextNotation('(foo bar)•|(deftest a-test• [baz| ; foo• gaz])'); + const a = docFromTextNotation('(foo bar)§(deftest a-test§ [baz ; f|oo§ gaz])'); + const b = docFromTextNotation('(foo bar)§|(deftest a-test§ [baz| ; foo§ gaz])'); const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; const trail = '])'; expect(getText.currentTopLevelFormToCursor(a)).toEqual([ @@ -83,9 +83,9 @@ describe('get text', () => { describe('startOfFileToCursor', () => { it('Builds form from start of file to cursor, when cursor in line comment', () => { - const a = docFromTextNotation('(foo bar)•(deftest a-test• [baz ; f|oo• gaz])•(bar baz)'); + const a = docFromTextNotation('(foo bar)§(deftest a-test§ [baz ; f|oo§ gaz])§(bar baz)'); const b = docFromTextNotation( - '|(foo bar)•(deftest a-test• [baz| ; foo• gaz])•(bar baz)' + '|(foo bar)§(deftest a-test§ [baz| ; foo§ gaz])§(bar baz)' ); const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; const trail = '])'; @@ -96,10 +96,10 @@ describe('get text', () => { }); it('Builds form from start of file to cursor, when cursor in comment macro', () => { const a = docFromTextNotation( - '(foo bar)(comment• (deftest a-test• [baz ; f|oo• gaz])•(bar baz))' + '(foo bar)(comment§ (deftest a-test§ [baz ; f|oo§ gaz])§(bar baz))' ); const b = docFromTextNotation( - '|(foo bar)(comment• (deftest a-test• [baz| ; foo• gaz])•(bar baz))' + '|(foo bar)(comment§ (deftest a-test§ [baz| ; foo§ gaz])§(bar baz))' ); const range: [number, number] = [b.selections[0].anchor, b.selections[0].active]; const trail = ']))'; From dfed45ec682ba421ff28638c01c27ea282b125bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sat, 9 Apr 2022 15:12:13 +0200 Subject: [PATCH 28/49] Add link to text-notation video --- src/extension-test/unit/common/text-notation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index cd4e02803..d878edddf 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -4,6 +4,7 @@ import { clone, entries, cond, toInteger, last, first, cloneDeep, orderBy } from /** * Text Notation for expressing states of a document, including * current text and selection. + * See this video for an intro: https://www.youtube.com/watch?v=Sy3arG-Degw * * Since JavasScript makes it clumsy with multiline strings, * newlines are denoted with the paragraph character: `§` * * Selections are denoted like so From 7c7a92623085eb527c58c389d2bcc732e6033338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sat, 9 Apr 2022 15:43:40 +0200 Subject: [PATCH 29/49] Add some multi-cursor tests --- .../unit/cursor-doc/paredit-test.ts | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index 6e1e9e2a3..c3fd457e7 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -1,7 +1,12 @@ import * as expect from 'expect'; import * as paredit from '../../../cursor-doc/paredit'; import * as model from '../../../cursor-doc/model'; -import { docFromTextNotation, textAndSelection, getText } from '../common/text-notation'; +import { + docFromTextNotation, + textAndSelection, + getText, + textNotationFromDoc, +} from '../common/text-notation'; import { ModelEditSelection } from '../../../cursor-doc/model'; import { last, method } from 'lodash'; @@ -24,19 +29,38 @@ describe('paredit', () => { 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])|'); + const a = docFromTextNotation('|(a b [c])'); + const b = docFromTextNotation('|(a b [c])|'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors find the list in front', () => { + const a = docFromTextNotation('|(a b [c])§|1(a b [c])'); + const b = docFromTextNotation('|(a b [c])|§|1(a b [c])|1'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds the list in front through metadata', () => { - const a = docFromTextNotation('|^:foo (def foo [vec])'); - const b = docFromTextNotation('|^:foo (def foo [vec])|'); + const a = docFromTextNotation('|^:a (b c [d])'); + const b = docFromTextNotation('|^:a (b c [d])|'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors find the list in front through metadata', () => { + const a = docFromTextNotation('|^:a (b c [d])§|1^:a (b c [d])'); + const b = docFromTextNotation('|^:a (b c [d])|§|1^:a (b c [d])|1'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds the list in front through metadata and readers', () => { - const a = docFromTextNotation('|^:f #a #b (def foo [vec])'); - const b = docFromTextNotation('|^:f #a #b (def foo [vec])|'); + const a = docFromTextNotation('|^:f #a #b (c d [e])'); + const b = docFromTextNotation('|^:f #a #b (c d [e])|'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors find the list in front through metadata and readers', () => { + const a = docFromTextNotation('|^:f #a #b (c d [e])§|1^:f #a #b (c d [e])'); + const b = docFromTextNotation('|^:f #a #b (c d [e])|§|1^:f #a #b (c d [e])|1'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors find the list in front through metadata and readers', () => { + const a = docFromTextNotation('|^:f #a #b (c d [e])§|1^:f #a #b (c d [e])'); + const b = docFromTextNotation('|^:f #a #b (c d [e])|§|1^:f #a #b (c d [e])|1'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds the list in front through reader metadata reader', () => { @@ -74,9 +98,19 @@ describe('paredit', () => { const b = docFromTextNotation('(def foo [:foo :bar |:baz|])'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); - it('Returns empty range when no forward sexp', () => { - const a = docFromTextNotation('(def foo [:foo :bar :baz|])'); - const b = docFromTextNotation('(def foo [:foo :bar :baz|])'); + it('Does not find anything at end of list', () => { + const a = docFromTextNotation('(|)'); + const b = docFromTextNotation('(|)'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors do not find anything at end of list', () => { + const a = docFromTextNotation('(|)§(|1)'); + const b = docFromTextNotation('(|)§(|1)'); + expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Cursor 1 finds fwd sexp, cursor 2 does not, cursor 3 does', () => { + const a = docFromTextNotation('(|a)§(|1)§|2(b)'); + const b = docFromTextNotation('(|a|)§(|1)§|2(b)|2'); expect(paredit.forwardSexpRange(a)).toEqual(textAndSelection(b)[1]); }); it('Finds next symbol, including leading space', () => { From fc51d29daa2fdf66f8e32a7950a290cd112ebfe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sat, 9 Apr 2022 17:19:25 +0200 Subject: [PATCH 30/49] Add command for creating doc from text-notation --- package.json | 5 ++++ .../unit/common/text-notation.ts | 2 +- src/paredit/extension.ts | 24 +++++++++++++++++-- test-data/multi_cursor_sandbox.clj | 3 +++ 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 test-data/multi_cursor_sandbox.clj diff --git a/package.json b/package.json index 13cff6291..9f2585677 100644 --- a/package.json +++ b/package.json @@ -922,6 +922,11 @@ "title": "Print TextNotation from the current document to Calva says", "category": "Calva Diagnostics" }, + { + "command": "calva.diagnostics.createDocumentFromTextNotation", + "title": "Create a new Clojure Document from TextNotation", + "category": "Calva Diagnostics" + }, { "command": "calva.linting.resolveMacroAs", "title": "Resolve Macro As", diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index d878edddf..79a18d093 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -142,7 +142,7 @@ export function getText(doc: model.EditableDocument): 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][]] { +export function textAndSelection(doc: model.EditableDocument): [string, [number, number][]] { // return [text(doc), [doc.selection.anchor, doc.selection.active]]; return [getText(doc), doc.selections.map((s) => [s.anchor, s.active])]; } diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index e51a39877..513d5edd4 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -15,7 +15,7 @@ import * as paredit from '../cursor-doc/paredit'; import * as docMirror from '../doc-mirror/index'; import { EditableDocument, ModelEditResult } from '../cursor-doc/model'; import { assertIsDefined } from '../utilities'; -import { textNotationFromDoc } from '../extension-test/unit/common/text-notation'; +import * as textNotation from '../extension-test/unit/common/text-notation'; import * as calvaState from '../state'; const onPareditKeyMapChangedEmitter = new EventEmitter(); @@ -467,12 +467,32 @@ export function activate(context: ExtensionContext) { const doc = vscode.window.activeTextEditor?.document; if (doc && doc.languageId === 'clojure') { const mirrorDoc = docMirror.getDocument(vscode.window.activeTextEditor?.document); - const notation = textNotationFromDoc(mirrorDoc); + const notation = textNotation.textNotationFromDoc(mirrorDoc); const chan = calvaState.outputChannel(); const relPath = vscode.workspace.asRelativePath(doc.uri); chan.appendLine(`Text notation for: ${relPath}:\n${notation}`); } }), + commands.registerCommand('calva.diagnostics.createDocumentFromTextNotation', async () => { + const tn = await vscode.window.showInputBox({ + placeHolder: 'Text-notation', + prompt: 'Type the text-notation for the document you want to create', + }); + const cursorDoc = textNotation.docFromTextNotation(tn); + await vscode.workspace + .openTextDocument({ language: 'clojure', content: textNotation.getText(cursorDoc) }) + .then(async (doc) => { + const editor = await vscode.window.showTextDocument(doc, { + preview: false, + preserveFocus: false, + }); + editor.selections = cursorDoc.selections.map((selection) => { + const anchor = doc.positionAt(selection.anchor), + active = doc.positionAt(selection.active); + return new vscode.Selection(anchor, active); + }); + }); + }), window.onDidChangeActiveTextEditor( (e) => e && e.document && languages.has(e.document.languageId) ), diff --git a/test-data/multi_cursor_sandbox.clj b/test-data/multi_cursor_sandbox.clj new file mode 100644 index 000000000..fb529b0fa --- /dev/null +++ b/test-data/multi_cursor_sandbox.clj @@ -0,0 +1,3 @@ +;(|a)§(|1) +(a) +() \ No newline at end of file From 7a063779d9c94f265dafcf94145825cbcc9f0e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sat, 9 Apr 2022 17:20:03 +0200 Subject: [PATCH 31/49] Update changelog [skip ci] --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d12d179f0..94decb5fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Changes to Calva. - [Multi cursor](https://github.com/BetterThanTomorrow/calva/issues/610) - [Remove all cursor when-context logic - this might affect the behavior of those who make substantial use of them. Worth testing](https://github.com/BetterThanTomorrow/calva/pull/1606#issuecomment-1086567905) - Fix: [Jump between source/test does not work properly with multiple workspace folders](https://github.com/BetterThanTomorrow/calva/issues/1219) +- [New diagnostics command, Print TextNotation from current document](https://www.youtube.com/watch?v=Sy3arG-Degw) +- [New diagnostics command, Create untitled document from TextNotation](TODO: create somewhere to link) ## [2.0.265] - 2022-04-08 - [Update paredit sexp forward/backward command labels](https://github.com/BetterThanTomorrow/calva/issues/1660) From 0d372202892f021816b1e87b8ae9fd0ace1637da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sat, 9 Apr 2022 17:44:44 +0200 Subject: [PATCH 32/49] Add failing test for docFromTextNotation --- .../unit/common/text-notation-test.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/extension-test/unit/common/text-notation-test.ts b/src/extension-test/unit/common/text-notation-test.ts index 9f3a65b70..8fbffbb5b 100644 --- a/src/extension-test/unit/common/text-notation-test.ts +++ b/src/extension-test/unit/common/text-notation-test.ts @@ -1,13 +1,23 @@ import * as expect from 'expect'; -import { docFromTextNotation, textNotationFromDoc } from '../common/text-notation'; -import _ = require('lodash'); +import * as textNotation from '../common/text-notation'; describe('text-notation test utils', () => { describe('textNotationFromDoc', () => { it('should return the same input text to textNotationFromDoc', () => { const inputText = '(a b|1) (a b|) (a (b))'; - const doc = docFromTextNotation(inputText); - expect(textNotationFromDoc(doc)).toEqual(inputText); + const doc = textNotation.docFromTextNotation(inputText); + expect(textNotation.textNotationFromDoc(doc)).toEqual(inputText); + }); + }); + describe('docFromTextNotation', () => { + it('creates single cursor position', () => { + const tn = '(a b|1)'; + const text = '(a b)'; + // TODO: Figure out why we need undefined here? + // const selections = [undefined, [4, 4]]; + const selections = [[4, 4]]; + const doc = textNotation.docFromTextNotation(tn); + expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); }); }); }); From f5651f36c84beb6dfbcbf860ea46f5389fb0dc6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sat, 9 Apr 2022 22:02:18 +0200 Subject: [PATCH 33/49] Update changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94decb5fb..6425a36f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,11 @@ Changes to Calva. ## [Unreleased] -- [Multi cursor](https://github.com/BetterThanTomorrow/calva/issues/610) -- [Remove all cursor when-context logic - this might affect the behavior of those who make substantial use of them. Worth testing](https://github.com/BetterThanTomorrow/calva/pull/1606#issuecomment-1086567905) +- [Multi cursor support for Paredit](https://github.com/BetterThanTomorrow/calva/issues/610) +- [Remove when-contexts as a way to control when commands move by sexp or by word](https://github.com/BetterThanTomorrow/calva/pull/1606#issuecomment-1086567905) - Fix: [Jump between source/test does not work properly with multiple workspace folders](https://github.com/BetterThanTomorrow/calva/issues/1219) -- [New diagnostics command, Print TextNotation from current document](https://www.youtube.com/watch?v=Sy3arG-Degw) -- [New diagnostics command, Create untitled document from TextNotation](TODO: create somewhere to link) +- [New diagnostics command, Print TextNotation from current document](https://github.com/BetterThanTomorrow/calva/issues/1675) (See [this CalvaTV video](https://www.youtube.com/watch?v=Sy3arG-Degw)) +- [New diagnostics command, Create untitled document from TextNotation](https://github.com/BetterThanTomorrow/calva/issues/1676) ## [2.0.265] - 2022-04-08 - [Update paredit sexp forward/backward command labels](https://github.com/BetterThanTomorrow/calva/issues/1660) From ace972e3b64c8b8b2105ba4f0d0fc335a80625be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sat, 9 Apr 2022 22:50:09 +0200 Subject: [PATCH 34/49] Add tests exposing docFromTextNotation bug With right->left selectios --- .../unit/common/text-notation-test.ts | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/src/extension-test/unit/common/text-notation-test.ts b/src/extension-test/unit/common/text-notation-test.ts index 8fbffbb5b..36ca83a28 100644 --- a/src/extension-test/unit/common/text-notation-test.ts +++ b/src/extension-test/unit/common/text-notation-test.ts @@ -11,13 +11,57 @@ describe('text-notation test utils', () => { }); describe('docFromTextNotation', () => { it('creates single cursor position', () => { - const tn = '(a b|1)'; + const tn = '(a b|)'; const text = '(a b)'; - // TODO: Figure out why we need undefined here? - // const selections = [undefined, [4, 4]]; const selections = [[4, 4]]; const doc = textNotation.docFromTextNotation(tn); expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); }); + it('creates multiple cursor positions', () => { + const tn = '(|1a |2b|)'; + const text = '(a b)'; + const selections = [ + [4, 4], + [1, 1], + [3, 3], + ]; + const doc = textNotation.docFromTextNotation(tn); + expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); + }); + it('creates single range/selection', () => { + const tn = '(a |b|)'; + const text = '(a b)'; + const selections = [[3, 4]]; + const doc = textNotation.docFromTextNotation(tn); + expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); + }); + it('creates multiple ranges/selections', () => { + const tn = '|1(|1|2a|2 |b|)'; + const text = '(a b)'; + const selections = [ + [3, 4], + [0, 1], + [1, 2], + ]; + const doc = textNotation.docFromTextNotation(tn); + expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); + }); + it('creates single directed r->l range/selection', () => { + const tn = '(a l range/selection', () => { + const tn = '(<1a<1 Date: Sat, 9 Apr 2022 17:56:20 -0700 Subject: [PATCH 35/49] Fix tNToDoc regressions --- src/extension-test/unit/common/text-notation.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index 79a18d093..0a9294101 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -14,9 +14,12 @@ import { clone, entries, cond, toInteger, last, first, cloneDeep, orderBy } from * * Selections with direction right->left are denoted with `<0`, `<1`, `<2`, ... `<9` etc at the range boundaries */ function textNotationToTextAndSelection(content: string): [string, model.ModelEditSelection[]] { + const cursorSymbolRegExPattern = + /(?(?:(?<|>(?=\d{1})))|(?:\|))(?\d{1})?/g; const text = clone(content) .replace(/§/g, '\n') - .replace(/\|?[<>]?\|\d?/g, ''); + // .replace(/\|?[<>]?\|\d?/g, ''); + .replace(cursorSymbolRegExPattern, ''); /** * 3 capt groups: @@ -26,11 +29,7 @@ function textNotationToTextAndSelection(content: string): [string, model.ModelEd * the > or <, * 3 = only if there's a number, the number itself (eg multi cursor) */ - const matches = Array.from( - content.matchAll( - /(?(?:(?<|>(?=\d{1})))|(?:\|))(?\d{1})?/g - ) - ); + const matches = Array.from(content.matchAll(cursorSymbolRegExPattern)); // a map of cursor symbols (eg '>3' - including the cursor number if >1 ) to an an array of matches (for their positions mostly) in content string where that cursor is // for now, we hope that there are at most two positions per symbol From 7f133a27af75d934d2bbbddc2d81f0af989852dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sun, 10 Apr 2022 09:56:20 +0200 Subject: [PATCH 36/49] Add a selection smorgasboard test --- .../unit/common/text-notation-test.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/extension-test/unit/common/text-notation-test.ts b/src/extension-test/unit/common/text-notation-test.ts index 36ca83a28..1e5d4dcce 100644 --- a/src/extension-test/unit/common/text-notation-test.ts +++ b/src/extension-test/unit/common/text-notation-test.ts @@ -53,7 +53,7 @@ describe('text-notation test utils', () => { const doc = textNotation.docFromTextNotation(tn); expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); }); - it('creates multiple directed r->l range/selection', () => { + it('creates a variety directed r->l range/selection', () => { const tn = '(<1a<1 { const doc = textNotation.docFromTextNotation(tn); expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); }); + it('creates a variety of multi-cursor positions and selections', () => { + const tn = '|3a|3 <1b<1 >2c>2 d§>0e>0 f |4g<5<5'; + const text = 'a b c d§e f g'.replace(/§/g, '\n'); + const selections = [ + [8, 9], + [3, 2], + [4, 5], + [0, 1], + [12, 12], + [13, 13], + ]; + const doc = textNotation.docFromTextNotation(tn); + expect(textNotation.textAndSelection(doc)).toEqual([text, selections]); + }); }); }); From ac76eb10bf831e2820a85b606b15853695182739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sun, 10 Apr 2022 15:28:14 +0200 Subject: [PATCH 37/49] Add more tests barf and splice --- .../unit/common/text-notation-test.ts | 2 +- .../unit/cursor-doc/paredit-test.ts | 55 +++++++++++++++++-- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/extension-test/unit/common/text-notation-test.ts b/src/extension-test/unit/common/text-notation-test.ts index 1e5d4dcce..44e81126b 100644 --- a/src/extension-test/unit/common/text-notation-test.ts +++ b/src/extension-test/unit/common/text-notation-test.ts @@ -4,7 +4,7 @@ import * as textNotation from '../common/text-notation'; describe('text-notation test utils', () => { describe('textNotationFromDoc', () => { it('should return the same input text to textNotationFromDoc', () => { - const inputText = '(a b|1) (a b|) (a (b))'; + const inputText = '(a b|1) (a b|) (a <2(b)<2)'; const doc = textNotation.docFromTextNotation(inputText); expect(textNotation.textNotationFromDoc(doc)).toEqual(inputText); }); diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index c3fd457e7..db5a83b64 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -1145,6 +1145,21 @@ describe('paredit', () => { void paredit.forwardBarfSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); + it('Barfs symbols from vectors at multiple cursors positions', () => { + const a = docFromTextNotation('[|a§ b]§[|1a§ b]'); + const b = docFromTextNotation('[|a]§ b§[|1a]§ b'); + void paredit.forwardBarfSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('Barfs symbols from nested vectors at multiple cursors positions, needing post-format', () => { + // TODO: This behaves before formatting happens + // In the ”real” editor formatting happens after the barf + // and formatting is still a single-cursor thing... + const a = docFromTextNotation('([|a§ b])§([|1a§ b])'); + const b = docFromTextNotation('([|a]§ b)§([|1a]§ b)'); + void paredit.forwardBarfSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); }); describe('Barfing backwards', () => { @@ -1477,24 +1492,54 @@ describe('paredit', () => { }); describe('splice sexp', () => { - it('splice empty', () => { + it('does not splice empty', () => { const a = docFromTextNotation('|'); const b = docFromTextNotation('|'); void paredit.spliceSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('splice list', () => { - const a = docFromTextNotation('(a|a b c)'); - const b = docFromTextNotation('a|a b c'); + it('splices empty list', () => { + const a = docFromTextNotation('(|)'); + const b = docFromTextNotation('|'); void paredit.spliceSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('splice list also when forms have meta and readers', () => { + it('splices two list (multi-cursor', () => { + const a = docFromTextNotation('(|a)§(|1b)'); + // TODO: This is quite strange. + // In the VS Code editor the result is: + // '|a§b|1' + // Running this test, the result is: + // '|a§(b|1' + // (Both are wrong) + const b = docFromTextNotation('|a§|1b'); + void paredit.spliceSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('splices multiple list', () => { + const a = docFromTextNotation('(|a)§(|1b b)§(|2c c c)§(|3d d d d)'); + const b = docFromTextNotation('|a§|1b b§|2c c c§|3d d d d'); + // TODO: Same weirdness as with the two lists above + // What I get in VS Code: + // '|a§b |1b§c c |2c§d d d |3d' + // What this test produces: + // '|a§(b|1b)§c c|2c)§(dd d|3d)' + void paredit.spliceSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('splices list also when forms have meta and readers', () => { const a = docFromTextNotation('(^{:d e} #a|a b c)'); const b = docFromTextNotation('^{:d e} #a|a b c'); void paredit.spliceSexp(a); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); + it.skip('splices list with meta and readers', () => { + // TODO: Probably the meta data attached to the list should be removed + const a = docFromTextNotation('^{:d e} #a (|a b c)'); + const b = docFromTextNotation('|a b c'); + void paredit.spliceSexp(a); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); it('splice vector', () => { const a = docFromTextNotation('[a| b c]'); const b = docFromTextNotation('a| b c'); From 5d5c1e410fb505b3331fe81840940fb6d9c51621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sun, 10 Apr 2022 17:47:33 +0200 Subject: [PATCH 38/49] Add some tests for Wrap Sexp --- .../unit/cursor-doc/paredit-test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index db5a83b64..a89cd4e38 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -1576,5 +1576,45 @@ describe('paredit', () => { expect(getText(a)).toEqual('hello'); }); }); + + describe('wrap sexp', () => { + it('wraps symbol', () => { + const a = docFromTextNotation('|a'); + const b = docFromTextNotation('(|a)'); + void paredit.wrapSexpr(a, '(', ')'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps multiple symbols (multi-cursor)', () => { + const a = docFromTextNotation('|a§|1b'); + const b = docFromTextNotation('(|a)§(|1b)'); + void paredit.wrapSexpr(a, '(', ')'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps list', () => { + const a = docFromTextNotation('(a)|'); + const b = docFromTextNotation('[(a)|]'); + void paredit.wrapSexpr(a, '[', ']'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps multiple list (multi-cursor)', () => { + const a = docFromTextNotation('(a)|§(b)|1'); + const b = docFromTextNotation('[(a)|]§[(b)|1]'); + void paredit.wrapSexpr(a, '[', ']'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps selection', () => { + // TODO: See if we can maintain the selection here + const a = docFromTextNotation('|a|'); + const b = docFromTextNotation('(|a)'); + void paredit.wrapSexpr(a, '(', ')'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps multiple selections (multi-cursor)', () => { + const a = docFromTextNotation('|a|§|1b|1'); + const b = docFromTextNotation('(|a)§(|1b)'); + void paredit.wrapSexpr(a, '(', ')'); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + }); }); }); From 180c757ccf403d98a69f7c1eaf0ad49e4ec1ba71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sun, 10 Apr 2022 18:43:14 +0200 Subject: [PATCH 39/49] Add some tests for kill-right --- .../unit/cursor-doc/paredit-test.ts | 72 +++++++++++++++++-- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index a89cd4e38..dceff0cca 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -167,6 +167,13 @@ describe('paredit', () => { const actual = paredit.forwardHybridSexpRange(a); expect(actual).toEqual(expected); }); + it('Multi-cursors find end of string', () => { + const a = docFromTextNotation('"a |b c"§"c |1d e"'); + const b = docFromTextNotation('"a |b c|"§"c |1d e|1"'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); it('Finds newline in multi line string', () => { const a = docFromTextNotation('"This |needs to find the end\n of the string."'); @@ -175,6 +182,13 @@ describe('paredit', () => { const actual = paredit.forwardHybridSexpRange(a); expect(actual).toEqual(expected); }); + it('Multi-cursors find newline in multi line string', () => { + const a = docFromTextNotation('"a |b §c"§"c |1d §e"'); + const b = docFromTextNotation('"a |b |cc"§"c |1d |1§e"'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); it('Finds newline in multi line string (Windows)', () => { const a = docFromTextNotation('"This |needs to find the end\r\n of the string."'); @@ -185,8 +199,15 @@ describe('paredit', () => { }); it('Finds end of comment', () => { - const a = docFromTextNotation('(a |;; foo\n e)'); - const b = docFromTextNotation('(a |;; foo|\n e)'); + const a = docFromTextNotation('(a |;; foo§ e)'); + const b = docFromTextNotation('(a |;; foo|§ e)'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + it('Multi-cursors find end of comment', () => { + const a = docFromTextNotation('(a |; b§ c)§(c |1; d§ e)'); + const b = docFromTextNotation('(a |; b|§ c)§(c |1; d|1§ e)'); const expected = textAndSelection(b)[1]; const actual = paredit.forwardHybridSexpRange(a); expect(actual).toEqual(expected); @@ -201,8 +222,15 @@ describe('paredit', () => { }); it('Maintains balanced delimiters 1', () => { - const a = docFromTextNotation('(a| b (c\n d) e)'); - const b = docFromTextNotation('(a| b (c\n d)| e)'); + const a = docFromTextNotation('(a| b (c§ d) e)'); + const b = docFromTextNotation('(a| b (c§ d)| e)'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + it('Multi-cursors maintain balanced delimiters 1', () => { + const a = docFromTextNotation('(a| b (c§ d) e)§(f|1 g (h§ i) j)'); + const b = docFromTextNotation('(a| b (c§ d)| e)§(f|1 g (h§ i)|1 j)'); const expected = textAndSelection(b)[1]; const actual = paredit.forwardHybridSexpRange(a); expect(actual).toEqual(expected); @@ -218,8 +246,18 @@ describe('paredit', () => { }); it('Maintains balanced delimiters 2', () => { - const a = docFromTextNotation('(aa| (c (e\nf)) g)'); - const b = docFromTextNotation('(aa| (c (e\nf))|g)'); + const a = docFromTextNotation('(aa| (c d(e§f)) g)'); + const b = docFromTextNotation('(aa| (c d(e§f))|g)'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + it('Multi-cursors maintain balanced delimiters 2', () => { + const a = docFromTextNotation('(a| (c d (e§f)) g)§(h|1 (i j (k§l)) m)'); + // TODO: This behaves in VS Code, but the test fails + const b = docFromTextNotation('(a| (c d (e§f))|g)§(h|1 (i j (k§l))|1m)'); + // the result matches this (the second cursor is where it differes) + //const b = docFromTextNotation('(a| (c d (e§f))|g)§(h |1(i j (k§l)) |1m)'); const expected = textAndSelection(b)[1]; const actual = paredit.forwardHybridSexpRange(a); expect(actual).toEqual(expected); @@ -427,11 +465,21 @@ describe('paredit', () => { const b = docFromTextNotation('(def foo [:foo :bar |:baz|])'); expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); - it('Leaves an sexp if at the end', () => { + it('Leaves a sexp if at the end', () => { const a = docFromTextNotation('(def foo [:foo :bar :baz|])'); const b = docFromTextNotation('(def foo [:foo :bar :baz|]|)'); expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); + it('Multi-cursors leave sexp if at the end', () => { + const a = docFromTextNotation('(a|)§(b|1)'); + const b = docFromTextNotation('(a|)|§(b|1)|1'); + expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors one goes fwd a sexp, one leaves sexp b/c at the end', () => { + const a = docFromTextNotation('(|a)§(b|1)'); + const b = docFromTextNotation('(|a|)§(b|1)|1'); + expect(paredit.forwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); + }); it('Finds next symbol, including leading space', () => { const a = docFromTextNotation('(|def| foo [vec])'); const b = docFromTextNotation('(def| foo| [vec])'); @@ -481,6 +529,16 @@ describe('paredit', () => { const b = docFromTextNotation('(def x |(|inc 1))'); expect(paredit.backwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); }); + it('Multi-cursors go up when at front bounds', () => { + const a = docFromTextNotation('(|1a (|b 1))'); + const b = docFromTextNotation('|1(|1a |(|b 1))'); + expect(paredit.backwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); + }); + it('Multi-cursors, one go up when at front bounds, the other back sexp', () => { + const a = docFromTextNotation('(|1a (b c|))'); + const b = docFromTextNotation('|1(|1a (b |c|))'); + expect(paredit.backwardSexpOrUpRange(a)).toEqual(textAndSelection(b)[1]); + }); }); describe('moveToRangeRight', () => { From 29da58ec8c128aaf736d5cd595ae1098a51f43f5 Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 14 Apr 2022 15:34:53 -0700 Subject: [PATCH 40/49] Simplify tasks.json --- .vscode/tasks.json | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4e4b7a583..d95c219ee 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -56,25 +56,6 @@ "kind": "test", "isDefault": false }, - "problemMatcher": { - // "owner": "mocha", - "fileLocation": ["relative", "${workspaceRoot}"], - "pattern": [ - { - "regexp": "^not\\sok\\s\\d+\\s(.*)$" - }, - { - "regexp": "\\s+(.*)$", - "message": 1 - }, - { - "regexp": "\\s+at\\s(.*)\\s\\((.*):(\\d+):(\\d+)\\)", - "file": 2, - "line": 3, - "column": 4 - } - ] - }, "presentation": { "panel": "dedicated", "group": "defaultCalva" @@ -89,7 +70,7 @@ "kind": "build", "isDefault": false }, - "problemMatcher": "$eslint-compact", + "problemMatcher": "$eslint-stylish", "presentation": { "panel": "dedicated", "group": "defaultCalva" From 98750bd092b1811efe185309d83673310fd3855b Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 14 Apr 2022 15:36:58 -0700 Subject: [PATCH 41/49] Add some paredit related utils --- src/cursor-doc/cursor-doc-utils.ts | 49 ++++++++++++------- src/cursor-doc/model.ts | 22 +++++++++ src/doc-mirror/index.ts | 4 ++ .../unit/common/text-notation.ts | 6 ++- 4 files changed, 61 insertions(+), 20 deletions(-) diff --git a/src/cursor-doc/cursor-doc-utils.ts b/src/cursor-doc/cursor-doc-utils.ts index 066450b14..bfb8ec68d 100644 --- a/src/cursor-doc/cursor-doc-utils.ts +++ b/src/cursor-doc/cursor-doc-utils.ts @@ -1,20 +1,33 @@ -import { EditableDocument, ModelEditSelection } from './model'; +import { first, last } from 'lodash'; +import { 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]; +export type SimpleRange = [start: number, end: number]; +export type SimpleDirectedRange = [anchor: number, active: number]; + +export const rangeOrSelProp = + (property: PROP_NAME) => + (o: T): number => { + if (o instanceof ModelEditSelection) { + return o[property]; + } else if (Array.isArray(o)) { + let fn; + switch (property) { + case 'start': + fn = Math.min; + break; + case 'end': + fn = Math.max; + break; + case 'anchor': + fn = (...x) => first(x); + break; + case 'active': + fn = (...x) => last(x); + break; + default: + first; + } + + return fn(...o); } - } -} + }; diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index 0cc684180..dc80aaf2c 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -190,6 +190,23 @@ export class ModelEditSelection { clone() { return new ModelEditSelection(this._anchor, this._active); } + + /** + * Returns a simple 2-item tuple representing the + * [leftmost/earliest/start position, rightmost, farthest, end position]. + */ + get asRange() { + return [this.start, this.end] as [start: number, end: number]; + } + + /** + * Same as `ModelEditSelection.asRange` but with the leftmost item being the anchor position, and the rightmost item + * being the active position. This way, you can tell if it's reversed by checking if the leftmost item is greater + * than the rightmost item. + */ + get asDirectedRange() { + return [this.anchor, this.active] as [anchor: number, active: number]; + } } export type ModelEditOptions = { @@ -206,6 +223,7 @@ export type ModelEditResult = { }; export interface EditableModel { readonly lineEndingLength: number; + readonly lineEnding: string; /** * Performs a model edit batch. @@ -252,6 +270,10 @@ export class LineInputModel implements EditableModel { /** How many characters in the line endings of the text of this model? */ constructor(readonly lineEndingLength: number = 1, private document?: EditableDocument) {} + get lineEnding() { + return this.lineEndingLength === 1 ? '\n' : '\r\n'; + } + /** The input lines. */ lines: TextLine[] = [new TextLine('', this.getStateForLine(0))]; diff --git a/src/doc-mirror/index.ts b/src/doc-mirror/index.ts index d15cd464b..d2838361a 100644 --- a/src/doc-mirror/index.ts +++ b/src/doc-mirror/index.ts @@ -25,6 +25,10 @@ export class DocumentModel implements EditableModel { this.lineInputModel = new LineInputModel(this.lineEndingLength); } + get lineEnding() { + return this.lineEndingLength == 2 ? '\r\n' : '\n'; + } + edit(modelEdits: ModelEdit[], options: ModelEditOptions): Thenable { const editor = utilities.getActiveTextEditor(), undoStopBefore = !!options.undoStopBefore; diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index 0a9294101..09fd21d96 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -133,8 +133,10 @@ export function textNotationFromDoc(doc: model.EditableDocument): string { * @param doc * @returns string */ -export function getText(doc: model.EditableDocument): string { - return doc.model.getText(0, Infinity); +export function getText(doc: model.EditableDocument, replaceNewLine = false): string { + const text = doc.model.getText(0, Infinity); + + return replaceNewLine ? text.split(doc.model.lineEnding).join('§') : text; } /** From 394c9fe56de548b66c98bc98e8e8df562774f21e Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 14 Apr 2022 15:38:31 -0700 Subject: [PATCH 42/49] Start experimental multi cursor copy-paste paredit --- src/paredit/extension.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 513d5edd4..bcaabfa3f 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -17,6 +17,7 @@ import { EditableDocument, ModelEditResult } from '../cursor-doc/model'; import { assertIsDefined } from '../utilities'; import * as textNotation from '../extension-test/unit/common/text-notation'; import * as calvaState from '../state'; +import _ = require('lodash'); const onPareditKeyMapChangedEmitter = new EventEmitter(); const languages = new Set(['clojure', 'lisp', 'scheme']); @@ -27,14 +28,19 @@ const enabled = true; * @param doc * @param range */ -export function copyRangeToClipboard(doc: EditableDocument, ranges: Array<[number, number]>) { - // FIXME: This is tricky. Somehow, vsc natively support cut/copy & pasting for multiple selections. - // But, how it does so is not known to me at this time. - // So, I am using the native copy command for now with only the first range (presumably the primary selection). - const range = ranges[0]; - const [start, end] = range; - const text = doc.model.getText(start, end); - void env.clipboard.writeText(text); +export function copyRangeToClipboard( + doc: EditableDocument, + ranges: Array<[number, number]> = _(doc.selections) + .map((s) => s.asRange) + .value() +) { + const texts: string[] = _(ranges) + // Why do we reorder the ranges? Because vscode does consider the cursor order when pasting, and considers the cursors sorted in the order they appear in the document (ascending) + .orderBy((r) => Math.min(...r), 'asc') + .map(([start, end]) => doc.model.getText(start, end)) + .value(); + // TODO: Evaluate if we need to use a different line ending per os for the clipboard multiline copy/paste + void env.clipboard.writeText(texts.join('\n')); } /** From cd90fa9659bd361408c2597ee01f3c42f408cc66 Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 14 Apr 2022 15:39:13 -0700 Subject: [PATCH 43/49] Some cleanup, spelling --- .vscode/settings.json | 2 ++ src/nrepl/index.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6183d1eca..68aa3b9d8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "arglist", "arglists", "autoindent", + "avli", "Batsov", "behaviour", "bencode", @@ -31,6 +32,7 @@ "circleci", "clangd", "classpath", + "clientrc", "cljc", "cljd", "cljfmt", diff --git a/src/nrepl/index.ts b/src/nrepl/index.ts index 9054d53f8..d4b12c844 100644 --- a/src/nrepl/index.ts +++ b/src/nrepl/index.ts @@ -18,7 +18,7 @@ function hasStatus(res: any, status: string): boolean { return res.status && res.status.indexOf(status) > -1; } -// When a command fails becuase of an unknown-op (usually caused by missing +// When a command fails because of an unknown-op (usually caused by missing // middleware), we can mark the operation as failed, so that we can show a message // in the UI to the user. // https://nrepl.org/nrepl/design/handlers.html From 345e8cd5fef3149d71cb1d3f568f60e2093cac2a Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 14 Apr 2022 15:39:54 -0700 Subject: [PATCH 44/49] Decouple textNotationFromDoc into textNotationFromTextAndSelection --- .../unit/common/text-notation.ts | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index 09fd21d96..027af9dfa 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -85,21 +85,34 @@ export function docFromTextNotation(s: string): model.StringDocument { export function textNotationFromDoc(doc: model.EditableDocument): string { const selections = doc.selections ?? []; + const ranges = selections.map((s) => s.asDirectedRange); + + const text = getText(doc, true); + + return textNotationFromTextAndSelections(text, ranges); +} + +export function textNotationFromTextAndSelections( + text: string, + ranges: Array<[number, number]> +): string { let cursorSymbols: [number, string][] = []; - selections.forEach((s, cursorNumber) => { - const cursorType = s.isReversed ? '<' : '|'; - cursorSymbols.push([s.start, `${cursorType}${cursorNumber || ''}`]); - if (s.isSelection) { - cursorSymbols.push([s.end, `${cursorType}${cursorNumber || ''}`]); + ranges.forEach((r, cursorNumber) => { + const [anchor, active] = r; + const isReversed = anchor > active; + const isSelection = anchor - active !== 0; + const start = Math.min(anchor, active); + const end = Math.max(anchor, active); + + const cursorType = isReversed ? '<' : '|'; + cursorSymbols.push([start, `${cursorType}${cursorNumber || ''}`]); + if (isSelection) { + cursorSymbols.push([end, `${cursorType}${cursorNumber || ''}`]); } }); cursorSymbols = orderBy(cursorSymbols, (c) => c[0]); - const text = getText(doc) - .split(doc.model.lineEndingLength === 1 ? '\n' : '\r\n') - .join('§'); - // basically split up the text into chunks separated by where they'd have had cursor symbols, and append cursor symbols after each chunk, before joining back together // this way we can insert the cursor symbols in the right place without having to worry about the cumulative offsets created by appending the cursor symbols const textSegments = cursorSymbols From d33beddab16f5261bd33da56187c7ffc4a682d44 Mon Sep 17 00:00:00 2001 From: Rayat Date: Thu, 14 Apr 2022 15:40:37 -0700 Subject: [PATCH 45/49] Make 'Multi-cursors maintain balanced delimiters" pass --- src/extension-test/unit/cursor-doc/paredit-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index dceff0cca..5d02f6514 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -255,8 +255,8 @@ describe('paredit', () => { it('Multi-cursors maintain balanced delimiters 2', () => { const a = docFromTextNotation('(a| (c d (e§f)) g)§(h|1 (i j (k§l)) m)'); // TODO: This behaves in VS Code, but the test fails - const b = docFromTextNotation('(a| (c d (e§f))|g)§(h|1 (i j (k§l))|1m)'); - // the result matches this (the second cursor is where it differes) + const b = docFromTextNotation('(a| (c d (e§f))| g)§(h|1 (i j (k§l))|1 m)'); + // the result matches this (the second cursor is where it differs) //const b = docFromTextNotation('(a| (c d (e§f))|g)§(h |1(i j (k§l)) |1m)'); const expected = textAndSelection(b)[1]; const actual = paredit.forwardHybridSexpRange(a); From ead4af0a33ecd98682e7b99bd38cb684faefcf31 Mon Sep 17 00:00:00 2001 From: Rayat Date: Fri, 15 Apr 2022 02:03:18 -0700 Subject: [PATCH 46/49] Latest multi cursor tests pass --- src/cursor-doc/cursor-doc-utils.ts | 86 ++++++++--- src/cursor-doc/model.ts | 16 ++ src/cursor-doc/paredit.ts | 144 ++++++++++-------- .../unit/common/text-notation.ts | 25 ++- .../unit/cursor-doc/paredit-test.ts | 18 ++- src/util/array.ts | 14 ++ 6 files changed, 205 insertions(+), 98 deletions(-) diff --git a/src/cursor-doc/cursor-doc-utils.ts b/src/cursor-doc/cursor-doc-utils.ts index bfb8ec68d..b5af5fc63 100644 --- a/src/cursor-doc/cursor-doc-utils.ts +++ b/src/cursor-doc/cursor-doc-utils.ts @@ -1,33 +1,69 @@ -import { first, last } from 'lodash'; +import { first, isArray, isNumber, last, ListIteratee, ListIterator } from 'lodash'; +import _ = require('lodash'); import { ModelEditSelection } from './model'; export type SimpleRange = [start: number, end: number]; export type SimpleDirectedRange = [anchor: number, active: number]; -export const rangeOrSelProp = - (property: PROP_NAME) => - (o: T): number => { - if (o instanceof ModelEditSelection) { - return o[property]; - } else if (Array.isArray(o)) { - let fn; - switch (property) { - case 'start': - fn = Math.min; - break; - case 'end': - fn = Math.max; - break; - case 'anchor': - fn = (...x) => first(x); - break; - case 'active': - fn = (...x) => last(x); - break; - default: - first; +export const mapRangeOrSelectionToOffset = + /* */ + + + (whichOffset: 'start' | 'end' | 'anchor' | 'active' = 'start') => + (t: T | [T, number]): number => { + const rangeOrSel = isModelEditSelection(t) || isSimpleRange(t) ? t : first(t); + + if (rangeOrSel instanceof ModelEditSelection) { + return rangeOrSel[whichOffset]; + } else if (Array.isArray(rangeOrSel)) { + let fn; + switch (whichOffset) { + case 'start': + fn = Math.min; + break; + case 'end': + fn = Math.max; + break; + case 'anchor': + fn = (...x) => first(x); + break; + case 'active': + fn = (...x) => last(x); + break; + default: + first; + } + + return fn(...rangeOrSel); } + }; + +export function repositionRangeOrSelectionByCumulativeOffsets( + offsetGetter: ListIterator | number +) { + return ( + s: ModelEditSelection, + index: number, + array: ModelEditSelection[] + ): ModelEditSelection => { + const newSel = s.clone(); - return fn(...o); - } + const getItemOffset = isNumber(offsetGetter) ? () => offsetGetter : offsetGetter; + + const offset = _(array) + .filter((x) => x.start < s.start) + .map(getItemOffset) + .sum(); + + newSel.reposition(offset); + return newSel; }; +} + +export function isSimpleRange(o: any): o is SimpleRange { + return isArray(o) && o.length === 2 && isNumber(o[0]) && isNumber(o[1]); +} + +export function isModelEditSelection(o: any): o is ModelEditSelection { + return o instanceof ModelEditSelection; +} diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index dc80aaf2c..dbcadfb8a 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -207,6 +207,22 @@ export class ModelEditSelection { get asDirectedRange() { return [this.anchor, this.active] as [anchor: number, active: number]; } + + /** + * Mutates itself! + * Very basic, offsets both active/anchor by a positive or negative number lol, with no attempt at clamping. + * + * Returns self for convenience + * @param offset number + */ + reposition(offset: number) { + this.active += offset; + this.anchor += offset; + + this._updateDirection(); + + return this; + } } export type ModelEditOptions = { diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 426e6b23d..39c18b2b8 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -1,4 +1,4 @@ -import { isEqual, last, pick, property, clone, isBoolean, orderBy } from 'lodash'; +import { isEqual, last, pick, property, clone, isBoolean, orderBy, isNil, flatten } from 'lodash'; import _ = require('lodash'); import { validPair } from './clojure-lexer'; import { @@ -9,7 +9,11 @@ import { ModelEditFunctionArgs, } from './model'; import { LispTokenCursor } from './token-cursor'; -import { replaceAt } from '../util/array'; +import { mapToItemAndOrder, mapToOriginalItem, mapToOriginalOrder, replaceAt } from '../util/array'; +import { + mapRangeOrSelectionToOffset, + repositionRangeOrSelectionByCumulativeOffsets, +} from './cursor-doc-utils'; // NB: doc.model.edit returns a Thenable, so that the vscode Editor can compose commands. // But don't put such chains in this module because that won't work in the repl-console. @@ -553,67 +557,68 @@ export function rangeToBackwardList( } } -// TODO: test export function wrapSexpr( doc: EditableDocument, open: string, close: string, - // _start: number, // = doc.selections.anchor, - // _end: number, // = doc.selections.active, options = { skipFormat: false } -): 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( - [ +): Thenable { + // TODO: (The following applies to all multi cursor work that involved a ModelEditResult and where not every cursor has a valid operation) Restore all cursors to their original positions after batch edit, except those explicitly operated upon. + + const edits = [], + // selections = clone(doc.selections).map(mapToItemAndOrder); + selections = clone(doc.selections); + + _(doc.selections) + // iterate backwards to simplify dealing with cumulative cursor position offsets as document's text is added to with + // parens or whatever is being wrapped with. + .map(mapToItemAndOrder) + .orderBy(([r]) => mapRangeOrSelectionToOffset('start')(r), 'desc') + .forEach(([sel, index]) => { + const { start, end } = sel; + const cursor = doc.getTokenCursor(end); + if (cursor.withinString() && open == '"') { + open = close = '\\"'; + } + if (start == end) { + // No selection + const currentFormRange = cursor.rangeForCurrentForm(start); + if (currentFormRange) { + const range = currentFormRange; + edits.push( new ModelEdit('insertString', [range[1], close]), new ModelEdit('insertString', [ range[0], open, // [end, end], // [start + open.length, start + open.length], - ]), - ], - { - selections: [new ModelEditSelection(start + open.length)], - skipFormat: options.skipFormat, - } + ]) + ); + // don't forget to include the index with the selection for later reordering to original order; + // selections[index] = [new ModelEditSelection(start + open.length), index]; + selections[index] = new ModelEditSelection(start + open.length); + } + } else { + // there is a selection + const range = [Math.min(start, end), Math.max(start, end)]; + edits.push( + new ModelEdit('insertString', [range[1], close]), + new ModelEdit('insertString', [range[0], open]) ); + // don't forget to include the index with the selection for later reordering to original order; + // selections[index] = [new ModelEditSelection(start + open.length), index]; + selections[index] = new ModelEditSelection(start + open.length); } - } 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]), - ], - { - selections: [new ModelEditSelection(start + open.length)], - skipFormat: options.skipFormat, - } - ); - } + return undefined; + }); + return doc.model.edit(edits, { + selections: selections.map(repositionRangeOrSelectionByCumulativeOffsets(2)), + skipFormat: options.skipFormat, }); } // TODO: test -export function rewrapSexpr( - doc: EditableDocument, - open: string, - close: string - // _start: number, // = doc.selections.anchor, - // _end: number // = doc.selections.active -) { +export function rewrapSexpr(doc: EditableDocument, open: string, close: string) { doc.selections.forEach((sel) => { const { start, end } = sel; const cursor = doc.getTokenCursor(end); @@ -705,28 +710,35 @@ export function spliceSexp( ): 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, '']) - ); - selections[index] = new ModelEditSelection(start - 1); + + _(doc.selections) + .map(mapToItemAndOrder) + .orderBy(mapRangeOrSelectionToOffset(), 'desc') + .forEach(([selection, originalOrder]) => { + const { start, end } = selection; + const cursor = doc.getTokenCursor(start); + // TODO: this should unwrap the string, not the enclosing list. + cursor.backwardList(); + const open = cursor.getPrevToken(); + const beginning = cursor.offsetStart; + if (open.type == 'open') { + cursor.forwardList(); + const close = cursor.getToken(); + const end = cursor.offsetStart; + if (close.type == 'close' && validPair(open.raw, close.raw)) { + edits.push( + new ModelEdit('changeRange', [end, end + close.raw.length, '']), + new ModelEdit('changeRange', [beginning - open.raw.length, beginning, '']) + ); + selections[originalOrder] = new ModelEditSelection(start - 1); + } } - } - }); + }); - return doc.model.edit(edits, { undoStopBefore, selections }); + return doc.model.edit(edits, { + undoStopBefore, + selections: _(selections).map(repositionRangeOrSelectionByCumulativeOffsets(-2)).value(), + }); } export function killSexpBackward( diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index 027af9dfa..e960c5a0b 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -1,5 +1,15 @@ import * as model from '../../../cursor-doc/model'; -import { clone, entries, cond, toInteger, last, first, cloneDeep, orderBy } from 'lodash'; +import { + clone, + entries, + cond, + toInteger, + last, + first, + cloneDeep, + orderBy, + partialRight, +} from 'lodash'; /** * Text Notation for expressing states of a document, including @@ -83,18 +93,19 @@ export function docFromTextNotation(s: string): model.StringDocument { return doc; } -export function textNotationFromDoc(doc: model.EditableDocument): string { +export function textNotationFromDoc(doc: model.EditableDocument, prettyPrint = false): string { const selections = doc.selections ?? []; const ranges = selections.map((s) => s.asDirectedRange); const text = getText(doc, true); - return textNotationFromTextAndSelections(text, ranges); + return textNotationFromTextAndSelections(text, ranges, prettyPrint); } export function textNotationFromTextAndSelections( text: string, - ranges: Array<[number, number]> + ranges: Array<[number, number]>, + prettyPrint = false ): string { let cursorSymbols: [number, string][] = []; ranges.forEach((r, cursorNumber) => { @@ -138,9 +149,13 @@ export function textNotationFromTextAndSelections( ) .map((s) => s[1]); - return textSegments.join(''); + const textNotation = textSegments.join(''); + return prettyPrint ? textNotation.replace(/§/g, '\n') : textNotation; } +textNotationFromDoc.pretty = partialRight(textNotationFromDoc, true); +textNotationFromTextAndSelections.pretty = partialRight(textNotationFromTextAndSelections, true); + /** * Utility function to get the text from a document. * @param doc diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index 5d02f6514..858776187 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -6,6 +6,7 @@ import { textAndSelection, getText, textNotationFromDoc, + textNotationFromTextAndSelections, } from '../common/text-notation'; import { ModelEditSelection } from '../../../cursor-doc/model'; import { last, method } from 'lodash'; @@ -1572,6 +1573,7 @@ describe('paredit', () => { // (Both are wrong) const b = docFromTextNotation('|a§|1b'); void paredit.spliceSexp(a); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('splices multiple list', () => { @@ -1583,6 +1585,7 @@ describe('paredit', () => { // What this test produces: // '|a§(b|1b)§c c|2c)§(dd d|3d)' void paredit.spliceSexp(a); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('splices list also when forms have meta and readers', () => { @@ -1646,6 +1649,7 @@ describe('paredit', () => { const a = docFromTextNotation('|a§|1b'); const b = docFromTextNotation('(|a)§(|1b)'); void paredit.wrapSexpr(a, '(', ')'); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('wraps list', () => { @@ -1658,6 +1662,7 @@ describe('paredit', () => { const a = docFromTextNotation('(a)|§(b)|1'); const b = docFromTextNotation('[(a)|]§[(b)|1]'); void paredit.wrapSexpr(a, '[', ']'); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); it('wraps selection', () => { @@ -1667,10 +1672,19 @@ describe('paredit', () => { void paredit.wrapSexpr(a, '(', ')'); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); - it('wraps multiple selections (multi-cursor)', () => { + it('wraps selection even at newlines', () => { + // TODO: See if we can maintain the selection here + const a = docFromTextNotation('a§|b|'); + const b = docFromTextNotation('a§(|b)'); + void paredit.wrapSexpr(a, '(', ')'); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(textAndSelection(a)).toEqual(textAndSelection(b)); + }); + it('wraps multiple selections (multi-cursor)', async () => { const a = docFromTextNotation('|a|§|1b|1'); const b = docFromTextNotation('(|a)§(|1b)'); - void paredit.wrapSexpr(a, '(', ')'); + await paredit.wrapSexpr(a, '(', ')'); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); expect(textAndSelection(a)).toEqual(textAndSelection(b)); }); }); diff --git a/src/util/array.ts b/src/util/array.ts index 0480a8cbd..72a8b9c79 100644 --- a/src/util/array.ts +++ b/src/util/array.ts @@ -10,3 +10,17 @@ export const replaceAt = (array: A[], index: number, replacement: A): A[] => _.mixin({ replaceAt, }); + +/** + * Perhaps silly utility. + * Designed to help out with when you must sort an array and then resort it back to its original order. + * + * First you `.map(mapToItemAndOrder)` the array, then you sort or order it however you want, after which you can simply resort it by + * + * , specifically where `map(mapToItemAndOrder) is followed up with with a sort operation on the result, using the `idx` (2nd return array item) + */ +export const mapToItemAndOrder = (o: A, idx: number) => [o, idx] as [A, number]; + +export const mapToOriginalOrder = ([_o, idx]: [A, number]) => idx; + +export const mapToOriginalItem = ([o]: [A, number]) => o; From a095c458fa767abbab11d34b3ae174ddc78b5948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Mon, 18 Apr 2022 12:09:12 +0200 Subject: [PATCH 47/49] Add test for one-line text-notation w trailing cursor --- src/extension-test/unit/common/text-notation-test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/extension-test/unit/common/text-notation-test.ts b/src/extension-test/unit/common/text-notation-test.ts index 44e81126b..e74637f6e 100644 --- a/src/extension-test/unit/common/text-notation-test.ts +++ b/src/extension-test/unit/common/text-notation-test.ts @@ -3,11 +3,16 @@ import * as textNotation from '../common/text-notation'; describe('text-notation test utils', () => { describe('textNotationFromDoc', () => { - it('should return the same input text to textNotationFromDoc', () => { + it('returns the same input text to textNotationFromDoc', () => { const inputText = '(a b|1) (a b|) (a <2(b)<2)'; const doc = textNotation.docFromTextNotation(inputText); expect(textNotation.textNotationFromDoc(doc)).toEqual(inputText); }); + it('has cursor at end when no trailing newline', () => { + const inputText = '()|'; + const doc = textNotation.docFromTextNotation(inputText); + expect(textNotation.textNotationFromDoc(doc)).toEqual(inputText); + }); }); describe('docFromTextNotation', () => { it('creates single cursor position', () => { From 86dff85ddec8cf49800f3ffebbd57ae566ca5619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Mon, 18 Apr 2022 12:09:29 +0200 Subject: [PATCH 48/49] Remove funny precompile command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d4110cb4..01e99c160 100644 --- a/package.json +++ b/package.json @@ -2722,7 +2722,7 @@ "watch-docs": "mkdocs serve", "clean": "rimraf ./out && rimraf ./tsconfig.tsbuildinfo && rimraf ./cljs-out", "update-grammar": "node ./src/calva-fmt/update-grammar.js ./src/calva-fmt/atom-language-clojure/grammars/clojure.cson clojure.tmLanguage.json", - "precompile": "npm i && npm run clean && npm run update-grammar && npm run prettier-format", + "precompile": "npm i && npm run clean && npm run update-grammar", "compile-cljs": "npx shadow-cljs compile :calva-lib :test", "compile-ts": "npx tsc --project ./tsconfig.json", "compile": "npm run compile-cljs && npm run compile-ts", From 5975c486046854e8f997272211029523ffbe15d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Mon, 18 Apr 2022 12:15:25 +0200 Subject: [PATCH 49/49] Use dev tasks.json --- .vscode/tasks.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d95c219ee..4e4b7a583 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -56,6 +56,25 @@ "kind": "test", "isDefault": false }, + "problemMatcher": { + // "owner": "mocha", + "fileLocation": ["relative", "${workspaceRoot}"], + "pattern": [ + { + "regexp": "^not\\sok\\s\\d+\\s(.*)$" + }, + { + "regexp": "\\s+(.*)$", + "message": 1 + }, + { + "regexp": "\\s+at\\s(.*)\\s\\((.*):(\\d+):(\\d+)\\)", + "file": 2, + "line": 3, + "column": 4 + } + ] + }, "presentation": { "panel": "dedicated", "group": "defaultCalva" @@ -70,7 +89,7 @@ "kind": "build", "isDefault": false }, - "problemMatcher": "$eslint-stylish", + "problemMatcher": "$eslint-compact", "presentation": { "panel": "dedicated", "group": "defaultCalva"