diff --git a/CHANGELOG.md b/CHANGELOG.md index ce4dc623d..8967d20a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ Changes to Calva. ## [Unreleased] -- [Implement experimental support for multicursor rewrap commands](https://github.com/BetterThanTomorrow/calva/issues/2448). Enable `calva.paredit.multicursor` in your settings to try it out. Closes [#2473](https://github.com/BetterThanTomorrow/calva/issues/2473) +- [Implement experimental support for multicursor selectCurrentForm command](https://github.com/BetterThanTomorrow/calva/issues/2476). Enable `calva.paredit.multicursor` in your settings to try it out. Closes [#2476](https://github.com/BetterThanTomorrow/calva/issues/2476) +- [Implement experimental support for multicursor rewrap commands](https://github.com/BetterThanTomorrow/calva/issues/2473). Enable `calva.paredit.multicursor` in your settings to try it out. Closes [#2473](https://github.com/BetterThanTomorrow/calva/issues/2473) ## [2.0.432] - 2024-03-26 diff --git a/docs/site/paredit.md b/docs/site/paredit.md index c92e27161..0b589cba1 100644 --- a/docs/site/paredit.md +++ b/docs/site/paredit.md @@ -125,7 +125,7 @@ Default keybinding | Action | Description You can have the *kill* commands always copy the deleted code to the clipboard by setting `calva.paredit.killAlsoCutsToClipboard` to `true`. If you want to do this more on-demand, you can kill text by using the [selection commands](#selecting) and then *Cut* once you have the selection. !!! Note "clojure-lsp drag fwd/back overlap" - As an experimental feature, the two commands for dragging forms forward and backward have clojure-lsp alternativs. See the [clojure-lsp](clojure-lsp.md#clojure-lsp-drag-fwdback) page. + As an experimental feature, the two commands for dragging forms forward and backward have clojure-lsp alternatives. See the [clojure-lsp](clojure-lsp.md#clojure-lsp-drag-fwdback) page. ### Drag bindings forward/backward diff --git a/package.json b/package.json index 46b55e33c..1770cb843 100644 --- a/package.json +++ b/package.json @@ -1343,12 +1343,6 @@ "enablement": "calva:connected", "category": "Calva" }, - { - "command": "calva.selectCurrentForm", - "title": "Select Current Form", - "category": "Calva", - "enablement": "editorLangId == clojure" - }, { "command": "calva.clearInlineResults", "title": "Clear Inline Evaluation Results", @@ -1567,6 +1561,12 @@ "title": "Move Cursor Forward to List End/Close", "enablement": "editorLangId == clojure" }, + { + "category": "Calva Paredit", + "command": "calva.selectCurrentForm", + "title": "Select Current Form", + "enablement": "editorLangId == clojure" + }, { "category": "Calva Paredit", "command": "paredit.selectForwardSexp", diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index a4b3b7789..3fcc489c6 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -49,6 +49,34 @@ export function selectRange(doc: EditableDocument, ranges: ModelEditRange[]) { growSelectionStack(doc, ranges); } +export function selectCurrentForm( + doc: EditableDocument, + topLevel: boolean, + selections = doc.selections +) { + const newSels = selections.map((sel) => { + const selection = sel; + if (selection.isCursor) { + let codeSelection; + const cursor = doc.getTokenCursor(selection.active); + const range = topLevel + ? cursor.rangeForDefun(selection.active) + : cursor.rangeForCurrentForm(selection.active); + if (range) { + codeSelection = new ModelEditSelection(range[0], range[1]); + } else { + codeSelection = undefined; + } + if (codeSelection) { + return codeSelection; + } + } + return sel; + }); + + growSelectionStack(doc, newSels.map(_.property('asDirectedRange'))); +} + export function selectRangeForward( doc: EditableDocument, ranges: ModelEditRange[], diff --git a/src/extension-test/unit/paredit/commands-test.ts b/src/extension-test/unit/paredit/commands-test.ts index 93d4a5f8a..3133d4d25 100644 --- a/src/extension-test/unit/paredit/commands-test.ts +++ b/src/extension-test/unit/paredit/commands-test.ts @@ -333,6 +333,99 @@ describe('paredit commands', () => { }); describe('selection', () => { + describe('selectCurrentForm', () => { + it('Single-cursor: handles cases like reader tags/metadata + keeps other selections ', () => { + const a = docFromTextNotation( + '(defn|1 [a b]•(let [^js a|a #p (+ a)•b b]•{:a aa•:b b}))•(:a)' + ); + const aSelections = a.selections; + const b = docFromTextNotation( + '(defn [a b]•(let [|^js aa| #p (+ a)•b b]•{:a aa•:b b}))•(:a)' + ); + handlers.selectCurrentForm(a, false); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(a.selectionsStack).toEqual([aSelections, b.selections]); + }); + it('Multi-cursor: handles cases like reader tags/metadata + keeps other selections ', () => { + const a = docFromTextNotation( + '(defn|1 |2[a b]•(let [|3^js aa |4#p (+ a)•<5b b<5]•{:a aa•:b b}))•(:|a)' + ); + const aSelections = a.selections; + const b = docFromTextNotation( + '(|1defn|1 |2[a b]|2•(let [|3^js aa|3 |4#p (+ a)|4•<5b b<5]•{:a aa•:b b}))•(|:a|)' + ); + handlers.selectCurrentForm(a, true); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(a.selectionsStack).toEqual([aSelections, b.selections]); + }); + + it('Single-cursor: handles cursor at a distance from form', () => { + const a = docFromTextNotation('[|1 a b |2c d { e f}|3 g |]'); + const aSelections = a.selections; + const b = docFromTextNotation('[ a b c d { e f} |g| ]'); + handlers.selectCurrentForm(a, false); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(a.selectionsStack).toEqual([aSelections, b.selections]); + }); + it('Multi-cursor: handles cursor at a distance from form', () => { + const a = docFromTextNotation('[|1 a b |2c d { e f}|3 g |]'); + const aSelections = a.selections; + const b = docFromTextNotation('[ |1a|1 b |2c|2 d |3{ e f}|3 |g| ]'); + handlers.selectCurrentForm(a, true); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(a.selectionsStack).toEqual([aSelections, b.selections]); + }); + + it('Single-cursor: collapses overlapping selections', () => { + const a = docFromTextNotation( + '(de|1fn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)' + ); + const aSelections = a.selections; + const b = docFromTextNotation( + '(|defn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)' + ); + handlers.selectCurrentForm(a, false); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(a.selectionsStack).toEqual([aSelections, b.selections]); + }); + it('Multi-cursor: collapses overlapping selections', () => { + const a = docFromTextNotation( + '(de|5fn|1 |2[a b]•(let [|3^js aa |4#p (+ a)•<5b b<5]•{:a aa•:b b}))•(:|a)' + ); + const aSelections = a.selections; + const b = docFromTextNotation( + '(|1defn|1 |2[a b]|2•(let [|3^js aa|3 |4#p (+ a)|4•<5b b<5]•{:a aa•:b b}))•(|:a|)' + ); + handlers.selectCurrentForm(a, true); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(a.selectionsStack).toEqual([aSelections, b.selections]); + }); + + it('Single-cursor: collapses overlapping selections preferring the larger one', () => { + const a = docFromTextNotation( + '(de|1fn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)' + ); + const aSelections = a.selections; + const b = docFromTextNotation( + '(|defn| [a b]•(let [^js aa #p (+ a)•b b]•{:a aa•:b b}))•(:a)' + ); + handlers.selectCurrentForm(a, false); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(a.selectionsStack).toEqual([aSelections, b.selections]); + }); + it('Multi-cursor: collapses overlapping selections preferring the larger one', () => { + const a = docFromTextNotation( + '(defn [a b]•(let [^js aa #p (+ a)•b |b]|1•|3{:a a|2a•:b b}))•(:a)' + ); + const aSelections = a.selections; + const b = docFromTextNotation( + '(defn [a b]•(let |1[^js aa #p (+ a)•b b]|1•|3{:a aa•:b b}|3))•(:a)' + ); + handlers.selectCurrentForm(a, true); + expect(textNotationFromDoc(a)).toEqual(textNotationFromDoc(b)); + expect(a.selectionsStack).toEqual([aSelections, b.selections]); + }); + }); describe('rangeForDefun', () => { it('Single-cursor:', () => { const a = docFromTextNotation( diff --git a/src/extension.ts b/src/extension.ts index c3a436ec2..1c19994d6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,7 +16,6 @@ import * as definition from './providers/definition'; import { CalvaSignatureHelpProvider } from './providers/signature'; import testRunner from './testRunner'; import annotations from './providers/annotations'; -import * as select from './select'; import eval from './evaluate'; import refresh from './refresh'; import * as greetings from './greet'; @@ -258,7 +257,6 @@ async function activate(context: vscode.ExtensionContext) { runCustomREPLCommand: snippets.evaluateCustomCodeSnippetCommand, runNamespaceTests: () => testRunner.runNamespaceTestsCommand(testController), runTestUnderCursor: () => testRunner.runTestUnderCursorCommand(testController), - selectCurrentForm: select.selectCurrentForm, sendCurrentFormToOutputWindow: outputWindow.appendCurrentForm, openFiddleForSourceFile: fiddleFiles.openFiddleForSourceFile, evaluateFiddleForSourceFile: fiddleFiles.evaluateFiddleForSourceFile, diff --git a/src/paredit/commands.ts b/src/paredit/commands.ts index 9e29d28f8..6ccf7208c 100644 --- a/src/paredit/commands.ts +++ b/src/paredit/commands.ts @@ -56,6 +56,9 @@ export function openList(doc: EditableDocument, isMulti: boolean = false) { // SELECTION +export function selectCurrentForm(doc: EditableDocument, isMulti: boolean = false) { + paredit.selectCurrentForm(doc, false, isMulti ? doc.selections : [doc.selections[0]]); +} export function rangeForDefun(doc: EditableDocument, isMulti: boolean) { const selections = isMulti ? doc.selections : [doc.selections[0]]; const ranges = selections.map((s) => paredit.rangeForDefun(doc, s.active)); diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index a30c421e0..e7899d973 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -114,6 +114,13 @@ const pareditCommands: PareditCommand[] = [ }, // SELECTING + { + command: 'calva.selectCurrentForm', // legacy command id for backward compat + handler: (doc: EditableDocument) => { + const isMulti = multiCursorEnabled(); + handlers.selectCurrentForm(doc, isMulti); + }, + }, { command: 'paredit.rangeForDefun', handler: (doc: EditableDocument) => { diff --git a/src/select.ts b/src/select.ts index 5bcd4d2dd..fd755fb24 100644 --- a/src/select.ts +++ b/src/select.ts @@ -37,28 +37,3 @@ export function getEnclosingFormSelection( } } } - -function selectForm( - document = {}, - selectionFn: ( - doc: vscode.TextDocument, - pos: vscode.Position, - topLevel: boolean - ) => vscode.Selection | undefined, - toplevel: boolean -) { - const editor = util.getActiveTextEditor(), - doc = util.getDocument(document), - selection = editor.selections[0]; - - if (selection.isEmpty) { - const codeSelection = selectionFn(doc, selection.active, toplevel); - if (codeSelection) { - editor.selections = [codeSelection]; - } - } -} - -export function selectCurrentForm(document = {}) { - selectForm(document, getFormSelection, false); -}