diff --git a/CHANGELOG.md b/CHANGELOG.md index 44da1c98f..6d10b31e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changes to Calva. ## [Unreleased] - Fix [The schema for the setting `calva.highlight.bracketColors` is broken](https://github.com/BetterThanTomorrow/calva/issues/1290) +- [Add Paredit kill right functionality](https://github.com/BetterThanTomorrow/calva/issues/1024) ## [2.0.211] - 2021-09-01 - [Add setting for letting Paredit Kill commands copy the deleted code to the clipboard](https://github.com/BetterThanTomorrow/calva/issues/1283) diff --git a/package.json b/package.json index 7f8d69ffd..903e98f57 100644 --- a/package.json +++ b/package.json @@ -1862,6 +1862,11 @@ "key": "ctrl+shift+c", "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" }, + { + "command": "paredit.killHybridSexpForward", + "key": "ctrl+k", + "when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/" + }, { "command": "paredit.killSexpForward", "key": "ctrl+shift+delete", diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index f9ba086a8..991976d47 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -138,6 +138,48 @@ export function backwardListRange(doc: EditableDocument, start: number = doc.sel return [cursor.offsetStart, start]; } + +/** + * Modeled after paredit killRight or smartparens sp-kill-hybrid-sexp. + * Aims to find the end of the current form (list|string|vector etc) the + * first newline and delete up to the closing delimiter or newline. + * If deleting the new line would yield an invalid form then delete + * up to the end of the form that contains the newline. + * @param doc + * @param offset + * @param goPastWhitespace + * @returns [number, number] + */ +export function forwardHybridSexpRange(doc: EditableDocument, offset = Math.max(doc.selection.anchor, doc.selection.active), goPastWhitespace = false): [number, number] { + const cursor = doc.getTokenCursor(offset); + if (cursor.getToken().type === 'open') { + return forwardSexpRange(doc); + } + cursor.forwardList(); // move to the end of the current list|string|vector etc + const text = doc.model.getText(offset, cursor.offsetStart); + // should this be a regex that checks for \r, \n and/or \r\n? + const newLineIndex = text.indexOf("\n"); + let end = cursor.offsetStart; + if (newLineIndex > 0) { + // deleting to the newline could leave an invalid form + // so go forwardList from the newline and compare if + // the cursor offsets are different + const cursor2 = doc.getTokenCursor(offset + newLineIndex); + cursor2.forwardList(); + // no change offset? set end to be location of the newline + if (cursor.offsetStart === cursor2.offsetStart) { + end = offset + newLineIndex; + } else { + // offsets are different, so go to the end of cursor2.offsetEnd + // to include the closing delimiter + end = cursor2.offsetEnd; + } + } + return [offset, end]; +} + + + export function rangeToForwardUpList(doc: EditableDocument, offset: number = Math.max(doc.selection.anchor, doc.selection.active), goPastWhitespace = false): [number, number] { const cursor = doc.getTokenCursor(offset); cursor.forwardList(); diff --git a/src/extension-test/unit/cursor-doc/paredit-test.ts b/src/extension-test/unit/cursor-doc/paredit-test.ts index e763e9cea..62b0322e2 100644 --- a/src/extension-test/unit/cursor-doc/paredit-test.ts +++ b/src/extension-test/unit/cursor-doc/paredit-test.ts @@ -87,6 +87,88 @@ describe('paredit', () => { }); }) + describe('forwardHybridSexpRange', () => { + it('Finds end of string', () => { + const a = docFromTextNotation('"This |needs to find the end of the string."'); + const b = docFromTextNotation('"This |needs to find the end of the string.|"'); + 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."'); + const b = docFromTextNotation('"This |needs to find the end|\n of the string."'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + + it('Finds end of comment', () => { + const a = docFromTextNotation('(a |;; foo\n e)'); + const b = docFromTextNotation('(a |;; foo|\n e)'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + + it('Maintains balanced delimiters', () => { + const a = docFromTextNotation('(a| b (c\n d) e)'); + const b = docFromTextNotation('(a| b (c\n d)| e)'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + + it('Finds end of vectors', () => { + const a = docFromTextNotation('[a [b |c d e f] g h]'); + const b = docFromTextNotation('[a [b |c d e f|] g h]'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + + it('Finds end of maps', () => { + const a = docFromTextNotation('{:a 1 |:b 2 :c 3}'); + const b = docFromTextNotation('{:a 1 |:b 2 :c 3|}'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + + it('Finds end of line in multiline maps', () => { + const a = docFromTextNotation('{:a 1 |:b 2\n:c 3}'); + const b = docFromTextNotation('{:a 1 |:b 2|:c 3}'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + + it('Finds end of expr in multiline maps', () => { + const a = docFromTextNotation('{:a 1 |:b (+\n 0\n 2\n) :c 3}'); + const b = docFromTextNotation('{:a 1 |:b (+\n 0\n 2\n)| :c 3}'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + + it('Finds end of line in bindings', () => { + const a = docFromTextNotation('(let [|a (+ 1 2)\n b (+ 2 3)] (+ a b))'); + const b = docFromTextNotation('(let [|a (+ 1 2)|\n b (+ 2 3)] (+ a b))'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + + it('Finds end of expr in multiline bindings', () => { + const a = docFromTextNotation('(let [|a (+\n 1 \n 2)\n b (+ 2 3)] (+ a b))'); + const b = docFromTextNotation('(let [|a (+\n 1 \n 2)|\n b (+ 2 3)] (+ a b))'); + const expected = textAndSelection(b)[1]; + const actual = paredit.forwardHybridSexpRange(a); + expect(actual).toEqual(expected); + }); + }) + describe('moveToRangeRight', () => { it('Places cursor at the right end of the selection', () => { const a = docFromTextNotation('(def |>|foo|>| [vec])'); diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 4b25499ee..1708d79e7 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -184,6 +184,16 @@ const pareditCommands: PareditCommand[] = [ command: 'paredit.convolute', handler: paredit.convolute }, + { + command: 'paredit.killHybridSexpForward', + handler: (doc: EditableDocument) => { + const range = paredit.forwardHybridSexpRange(doc); + if (shouldKillAlsoCutToClipboard()) { + copyRangeToClipboard(doc, range); + } + paredit.killRange(doc, range); + } + }, { command: 'paredit.killSexpForward', handler: (doc: EditableDocument) => {