diff --git a/CHANGELOG.md b/CHANGELOG.md index 4975b0915..b496252d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Changes to Calva. ## [Unreleased] +## [2.0.323] - 2023-01-07 + +- Fix: [Provider completions not handling errors gracefully](https://github.com/BetterThanTomorrow/calva/issues/2006) +- Partly fix (indenter): [Indenter and formatter fails while typing out body of deftype method](https://github.com/BetterThanTomorrow/calva/issues/1957) + ## [2.0.322] - 2022-12-14 - Fix: [Clojure notebooks don't seem to work on MS-Windows](https://github.com/BetterThanTomorrow/calva/issues/1994) diff --git a/package-lock.json b/package-lock.json index 4e8d75ce8..27ee023b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "calva", - "version": "2.0.322", + "version": "2.0.323", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "calva", - "version": "2.0.322", + "version": "2.0.323", "license": "MIT", "dependencies": { "acorn": "^6.4.1", diff --git a/package.json b/package.json index 2a8bf1f71..96bc1382d 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.322", + "version": "2.0.323", "publisher": "betterthantomorrow", "author": { "name": "Better Than Tomorrow", @@ -14,6 +14,9 @@ "url": "https://github.com/BetterThanTomorrow/calva.git" }, "license": "MIT", + "bugs": { + "url": "https://github.com/BetterThanTomorrow/calva/issues" + }, "engines": { "node": ">=16", "vscode": "^1.67.0" diff --git a/publish.clj b/publish.clj index 3d79105ca..a7ec62f9a 100755 --- a/publish.clj +++ b/publish.clj @@ -3,9 +3,10 @@ ;; Note: The shell commands may need to be modified if you're using Windows. ;; At the time of this writing, both people who push use Unix-based machines. -(require '[clojure.string :as str] - '[cheshire.core :as json] - '[clojure.java.shell :as shell]) +(ns publish + (:require [clojure.string :as str] + [cheshire.core :as json] + [clojure.java.shell :as shell])) (def changelog-filename "CHANGELOG.md") (def changelog-text (slurp changelog-filename)) @@ -37,11 +38,12 @@ (format "[Unreleased]\n\n%s\n\n" new-header))] new-text)) -(defn throw-if-error [{:keys [exit out err]}] - (when-not (= exit 0) +(defn throw-if-error [{:keys [exit out err] :as result}] + (if-not (= exit 0) (throw (Exception. (if (empty? out) err - out))))) + out))) + result)) (defn commit-changelog [file-name message] (println "Committing") @@ -60,6 +62,18 @@ (println "Pushing") (throw-if-error (shell/sh "git" "push" "--follow-tags"))) +(defn git-status [] + (println "Checking git status") + (let [result (throw-if-error (shell/sh "git" "status")) + out (:out result) + [_ branch] (re-find #"^On branch (\S+)\n" out) + up-to-date (re-find #"Your branch is up to date" out) + clean (re-find #"nothing to commit, working tree clean" out)] + (cond-> #{} + (not= "dev" branch) (conj :not-on-dev) + (not up-to-date) (conj :not-up-to-date) + (not clean) (conj :branch-not-clean)))) + (defn publish [] (tag calva-version) (push) @@ -69,15 +83,22 @@ (when (= *file* (System/getProperty "babashka.file")) (let [unreleased-changelog-text (get-unreleased-changelog-text changelog-text - unreleased-header-re)] + unreleased-header-re) + status (git-status)] + (when (or (seq status) + (empty? unreleased-changelog-text)) + (when (seq status) + (println "Git status issues: " status)) + (when (empty? unreleased-changelog-text) + (print "There are no unreleased changes in the changelog.")) + (println "Release anyway? YES/NO: ") + (flush) + (let [answer (read)] + (when-not (= "YES" answer) + (println "Aborting publish.") + (System/exit 0)))) (if (empty? unreleased-changelog-text) - (do - (print "There are no unreleased changes in the changelog. Release anyway? y/n: ") - (flush) - (let [answer (read)] - (if (= (str answer) "y") - (publish) - (println "Aborting publish.")))) + (publish) (let [updated-changelog-text (new-changelog-text changelog-text unreleased-header-re calva-version)] diff --git a/src/calva-fmt/src/config.ts b/src/calva-fmt/src/config.ts index fbd3a6436..a1eda5da2 100644 --- a/src/calva-fmt/src/config.ts +++ b/src/calva-fmt/src/config.ts @@ -13,6 +13,16 @@ const defaultCljfmtContent = const LSP_CONFIG_KEY = 'CLOJURE-LSP'; let lspFormatConfig: string | undefined; +function cljfmtOptionsFromString(cljfmt: string) { + const options = cljsLib.cljfmtOptionsFromString(cljfmt); + return { + ...options, + // because we can't correctly pass ordered map from cljs + // but we need it to determine the order of applying indent rules + indents: Object.fromEntries(options['indents']), + }; +} + function configuration(workspaceConfig: vscode.WorkspaceConfiguration, cljfmt: string) { return { 'format-as-you-type': !!formatOnTypeEnabled(), @@ -20,7 +30,7 @@ function configuration(workspaceConfig: vscode.WorkspaceConfiguration, cljfmt: s 'keepCommentTrailParenOnOwnLine' ), 'cljfmt-options-string': cljfmt, - 'cljfmt-options': cljsLib.cljfmtOptionsFromString(cljfmt), + 'cljfmt-options': cljfmtOptionsFromString(cljfmt), }; } diff --git a/src/cljs-lib/src/calva/fmt/formatter.cljs b/src/cljs-lib/src/calva/fmt/formatter.cljs index ed31a7494..60646b5c1 100644 --- a/src/cljs-lib/src/calva/fmt/formatter.cljs +++ b/src/cljs-lib/src/calva/fmt/formatter.cljs @@ -16,6 +16,13 @@ indents (merge cljfmt/default-indents indents))) +(defn- sort-indents + "Sorts rules in order to prevent default rule application + before specific one" + [indents] + #_{:clj-kondo/ignore [:private-call]} + (sort-by cljfmt/indent-order indents)) + (def ^:private default-fmt {:remove-surrounding-whitespace? true :remove-trailing-whitespace? true @@ -27,6 +34,7 @@ [fmt] (as-> fmt $ (update $ :indents merge-default-indents) + (update $ :indents sort-indents) (merge default-fmt $))) (defn- read-cljfmt @@ -259,7 +267,7 @@ (remove-indent-token-if-empty-current-line) (remove-trail-symbol-if-comment range))) (comment - + :rcf) (defn format-text-at-idx-on-type "Relax formating some when used as an on-type handler" diff --git a/src/cljs-lib/test/calva/fmt/formatter_test.cljs b/src/cljs-lib/test/calva/fmt/formatter_test.cljs index a43860c35..586460b0c 100644 --- a/src/cljs-lib/test/calva/fmt/formatter_test.cljs +++ b/src/cljs-lib/test/calva/fmt/formatter_test.cljs @@ -1,7 +1,7 @@ (ns calva.fmt.formatter-test - (:require [cljs.test :include-macros true :refer [deftest is testing]] - [cljfmt.core :as cljfmt] - [calva.fmt.formatter :as sut])) + (:require [calva.fmt.formatter :as sut] + [cljfmt.core :as cljfmt :refer [includes?]] + [cljs.test :include-macros true :refer [deftest is testing]])) (deftest format-text-at-range (is (= "(foo)\n(defn bar\n [x]\n baz)" @@ -51,7 +51,7 @@ ccc {:a b :aa bb :ccc ccc}] :config {:align-associative? true} :range [0 56] :idx 0}))))) - + (testing "Does not align associative structures when `:align-associative` is not `true`" (is (= "(def foo (let [a b @@ -73,7 +73,7 @@ ccc {:a b :aa bb :ccc ccc}] :config {:remove-multiple-non-indenting-spaces? true} :range [0 56] :idx 0}))))) - + (testing "Does not trim space between forms when `:remove-multiple-non-indenting-spaces?` is missing" (is (= "(def foo (let [a b @@ -314,7 +314,7 @@ bar))" :range [22 25]}))) (is (= (+ 2 (count cljfmt/default-indents)) (count (:indents (sut/read-cljfmt "{:indents {foo [[:inner 0]] bar [[:block 1]]}}")))) "merges indents on top of cljfmt indent rules") - (is (= {'a [[:inner 0]]} + (is (= '([a [[:inner 0]]]) (:indents (sut/read-cljfmt "{:indents ^:replace {a [[:inner 0]]}}"))) "with :replace metadata hint overrides default indents") (is (= false @@ -330,7 +330,11 @@ bar))" :range [22 25]}))) (:remove-surrounding-whitespace? (sut/read-cljfmt "{:remove-surrounding-whitespace? false}"))) "including keys in cljfmt such as :remove-surrounding-whitespace? will override defaults.") (is (nil? (:foo (sut/read-cljfmt "{:bar false}"))) - "most keys don't have any defaults.")) + "most keys don't have any defaults.") + (is (empty? (let [indents (map (comp str first) (:indents (sut/read-cljfmt "{}"))) + indents-after-default (drop-while #(not= (str #"^def(?!ault)(?!late)(?!er)") %) indents)] + (filter (partial re-find #"^def") indents-after-default))) + "places default rule '^def(?!ault)(?!late)(?!er)' after all specific def rules")) (deftest cljfmt-options (is (= (count cljfmt/default-indents) @@ -339,12 +343,12 @@ bar))" :range [22 25]}))) (is (= (+ 2 (count cljfmt/default-indents)) (count (:indents (sut/merge-cljfmt '{:indents {foo [[:inner 0]] bar [[:block 1]]}})))) "merges indents on top of cljfmt indent rules") - (is (= {'a [[:inner 0]]} + (is (= '([a [[:inner 0]]]) (:indents (sut/merge-cljfmt '{:indents ^:replace {a [[:inner 0]]}}))) "with :replace metadata hint overrides default indents") (is (= true (:align-associative? (sut/merge-cljfmt {:align-associative? true - :cljfmt-string "{:align-associative? false}"}))) + :cljfmt-string "{:align-associative? false}"}))) "cljfmt :align-associative? has lower priority than config's option") (is (= false (:align-associative? (sut/merge-cljfmt {:cljfmt-string "{}"}))) diff --git a/src/cursor-doc/indent.ts b/src/cursor-doc/indent.ts index 6d71827c8..5e5490e8c 100644 --- a/src/cursor-doc/indent.ts +++ b/src/cursor-doc/indent.ts @@ -143,54 +143,84 @@ const testCljRe = (re, str) => { return matches && RegExp(matches[1]).test(str.replace(/^.*\//, '')); }; +const calculateDefaultIndent = (indentInfo: IndentInformation) => + indentInfo.exprsOnLine > 0 ? indentInfo.firstItemIdent : indentInfo.startIndent; + +const calculateInnerIndent = ( + currentIndent: number, + rule: IndentRule, + indentInfo: IndentInformation +) => { + if (rule.length !== 3 || rule[2] > indentInfo.argPos) { + return indentInfo.startIndent + 1; + } + + return currentIndent; +}; + +const calculateBlockIndent = ( + currentIndent: number, + rule: IndentRule, + indentInfo: IndentInformation +) => { + if (indentInfo.exprsOnLine > rule[1]) { + return indentInfo.firstItemIdent; + } + + if (indentInfo.argPos >= rule[1]) { + return indentInfo.startIndent + 1; + } + + return currentIndent; +}; + +const calculateIndent = ( + currentIndent: number, + rule: IndentRule, + indentInfo: IndentInformation, + stateSize: number, + pos: number +) => { + if (rule[0] === 'inner' && pos + rule[1] === stateSize) { + return calculateInnerIndent(currentIndent, rule, indentInfo); + } + + if (rule[0] === 'block' && pos === stateSize) { + return calculateBlockIndent(currentIndent, rule, indentInfo); + } + + return currentIndent; +}; + /** Returns the expected newline indent for the given position, in characters. */ -export function getIndent(document: EditableModel, offset: number, config?: any): number { - if (!config) { - config = { - 'cljfmt-options': { - indents: indentRules, - }, - }; +export function getIndent( + document: EditableModel, + offset: number, + config: any = { + 'cljfmt-options': { + indents: indentRules, + }, } +): number { const state = collectIndents(document, offset, config); - // now find applicable indent rules - let indent = -1; - const thisBlock = state[state.length - 1]; if (!state.length) { return 0; } - for (let pos = state.length - 1; pos >= 0; pos--) { - for (const rule of state[pos].rules) { - if (rule[0] == 'inner') { - if (pos + rule[1] == state.length - 1) { - if (rule.length == 3) { - if (rule[2] > thisBlock.argPos) { - indent = thisBlock.startIndent + 1; - } - } else { - indent = thisBlock.startIndent + 1; - } - } - } else if (rule[0] == 'block' && pos == state.length - 1) { - if (thisBlock.exprsOnLine <= rule[1]) { - if (thisBlock.argPos >= rule[1]) { - indent = thisBlock.startIndent + 1; - } - } else { - indent = thisBlock.firstItemIdent; - } - } - } + // now find applicable indent rules + let indent = -1; + const stateSize = state.length - 1; + const thisBlock = state.at(-1); + for (let pos = stateSize; pos >= 0; pos--) { + indent = state[pos].rules.reduce( + (currentIndent, rule) => calculateIndent(currentIndent, rule, thisBlock, stateSize, pos), + indent + ); } if (indent == -1) { - // no indentation styles applied, so use default style. - if (thisBlock.exprsOnLine > 0) { - indent = thisBlock.firstItemIdent; - } else { - indent = thisBlock.startIndent; - } + return calculateDefaultIndent(thisBlock); } + return indent; } diff --git a/src/extension-test/unit/cursor-doc/indent-test.ts b/src/extension-test/unit/cursor-doc/indent-test.ts index 1b75a0580..53b5e1a15 100644 --- a/src/extension-test/unit/cursor-doc/indent-test.ts +++ b/src/extension-test/unit/cursor-doc/indent-test.ts @@ -142,6 +142,30 @@ describe('indent', () => { ).toEqual(2); }); }); + + describe('deftype', () => { + it('calculates indents for cursor on the new line of a method implementation', () => { + const doc = docFromTextNotation(` +(deftype MyType [arg1 arg2] + IMyProto + (method1 [this] +|(print "hello")))`); + + expect( + indent.getIndent( + doc.model, + textAndSelection(doc)[1][0], + mkConfig({ + deftype: [ + ['block', 2], + ['inner', 1], + ], + '#"^def(?!ault)(?!late)(?!er)"': [['inner', 0]], + }) + ) + ).toEqual(4); + }); + }); }); describe('collectIndents', () => { diff --git a/src/lsp/main.ts b/src/lsp/main.ts index eb472a904..56c499ecd 100644 --- a/src/lsp/main.ts +++ b/src/lsp/main.ts @@ -788,6 +788,17 @@ export async function getClojuredocs(symName: string, symNs: string): Promise { + const client: LanguageClient = getStateValue(LSP_CLIENT_KEY); + return client.sendRequest('clojure/cursorInfo/raw', { + textDocument: { uri: textDocument.uri.toString() }, + position: { line: position.line, character: position.character }, + }); +} + // TODO: This feels a bit brute, what are other ways to wait for the client to initialize? export function getClient(timeout: number): Promise { const start = Date.now(); diff --git a/src/providers/completion.ts b/src/providers/completion.ts index b80c4f496..eb6fff7df 100644 --- a/src/providers/completion.ts +++ b/src/providers/completion.ts @@ -51,7 +51,15 @@ async function provideCompletionItems( if (results.length && !completionProviderOptions.merge) { break; } - const completions = await completionFunctions[provider](document, position, token, context); + + const completions = await completionFunctions[provider]( + document, + position, + token, + context + ).catch((err) => { + console.log('Failed to get results from remote', err); + }); if (completions) { results = [ diff --git a/test-data/.vscode/settings.json b/test-data/.vscode/settings.json index 5cf53cb3b..76cd8e319 100644 --- a/test-data/.vscode/settings.json +++ b/test-data/.vscode/settings.json @@ -42,5 +42,16 @@ "titleBar.inactiveBackground": "#90B4FEd5", "titleBar.inactiveForeground": "#13172299" }, - "calva.fmt.configPath": "cljfmt.edn" + "calva.fmt.configPath": "cljfmt.edn", + "calva.replConnectSequences": [ + { + "name": "pirate-lang", + "projectType": "deps.edn", + "afterCLJReplJackInCode": "(require 'repl)", + "cljsType": "none", + "menuSelections": { + "cljAliases": ["dev", "test"] + } + } + ] } diff --git a/test-data/projects/pirate-lang/deps.edn b/test-data/projects/pirate-lang/deps.edn index 18f66e61d..95e470a91 100644 --- a/test-data/projects/pirate-lang/deps.edn +++ b/test-data/projects/pirate-lang/deps.edn @@ -4,6 +4,7 @@ {:socket {:jvm-opts ["-Dclojure.server.repl={:port 5555 :accept clojure.core.server/repl}"]} :test {:extra-paths ["test"] :extra-deps {org.clojure/test.check {:mvn/version "0.10.0"}}} + :dev {:extra-paths ["env/dev"]} :runner {:extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner" diff --git a/test-data/projects/pirate-lang/env/dev/repl.clj b/test-data/projects/pirate-lang/env/dev/repl.clj new file mode 100644 index 000000000..24decd64d --- /dev/null +++ b/test-data/projects/pirate-lang/env/dev/repl.clj @@ -0,0 +1,2 @@ +(ns repl + (:require [pez.pirate-lang-test])) \ No newline at end of file