Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Changes to Calva.

## [Unreleased]

- Fix: [Make sexp cursor movement (and selection) in comments consistently act like VSCode's default. The changes apply to SelectForwardSexp, SelectBackwardSexp,
ForwardSexp, and BackwardSexp](https://github.com/BetterThanTomorrow/calva/issues/2878)

## [2.0.523] - 2025-07-14

- Fix: [[doc] Unneeded send-off function call in sample code](https://github.com/BetterThanTomorrow/calva/issues/2888)
Expand Down
4 changes: 4 additions & 0 deletions docs/site/customizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ The following contexts are available with Calva:
* `calva:cursorInComment`: `true` when the cursor is in, or adjacent to a line comment
* `calva:cursorBeforeComment`: `true` when the cursor is adjacent before a line comment
* `calva:cursorAfterComment`: `true` when the cursor is adjacent after a line comment
* `calva:cursorSeesCommentPrev`: `true` when the the previous character, excluding whitespace, is a comment character.
It is true even if the cursor is inside a comment, but not if it is before the comment character.
* `calva:cursorSeesCommentNext`: `true` when the next character, excluding whitespace, is part of a comment.
It is true even if inside a comment, but not if at the newline that ends a comment.
* `calva:cursorAtStartOfLine`: `true` when the cursor is at the start of a line including any leading whitespace
* `calva:cursorAtEndOfLine`: `true` when the cursor is at the end of a line including any trailing whitespace

Expand Down
4 changes: 4 additions & 0 deletions docs/site/when-clauses.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ description: Calva comes with batteries included and preconfigured, and if you d
* `calva:cursorInComment`: `true` when the cursor is in, or adjacent to a line comment
* `calva:cursorBeforeComment`: `true` when the cursor is adjacent before a line comment
* `calva:cursorAfterComment`: `true` when the cursor is adjacent after a line comment
* `calva:cursorSeesCommentPrev`: `true` when the the previous character, excluding whitespace, is a comment character.
It is true even if the cursor is inside a comment, but not if it is before the comment character.
* `calva:cursorSeesCommentNext`: `true` when the next character, excluding whitespace, is part of a comment.
It is true even if inside a comment, but not if at the newline that ends a comment.
* `calva:cursorAtStartOfLine`: `true` when the cursor is at the start of a line including any leading whitespace
* `calva:cursorAtEndOfLine`: `true` when the cursor is at the end of a line including any trailing whitespace
* `calva:projectRoot`: A string with the absolute path to the repl project root, _without trailing slash_
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Calva: Clojure & ClojureScript Interactive Programming",
"description": "Integrated REPL, formatter, Paredit, and more. Powered by cider-nrepl and clojure-lsp.",
"icon": "assets/calva.png",
"version": "2.0.523",
"version": "2.0.524",
"publisher": "betterthantomorrow",
"author": {
"name": "Better Than Tomorrow",
Expand Down Expand Up @@ -2385,28 +2385,28 @@
"mac": "ctrl+left",
"win": "alt+left",
"linux": "alt+left",
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorBeforeComment"
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !config.calva.paredit.hijackVSCodeDefaults && !calva:cursorSeesCommentPrev"
},
{
"command": "paredit.backwardSexp",
"mac": "alt+left",
"win": "ctrl+left",
"linux": "ctrl+left",
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorBeforeComment"
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorSeesCommentPrev"
},
{
"command": "paredit.forwardSexp",
"mac": "ctrl+right",
"win": "alt+right",
"linux": "alt+right",
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && !config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorAfterComment"
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorSeesCommentNext"
},
{
"command": "paredit.forwardSexp",
"mac": "alt+right",
"win": "ctrl+right",
"linux": "ctrl+right",
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment || calva:cursorAfterComment"
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorSeesCommentNext"
},
{
"command": "paredit.forwardDownSexp",
Expand Down Expand Up @@ -2443,7 +2443,7 @@
"mac": "shift+alt+right",
"win": "shift+ctrl+right",
"linux": "shift+ctrl+right",
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment && !calva:cursorAfterComment && !calva:cursorBeforeComment"
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorSeesCommentNext"
},
{
"command": "paredit.selectRight",
Expand All @@ -2457,7 +2457,7 @@
"mac": "shift+alt+left",
"win": "shift+ctrl+left",
"linux": "shift+ctrl+left",
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorInComment && !calva:cursorAfterComment && !calva:cursorBeforeComment"
"when": "calva:keybindingsEnabled && editorLangId == clojure && editorTextFocus && paredit:keyMap =~ /original|strict/ && config.calva.paredit.hijackVSCodeDefaults && !calva:cursorSeesCommentPrev"
},
{
"command": "paredit.selectForwardDownSexp",
Expand Down
58 changes: 58 additions & 0 deletions src/cursor-doc/cursor-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const allCursorContexts = [
'calva:cursorAtEndOfLine',
'calva:cursorBeforeComment',
'calva:cursorAfterComment',
'calva:cursorSeesCommentNext',
'calva:cursorSeesCommentPrev',
] as const;

export type CursorContext = typeof allCursorContexts[number];
Expand Down Expand Up @@ -56,6 +58,54 @@ export function isAtLineEndInclWS(doc: EditableDocument, offset = doc.selections
return false;
}

/**
* when true, a comment is visible upstream from the cursor. Because this is
* used to govern SelectBackwardSexp, eol (actually beginning of line) is considered a
* comment so selection reverts back to VSCode's selection
*/
function hasPrevComment(doc: EditableDocument, offset: number) {
const findCursorLineOffset = (doc: EditableDocument, cursorOffset: number): number => {
const documentText = doc.model.getText(0, cursorOffset);
const startOfLineOffset = documentText.lastIndexOf('\n') + 1;
return cursorOffset - startOfLineOffset;
};

const backCursor = doc.getTokenCursor(offset);

while (!backCursor.atStart()) {
const tokenType = backCursor.getPrevToken().type;
if (tokenType === 'comment' || tokenType === 'prompt') {
return true;
} else if (tokenType === 'eol' || tokenType === 'ws') {
backCursor.previous();
if (['comment', 'prompt', 'eol'].includes(backCursor.getPrevToken().type)) {
return true;
} else {
// non-comment token, check forward beyond any whitespace for a comment
// that starts before the cursor position
backCursor.forwardWhitespace(false);
const cursorLineOffset = findCursorLineOffset(doc, offset);
const currToken = backCursor.getToken();
return currToken.type === 'comment' && currToken.offset < cursorLineOffset;
}
} else {
return backCursor.getToken().type === 'comment';
}
}
return false;
}

/**
* when true, a comment is visible downstream from the cursor. Because this is
* used to govern SelectForwardSexp, eol is considered a comment so selection reverts
* back to VSCode's selection
*/
function hasNextComment(doc: EditableDocument, offset: number) {
const nextCursor = doc.getTokenCursor(offset);
nextCursor.forwardWhitespace(false);
return nextCursor.getToken().type === 'comment' || nextCursor.getToken().type === 'eol';
}

export function determineContexts(
doc: EditableDocument,
offset = doc.selections[0].active
Expand Down Expand Up @@ -90,5 +140,13 @@ export function determineContexts(
}
}

if (hasNextComment(doc, offset)) {
contexts.push('calva:cursorSeesCommentNext');
}

if (hasPrevComment(doc, offset)) {
contexts.push('calva:cursorSeesCommentPrev');
}

return contexts;
}
2 changes: 1 addition & 1 deletion src/cursor-doc/paredit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function selectForwardSexp(doc: EditableDocument, selections = doc.select
const ranges = selections.map((selection) =>
selection.active >= selection.anchor
? forwardSexpRange(doc, selection.end)
: forwardSexpRange(doc, selection.active, true)
: forwardSexpRange(doc, selection.active, false)
);
selectRangeForward(doc, ranges, selections);
}
Expand Down
96 changes: 96 additions & 0 deletions src/extension-test/unit/cursor-doc/cursor-context-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,102 @@ describe('Cursor Contexts', () => {
expect(contexts.includes('calva:cursorInComment')).toBe(false);
});
});
describe('cursorSeesCommentPrev', () => {
it('sees a comment upstream', () => {
const contexts = context.determineContexts(docFromTextNotation(';; my comment |'));
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(true);
});
it('sees an sexp upstream', () => {
const contexts = context.determineContexts(docFromTextNotation('(+ 1 1)|'));
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(false);
});
it('sees an sexp upstream and a comment downstream, cursor on the comment offset', () => {
const contexts = context.determineContexts(docFromTextNotation('(+ 1 1) |; my comment'));
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(false);
});
it('sees a comment followed by an sexp upstream', () => {
const contexts = context.determineContexts(docFromTextNotation('(+ 1 1) ;| my comment \n'));
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(true);
});
it('inside an sexp, no comment seen', () => {
const contexts = context.determineContexts(docFromTextNotation('(+ 1 1|) ; my comment \n'));
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(false);
});
it('beginning of document is treated as comment', () => {
const contexts = context.determineContexts(
docFromTextNotation('\n | (+ 1 1) ; my comment \n')
);
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(true);
});
it('sees a comment prev when cursor is after a comment', () => {
const contexts = context.determineContexts(
docFromTextNotation('(do-something) ; a comment|')
);
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(true);
});
it('sees a comment upstream when cursor is on an empty line', () => {
const contexts = context.determineContexts(docFromTextNotation(';; a comment\n|\n(def a 1)'));
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(true);
});
it('does not see a comment upstream when cursor is on an empty line with code after', () => {
const contexts = context.determineContexts(docFromTextNotation('(def a 1)\n|\n(def b 2)'));
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(false);
});
it('should see a comment upstream when there are multiple breaks', () => {
const contexts = context.determineContexts(
docFromTextNotation('{}\n ;; More comments go |here\n\n#{}\n')
);
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(true);
});
it('should see a comment upstream when the previous token is a close', () => {
const contexts = context.determineContexts(
docFromTextNotation('{}\n {};; More comments go |here\n\n#{}\n')
);
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(true);
});
it('should see a comment upstream when the previous token is an sexp', () => {
const contexts = context.determineContexts(
docFromTextNotation('{}\n {};|; More comments go here\n\n#{}\n')
);
expect(contexts.includes('calva:cursorSeesCommentPrev')).toBe(true);
});
});
describe('cursorSeesCommentNext', () => {
it('sees a comment downstream', () => {
const contexts = context.determineContexts(docFromTextNotation('| ;; my comment'));
expect(contexts.includes('calva:cursorSeesCommentNext')).toBe(true);
});
it('sees an sexp downstream', () => {
const contexts = context.determineContexts(docFromTextNotation('| (+ 1 1)'));
expect(contexts.includes('calva:cursorSeesCommentNext')).toBe(false);
});
it('sees an sexp followed by a comment downstream', () => {
const contexts = context.determineContexts(docFromTextNotation('| (+ 1 1) ; my comment'));
expect(contexts.includes('calva:cursorSeesCommentNext')).toBe(false);
});
it('sees a comment followed by an sexp downstream', () => {
const contexts = context.determineContexts(docFromTextNotation('| ; my comment \n (+ 1 1) '));
expect(contexts.includes('calva:cursorSeesCommentNext')).toBe(true);
});
it('sees a comment downstream with sexp upstream', () => {
const contexts = context.determineContexts(docFromTextNotation('(+ 1 1) | ; my comment \n'));
expect(contexts.includes('calva:cursorSeesCommentNext')).toBe(true);
});
it('end of document is treated as comment', () => {
const contexts = context.determineContexts(
docFromTextNotation(' (+ 1 1) ; my comment \n|\n')
);
expect(contexts.includes('calva:cursorSeesCommentNext')).toBe(true);
});
it('sees a comment downstream when cursor is on an empty line', () => {
const contexts = context.determineContexts(docFromTextNotation('(def a 1)\n|\n;; a comment'));
expect(contexts.includes('calva:cursorSeesCommentNext')).toBe(true);
});
it('does not see a comment downstream when cursor is on an empty line with code after', () => {
const contexts = context.determineContexts(docFromTextNotation('(def a 1)\n|\n(def b 2)'));
expect(contexts.includes('calva:cursorSeesCommentNext')).toBe(false);
});
});
describe('cursorBeforeComment', () => {
it('is false in comment', () => {
const contexts = context.determineContexts(
Expand Down
2 changes: 1 addition & 1 deletion src/extension-test/unit/paredit/commands-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ describe('paredit commands', () => {
);
const aSelections = a.selections;
const b = docFromTextNotation(
'(defn|1 |1[a b]•(let [^js |2aa #p (+ a)|2•b b]•{:a aa•:b b}))•(:|a|)'
'(defn|1 [a b]•(let [^js |2aa #p (+ a)|2•b b]•{:a aa•:b b}))•(:|a|)'
);
handlers.selectForwardSexp(a, true);
expect(a.selectionsStack).toEqual([aSelections, b.selections]);
Expand Down