diff --git a/CHANGELOG.md b/CHANGELOG.md index 63a896bbc..799a6c840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ Changes to Calva. ## [Unreleased] +## [2.0.481] - 2024-10-29 + +- [Add extension when contexts for Calva states such as project root, session type, ns](https://github.com/BetterThanTomorrow/calva/issues/2652) +- Fix: [Calva internals: The `backwardSexp` function can't handle skipping ignored forms, even though it says it can](https://github.com/BetterThanTomorrow/calva/issues/2657) +- Fix: [Keep support for evaluating top level form in ignored forms when at top level](https://github.com/BetterThanTomorrow/calva/issues/2655) +- [Enable separate styling for top level ignored forms](https://github.com/BetterThanTomorrow/calva/issues/2660) + ## [2.0.480] - 2024-10-21 - Fix: [Custom command snippets use the wrong ns when repl sessions types do not match](https://github.com/BetterThanTomorrow/calva/issues/2653) diff --git a/docs/site/evaluation.md b/docs/site/evaluation.md index d2c29a4d0..53b295ebd 100644 --- a/docs/site/evaluation.md +++ b/docs/site/evaluation.md @@ -35,7 +35,7 @@ Some of the commands also let you choose what should happen with the results: * The `line` style is the default. * The `ignore` style will put an ignore marker (`#_`) before the result. * The `rcf` style will wrap the result in a rich comment form ( `(comment ...)`). - + Here are some example keybindings for using the different comment styles with the **Evaluate Top Level Form (defun) to Comment** command: ```jsonc @@ -108,6 +108,8 @@ The **current top-level form** means top-level in a structural sense. It is _not An ”exception” is introduced by the `comment` form. It will create a new top level context, so that any forms immediately inside a `(comment ...)` form will be considered top-level by Calva. This is to support a workflow with what is often referred to the [Rich Comments](rich-comments.md). +A special case is ignored forms (using the `#_` marker) at the top level. They will always be selected as top level forms separately from their ignore marker, enabling evaluating them as top level forms. Similar to Rich Comments. + At the top level the selection of which form is the current top level form follows the same rules as those for [the current form](#current-form). ### Evaluate Enclosing Form diff --git a/docs/site/syntax-highlighting.md b/docs/site/syntax-highlighting.md index c85ebdafa..83f6cc41b 100644 --- a/docs/site/syntax-highlighting.md +++ b/docs/site/syntax-highlighting.md @@ -43,7 +43,8 @@ You are in charge of how brackets and comments are highlighted via the `calva.hi | `cycleBracketColors` | Whether same colors should be
reused for deeply nested brackets | `true` | | `misplacedBracketStyle` | Style of misplaced bracket | `{ "border": "2px solid #c33" }` | | `matchedBracketStyle` | Style of bracket pair highlight | `{"backgroundColor": "#E0E0E0"}` | -| `ignoredFormStyle` | Style of `#_...` form | `{"textDecoration": "none; opacity: 0.5"}` | +| `ignoredFormStyle` | Style of `#_...` forms | `{"textDecoration": "none; opacity: 0.5"}` | +| `ignoredTopLevelFormStyle` | Style of `#_...` forms at the top level. (If not set, uses `ignoredFormStyle`) | `{ "textDecoration": "none; text-shadow: 2px 2px 5px rgba(255, 215, 0, 0.75)" }` | | `commentFormStyle` | Style of `(comment ...)` form | `{"fontStyle": "italic"}` | !!! Note "Calva disables the VS Code built-in indent guides" diff --git a/docs/site/when-clauses.md b/docs/site/when-clauses.md index 9b6174a51..870b30925 100644 --- a/docs/site/when-clauses.md +++ b/docs/site/when-clauses.md @@ -19,4 +19,6 @@ description: Calva comes with batteries included and preconfigured, and if you d * `calva:cursorAfterComment`: `true` when the cursor is adjacent after a line 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_ +* `calva:ns`: A string with the current namespace +* `calva:replSessionType`: `clj`, or `cljs` depending on the file type of the current file diff --git a/package-lock.json b/package-lock.json index 3e6f8b4f8..1391133c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "calva", - "version": "2.0.480", + "version": "2.0.481", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "calva", - "version": "2.0.480", + "version": "2.0.481", "license": "MIT", "dependencies": { "@vscode/debugadapter": "^1.64.0", diff --git a/package.json b/package.json index ec7c5185c..b425e30ab 100644 --- a/package.json +++ b/package.json @@ -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.480", + "version": "2.0.481", "publisher": "betterthantomorrow", "author": { "name": "Better Than Tomorrow", @@ -1142,6 +1142,12 @@ "default": null, "description": "Style of `#_` ignored forms", "scope": "resource" + }, + "calva.highlight.ignoredTopLevelFormStyle": { + "type": "object", + "default": null, + "markdownDescription": "Style of top level `#_` ignored forms. If not specified, it will be the same as what's set for `calva.highlight.ignoredFormStyle`", + "scope": "resource" } } } diff --git a/src/cursor-doc/token-cursor.ts b/src/cursor-doc/token-cursor.ts index f4e91b041..caf3e87fd 100644 --- a/src/cursor-doc/token-cursor.ts +++ b/src/cursor-doc/token-cursor.ts @@ -316,12 +316,7 @@ export class LispTokenCursor extends TokenCursor { * * @returns true if the cursor was moved, false otherwise. */ - backwardSexp( - skipComments = true, - skipMetadata = false, - skipIgnoredForms = false, - skipReaders = true - ) { + backwardSexp(skipComments = true, skipMetadata = false, skipReaders = true) { const stack = []; this.backwardWhitespace(skipComments); if (this.getPrevToken().type === 'open') { @@ -345,9 +340,9 @@ export class LispTokenCursor extends TokenCursor { } if (skipMetadata) { const metaCursor = this.clone(); - metaCursor.backwardSexp(true, false, false, false); + metaCursor.backwardSexp(true, false, false); if (metaCursor.tokenBeginsMetadata()) { - this.backwardSexp(skipComments, skipMetadata, skipIgnoredForms); + this.backwardSexp(skipComments, skipMetadata, skipReaders); } } if (skipReaders) { @@ -376,9 +371,9 @@ export class LispTokenCursor extends TokenCursor { } if (skipMetadata) { const metaCursor = this.clone(); - metaCursor.backwardSexp(true, false, false, false); + metaCursor.backwardSexp(true, false, false); if (metaCursor.tokenBeginsMetadata()) { - this.backwardSexp(skipComments, skipMetadata, skipIgnoredForms); + this.backwardSexp(skipComments, skipMetadata, skipReaders); } } if (skipReaders) { diff --git a/src/cursor-doc/utilities.ts b/src/cursor-doc/utilities.ts index 00cafdb03..b2c110e4a 100644 --- a/src/cursor-doc/utilities.ts +++ b/src/cursor-doc/utilities.ts @@ -41,7 +41,7 @@ export function isRightSexpStructural(cursor: LispTokenCursor): boolean { return false; } cursor.forwardSexp(true, true, false); - cursor.backwardSexp(false, false, false, false); + cursor.backwardSexp(false, false, false); const token = cursor.getToken(); if (token.type === 'open') { 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 9c2692180..2bd377d6e 100644 --- a/src/extension-test/unit/cursor-doc/token-cursor-test.ts +++ b/src/extension-test/unit/cursor-doc/token-cursor-test.ts @@ -228,6 +228,20 @@ describe('Token Cursor', () => { cursor.backwardSexp(true, true); expect(cursor.offsetStart).toBe(b.selections[0].anchor); }); + it('Does not skip ignored forms if skipIgnoredForms', () => { + const a = docFromTextNotation('(a #_1 #_2 |3)'); + const b = docFromTextNotation('(a #_a #_|2 3)'); + const cursor = a.getTokenCursor(a.selections[0].anchor); + cursor.backwardSexp(true, true); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); + }); + it('Does not skip stacked ignored forms', () => { + const a = docFromTextNotation('(a #_ #_ 1 2 |3)'); + const b = docFromTextNotation('(a #_ #_ 1 |2 3)'); + const cursor = a.getTokenCursor(a.selections[0].anchor); + cursor.backwardSexp(true, true); + expect(cursor.offsetStart).toBe(b.selections[0].anchor); + }); }); describe('downList', () => { @@ -790,6 +804,13 @@ describe('Token Cursor', () => { const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1]); }); + // https://github.com/BetterThanTomorrow/calva/issues/2655 + it('Does not include ignore marker', () => { + const a = docFromTextNotation('a #_ [b (c|)] [d]'); + const b = docFromTextNotation('a #_ |[b (c)]| [d]'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1]); + }); describe('Rich Comment Form top level context', () => { it('Finds range for a top level form inside a comment', () => { const a = docFromTextNotation('aaa (comment [bbb cc|c] ddd)'); @@ -940,6 +961,13 @@ describe('Token Cursor', () => { const cursor: LispTokenCursor = a.getTokenCursor(0); expect(cursor.rangeForDefun(a.selections[0].anchor)).toEqual(textAndSelection(b)[1]); }); + // https://github.com/BetterThanTomorrow/calva/issues/2655 + it('Does not include ignore marker', () => { + const a = docFromTextNotation('aaa (comment #_ [bbb ccc|] ddd)'); + const b = docFromTextNotation('aaa (comment #_ |[bbb ccc]| ddd)'); + const cursor: LispTokenCursor = a.getTokenCursor(a.selections[0].active); + expect(cursor.rangeForDefun(a.selections[0].active)).toEqual(textAndSelection(b)[1]); + }); }); }); diff --git a/src/highlight/src/extension.ts b/src/highlight/src/extension.ts index 77ed1d0b1..a5723e6c2 100755 --- a/src/highlight/src/extension.ts +++ b/src/highlight/src/extension.ts @@ -39,6 +39,8 @@ let lastHighlightedEditor, commentFormType: vscode.TextEditorDecorationType, ignoredFormStyle, ignoredFormType: vscode.TextEditorDecorationType, + ignoredTopLevelFormStyle, + ignoredTopLevelFormType: vscode.TextEditorDecorationType, enableBracketColors, useRainbowIndentGuides, highlightActiveIndent, @@ -152,6 +154,13 @@ function reset_styles() { } ignoredFormType = decorationType(ignoredFormStyle || { textDecoration: 'none; opacity: 0.5' }); + if (ignoredTopLevelFormType) { + activeEditor.setDecorations(ignoredTopLevelFormType, []); + } + ignoredTopLevelFormType = decorationType( + ignoredTopLevelFormStyle || ignoredFormStyle || { textDecoration: 'none; opacity: 0.5' } + ); + dirty = false; } @@ -206,6 +215,11 @@ function reloadConfig() { dirty = true; } + if (!isEqual(ignoredTopLevelFormStyle, configuration.get('ignoredTopLevelFormStyle'))) { + ignoredTopLevelFormStyle = configuration.get('ignoredTopLevelFormStyle'); + dirty = true; + } + if (dirty) { scheduleRainbowBrackets(); } @@ -231,18 +245,19 @@ function updateRainbowBrackets() { reset_styles(); } - const doc = activeEditor.document, - mirrorDoc = docMirror.getDocument(doc), - rainbow = rainbowTypes.map(() => []), - rainbowGuides = rainbowTypes.map(() => []), - misplaced = [], - comment_forms = [], - ignores = [], - len = rainbowTypes.length, - colorsEnabled = enableBracketColors && len > 0, - guideColorsEnabled = useRainbowIndentGuides && len > 0, - activeGuideEnabled = highlightActiveIndent && len > 0, - colorIndex = cycleBracketColors ? (i) => i % len : (i) => Math.min(i, len - 1); + const doc = activeEditor.document; + const mirrorDoc = docMirror.getDocument(doc); + const rainbow = rainbowTypes.map(() => []); + const rainbowGuides = rainbowTypes.map(() => []); + const misplaced = []; + const comment_forms = []; + const ignores = []; + const topLevelIgnores = []; + const len = rainbowTypes.length; + const colorsEnabled = enableBracketColors && len > 0; + const guideColorsEnabled = useRainbowIndentGuides && len > 0; + const activeGuideEnabled = highlightActiveIndent && len > 0; + const colorIndex = cycleBracketColors ? (i) => i % len : (i) => Math.min(i, len - 1); let in_comment_form = false; let stack_depth = 0; @@ -252,14 +267,14 @@ function updateRainbowBrackets() { placedGuidesColor = new Map(); activeEditor.visibleRanges.forEach((range) => { // Find the visible forms - const startOffset = doc.offsetAt(range.start), - endOffset = doc.offsetAt(range.end), - startCursor: LispTokenCursor = mirrorDoc.getTokenCursor(0), - startRange = startCursor.rangeForDefun(startOffset, false), - endCursor: LispTokenCursor = mirrorDoc.getTokenCursor(endOffset), - endRange = endCursor.rangeForDefun(endOffset, false), - rangeStart = startRange ? startRange[0] : startOffset, - rangeEnd = endRange ? endRange[1] : endOffset; + const startOffset = doc.offsetAt(range.start); + const endOffset = doc.offsetAt(range.end); + const startCursor: LispTokenCursor = mirrorDoc.getTokenCursor(0); + const startRange = startCursor.rangeForDefun(startOffset, false); + const endCursor: LispTokenCursor = mirrorDoc.getTokenCursor(endOffset); + const endRange = endCursor.rangeForDefun(endOffset, false); + const rangeStart = startRange ? startRange[0] : startOffset; + const rangeEnd = endRange ? endRange[1] : endOffset; // Look for top level ignores, and adjust starting point if found const topLevelSentinelCursor = mirrorDoc.getTokenCursor(rangeStart); let startPaintingFrom = rangeStart; @@ -299,7 +314,11 @@ function updateRainbowBrackets() { ignoreCursor.forwardSexp(true, true, true); } const ignore_end = activeEditor.document.positionAt(ignoreCursor.offsetStart); - ignores.push(new Range(ignore_start, ignore_end)); + if (cursor.atTopLevel()) { + topLevelIgnores.push(new Range(ignore_start, ignore_end)); + } else { + ignores.push(new Range(ignore_start, ignore_end)); + } } } const token = cursor.getToken(), @@ -399,6 +418,7 @@ function updateRainbowBrackets() { activeEditor.setDecorations(misplacedType, misplaced); activeEditor.setDecorations(commentFormType, comment_forms); activeEditor.setDecorations(ignoredFormType, ignores); + activeEditor.setDecorations(ignoredTopLevelFormType, topLevelIgnores); matchPairs(); if (activeGuideEnabled) { decorateActiveGuides(); diff --git a/src/state.ts b/src/state.ts index 6e319f71b..12d1b1c1c 100644 --- a/src/state.ts +++ b/src/state.ts @@ -202,6 +202,8 @@ export async function initProjectDir( ); } if (projectRootPath) { + console.log('Setting project root to: ', projectRootPath.fsPath); + void vscode.commands.executeCommand('setContext', 'calva:projectRoot', projectRootPath.fsPath); setStateValue(PROJECT_DIR_KEY, projectRootPath.fsPath); setStateValue(PROJECT_DIR_URI_KEY, projectRootPath); return projectRootPath; diff --git a/src/when-contexts.ts b/src/when-contexts.ts index 94a2e81f7..f2556979b 100644 --- a/src/when-contexts.ts +++ b/src/when-contexts.ts @@ -3,6 +3,9 @@ import { deepEqual } from './util/object'; import * as docMirror from './doc-mirror'; import * as context from './cursor-doc/cursor-context'; import * as util from './utilities'; +import * as namespace from './namespace'; +import * as session from './nrepl/repl-session'; +import { cljsLib } from './utilities'; export let lastContexts: context.CursorContext[] = []; export let currentContexts: context.CursorContext[] = []; @@ -18,6 +21,10 @@ export function setCursorContextIfChanged(editor: vscode.TextEditor) { } const contexts = determineCursorContexts(editor.document, editor.selections[0].active); setCursorContexts(contexts); + const [ns, _form] = namespace.getDocumentNamespace(editor.document); + void vscode.commands.executeCommand('setContext', 'calva:ns', ns); + const sessionType = session.getReplSessionType(cljsLib.getStateValue('connected')); + void vscode.commands.executeCommand('setContext', 'calva:replSessionType', sessionType); } function determineCursorContexts( diff --git a/test-data/.vscode/settings.json b/test-data/.vscode/settings.json index 4c610fb24..f9cd699ae 100644 --- a/test-data/.vscode/settings.json +++ b/test-data/.vscode/settings.json @@ -139,5 +139,8 @@ "afterCL]ReplJackInCode": ["(println :hello)", "(println :world!)"], "cljsType": "none" } - ] + ], + "calva.highlight.ignoredTopLevelFormStyle": { + "textDecoration": "none; text-shadow: 2px 2px 5px rgba(255, 215, 0, 0.75)" + } } diff --git a/test-data/test-files/highlight_test.clj b/test-data/test-files/highlight_test.clj index 4ce8cd123..7f9489f73 100644 --- a/test-data/test-files/highlight_test.clj +++ b/test-data/test-files/highlight_test.clj @@ -22,7 +22,7 @@ ;; \ "()" ;; \ -;; +;; (((#((()))))) ([ #{ }()[]]) @@ -54,10 +54,10 @@ (println "I ❤️Clojure") ([{} () []])) - + (comment (+ (* 2 2) - 2) + 2) (Math/abs -1) (defn hello [s] (str "Hello " s)) @@ -143,7 +143,7 @@ bar [:c {:d :e}]] [:b [:c {:d :e}]]] -(comment +(comment (foo #_"bar" baz)) #_{:foo "foo" :bar (comment [["bar"]])}