diff --git a/CHANGELOG.md b/CHANGELOG.md index ba2abf08b..3efa8d8b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes to Calva. ## [Unreleased] +- [Dragging sexps with adjacent comments](https://github.com/BetterThanTomorrow/calva/issues/3073) + ## [2.0.582] - 2026-05-03 - [Make Orphaned/dicsonnected WebSocket servers closable](https://github.com/BetterThanTomorrow/calva/issues/3195) diff --git a/docs/site/paredit.md b/docs/site/paredit.md index 4099cbb88..9f26767f1 100644 --- a/docs/site/paredit.md +++ b/docs/site/paredit.md @@ -171,6 +171,14 @@ And like so (wait for it): ![](images/paredit/drag-pairs-in-maps.gif) +Drag forward/backward also treats certain line comments as attached to forms: + +- Contiguous comment lines immediately above a form move with that form. +- Trailing same-line comments move with the form on that line. +- Tight result comments such as `;=>` and `;;=>` are attached to the form immediately above them, and move with that form when it is dragged. +- When the cursor is on an attached comment line, dragging acts on the + associated form-comment pair rather than on the comment line by itself. + ## Toggle Comment Behavior The **Toggle Comment** command (`ctrl+/` / `cmd+/`) behavior when there is no text selected and the cursor is not in a line comment is controlled by the `calva.paredit.toggleCommentBehavior` setting: diff --git a/package.json b/package.json index 2ffef8670..59302545f 100644 --- a/package.json +++ b/package.json @@ -2994,22 +2994,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 =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.dragSexprForward", "key": "alt+down", - "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment" + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults" }, { "command": "paredit.dragSexprBackwardUp", diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index d2404a372..ce93f7c3f 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -2337,6 +2337,179 @@ export function currentSexpsRange( return currentSingleRange; } +type LineInfo = { + start: number; + end: number; + content: string; + trimmed: string; +}; + +function lineInfoAt(text: string, offset: number): LineInfo { + const start = text.lastIndexOf('\n', Math.max(0, offset - 1)) + 1; + const nl = text.indexOf('\n', offset); + const end = nl === -1 ? text.length : nl; + const content = text.substring(start, end); + return { start, end, content, trimmed: content.trimStart() }; +} + +const prevLine = (text: string, lineStart: number) => + lineStart > 0 ? lineInfoAt(text, lineStart - 1) : null; + +const nextLine = (text: string, lineEnd: number) => + lineEnd < text.length ? lineInfoAt(text, lineEnd + 1) : null; + +const isCommentLine = (line: LineInfo) => line.trimmed.startsWith(';'); +const isBlankLine = (line: LineInfo) => line.trimmed === ''; +const isResultCommentStartLine = (line: LineInfo) => /^;{1,2}=>/.test(line.trimmed); + +function isResultCommentBlock(lines: LineInfo[]): boolean { + if (!lines.length || !isResultCommentStartLine(lines[0])) { + return false; + } + + const semicolonPrefix = lines[0].trimmed.startsWith(';;=>') ? ';;' : ';'; + return lines.slice(1).every((line) => line.trimmed.startsWith(`${semicolonPrefix} `)); +} + +function trailingResultCommentBlockEnd(text: string, fromLineEnd: number): number | null { + const first = nextLine(text, fromLineEnd); + if (!first || !isResultCommentStartLine(first)) { + return null; + } + + const semicolonPrefix = first.trimmed.startsWith(';;=>') ? ';;' : ';'; + let end = first.end; + let line = nextLine(text, first.end); + while (line && isCommentLine(line) && line.trimmed.startsWith(`${semicolonPrefix} `)) { + end = line.end; + line = nextLine(text, line.end); + } + return end; +} + +function extendRangeToLineIndent(text: string, [start, end]: [number, number]): [number, number] { + const startLine = lineInfoAt(text, start); + return text.substring(startLine.start, start).trim() === '' + ? [startLine.start, end] + : [start, end]; +} + +function normalizeSwapRangesForLeadingComments( + text: string, + leftRange: [number, number], + rightRange: [number, number] +): [[number, number], [number, number]] { + const leftStartsWithComment = isCommentLine(lineInfoAt(text, leftRange[0])); + const rightStartsWithComment = isCommentLine(lineInfoAt(text, rightRange[0])); + + return [ + rightStartsWithComment ? extendRangeToLineIndent(text, leftRange) : leftRange, + leftStartsWithComment ? extendRangeToLineIndent(text, rightRange) : rightRange, + ]; +} + +/** + * Extends a form's range to include its attached line comments: + * - Backward: contiguous comment lines immediately above the form (only when + * the form's line has nothing but whitespace before the form). + * - Forward: a trailing `;…` on the form's last line, plus comment lines + * below that are NOT leading a following form (terminated by a blank line + * or EOF). + */ +function extendRangeOverAttachedComments( + doc: EditableDocument, + [start, end]: [number, number] +): [number, number] { + const text = doc.model.getText(0, Number.MAX_SAFE_INTEGER); + + let lo = start; + const startLine = lineInfoAt(text, start); + if (text.substring(startLine.start, start).trim() === '') { + const leadingCommentLines: LineInfo[] = []; + for (let line = prevLine(text, startLine.start); line && isCommentLine(line); ) { + leadingCommentLines.unshift(line); + line = prevLine(text, line.start); + } + if (leadingCommentLines.length && !isResultCommentBlock(leadingCommentLines)) { + lo = leadingCommentLines[0].start; + } + } + + let hi = end; + const endLine = lineInfoAt(text, end); + if (/^\s*;/.test(text.substring(end, endLine.end))) { + hi = endLine.end; + } + + const resultCommentsEnd = trailingResultCommentBlockEnd(text, endLine.end); + if (resultCommentsEnd !== null) { + hi = resultCommentsEnd; + } + + const trailingEnds: number[] = []; + let below = nextLine(text, endLine.end); + while (below && isCommentLine(below)) { + trailingEnds.push(below.end); + below = nextLine(text, below.end); + } + if (trailingEnds.length && (!below || isBlankLine(below))) { + hi = trailingEnds[trailingEnds.length - 1]; + } + + return [lo, hi]; +} + +/** + * If `offset` is on a line-comment line, returns the range of the form that + * comment is attached to: the form immediately below when contiguous (no + * blank line between), else the form immediately above when the comment is a + * trailing annotation. Returns `null` when the cursor isn't on a comment, or + * when no form is attached. + */ +function formAttachedToCommentAt(doc: EditableDocument, offset: number): [number, number] | null { + const text = doc.model.getText(0, Number.MAX_SAFE_INTEGER); + const here = lineInfoAt(text, offset); + if (!isCommentLine(here)) { + return null; + } + + if (isResultCommentStartLine(here)) { + let above = prevLine(text, here.start); + while (above && isCommentLine(above)) { + above = prevLine(text, above.start); + } + if (above && !isBlankLine(above)) { + const formEnd = above.start + above.content.trimEnd().length; + const cursor = doc.getTokenCursor(formEnd); + cursor.backwardSexp(); + return cursor.rangeForCurrentForm(cursor.offsetStart); + } + return null; + } + + let below = nextLine(text, here.end); + while (below && isCommentLine(below)) { + below = nextLine(text, below.end); + } + if (below && !isBlankLine(below)) { + const formStart = below.start + (below.content.length - below.trimmed.length); + return doc.getTokenCursor(formStart).rangeForCurrentForm(formStart); + } + + let above = prevLine(text, here.start); + while (above && isCommentLine(above)) { + above = prevLine(text, above.start); + } + if (above && !isBlankLine(above)) { + const formEnd = above.start + above.content.trimEnd().length; + const cursor = doc.getTokenCursor(formEnd); + cursor.backwardSexp(); + return cursor.rangeForCurrentForm(cursor.offsetStart); + } + + return null; +} + export async function dragSexprBackward( doc: EditableDocument, left = doc.selections[0].anchor, @@ -2345,21 +2518,36 @@ export async function dragSexprBackward( ) { const cursor = doc.getTokenCursor(right); const usePairs = isInPairsList(cursor, config); - const currentRange = currentSexpsRange(doc, cursor, right, usePairs, config); - const newPosOffset = right - currentRange[0]; - const backCursor = doc.getTokenCursor(currentRange[0]); + const text = doc.model.getText(0, Number.MAX_SAFE_INTEGER); + const baseRange = + formAttachedToCommentAt(doc, right) ?? currentSexpsRange(doc, cursor, right, usePairs, config); + const currentRange = extendRangeOverAttachedComments(doc, baseRange); + const backCursor = doc.getTokenCursor(baseRange[0]); backCursor.backwardSexp(); - const backRange = currentSexpsRange(doc, backCursor, backCursor.offsetStart, usePairs, config); - 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]); + const backBase = currentSexpsRange(doc, backCursor, backCursor.offsetStart, usePairs, config); + if (backBase[0] !== baseRange[0]) { + const backRange = extendRangeOverAttachedComments(doc, backBase); + const [normalizedCurrentRange, normalizedBackRange] = normalizeSwapRangesForLeadingComments( + text, + currentRange, + backRange + ); + const leftText = doc.model.getText(normalizedBackRange[0], normalizedBackRange[1]); + const currentText = doc.model.getText(normalizedCurrentRange[0], normalizedCurrentRange[1]); return doc.model.edit( [ - new ModelEdit('changeRange', [currentRange[0], currentRange[1], leftText]), - new ModelEdit('changeRange', [backRange[0], backRange[1], currentText]), + new ModelEdit('changeRange', [ + normalizedCurrentRange[0], + normalizedCurrentRange[1], + leftText, + ]), + new ModelEdit('changeRange', [normalizedBackRange[0], normalizedBackRange[1], currentText]), ], - { selections: [new ModelEditSelection(backRange[0] + newPosOffset)] } + { + selections: [ + new ModelEditSelection(normalizedBackRange[0] + right - normalizedCurrentRange[0]), + ], + } ); } } @@ -2372,31 +2560,55 @@ export async function dragSexprForward( ) { const cursor = doc.getTokenCursor(right); const usePairs = isInPairsList(cursor, config); - const currentRange = currentSexpsRange(doc, cursor, right, usePairs, config); - const newPosOffset = currentRange[1] - right; - const forwardCursor = doc.getTokenCursor(currentRange[1]); - forwardCursor.forwardSexp(); - const forwardRange = currentSexpsRange( + const text = doc.model.getText(0, Number.MAX_SAFE_INTEGER); + const baseRange = + formAttachedToCommentAt(doc, right) ?? currentSexpsRange(doc, cursor, right, usePairs, config); + const currentRange = extendRangeOverAttachedComments(doc, baseRange); + const forwardCursor = doc.getTokenCursor(baseRange[1]); + forwardCursor.forwardWhitespace(); + let forwardBase = currentSexpsRange( doc, forwardCursor, forwardCursor.offsetStart, usePairs, config ); - 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]); + if (forwardBase[0] === baseRange[0]) { + forwardCursor.forwardSexp(); + forwardCursor.forwardWhitespace(); + forwardBase = currentSexpsRange( + doc, + forwardCursor, + forwardCursor.offsetStart, + usePairs, + config + ); + } + if (forwardBase[0] !== baseRange[0]) { + const forwardRange = extendRangeOverAttachedComments(doc, forwardBase); + const [normalizedCurrentRange, normalizedForwardRange] = normalizeSwapRangesForLeadingComments( + text, + currentRange, + forwardRange + ); + const leftText = doc.model.getText(normalizedCurrentRange[0], normalizedCurrentRange[1]); + const rightText = doc.model.getText(normalizedForwardRange[0], normalizedForwardRange[1]); return doc.model.edit( [ - new ModelEdit('changeRange', [forwardRange[0], forwardRange[1], currentText]), - new ModelEdit('changeRange', [currentRange[0], currentRange[1], rightText]), + new ModelEdit('changeRange', [ + normalizedForwardRange[0], + normalizedForwardRange[1], + leftText, + ]), + new ModelEdit('changeRange', [ + normalizedCurrentRange[0], + normalizedCurrentRange[1], + rightText, + ]), ], { selections: [ - new ModelEditSelection( - currentRange[1] + (forwardRange[1] - currentRange[1]) - newPosOffset - ), + new ModelEditSelection(normalizedForwardRange[1] + right - normalizedCurrentRange[1]), ], } ); diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index 29988d541..cf5178bd0 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -2136,6 +2136,358 @@ describe('paredit', () => { .expect(textNotation.textAndSelection(a)) .toEqual(textNotation.textAndSelection(b)); }); + + describe('with attached comments', () => { + describe('form-comment pairs', () => { + it('keeps leading comments attached when dragging from trailing inline comment', async () => { + const a = textNotation.docFromTextNotation( + `(do•;;b•(str "Hello" " " "World")•"B" ;a|•)` + ); + const b = textNotation.docFromTextNotation( + `(do•"B" ;a|•;;b•(str "Hello" " " "World")•)` + ); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('keeps leading comments attached at top level when dragging backward from trailing inline comment', async () => { + const a = textNotation.docFromTextNotation(`;;b•(str "Hello" " " "World")•"B" ;a|`); + const b = textNotation.docFromTextNotation(`"B" ;a|•;;b•(str "Hello" " " "World")`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags forward across a blank line when caret is before a form with trailing inline comment', async () => { + const a = textNotation.docFromTextNotation(`|"B" ;a••;;b•(str "Hello" " " "World")`); + const b = textNotation.docFromTextNotation(`;;b•(str "Hello" " " "World")••"B" ;a|`); + await paredit.dragSexprForward(a); + expectLib.expect(a.model.getText(0, Infinity)).toEqual(b.model.getText(0, Infinity)); + }); + + it('drags forward across a blank line when caret is after trailing inline comment', async () => { + const a = textNotation.docFromTextNotation(`"B" ;a|••;;b•(str "Hello" " " "World")`); + const b = textNotation.docFromTextNotation(`;;b•(str "Hello" " " "World")••"B" ;a|`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags forward across a blank line when caret is at start of trailing inline comment', async () => { + const a = textNotation.docFromTextNotation(`"B" |;a••;b•(str "Hello" " " "World")`); + const b = textNotation.docFromTextNotation(`;b•(str "Hello" " " "World")••"B" |;a`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags backward from end of trailing inline comment and keeps single-semicolon comment attached', async () => { + const a = textNotation.docFromTextNotation(`;b•(str "Hello" " " "World")••"B" ;a|`); + const b = textNotation.docFromTextNotation(`"B" ;a|••;b•(str "Hello" " " "World")`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags backward when caret is in whitespace before trailing inline comment', async () => { + const a = textNotation.docFromTextNotation(`;b•(str "Hello" " " "World")••"B" |;a`); + const b = textNotation.docFromTextNotation(`"B" |;a••;b•(str "Hello" " " "World")`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('keeps ;=> result comments attached to form under drag', async () => { + const a = textNotation.docFromTextNotation(`(+ 1 2)•;=> 3••"B" ;a|`); + const b = textNotation.docFromTextNotation(`"B" ;a|••(+ 1 2)•;=> 3`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + }); + + describe('preceding line comments', () => { + it('drags sexp backward with its preceding comment', async () => { + const a = textNotation.docFromTextNotation( + `;; Foo•(do foo)••;; Bar•(do bar)••;; Baz•(do baz)|` + ); + const b = textNotation.docFromTextNotation( + `;; Foo•(do foo)••;; Baz•(do baz)|••;; Bar•(do bar)` + ); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp forward with its preceding comment', async () => { + const a = textNotation.docFromTextNotation( + `;; Foo•(do foo)••;; Bar•(do bar)|••;; Baz•(do baz)` + ); + const b = textNotation.docFromTextNotation( + `;; Foo•(do foo)••;; Baz•(do baz)••;; Bar•(do bar)|` + ); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp backward without a preceding comment when there is a blank line', async () => { + // A blank line between a comment and a form breaks the association. + const a = textNotation.docFromTextNotation( + `;; Foo•(do foo)••;; Bar•(do bar)•••(do baz)|` + ); + const b = textNotation.docFromTextNotation( + `;; Foo•(do foo)••(do baz)|•••;; Bar•(do bar)` + ); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp forward without a preceding comment when there is a blank line', async () => { + const a = textNotation.docFromTextNotation( + `;; Foo•(do foo)•••;; Bar•(do bar)|••;; Baz•(do baz)` + ); + const b = textNotation.docFromTextNotation( + `;; Foo•(do foo)•••;; Baz•(do baz)••;; Bar•(do bar)|` + ); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp backward with multiple preceding comment lines', async () => { + const a = textNotation.docFromTextNotation( + `;; Foo•(do foo)••;; Bar•;; Extra bar comment•(do bar)••;; Baz•(do baz)|` + ); + const b = textNotation.docFromTextNotation( + `;; Foo•(do foo)••;; Baz•(do baz)|••;; Bar•;; Extra bar comment•(do bar)` + ); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp forward without comment when form has no preceding comment', async () => { + const a = textNotation.docFromTextNotation(`(do foo)•(do bar)|••;; Baz•(do baz)`); + const b = textNotation.docFromTextNotation(`(do foo)•;; Baz•(do baz)••(do bar)|`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp backward when only preceding form has a comment', async () => { + const a = textNotation.docFromTextNotation(`;; Foo•(do foo)•(do bar)|`); + const b = textNotation.docFromTextNotation(`(do bar)|•;; Foo•(do foo)`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags indented sexp backward with its preceding comment inside a container', async () => { + const a = textNotation.docFromTextNotation(`(do• ;; A• (form-a)• ;; B• (form-b)|)`); + const b = textNotation.docFromTextNotation(`(do• ;; B• (form-b)|• ;; A• (form-a))`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags indented sexp forward with its preceding comment inside a container', async () => { + const a = textNotation.docFromTextNotation(`(do• ;; A• (form-a)|• ;; B• (form-b))`); + const b = textNotation.docFromTextNotation(`(do• ;; B• (form-b)• ;; A• (form-a)|)`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags a commented form forward in a comment form without adding indentation', async () => { + const a = textNotation.docFromTextNotation(`(comment• ; a• :a|• :b• )`); + const b = textNotation.docFromTextNotation(`(comment• :b• ; a• :a|• )`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags a commented form backward after a forward drag without accumulating indentation', async () => { + const a = textNotation.docFromTextNotation(`(comment• :b• ; a• :a|• )`); + const b = textNotation.docFromTextNotation(`(comment• ; a• :a|• :b• )`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('keeps both comments attached when dragging forward in a comment form', async () => { + const a = textNotation.docFromTextNotation(`(comment• ; a• :a|• ; b• :b• )`); + const b = textNotation.docFromTextNotation(`(comment• ; b• :b• ; a• :a|• )`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags comment-form pair backward when cursor is in the comment', async () => { + const a = textNotation.docFromTextNotation( + `(str "a")••;; b|•(str "Hello" " " "world")` + ); + const b = textNotation.docFromTextNotation( + `;; b|•(str "Hello" " " "world")••(str "a")` + ); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags comment-form pair forward when cursor is in the comment', async () => { + const a = textNotation.docFromTextNotation( + `(str "a")••;; b|•(str "Hello" " " "world")••(str "z")` + ); + const b = textNotation.docFromTextNotation( + `(str "a")••(str "z")••;; b|•(str "Hello" " " "world")` + ); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp backward with its trailing same-line comment', async () => { + const a = textNotation.docFromTextNotation(`(+ 2 3)•(+ 1 2)| ; => 3`); + const b = textNotation.docFromTextNotation(`(+ 1 2)| ; => 3•(+ 2 3)`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp forward with its trailing same-line comment', async () => { + const a = textNotation.docFromTextNotation(`(+ 1 2)| ; => 3•(+ 2 3)`); + const b = textNotation.docFromTextNotation(`(+ 2 3)•(+ 1 2)| ; => 3`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp backward with a comment on the line below (before a blank)', async () => { + const a = textNotation.docFromTextNotation(`(+ 2 3)••(+ 1 2)|•;=> 3`); + const b = textNotation.docFromTextNotation(`(+ 1 2)|•;=> 3••(+ 2 3)`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp forward with a comment on the line below (before a blank)', async () => { + const a = textNotation.docFromTextNotation(`(+ 1 2)|•;=> 3••(+ 2 3)`); + const b = textNotation.docFromTextNotation(`(+ 2 3)••(+ 1 2)|•;=> 3`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp backward with ;;=> result comment when next form follows immediately', async () => { + const a = textNotation.docFromTextNotation(`(+ 2 3)•(+ 1 2)|•;;=> 3`); + const b = textNotation.docFromTextNotation(`(+ 1 2)|•;;=> 3•(+ 2 3)`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp backward with ;=> result comment when next form follows immediately', async () => { + const a = textNotation.docFromTextNotation(`(+ 2 3)•(+ 1 2)|•;=> 3`); + const b = textNotation.docFromTextNotation(`(+ 1 2)|•;=> 3•(+ 2 3)`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp forward with ;;=> result comment when next form follows immediately', async () => { + const a = textNotation.docFromTextNotation(`(+ 1 2)|•;;=> 3•(+ 2 3)`); + const b = textNotation.docFromTextNotation(`(+ 2 3)•(+ 1 2)|•;;=> 3`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags sexp forward with ;=> result comment when next form follows immediately', async () => { + const a = textNotation.docFromTextNotation(`(+ 1 2)|•;=> 3•(+ 2 3)`); + const b = textNotation.docFromTextNotation(`(+ 2 3)•(+ 1 2)|•;=> 3`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags a form forward with its tight result comment', async () => { + const a = textNotation.docFromTextNotation(`(comment• :a• :b|• ;=> b• :c• )`); + const b = textNotation.docFromTextNotation(`(comment• :a• :c• :b|• ;=> b• )`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags a form backward with its tight result comment', async () => { + const a = textNotation.docFromTextNotation(`(comment• :a• :b|• ;=> b• :c• )`); + const b = textNotation.docFromTextNotation(`(comment• :b|• ;=> b• :a• :c• )`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('does not attach tight result comments to the following form on drag backward', async () => { + const a = textNotation.docFromTextNotation(`(comment• :a• ;=> a• :b• :c|• )`); + const b = textNotation.docFromTextNotation(`(comment• :a• ;=> a• :c|• :b• )`); + await paredit.dragSexprBackward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('does not move a tight result comment when dragging the following form forward', async () => { + const a = textNotation.docFromTextNotation(`(comment• :a• ;=> a• :b|• :c• )`); + const b = textNotation.docFromTextNotation(`(comment• :a• ;=> a• :c• :b|• )`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + + it('drags forward when cursor is in a tight result comment line', async () => { + const a = textNotation.docFromTextNotation(`(comment• :c• :b• ;=> b|• :a• )`); + const b = textNotation.docFromTextNotation(`(comment• :c• :a• :b• ;=> b|• )`); + await paredit.dragSexprForward(a); + expectLib + .expect(textNotation.textAndSelection(a)) + .toEqual(textNotation.textAndSelection(b)); + }); + }); + }); }); describe('backwardUp - one line', () => {