From c1039f369b38541ddd8e79c6fd1e15b42c230441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Mon, 12 Jul 2021 05:27:38 +0200 Subject: [PATCH 1/8] WIP add parallell defun range method --- src/cursor-doc/token-cursor.ts | 14 ++++++ .../unit/cursor-doc/token-cursor-test.ts | 50 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/cursor-doc/token-cursor.ts b/src/cursor-doc/token-cursor.ts index d9d4d7f2f..ded90378a 100644 --- a/src/cursor-doc/token-cursor.ts +++ b/src/cursor-doc/token-cursor.ts @@ -613,6 +613,7 @@ export class LispTokenCursor extends TokenCursor { */ rangeForDefun(offset: number, depth = 0, commentCreatesTopLevel = true): [number, number] { const cursor = this.clone(); + while (cursor.forwardSexp()) { if (cursor.offsetEnd >= offset) { if (depth < 1 && cursor.getPrevToken().raw === ')') { @@ -633,6 +634,19 @@ export class LispTokenCursor extends TokenCursor { return [offset, offset] } + rangeForDefun2(offset: number, commentCreatesTopLevel = true): [number, number] { + const cursor = this.clone(); + let lastCandidateRange: [number, number] = cursor.rangeForCurrentForm(offset); + while (cursor.forwardList() && cursor.upList()) { + const commentCursor = cursor.clone(); + commentCursor.backwardDownList(); + if (!commentCreatesTopLevel || commentCursor.getToken().raw !== ')' || commentCursor.getFunctionName() !== 'comment') { + lastCandidateRange = cursor.rangeForCurrentForm(cursor.offsetStart); + } + } + return lastCandidateRange; + } + rangesForTopLevelForms(): [number, number][] { const cursor = new LispTokenCursor(this.doc, 0, 0); let ranges: [number, number][] = []; 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 11e624e19..539253511 100644 --- a/src/extension-test/unit/cursor-doc/token-cursor-test.ts +++ b/src/extension-test/unit/cursor-doc/token-cursor-test.ts @@ -358,6 +358,12 @@ describe('Token Cursor', () => { const cursor: LispTokenCursor = a.getTokenCursor(0); expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); }); + 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)'); + 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(0); + expect(cursor.rangeForDefun(a.selectionLeft, 0, false)).toEqual(textAndSelection(b)[1]); + }); it('Finds comment range when comments are nested', () => { // TODO: Consider changing this behavior const a = docFromTextNotation('aaa (comment (comment [bbb ccc] | ddd))'); const b = docFromTextNotation('aaa (comment |(comment [bbb ccc] ddd)|)'); @@ -384,6 +390,50 @@ describe('Token Cursor', () => { }); }); + describe('Top Level Form 2', () => { + it('Finds range for a regular top level form', () => { + 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)'); + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); + 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)'); + 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.rangeForDefun2(a.selection.active, false)).toEqual(textAndSelection(b)[1]); + }); + it('Finds comment range when comments are nested', () => { // TODO: Consider changing this behavior + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); + }); describe('Location State', () => { it('Knows when inside string', () => { const doc = docFromTextNotation('(str [] "", "foo" "f b b" " f b b " "\\"" \\")'); From b6a720e72579797a9d1217f968a36fc4af8cc61d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Mon, 12 Jul 2021 06:15:23 +0200 Subject: [PATCH 2/8] Increase defun test coverage --- .../unit/cursor-doc/token-cursor-test.ts | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) 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 539253511..ad7595479 100644 --- a/src/extension-test/unit/cursor-doc/token-cursor-test.ts +++ b/src/extension-test/unit/cursor-doc/token-cursor-test.ts @@ -346,12 +346,24 @@ describe('Token Cursor', () => { }); describe('Top Level Form', () => { - it('Finds range for a regular 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)'); 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(0); expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); }); + it('Finds range when in current form is top level', () => { // Bug, documenting how it behaves + 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)'); + 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(0); + expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); + }); + 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)'); + 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(0); + expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); + }); it('Finds 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)'); 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)'); @@ -364,19 +376,31 @@ describe('Token Cursor', () => { const cursor: LispTokenCursor = a.getTokenCursor(0); expect(cursor.rangeForDefun(a.selectionLeft, 0, false)).toEqual(textAndSelection(b)[1]); }); + it('Finds empty 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(0); + expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); + }); it('Finds comment range when comments are nested', () => { // TODO: Consider changing this behavior 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.selectionLeft)).toEqual(textAndSelection(b)[1]); }); + 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)| fff'); + 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)| fff'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); + expect(cursor.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); 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(0); expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); }); - it('Finds the preceding range when cursor is between to forms on the same line', () => { + 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); @@ -388,15 +412,26 @@ describe('Token Cursor', () => { const cursor: LispTokenCursor = a.getTokenCursor(0); expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); }); - }); describe('Top Level Form 2', () => { - it('Finds range for a regular 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)'); 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); }); + 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)'); + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); + 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)'); + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); 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))'); @@ -409,12 +444,24 @@ describe('Token Cursor', () => { const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); expect(cursor.rangeForDefun2(a.selection.active, false)).toEqual(textAndSelection(b)[1]); }); - it('Finds comment range when comments are nested', () => { // TODO: Consider changing this behavior + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); }); + 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)'); + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); it('Includes reader tag', () => { const a = docFromTextNotation('aaa (comment #r [bbb ccc|] ddd)'); const b = docFromTextNotation('aaa (comment |#r [bbb ccc]| ddd)'); From 3afa79fff4d8a59ddc463471fdb65d620dd5d618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Thu, 15 Jul 2021 13:33:30 +0200 Subject: [PATCH 3/8] Install rangeForDefun2 --- src/cursor-doc/paredit.ts | 6 +++--- src/cursor-doc/token-cursor.ts | 2 +- src/debugger/util.ts | 2 +- .../unit/cursor-doc/token-cursor-test.ts | 9 ++++++++- src/highlight/src/extension.ts | 6 +++--- src/results-output/results-doc.ts | 4 ++-- src/select.ts | 4 ++-- src/util/cursor-get-text.ts | 16 ++++++++-------- 8 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index 77c99ca42..a52212d36 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -90,9 +90,9 @@ export function selectOpenList(doc: EditableDocument) { * Gets the range for the ”current” top level form * @see ListTokenCursor.rangeForDefun */ -export function rangeForDefun(doc: EditableDocument, offset: number = doc.selectionLeft, start: number = 0): [number, number] { - const cursor = doc.getTokenCursor(start); - return cursor.rangeForDefun(offset); +export function rangeForDefun(doc: EditableDocument, offset: number = doc.selection.active): [number, number] { + const cursor = doc.getTokenCursor(offset); + return cursor.rangeForDefun2(offset); } export function forwardSexpRange(doc: EditableDocument, offset = Math.max(doc.selection.anchor, doc.selection.active), goPastWhitespace = false): [number, number] { diff --git a/src/cursor-doc/token-cursor.ts b/src/cursor-doc/token-cursor.ts index ded90378a..ae21ba100 100644 --- a/src/cursor-doc/token-cursor.ts +++ b/src/cursor-doc/token-cursor.ts @@ -635,7 +635,7 @@ export class LispTokenCursor extends TokenCursor { } rangeForDefun2(offset: number, commentCreatesTopLevel = true): [number, number] { - const cursor = this.clone(); + const cursor = this.doc.getTokenCursor(offset); let lastCandidateRange: [number, number] = cursor.rangeForCurrentForm(offset); while (cursor.forwardList() && cursor.upList()) { const commentCursor = cursor.clone(); diff --git a/src/debugger/util.ts b/src/debugger/util.ts index 3a63915e3..35e72400f 100644 --- a/src/debugger/util.ts +++ b/src/debugger/util.ts @@ -21,7 +21,7 @@ function moveCursorPastStringInList(tokenCursor: LispTokenCursor, s: string): vo function moveTokenCursorToBreakpoint(tokenCursor: LispTokenCursor, debugResponse: any): LispTokenCursor { const errorMessage = "Error finding position of breakpoint"; - const [_, defunEnd] = tokenCursor.rangeForDefun(tokenCursor.offsetStart); + const [_, defunEnd] = tokenCursor.rangeForDefun2(tokenCursor.offsetStart); let inSyntaxQuote = false; const coor = [...debugResponse.coor]; // Copy the array so we do not modify the one stored in state 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 ad7595479..6bd59b9d9 100644 --- a/src/extension-test/unit/cursor-doc/token-cursor-test.ts +++ b/src/extension-test/unit/cursor-doc/token-cursor-test.ts @@ -438,7 +438,7 @@ describe('Token Cursor', () => { const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); expect(cursor.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); }); - it('Can find the comment range for a top level form inside a comment', () => { + 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)'); 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); @@ -480,6 +480,13 @@ describe('Token Cursor', () => { const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); expect(cursor.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); }); + 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); }); describe('Location State', () => { it('Knows when inside string', () => { diff --git a/src/highlight/src/extension.ts b/src/highlight/src/extension.ts index fb56551c4..540f39c91 100755 --- a/src/highlight/src/extension.ts +++ b/src/highlight/src/extension.ts @@ -216,9 +216,9 @@ function updateRainbowBrackets() { const startOffset = doc.offsetAt(range.start), endOffset = doc.offsetAt(range.end), startCursor: LispTokenCursor = mirrorDoc.getTokenCursor(0), - startRange = startCursor.rangeForDefun(startOffset, 1), - endCursor: LispTokenCursor = mirrorDoc.getTokenCursor(startRange[1]), - endRange = endCursor.rangeForDefun(endOffset, 1), + startRange = startCursor.rangeForDefun2(startOffset, false), + endCursor: LispTokenCursor = mirrorDoc.getTokenCursor(endOffset), + endRange = endCursor.rangeForDefun2(endOffset, false), rangeStart = startRange[0], rangeEnd = endRange[1]; // Look for top level ignores, and adjust starting point if found diff --git a/src/results-output/results-doc.ts b/src/results-output/results-doc.ts index 77ba9ffd4..250a910c9 100644 --- a/src/results-output/results-doc.ts +++ b/src/results-output/results-doc.ts @@ -154,8 +154,8 @@ export async function initResultsDoc(): Promise { const selectionCursor = mirrorDoc.getTokenCursor(idx); selectionCursor.forwardWhitespace(); if (selectionCursor.atEnd()) { - const tlCursor = mirrorDoc.getTokenCursor(0); - const topLevelFormRange = tlCursor.rangeForDefun(idx); + const tlCursor = mirrorDoc.getTokenCursor(idx); + const topLevelFormRange = tlCursor.rangeForDefun2(idx); submitOnEnter = topLevelFormRange && topLevelFormRange[0] !== topLevelFormRange[1] && idx >= topLevelFormRange[1]; diff --git a/src/select.ts b/src/select.ts index 18bbe3ca8..dddc514ff 100644 --- a/src/select.ts +++ b/src/select.ts @@ -8,8 +8,8 @@ function selectionFromOffsetRange(doc: vscode.TextDocument, range: [number, numb function getFormSelection(doc: vscode.TextDocument, pos: vscode.Position, topLevel): vscode.Selection { const idx = doc.offsetAt(pos); - const cursor = docMirror.getDocument(doc).getTokenCursor(topLevel ? 0 : idx); - const range = topLevel ? cursor.rangeForDefun(idx) : cursor.rangeForCurrentForm(idx); + const cursor = docMirror.getDocument(doc).getTokenCursor(idx); + const range = topLevel ? cursor.rangeForDefun2(idx) : cursor.rangeForCurrentForm(idx); if (range) { return selectionFromOffsetRange(doc, range); } diff --git a/src/util/cursor-get-text.ts b/src/util/cursor-get-text.ts index 2a5150140..681e8d518 100644 --- a/src/util/cursor-get-text.ts +++ b/src/util/cursor-get-text.ts @@ -7,8 +7,8 @@ import { EditableDocument } from "../cursor-doc/model"; export type RangeAndText = [[number, number], string]; export function currentTopLevelFunction(doc: EditableDocument): RangeAndText { - const defunCursor = doc.getTokenCursor(0); - const defunStart = defunCursor.rangeForDefun(doc.selection.active)[0]; + const defunCursor = doc.getTokenCursor(doc.selection.active); + const defunStart = defunCursor.rangeForDefun2(doc.selection.active)[0]; const cursor = doc.getTokenCursor(defunStart); while (cursor.downList()) { cursor.forwardWhitespace(); @@ -26,8 +26,8 @@ export function currentTopLevelFunction(doc: EditableDocument): RangeAndText { } export function currentTopLevelForm(doc: EditableDocument): RangeAndText { - const defunCursor = doc.getTokenCursor(0); - const defunRange = defunCursor.rangeForDefun(doc.selection.active); + const defunCursor = doc.getTokenCursor(doc.selection.active); + const defunRange = defunCursor.rangeForDefun2(doc.selection.active); return defunRange ? [defunRange, doc.model.getText(...defunRange)] : [undefined, '']; } @@ -53,13 +53,13 @@ export function currentEnclosingFormToCursor(doc: EditableDocument): RangeAndTex } export function currentTopLevelFormToCursor(doc: EditableDocument): RangeAndText { - const cursor = doc.getTokenCursor(0); - const defunRange = cursor.rangeForDefun(doc.selection.active, 0); + const cursor = doc.getTokenCursor(doc.selection.active); + const defunRange = cursor.rangeForDefun2(doc.selection.active); return rangeOrStartOfFileToCursor(doc, defunRange, defunRange[0]); } export function startOfFileToCursor(doc: EditableDocument): RangeAndText { - const cursor = doc.getTokenCursor(0); - const defunRange = cursor.rangeForDefun(doc.selection.active, 0, false); + const cursor = doc.getTokenCursor(doc.selection.active); + const defunRange = cursor.rangeForDefun2(doc.selection.active, false); return rangeOrStartOfFileToCursor(doc, defunRange, 0); } From 3f30d58ead863ee3b35c149d559c316622859080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Thu, 15 Jul 2021 17:10:01 +0200 Subject: [PATCH 4/8] Add two tests for prompt navigation --- .../unit/cursor-doc/token-cursor-test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 6bd59b9d9..ba63728c3 100644 --- a/src/extension-test/unit/cursor-doc/token-cursor-test.ts +++ b/src/extension-test/unit/cursor-doc/token-cursor-test.ts @@ -256,7 +256,24 @@ describe('Token Cursor', () => { cursor.backwardSexp(); expect(cursor.offsetStart).toEqual(b.selectionLeft); }); - }) + }); + + describe('The REPL prompt', () => { + it('Backward sexp bypasses prompt', () => { + const a = docFromTextNotation('foo•clj꞉foo꞉> |'); + const b = docFromTextNotation('|foo•clj꞉foo꞉> '); + const cursor: LispTokenCursor = a.getTokenCursor(a.selectionLeft); + cursor.backwardSexp(); + expect(cursor.offsetStart).toEqual(b.selection.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.selectionLeft); + cursor.backwardSexp(false); + expect(cursor.offsetStart).toEqual(b.selection.active); + }); + }); describe('Current Form', () => { it('0: selects from within non-list form', () => { From 60bb98616b577a960f15473a821978b62782ca9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Thu, 15 Jul 2021 17:13:30 +0200 Subject: [PATCH 5/8] Check that cursor is right of prompt before enter submits --- src/results-output/results-doc.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/results-output/results-doc.ts b/src/results-output/results-doc.ts index 250a910c9..5710010a5 100644 --- a/src/results-output/results-doc.ts +++ b/src/results-output/results-doc.ts @@ -154,11 +154,13 @@ export async function initResultsDoc(): Promise { const selectionCursor = mirrorDoc.getTokenCursor(idx); selectionCursor.forwardWhitespace(); if (selectionCursor.atEnd()) { - const tlCursor = mirrorDoc.getTokenCursor(idx); - const topLevelFormRange = tlCursor.rangeForDefun2(idx); - submitOnEnter = topLevelFormRange && - topLevelFormRange[0] !== topLevelFormRange[1] && - idx >= topLevelFormRange[1]; + const promptCursor = mirrorDoc.getTokenCursor(idx); + do { + promptCursor.previous(); + } while (promptCursor.getPrevToken().type !== 'prompt' && !promptCursor.atStart()); + const submitRange = selectionCursor.rangeForCurrentForm(idx); + submitOnEnter = submitRange && + submitRange[1] > promptCursor.offsetStart; } } } From 2a7438f9de6aa80379615837656d1ecd5e021991 Mon Sep 17 00:00:00 2001 From: Tomas Brejla Date: Sat, 17 Jul 2021 13:42:09 +0200 Subject: [PATCH 6/8] renamed rangeForDefun2 -> rangeForDefun, tests cleanup, changelog --- CHANGELOG.md | 1 + src/cursor-doc/paredit.ts | 2 +- src/cursor-doc/token-cursor.ts | 34 +----- src/debugger/util.ts | 2 +- .../unit/cursor-doc/token-cursor-test.ts | 111 +++++------------- src/highlight/src/extension.ts | 4 +- src/select.ts | 2 +- src/util/cursor-get-text.ts | 8 +- 8 files changed, 43 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae0e4784c..9b5e2ed7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Changes to Calva. ## [Unreleased] +- Performance improvement [REPL is Slow and Performance Degrades as the Output Grows](https://github.com/BetterThanTomorrow/calva/issues/942) ## [2.0.205] - 2021-07-14 - [Use new custom LSP method for server info command and print info in "Calva says" output channel ](https://github.com/BetterThanTomorrow/calva/issues/1211) diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index a52212d36..4ca7af7fb 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -92,7 +92,7 @@ export function selectOpenList(doc: EditableDocument) { */ export function rangeForDefun(doc: EditableDocument, offset: number = doc.selection.active): [number, number] { const cursor = doc.getTokenCursor(offset); - return cursor.rangeForDefun2(offset); + return cursor.rangeForDefun(offset); } export function forwardSexpRange(doc: EditableDocument, offset = Math.max(doc.selection.anchor, doc.selection.active), goPastWhitespace = false): [number, number] { diff --git a/src/cursor-doc/token-cursor.ts b/src/cursor-doc/token-cursor.ts index ae21ba100..7f4af5c90 100644 --- a/src/cursor-doc/token-cursor.ts +++ b/src/cursor-doc/token-cursor.ts @@ -602,39 +602,9 @@ export class LispTokenCursor extends TokenCursor { } } return undefined; // 8. - } - - /** - * Gets the range for the ”current” top level form, visiting forms from the cursor towards `offset` - * With `commentCreatesTopLevel` as true (default): If the current top level form is a `(comment ...)`, consider it creating a new top level and continue the search. - * @param offset The ”current” position - * @param depth Controls if the cursor should consider `comment` top level (if > 0, it will not) - * @param commentIsTopLevel? Controls - */ - rangeForDefun(offset: number, depth = 0, commentCreatesTopLevel = true): [number, number] { - const cursor = this.clone(); - - while (cursor.forwardSexp()) { - if (cursor.offsetEnd >= offset) { - if (depth < 1 && cursor.getPrevToken().raw === ')') { - const commentCursor = cursor.clone(); - commentCursor.previous(); - if (commentCursor.getFunctionName() === 'comment' && commentCreatesTopLevel) { - commentCursor.backwardList(); - commentCursor.forwardWhitespace(); - commentCursor.forwardSexp(); - return commentCursor.rangeForDefun(offset, depth + 1); - } - } - const end = cursor.offsetStart; - cursor.backwardSexp(); - return [cursor.offsetStart, end]; - } - } - return [offset, offset] - } + } - rangeForDefun2(offset: number, commentCreatesTopLevel = true): [number, number] { + rangeForDefun(offset: number, commentCreatesTopLevel = true): [number, number] { const cursor = this.doc.getTokenCursor(offset); let lastCandidateRange: [number, number] = cursor.rangeForCurrentForm(offset); while (cursor.forwardList() && cursor.upList()) { diff --git a/src/debugger/util.ts b/src/debugger/util.ts index 35e72400f..3a63915e3 100644 --- a/src/debugger/util.ts +++ b/src/debugger/util.ts @@ -21,7 +21,7 @@ function moveCursorPastStringInList(tokenCursor: LispTokenCursor, s: string): vo function moveTokenCursorToBreakpoint(tokenCursor: LispTokenCursor, debugResponse: any): LispTokenCursor { const errorMessage = "Error finding position of breakpoint"; - const [_, defunEnd] = tokenCursor.rangeForDefun2(tokenCursor.offsetStart); + const [_, defunEnd] = tokenCursor.rangeForDefun(tokenCursor.offsetStart); let inSyntaxQuote = false; const coor = [...debugResponse.coor]; // Copy the array so we do not modify the one stored in state 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 ba63728c3..a4b5a4a68 100644 --- a/src/extension-test/unit/cursor-doc/token-cursor-test.ts +++ b/src/extension-test/unit/cursor-doc/token-cursor-test.ts @@ -366,145 +366,96 @@ describe('Token Cursor', () => { 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)'); 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(0); - expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); - }); - it('Finds range when in current form is top level', () => { // Bug, documenting how it behaves - 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)'); - 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(0); - expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); - }); - 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)'); - 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(0); - expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); - }); - it('Finds 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)'); - 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(0); - expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); - }); - 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)'); - 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(0); - expect(cursor.rangeForDefun(a.selectionLeft, 0, false)).toEqual(textAndSelection(b)[1]); - }); - it('Finds empty 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(0); - expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); - }); - it('Finds comment range when comments are nested', () => { // TODO: Consider changing this behavior - 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.selectionLeft)).toEqual(textAndSelection(b)[1]); - }); - 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)| fff'); - 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)| fff'); const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); - }); - 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(0); - expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); - }); - 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.selectionLeft)).toEqual(textAndSelection(b)[1]); - }); - 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(0); - expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); - }); - }); - describe('Top Level Form 2', () => { - 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)'); - 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); }); 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)'); 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); }); 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)'); 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); }); 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); }); 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)'); 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.rangeForDefun2(a.selection.active, false)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active, false)).toEqual(textAndSelection(b)[1]); }); it('Finds comment range for empty comment form', () => { // Unimportant use case, just documenting how it behaves const a = docFromTextNotation('aaa (comment | ) bbb'); const b = docFromTextNotation('aaa (|comment| ) bbb'); const cursor: LispTokenCursor = a.getTokenCursor(a.selection.active); - expect(cursor.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); }); 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); }); 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)'); 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); }); 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); }); 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); }); 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); }); 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.rangeForDefun2(a.selection.active)).toEqual(textAndSelection(b)[1]); + expect(cursor.rangeForDefun(a.selection.active)).toEqual(textAndSelection(b)[1]); + }); + 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)'); + 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(0); + expect(cursor.rangeForDefun(a.selectionLeft, false)).toEqual(textAndSelection(b)[1]); + }); + 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.selectionLeft)).toEqual(textAndSelection(b)[1]); + }); + 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.selectionLeft)).toEqual(textAndSelection(b)[1]); }); }); + describe('Location State', () => { it('Knows when inside string', () => { const doc = docFromTextNotation('(str [] "", "foo" "f b b" " f b b " "\\"" \\")'); diff --git a/src/highlight/src/extension.ts b/src/highlight/src/extension.ts index 540f39c91..a82a7375e 100755 --- a/src/highlight/src/extension.ts +++ b/src/highlight/src/extension.ts @@ -216,9 +216,9 @@ function updateRainbowBrackets() { const startOffset = doc.offsetAt(range.start), endOffset = doc.offsetAt(range.end), startCursor: LispTokenCursor = mirrorDoc.getTokenCursor(0), - startRange = startCursor.rangeForDefun2(startOffset, false), + startRange = startCursor.rangeForDefun(startOffset, false), endCursor: LispTokenCursor = mirrorDoc.getTokenCursor(endOffset), - endRange = endCursor.rangeForDefun2(endOffset, false), + endRange = endCursor.rangeForDefun(endOffset, false), rangeStart = startRange[0], rangeEnd = endRange[1]; // Look for top level ignores, and adjust starting point if found diff --git a/src/select.ts b/src/select.ts index dddc514ff..02c4fe9c3 100644 --- a/src/select.ts +++ b/src/select.ts @@ -9,7 +9,7 @@ function selectionFromOffsetRange(doc: vscode.TextDocument, range: [number, numb function getFormSelection(doc: vscode.TextDocument, pos: vscode.Position, topLevel): vscode.Selection { const idx = doc.offsetAt(pos); const cursor = docMirror.getDocument(doc).getTokenCursor(idx); - const range = topLevel ? cursor.rangeForDefun2(idx) : cursor.rangeForCurrentForm(idx); + const range = topLevel ? cursor.rangeForDefun(idx) : cursor.rangeForCurrentForm(idx); if (range) { return selectionFromOffsetRange(doc, range); } diff --git a/src/util/cursor-get-text.ts b/src/util/cursor-get-text.ts index 681e8d518..8d40702d2 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]; export function currentTopLevelFunction(doc: EditableDocument): RangeAndText { const defunCursor = doc.getTokenCursor(doc.selection.active); - const defunStart = defunCursor.rangeForDefun2(doc.selection.active)[0]; + const defunStart = defunCursor.rangeForDefun(doc.selection.active)[0]; const cursor = doc.getTokenCursor(defunStart); while (cursor.downList()) { cursor.forwardWhitespace(); @@ -27,7 +27,7 @@ export function currentTopLevelFunction(doc: EditableDocument): RangeAndText { export function currentTopLevelForm(doc: EditableDocument): RangeAndText { const defunCursor = doc.getTokenCursor(doc.selection.active); - const defunRange = defunCursor.rangeForDefun2(doc.selection.active); + const defunRange = defunCursor.rangeForDefun(doc.selection.active); return defunRange ? [defunRange, doc.model.getText(...defunRange)] : [undefined, '']; } @@ -54,12 +54,12 @@ export function currentEnclosingFormToCursor(doc: EditableDocument): RangeAndTex export function currentTopLevelFormToCursor(doc: EditableDocument): RangeAndText { const cursor = doc.getTokenCursor(doc.selection.active); - const defunRange = cursor.rangeForDefun2(doc.selection.active); + const defunRange = cursor.rangeForDefun(doc.selection.active); return rangeOrStartOfFileToCursor(doc, defunRange, defunRange[0]); } export function startOfFileToCursor(doc: EditableDocument): RangeAndText { const cursor = doc.getTokenCursor(doc.selection.active); - const defunRange = cursor.rangeForDefun2(doc.selection.active, false); + const defunRange = cursor.rangeForDefun(doc.selection.active, false); return rangeOrStartOfFileToCursor(doc, defunRange, 0); } From d6a94ac7a3baf8a8acdbef85b6dfcade07207c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Tue, 3 Aug 2021 12:04:37 +0200 Subject: [PATCH 7/8] Guard against top level form being undefined --- src/highlight/src/extension.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/highlight/src/extension.ts b/src/highlight/src/extension.ts index a82a7375e..ae5ce274b 100755 --- a/src/highlight/src/extension.ts +++ b/src/highlight/src/extension.ts @@ -219,8 +219,8 @@ function updateRainbowBrackets() { startRange = startCursor.rangeForDefun(startOffset, false), endCursor: LispTokenCursor = mirrorDoc.getTokenCursor(endOffset), endRange = endCursor.rangeForDefun(endOffset, false), - rangeStart = startRange[0], - rangeEnd = endRange[1]; + rangeStart = startRange ? startRange[0] : startOffset, + rangeEnd = endRange ? endRange[1] : endOffset; // Look for top level ignores, and adjust starting point if found const topLevelSentinelCursor = mirrorDoc.getTokenCursor(rangeStart); let startPaintingFrom = rangeStart; From 225e771858ee3f7e7bda1dda450a6eb7abdd4b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Tue, 3 Aug 2021 14:56:25 +0200 Subject: [PATCH 8/8] Make forwardList check for unbalance --- src/cursor-doc/token-cursor.ts | 7 +- .../unit/cursor-doc/token-cursor-test.ts | 72 +++++++++++++++---- test-data/unbalance.clj | 5 ++ 3 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 test-data/unbalance.clj diff --git a/src/cursor-doc/token-cursor.ts b/src/cursor-doc/token-cursor.ts index 7f4af5c90..4b37872b9 100644 --- a/src/cursor-doc/token-cursor.ts +++ b/src/cursor-doc/token-cursor.ts @@ -388,8 +388,11 @@ export class LispTokenCursor extends TokenCursor { let cursor = this.clone(); while (cursor.forwardSexp()) { } if (cursor.getToken().type === "close") { - this.set(cursor); - return true; + const backCursor = cursor.clone(); + if (backCursor.backwardList()) { + this.set(cursor); + return true; + } } return false; } 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 a4b5a4a68..e64445d28 100644 --- a/src/extension-test/unit/cursor-doc/token-cursor-test.ts +++ b/src/extension-test/unit/cursor-doc/token-cursor-test.ts @@ -203,12 +203,35 @@ describe('Token Cursor', () => { }); }); - it('forwardList', () => { - 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.selectionLeft); - cursor.forwardList(); - expect(cursor.offsetStart).toBe(b.selectionLeft); + describe('forwardList', () => { + it('Moves to closing end of 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.selectionLeft); + cursor.forwardList(); + expect(cursor.offsetStart).toBe(b.selectionLeft); + }); + 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.selectionLeft); + cursor.forwardList(); + expect(cursor.offsetStart).toBe(b.selectionLeft); + }); + 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.selectionLeft); + cursor.forwardList(); + expect(cursor.offsetStart).toBe(b.selectionLeft); + }); + 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.selectionLeft); + cursor.forwardList(); + expect(cursor.offsetStart).toBe(b.selectionLeft); + }); }); it('upList', () => { const a = docFromTextNotation('(a(b(c•#f•(#b •[:f :b :z])•#z•1|)))'); @@ -217,12 +240,35 @@ describe('Token Cursor', () => { cursor.upList(); expect(cursor.offsetStart).toBe(b.selectionLeft); }); - it('backwardList', () => { - 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.selectionLeft); - cursor.backwardList(); - expect(cursor.offsetStart).toBe(b.selectionLeft); + describe('backwardList', () => { + it('backwardList', () => { + 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.selectionLeft); + cursor.backwardList(); + expect(cursor.offsetStart).toBe(b.selectionLeft); + }); + 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.selectionLeft); + cursor.forwardList(); + expect(cursor.offsetStart).toBe(b.selectionLeft); + }); + 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.selectionLeft); + cursor.forwardList(); + expect(cursor.offsetStart).toBe(b.selectionLeft); + }); + 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.selectionLeft); + cursor.forwardList(); + expect(cursor.offsetStart).toBe(b.selectionLeft); + }); }); it('backwardUpList', () => { @@ -455,7 +501,7 @@ describe('Token Cursor', () => { expect(cursor.rangeForDefun(a.selectionLeft)).toEqual(textAndSelection(b)[1]); }); }); - + describe('Location State', () => { it('Knows when inside string', () => { const doc = docFromTextNotation('(str [] "", "foo" "f b b" " f b b " "\\"" \\")'); diff --git a/test-data/unbalance.clj b/test-data/unbalance.clj new file mode 100644 index 000000000..00998a70e --- /dev/null +++ b/test-data/unbalance.clj @@ -0,0 +1,5 @@ +(ns unbalance) + +(def foo)) + +:foo