diff --git a/.github/workflows/indent.yml b/.github/workflows/indent.yml new file mode 100644 index 0000000..d75dde4 --- /dev/null +++ b/.github/workflows/indent.yml @@ -0,0 +1,28 @@ +name: Indent +on: + push: { branches: [ master ] } + pull_request: + workflow_dispatch: + +jobs: + vim-latest: + runs-on: ubuntu-latest + steps: + - name: Fetch source + uses: actions/checkout@v4 + - name: Install Vim + uses: rhysd/action-setup-vim@v1 + with: { version: stable } + - name: Run indentation tests + run: EDITOR=vim dev/do/test-indent + + neovim-latest: + runs-on: ubuntu-latest + steps: + - name: Fetch source + uses: actions/checkout@v4 + - name: Install Neovim + uses: rhysd/action-setup-vim@v1 + with: { neovim: true, version: stable } + - name: Run indentation tests + run: EDITOR=nvim dev/do/test-indent diff --git a/.github/workflows/clojure.yml b/.github/workflows/syntax.yml similarity index 85% rename from .github/workflows/clojure.yml rename to .github/workflows/syntax.yml index 182f5d7..4862da4 100644 --- a/.github/workflows/clojure.yml +++ b/.github/workflows/syntax.yml @@ -1,5 +1,8 @@ -name: CI -on: [push, pull_request, workflow_dispatch] +name: Syntax +on: + push: { branches: [ master ] } + pull_request: + workflow_dispatch: jobs: lint: @@ -10,7 +13,7 @@ jobs: with: clj-kondo: latest - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Lint working-directory: ./clj @@ -19,7 +22,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: DeLaGuardo/setup-clojure@7.0 with: lein: 2.11.2 diff --git a/README.md b/README.md index febc9cc..53503ad 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,13 @@ # Clojure.vim -[Clojure][] syntax highlighting for Vim and Neovim, including: +**Configurable [Clojure][] syntax highlighting, indentation (and more) for Vim and Neovim!** -- [Augmentable](#syntax-options) syntax highlighting. -- [Configurable](#indent-options) indentation. -- Basic insert-mode completion of special forms and public vars in - `clojure.core`. (Invoke with `` or ``.) +> [!TIP] +> This plugin comes packaged with Vim and Neovim. However if you would like to +> always use the latest version, you can install this plugin like you would any +> other. - -## Installation - -These files are included in both Vim and Neovim. However if you would like the -latest changes just install this repository like any other plugin. - -Make sure that the following options are set in your vimrc so that all features -are enabled: +Make sure your vimrc contains the following options to enable all features: ```vim syntax on @@ -22,21 +15,9 @@ filetype plugin indent on ``` -## Configuration - -### Folding - -Setting `g:clojure_fold` to `1` will enable the folding of Clojure code. Any -list, vector or map that extends over more than one line can be folded using -the standard Vim fold commands. - -(Note that this option will not work with scripts that redefine the bracket -regions, such as rainbow parenphesis plugins.) - - -### Syntax options +## Syntax highlighting -#### `g:clojure_syntax_keywords` +### `g:clojure_syntax_keywords` Syntax highlighting of public vars in `clojure.core` is provided by default, but additional symbols can be highlighted by adding them to the @@ -60,127 +41,104 @@ will not be highlighted by default. This is useful for namespaces that have set `(:refer-clojure :only [])`. -#### `g:clojure_discard_macro` +### `g:clojure_discard_macro` Set this variable to `1` to enable highlighting of the "[discard reader macro](https://clojure.org/guides/weird_characters#_discard)". Due to current limitations in Vim's syntax rules, this option won't highlight -stacked discard macros (e.g. `#_#_`). This inconsitency is why this option is +stacked discard macros (e.g. `#_#_`). This inconsistency is why this option is disabled by default. -### Indent options - -Clojure indentation differs somewhat from traditional Lisps, due in part to -the use of square and curly brackets, and otherwise by community convention. -These conventions are not universally followed, so the Clojure indent script -offers a few configuration options. - -(If the current Vim does not include `searchpairpos()`, the indent script falls -back to normal `'lisp'` indenting, and the following options are ignored.) - - -#### `g:clojure_maxlines` +## Indentation -Sets maximum scan distance of `searchpairpos()`. Larger values trade -performance for correctness when dealing with very long forms. A value of -0 will scan without limits. The default is 300. +Clojure indentation differs somewhat from traditional Lisps, due in part to the +use of square and curly brackets, and otherwise by community convention. As +these conventions are not universally followed, the Clojure indent script +offers ways to adjust the indentation. +> [!WARNING] +> The indentation code has recently been rebuilt which included the +> removal/replacement of the following configuration options: +> +> | Config option | Replacement (if any) | +> |-----------------------------------|------------------------------------| +> | `clojure_maxlines` | | +> | `clojure_cljfmt_compat` | `clojure_indent_style` | +> | `clojure_align_subforms` | `clojure_indent_style` | +> | `clojure_align_multiline_strings` | `clojure_indent_multiline_strings` | +> | `clojure_fuzzy_indent` | | +> | `clojure_fuzzy_indent_blacklist` | | +> | `clojure_special_indent_words` | `clojure_indent_rules` | +> | `'lispwords'` | `clojure_indent_rules` | -#### `g:clojure_fuzzy_indent`, `g:clojure_fuzzy_indent_patterns`, `g:clojure_fuzzy_indent_blacklist` -The `'lispwords'` option is a list of comma-separated words that mark special -forms whose subforms should be indented with two spaces. +### Indentation style -For example: +The `clojure_indent_style` config option controls the general indentation style +to use. Choose from several common presets: -```clojure -(defn bad [] - "Incorrect indentation") - -(defn good [] - "Correct indentation") -``` - -If you would like to specify `'lispwords'` with a pattern instead, you can use -the fuzzy indent feature: +| Value | Default | Description | +|-------|---------|-------------| +| `standard` | ✅ | Conventional Clojure indentation. ([_Clojure Style Guide_](https://guide.clojure.style/).) | +| `traditional` | | Indent like traditional Lisps. (Earlier versions of Clojure.vim indented like this.) | +| `uniform` | | Indent uniformly to 2 spaces with no alignment (a.k.a. [_Tonsky_ indentation](https://tonsky.me/blog/clojurefmt/)). | ```vim -" Default -let g:clojure_fuzzy_indent = 1 -let g:clojure_fuzzy_indent_patterns = ['^with', '^def', '^let'] -let g:clojure_fuzzy_indent_blacklist = ['-fn$', '\v^with-%(meta|out-str|loading-context)$'] +let g:clojure_indent_style = 'uniform' " Set the default... +let b:clojure_indent_style = 'traditional' " ...or override it per-buffer. ``` -`g:clojure_fuzzy_indent_patterns` and `g:clojure_fuzzy_indent_blacklist` are -lists of patterns that will be matched against the unqualified symbol at the -head of a list. This means that a pattern like `"^foo"` will match all these -candidates: `foobar`, `my.ns/foobar`, and `#'foobar`. - -Each candidate word is tested for special treatment in this order: -1. Return true if word is literally in `'lispwords'` -2. Return false if word matches a pattern in `g:clojure_fuzzy_indent_blacklist` -3. Return true if word matches a pattern in `g:clojure_fuzzy_indent_patterns` -4. Return false and indent normally otherwise +### Indentation rules +> [!NOTE] +> These options are ignored if an indentation style of "uniform" is selected. -#### `g:clojure_special_indent_words` + -Some forms in Clojure are indented such that every subform is indented by only -two spaces, regardless of `'lispwords'`. If you have a custom construct that -should be indented in this idiosyncratic fashion, you can add your symbols to -the default list below. +`clojure_indent_rules` & `clojure_fuzzy_indent_patterns` -```vim -" Default -let g:clojure_special_indent_words = 'deftype,defrecord,reify,proxy,extend-type,extend-protocol,letfn' -``` +### Multi-line strings -#### `g:clojure_align_multiline_strings` +Control alignment of _new_ lines within Clojure multi-line strings and regular +expressions with `clojure_indent_multiline_strings`. -Align subsequent lines in multi-line strings to the column after the opening -quote, instead of the same column. +> [!NOTE] +> Indenting with = will not alter the indentation within multi-line +> strings, as this could break intentional formatting. -For example: +Pick from the following multi-line string indent styles: -```clojure -(def default - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut - enim ad minim veniam, quis nostrud exercitation ullamco laboris - nisi ut aliquip ex ea commodo consequat.") +| Value | Default | Description | +|-------|---------|-------------| +| `standard` | ✅ | Align to the _front_ of the `"` or `#"` delimiter. Ideal for doc-strings. | +| `pretty` | | Align to the _back_ of the `"` or `#"` delimiter. | +| `traditional` | | No indent: align to left edge of file. | -(def aligned - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut - enim ad minim veniam, quis nostrud exercitation ullamco laboris - nisi ut aliquip ex ea commodo consequat.") +```vim +let g:clojure_indent_multiline_strings = 'pretty' " Set the default... +let b:clojure_indent_multiline_strings = 'traditional' " ...or override it per-buffer. ``` -#### `g:clojure_align_subforms` +## Code folding -By default, parenthesized compound forms that look like function calls and -whose head subform is on its own line have subsequent subforms indented by -two spaces relative to the opening paren: +Setting `g:clojure_fold` to `1` will enable the folding of Clojure code. Any +list, vector or map that extends over more than one line can be folded using +the standard Vim fold commands. -```clojure -(foo - bar - baz) -``` +(Note that this option will not work with scripts that redefine the bracket +regions, such as rainbow parenthesis plugins.) -Setting this option to `1` changes this behaviour so that all subforms are -aligned to the same column, emulating the default behaviour of -[clojure-mode.el](https://github.com/clojure-emacs/clojure-mode): -```clojure -(foo - bar - baz) -``` +## Insert-mode completion + +Very basic insert-mode completion of special forms and public vars from +`clojure.core` is included in Clojure.vim. Invoke it with +Ctrl x Ctrl o or +Ctrl x Ctrl u. ## Contribute @@ -195,18 +153,15 @@ Pull requests are welcome! Make sure to read the _Vim-clojure-static_ was created by [Sung Pae](https://github.com/guns). The original copies of the packaged runtime files came from [Meikel Brandmeyer](http://kotka.de/)'s [VimClojure][] project with permission. - -Thanks to [Tim Pope](https://github.com/tpope/) for advice in -[#vim](https://www.vi-improved.org/). +Thanks to [Tim Pope](https://github.com/tpope/) for advice in `#vim` on IRC. ## License -Clojure.vim is licensed under the [Vim -License](http://vimdoc.sourceforge.net/htmldoc/uganda.html#license) for -distribution with Vim. +Clojure.vim is licensed under the [Vim License](http://vimdoc.sourceforge.net/htmldoc/uganda.html#license) +for distribution with Vim. -- Copyright © 2020–2021, The clojure-vim contributors. +- Copyright © 2020–2025, The clojure-vim contributors. - Copyright © 2013–2018, Sung Pae. - Copyright © 2008–2012, Meikel Brandmeyer. - Copyright © 2007–2008, Toralf Wittner. diff --git a/clj/resources/indent-test-cases/basic-sexp/in.clj b/clj/resources/indent-test-cases/basic-sexp/in.clj deleted file mode 100644 index 3551b97..0000000 --- a/clj/resources/indent-test-cases/basic-sexp/in.clj +++ /dev/null @@ -1,5 +0,0 @@ -(ns test-basic-sexp-indent - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, - quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat.") diff --git a/clj/resources/indent-test-cases/basic-sexp/out.clj b/clj/resources/indent-test-cases/basic-sexp/out.clj deleted file mode 100644 index 3551b97..0000000 --- a/clj/resources/indent-test-cases/basic-sexp/out.clj +++ /dev/null @@ -1,5 +0,0 @@ -(ns test-basic-sexp-indent - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, - quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat.") diff --git a/clj/resources/indent-test-cases/dispach-macro/in.clj b/clj/resources/indent-test-cases/dispach-macro/in.clj deleted file mode 100644 index b59b91e..0000000 --- a/clj/resources/indent-test-cases/dispach-macro/in.clj +++ /dev/null @@ -1,29 +0,0 @@ -(#(foo) -bar) - -(#(foo -bar)) - -(#(foo bar -a)) - -(#(foo bar) -a) - -(#{foo} -bar) - -(#{foo -bar}) - -(#{foo bar} -a) - -(#_(foo) -bar) - -(#_(foo -bar)) - -(#_(foo bar) -a) diff --git a/clj/resources/indent-test-cases/dispach-macro/out.clj b/clj/resources/indent-test-cases/dispach-macro/out.clj deleted file mode 100644 index 4f7356d..0000000 --- a/clj/resources/indent-test-cases/dispach-macro/out.clj +++ /dev/null @@ -1,29 +0,0 @@ -(#(foo) - bar) - -(#(foo - bar)) - -(#(foo bar - a)) - -(#(foo bar) - a) - -(#{foo} - bar) - -(#{foo - bar}) - -(#{foo bar} - a) - -(#_(foo) - bar) - -(#_(foo - bar)) - -(#_(foo bar) - a) diff --git a/clj/resources/indent-test-cases/inherit-indentation/config.edn b/clj/resources/indent-test-cases/inherit-indentation/config.edn deleted file mode 100644 index d972b2b..0000000 --- a/clj/resources/indent-test-cases/inherit-indentation/config.edn +++ /dev/null @@ -1,3 +0,0 @@ -{:indent? false - :extra-cmds ["normal! gg" - "exec \"normal! /α\\s\\Oa\\/β\\s\\\\\\\\\\\\\\\\\\b\\c\\\\d\\\""]} diff --git a/clj/resources/indent-test-cases/inherit-indentation/in.clj b/clj/resources/indent-test-cases/inherit-indentation/in.clj deleted file mode 100644 index 9d09168..0000000 --- a/clj/resources/indent-test-cases/inherit-indentation/in.clj +++ /dev/null @@ -1,7 +0,0 @@ -(foo bar - "This string has unpaired brackets [ - and is indented weirdly." -α - [β]) - -;; vim:ft=clojure: diff --git a/clj/resources/indent-test-cases/inherit-indentation/out.clj b/clj/resources/indent-test-cases/inherit-indentation/out.clj deleted file mode 100644 index e4851a1..0000000 --- a/clj/resources/indent-test-cases/inherit-indentation/out.clj +++ /dev/null @@ -1,13 +0,0 @@ -(foo bar - "This string has unpaired brackets [ - and is indented weirdly." - a - - [ - - b - c - - d]) - -;; vim:ft=clojure: diff --git a/clj/resources/indent-test-cases/multibyte-indentation/in.clj b/clj/resources/indent-test-cases/multibyte-indentation/in.clj deleted file mode 100644 index 4a17c24..0000000 --- a/clj/resources/indent-test-cases/multibyte-indentation/in.clj +++ /dev/null @@ -1,3 +0,0 @@ -(let [Δt (if foo - bar - baz)]) diff --git a/clj/resources/indent-test-cases/multibyte-indentation/out.clj b/clj/resources/indent-test-cases/multibyte-indentation/out.clj deleted file mode 100644 index 4a17c24..0000000 --- a/clj/resources/indent-test-cases/multibyte-indentation/out.clj +++ /dev/null @@ -1,3 +0,0 @@ -(let [Δt (if foo - bar - baz)]) diff --git a/clj/resources/indent-test-cases/reader-conditional/in.clj b/clj/resources/indent-test-cases/reader-conditional/in.clj deleted file mode 100644 index 9bc1c7b..0000000 --- a/clj/resources/indent-test-cases/reader-conditional/in.clj +++ /dev/null @@ -1,11 +0,0 @@ -(def DateTime #?(:clj org.joda.time.DateTime, - :cljs goog.date.UtcDateTime)) - -#?(:cljs - (extend-protocol ToDateTime - goog.date.Date - (-to-date-time [x] - (goog.date.UtcDateTime. (.getYear x) (.getMonth x) (.getDate x))))) - -#?@(:clj [5 6 7 8] - :cljs [1 2 3 4]))) diff --git a/clj/resources/indent-test-cases/reader-conditional/out.clj b/clj/resources/indent-test-cases/reader-conditional/out.clj deleted file mode 100644 index 5c5bfc2..0000000 --- a/clj/resources/indent-test-cases/reader-conditional/out.clj +++ /dev/null @@ -1,11 +0,0 @@ -(def DateTime #?(:clj org.joda.time.DateTime, - :cljs goog.date.UtcDateTime)) - -#?(:cljs - (extend-protocol ToDateTime - goog.date.Date - (-to-date-time [x] - (goog.date.UtcDateTime. (.getYear x) (.getMonth x) (.getDate x))))) - -#?@(:clj [5 6 7 8] - :cljs [1 2 3 4]))) diff --git a/clj/resources/indent-test-cases/side-effects-in-indentexpr/config.edn b/clj/resources/indent-test-cases/side-effects-in-indentexpr/config.edn deleted file mode 100644 index 8bbbf0f..0000000 --- a/clj/resources/indent-test-cases/side-effects-in-indentexpr/config.edn +++ /dev/null @@ -1,3 +0,0 @@ -{:indent? false - :extra-cmds ["normal! gg" - "exec \"normal! /α\\:call GetClojureIndent()\\rxj:call GetClojureIndent()\\ry\""]} diff --git a/clj/resources/indent-test-cases/side-effects-in-indentexpr/in.clj b/clj/resources/indent-test-cases/side-effects-in-indentexpr/in.clj deleted file mode 100644 index a3dcb24..0000000 --- a/clj/resources/indent-test-cases/side-effects-in-indentexpr/in.clj +++ /dev/null @@ -1,3 +0,0 @@ -(doseq [x (range 10) y (range 10)] - (println α) - (println β)) diff --git a/clj/resources/indent-test-cases/side-effects-in-indentexpr/out.clj b/clj/resources/indent-test-cases/side-effects-in-indentexpr/out.clj deleted file mode 100644 index 154c219..0000000 --- a/clj/resources/indent-test-cases/side-effects-in-indentexpr/out.clj +++ /dev/null @@ -1,3 +0,0 @@ -(doseq [x (range 10) y (range 10)] - (println x) - (println y)) diff --git a/clj/test/vim/helpers.clj b/clj/test/vim/helpers.clj deleted file mode 100644 index 65e4b87..0000000 --- a/clj/test/vim/helpers.clj +++ /dev/null @@ -1,21 +0,0 @@ -(ns vim.helpers - (:require [clojure.edn :as edn] - [clojure.java.shell :as shell]) - (:import [java.io File FileReader PushbackReader])) - -(defn read-edn-file [^File file] - (when (.exists file) - (with-open [rdr (FileReader. file)] - (edn/read (PushbackReader. rdr))))) - -(def ^:dynamic *vim* "vim") - -(defn vim! - "Run commands on a file in Vim." - [^File file cmds & {:keys [vimrc], :or {vimrc "NONE"}}] - (let [cmds (mapcat (fn [cmd] ["-c" cmd]) cmds) - args (concat ["--clean" "-N" "-u" (str vimrc)] cmds ["-c" "quitall!" "--" (str file)]) - ret (apply shell/sh *vim* args)] - (when (pos? (:exit ret)) - (throw (ex-info "Failed to run Vim command" - (assoc ret :vim *vim*, :args args)))))) diff --git a/clj/test/vim/indent_test.clj b/clj/test/vim/indent_test.clj deleted file mode 100644 index 2aa63e9..0000000 --- a/clj/test/vim/indent_test.clj +++ /dev/null @@ -1,43 +0,0 @@ -(ns vim.indent-test - (:require [clojure.test :refer [deftest testing is]] - [clojure.string :as str] - [clojure.java.io :as io] - [vim.helpers :as h]) - (:import [java.io File])) - -(defn get-test-cases [^File test-case-dir] - (into [] - (comp - (filter #(.isDirectory ^File %)) - (map #(.getName ^File %))) - (.listFiles test-case-dir))) - -(defn run-test-case [test-case-dir test-case] - (testing (str "Preparation for " test-case) - (let [input (io/file test-case-dir test-case "in.clj") - expected (io/file test-case-dir test-case "out.clj") - actual (File/createTempFile test-case ".clj") - config (let [f (io/file test-case-dir test-case "config.edn")] - (or (h/read-edn-file f) {})) - cmds (concat (:extra-cmds config) - (when (:indent? config true) ["normal! gg=G"]) - ["write"])] - (io/make-parents actual) - (io/copy input actual) - (h/vim! actual cmds :vimrc (io/file "vim/test-runtime.vim")) - {:test-case test-case - :expected (slurp expected) - :expected-file expected - :actual (slurp actual) - :actual-file actual}))) - -;; TODO: do this parallisation more intelligently with agents. -(deftest test-indent - "Runs all indentation tests in parallel" - (let [test-case-dir (io/file (io/resource "indent-test-cases")) - test-cases (get-test-cases test-case-dir)] - (doseq [{:keys [test-case expected expected-file actual actual-file]} - (pmap (partial run-test-case test-case-dir) test-cases)] - (testing test-case - (is (= expected actual) - (format "(not= \"%s\"\n \"%s\")" expected-file actual-file)))))) diff --git a/dev/do/test-indent b/dev/do/test-indent new file mode 100755 index 0000000..1d93d1a --- /dev/null +++ b/dev/do/test-indent @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +# Run Clojure.vim indentation tests. + +# TODO: option to enable/disable (Lua / Vim9script) versions. + +C_GREEN='\033[1;32m' +C_RED='\033[1;31m' +C_YELLOW='\033[1;33m' +C_BLUE='\033[1;34m' +C_RESET='\033[0m' + +log() { printf "$*$C_RESET\n"; } +logc() { log "$1$2"; } +succ() { logc "$C_GREEN" "$*"; } +warn() { logc "$C_YELLOW" "$*"; } +err() { logc "$C_RED" "$*"; } +info() { logc "$C_BLUE" "$*"; } +abort() { err "ABORT: $*"; exit 1; } +gh_do() { [ -n "$GITHUB_ACTIONS" ] && return 0 || return 1; } + +pushd "$(dirname "$0")/.." + +if [ "$EDITOR" != 'vim' ] && [ "$EDITOR" != 'nvim' ]; then + abort 'Set the "EDITOR" environment variable to "vim" or "nvim" and run again.' +fi + +extra_opts=() +[ "$EDITOR" = 'vim' ] && extra_opts+=('--not-a-term') + +PASSED=() +FAILED=() +SKIPPED=() + +tmp_base_dir='/tmp/clojure.vim/indent' +mkdir -p "$tmp_base_dir" +tmp_dir="$(mktemp --directory "$tmp_base_dir/XXXXXX")" +test_case_dir='tests' + +test_pass() { PASSED+=("$1"); } +test_fail() { + FAILED+=("$1") + gh_do \ + && echo "::error file=dev/$test_case_dir/$1/out.clj::Failed indent test case." \ + || err "Failed \"$1\"" +} +test_skip() { + SKIPPED+=("$1") + gh_do \ + && echo "::warning file=dev/$test_case_dir/$1/out.clj::Skipped indent test case." \ + || warn "Skipped \"$1\"" +} + +run_test_case() { + test_case="$1" + in_file="$test_case_dir/$test_case/in.clj" + expected_file="$test_case_dir/$test_case/out.clj" + + info "> $EDITOR: $test_case" + + if [ -f "$test_case_dir/$test_case/SKIP" ]; then + test_skip "$test_case" + else + actual_file="$tmp_dir/$test_case.clj" + cp "$in_file" "$actual_file" + + # Override the default test commands with a `test.vim` file. + test_script="$test_case_dir/$test_case/test.vim" + if [ -f "$test_script" ]; then + test_cmd=('-S' "$test_script") + else + test_cmd=('+normal! gg=G') + fi + + "$EDITOR" "${extra_opts[@]}" --clean -EsNXnu test-vimrc.vim \ + "${test_cmd[@]}" '+xall!' -- "$actual_file" + + diff --color=always -u "$expected_file" "$actual_file" + + [ $? -eq 0 ] && test_pass "$test_case" || test_fail "$test_case" + fi +} + +for tcase in $test_case_dir/*/; do + run_test_case "$(basename "$tcase")" +done + +printf "passed: $C_GREEN%s$C_RESET, failed: $C_RED%s$C_RESET, skipped: $C_YELLOW%s$C_RESET\n" \ + "${#PASSED[@]}" "${#FAILED[@]}" "${#SKIPPED[@]}" + +# If none passed, or some failed, exit with error. +if [ ${#PASSED[@]} -eq 0 ] || [ ${#FAILED[@]} -gt 0 ]; then + abort 'Failed test cases.' +fi diff --git a/clj/bin/indenttime b/dev/do/time-indent similarity index 65% rename from clj/bin/indenttime rename to dev/do/time-indent index 615c473..a7b95d7 100755 --- a/clj/bin/indenttime +++ b/dev/do/time-indent @@ -9,8 +9,8 @@ PREFIX='report_indent' while getopts :p: opt; do case "$opt" in - p) PREFIX="$OPTARG";; - h) abort_with_help;; + p) PREFIX="$OPTARG";; + h) abort_with_help;; esac done shift $((OPTIND-1)) @@ -21,12 +21,9 @@ VIMRC=" set runtimepath^=$(dirname "$0")/../.. filetype plugin indent on syntax on +let g:clojure_maxlines = 0 profile start $(echo "${PREFIX}-$(date +%s.%N).log") -profile! file $(dirname "$0")/../../syntax/clojure.vim profile! file $(dirname "$0")/../../indent/clojure.vim " -exec vim -N -u <(echo "$VIMRC") \ - -c 'call feedkeys("gg=G")' \ - -c 'call feedkeys(":silent quitall!\")' \ - "$1" +exec vim --clean -ENXnu <(echo "$VIMRC") '+normal! gg=G' '+quitall!' "$1" diff --git a/dev/test-vimrc.vim b/dev/test-vimrc.vim new file mode 100644 index 0000000..770bf0d --- /dev/null +++ b/dev/test-vimrc.vim @@ -0,0 +1,3 @@ +let &rtp = getcwd() . '/..,' . &rtp +filetype plugin indent on +syntax enable diff --git a/dev/tests/comments/in.clj b/dev/tests/comments/in.clj new file mode 100644 index 0000000..5de184f --- /dev/null +++ b/dev/tests/comments/in.clj @@ -0,0 +1,17 @@ +{:foo {:bar 1} ; Default. +:biz {:as 123, :asdf #{1 2 3}} +} + +;; Foo bar! { +{:foo {:bar 1} ; Default. { +:biz {:as 123, :asdf #{1 2 3}}} + +{:foo {:bar 1} ; Default. +;; Foo bar! { +:biz {:as 123, :asdf #{1 2 3}}} + +(comment +{:foo {:bar 1} ; Default. + ;; Foo bar! { + :biz {:as 123, :asdf #{1 2 3}}} +) diff --git a/dev/tests/comments/out.clj b/dev/tests/comments/out.clj new file mode 100644 index 0000000..1d5f218 --- /dev/null +++ b/dev/tests/comments/out.clj @@ -0,0 +1,17 @@ +{:foo {:bar 1} ; Default. + :biz {:as 123, :asdf #{1 2 3}} + } + +;; Foo bar! { +{:foo {:bar 1} ; Default. { + :biz {:as 123, :asdf #{1 2 3}}} + +{:foo {:bar 1} ; Default. + ;; Foo bar! { + :biz {:as 123, :asdf #{1 2 3}}} + +(comment + {:foo {:bar 1} ; Default. + ;; Foo bar! { + :biz {:as 123, :asdf #{1 2 3}}} + ) diff --git a/dev/tests/custom_types/SKIP b/dev/tests/custom_types/SKIP new file mode 100644 index 0000000..e69de29 diff --git a/dev/tests/custom_types/in.clj b/dev/tests/custom_types/in.clj new file mode 100644 index 0000000..78ff69e --- /dev/null +++ b/dev/tests/custom_types/in.clj @@ -0,0 +1,20 @@ +(defrecord Thing [a] + FileNameMap + (getContentTypeFor [_ file-name] + (str a "-" file-name)) + Object + (toString [_] + "My very own thing!!")) + +(defrecord TheNameOfTheRecord + [a pretty long argument list] + SomeType + (assoc [_ x] + (.assoc pretty x 10))) + +(extend-protocol MyProtocol + goog.date.Date +(-to-date-time [x] + (goog.date.UtcDateTime. (.getYear x) + (.getMonth x) + (.getDate x)))) diff --git a/dev/tests/custom_types/out.clj b/dev/tests/custom_types/out.clj new file mode 100644 index 0000000..18f9241 --- /dev/null +++ b/dev/tests/custom_types/out.clj @@ -0,0 +1,20 @@ +(defrecord Thing [a] + FileNameMap + (getContentTypeFor [_ file-name] + (str a "-" file-name)) + Object + (toString [_] + "My very own thing!!")) + +(defrecord TheNameOfTheRecord + [a pretty long argument list] + SomeType + (assoc [_ x] + (.assoc pretty x 10))) + +(extend-protocol MyProtocol + goog.date.Date + (-to-date-time [x] + (goog.date.UtcDateTime. (.getYear x) + (.getMonth x) + (.getDate x)))) diff --git a/dev/tests/def/in.clj b/dev/tests/def/in.clj new file mode 100644 index 0000000..4dd213c --- /dev/null +++ b/dev/tests/def/in.clj @@ -0,0 +1,5 @@ +(defn- insert! + ^Map [^Map ^String k ^Object v] + (if (.putIfAbsent m k v) + (recur m (str \@ k) v) + m)) diff --git a/dev/tests/def/out.clj b/dev/tests/def/out.clj new file mode 100644 index 0000000..4dd213c --- /dev/null +++ b/dev/tests/def/out.clj @@ -0,0 +1,5 @@ +(defn- insert! + ^Map [^Map ^String k ^Object v] + (if (.putIfAbsent m k v) + (recur m (str \@ k) v) + m)) diff --git a/dev/tests/letfn/SKIP b/dev/tests/letfn/SKIP new file mode 100644 index 0000000..e69de29 diff --git a/clj/resources/indent-test-cases/letfn/in.clj b/dev/tests/letfn/in.clj similarity index 100% rename from clj/resources/indent-test-cases/letfn/in.clj rename to dev/tests/letfn/in.clj diff --git a/clj/resources/indent-test-cases/letfn/out.clj b/dev/tests/letfn/out.clj similarity index 100% rename from clj/resources/indent-test-cases/letfn/out.clj rename to dev/tests/letfn/out.clj diff --git a/dev/tests/multi-line_strings_pretty/in.clj b/dev/tests/multi-line_strings_pretty/in.clj new file mode 100644 index 0000000..0efd8a2 --- /dev/null +++ b/dev/tests/multi-line_strings_pretty/in.clj @@ -0,0 +1,27 @@ +"foo + bar" + + asdf dfa sdfasdf " +asdf" + +(asdf [foo] + "hel + lo asd + fasdfa + sdf + asdf + as + as + asdf + df + df + world") + + #{:foo :bar + :biz + "ba + z"} + + #"foo + bar + biz" diff --git a/dev/tests/multi-line_strings_pretty/out.clj b/dev/tests/multi-line_strings_pretty/out.clj new file mode 100644 index 0000000..b9f361e --- /dev/null +++ b/dev/tests/multi-line_strings_pretty/out.clj @@ -0,0 +1,33 @@ +"foo + bar" + +asdf dfa sdfasdf " +asdf" + +(asdf [foo] + "hel + lo asd + fasdfa + sdf + asdf + as + as + asdf + df + df + world") + +#{:foo :bar + :biz + "ba + z"} + +#"foo + bar + biz" + +test "hello + world" + +regex #"asdf + bar" diff --git a/dev/tests/multi-line_strings_pretty/test.vim b/dev/tests/multi-line_strings_pretty/test.vim new file mode 100644 index 0000000..ebff716 --- /dev/null +++ b/dev/tests/multi-line_strings_pretty/test.vim @@ -0,0 +1,5 @@ +let g:clojure_indent_multiline_strings = 'pretty' +normal! gg=G +normal! G +exec "normal! o\test \"hello\world\"" +exec "normal! o\regex #\"asdf\bar\"" diff --git a/dev/tests/multi-line_strings_standard/in.clj b/dev/tests/multi-line_strings_standard/in.clj new file mode 100644 index 0000000..0efd8a2 --- /dev/null +++ b/dev/tests/multi-line_strings_standard/in.clj @@ -0,0 +1,27 @@ +"foo + bar" + + asdf dfa sdfasdf " +asdf" + +(asdf [foo] + "hel + lo asd + fasdfa + sdf + asdf + as + as + asdf + df + df + world") + + #{:foo :bar + :biz + "ba + z"} + + #"foo + bar + biz" diff --git a/dev/tests/multi-line_strings_standard/out.clj b/dev/tests/multi-line_strings_standard/out.clj new file mode 100644 index 0000000..49cb1f0 --- /dev/null +++ b/dev/tests/multi-line_strings_standard/out.clj @@ -0,0 +1,33 @@ +"foo + bar" + +asdf dfa sdfasdf " +asdf" + +(asdf [foo] + "hel + lo asd + fasdfa + sdf + asdf + as + as + asdf + df + df + world") + +#{:foo :bar + :biz + "ba + z"} + +#"foo + bar + biz" + +test "hello + world" + +regex #"asdf + bar" diff --git a/dev/tests/multi-line_strings_standard/test.vim b/dev/tests/multi-line_strings_standard/test.vim new file mode 100644 index 0000000..9c40753 --- /dev/null +++ b/dev/tests/multi-line_strings_standard/test.vim @@ -0,0 +1,5 @@ +let g:clojure_indent_multiline_strings = 'standard' +normal! gg=G +normal! G +exec "normal! o\test \"hello\world\"" +exec "normal! o\regex #\"asdf\bar\"" diff --git a/dev/tests/multi-line_strings_traditional/in.clj b/dev/tests/multi-line_strings_traditional/in.clj new file mode 100644 index 0000000..0efd8a2 --- /dev/null +++ b/dev/tests/multi-line_strings_traditional/in.clj @@ -0,0 +1,27 @@ +"foo + bar" + + asdf dfa sdfasdf " +asdf" + +(asdf [foo] + "hel + lo asd + fasdfa + sdf + asdf + as + as + asdf + df + df + world") + + #{:foo :bar + :biz + "ba + z"} + + #"foo + bar + biz" diff --git a/dev/tests/multi-line_strings_traditional/out.clj b/dev/tests/multi-line_strings_traditional/out.clj new file mode 100644 index 0000000..8860013 --- /dev/null +++ b/dev/tests/multi-line_strings_traditional/out.clj @@ -0,0 +1,33 @@ +"foo + bar" + +asdf dfa sdfasdf " +asdf" + +(asdf [foo] + "hel + lo asd + fasdfa + sdf + asdf + as + as + asdf + df + df + world") + +#{:foo :bar + :biz + "ba + z"} + +#"foo + bar + biz" + +test "hello +world" + +regex #"asdf +bar" diff --git a/dev/tests/multi-line_strings_traditional/test.vim b/dev/tests/multi-line_strings_traditional/test.vim new file mode 100644 index 0000000..25c7a13 --- /dev/null +++ b/dev/tests/multi-line_strings_traditional/test.vim @@ -0,0 +1,5 @@ +let g:clojure_indent_multiline_strings = 'traditional' +normal! gg=G +normal! G +exec "normal! o\test \"hello\world\"" +exec "normal! o\regex #\"asdf\bar\"" diff --git a/dev/tests/multibyte/in.clj b/dev/tests/multibyte/in.clj new file mode 100644 index 0000000..c05e75f --- /dev/null +++ b/dev/tests/multibyte/in.clj @@ -0,0 +1,15 @@ + (let [Δt (if foo + bar + baz)]) + + (let [Δt {:foo 'foo + :bar + 123}]) + + (let [Δt '[if foo + bar + baz]]) + + (let [Δt (assoc foo + :bar + 123)]) diff --git a/dev/tests/multibyte/out.clj b/dev/tests/multibyte/out.clj new file mode 100644 index 0000000..a117e52 --- /dev/null +++ b/dev/tests/multibyte/out.clj @@ -0,0 +1,15 @@ +(let [Δt (if foo + bar + baz)]) + +(let [Δt {:foo 'foo + :bar + 123}]) + +(let [Δt '[if foo + bar + baz]]) + +(let [Δt (assoc foo + :bar + 123)]) diff --git a/dev/tests/reader_conditionals/SKIP b/dev/tests/reader_conditionals/SKIP new file mode 100644 index 0000000..e69de29 diff --git a/dev/tests/reader_conditionals/in.clj b/dev/tests/reader_conditionals/in.clj new file mode 100644 index 0000000..f018a26 --- /dev/null +++ b/dev/tests/reader_conditionals/in.clj @@ -0,0 +1,11 @@ +(def DateTime #?(:clj org.joda.time.DateTime, + :cljs goog.date.UtcDateTime)) + +#?(:clj + (defn regexp? + "Returns true if x is a Java regular expression pattern." +[x] + (instance? java.util.regex.Pattern x))) + +#?@(:clj [5 6 7 8] + :cljs [1 2 3 4]) diff --git a/dev/tests/reader_conditionals/out.clj b/dev/tests/reader_conditionals/out.clj new file mode 100644 index 0000000..1554098 --- /dev/null +++ b/dev/tests/reader_conditionals/out.clj @@ -0,0 +1,11 @@ +(def DateTime #?(:clj org.joda.time.DateTime, + :cljs goog.date.UtcDateTime)) + +#?(:clj + (defn regexp? + "Returns true if x is a Java regular expression pattern." + [x] + (instance? java.util.regex.Pattern x))) + +#?@(:clj [5 6 7 8] + :cljs [1 2 3 4]) diff --git a/dev/tests/s-expr_standard/in.clj b/dev/tests/s-expr_standard/in.clj new file mode 100644 index 0000000..c0a3237 --- /dev/null +++ b/dev/tests/s-expr_standard/in.clj @@ -0,0 +1,140 @@ + (assoc {:foo 1} + :bar [2 + 3 + 4] + :biz 5) + + [:foo :bar +:biz :baz + "asdf" + 'a345r + 1234] + + {:hello "world" + :example "test" + 1234 'cake + [qwer + asdf + zxcv] #{1 2 + 3 4 :bar}} + + (qwer + [12 + 34 +56] + xczv) + + ((constantly +) +1 + 2) + + ((constantly +) 1 + 2) + + (filter + #(= 0 (mod % + 2)) + (range 1 10)) + +(#(foo) + bar) + + (#(foo + bar)) + + (#(foo bar +a)) + +(#(foo bar) +a) + +(#(foo bar) a + b) + +#_(:foo + {:foo 1}) + +(#_(foo) + bar) + + (#_(foo + bar)) + + (#_(foo bar) + a) + +(@foo bar + biz) + +(@foo +bar + biz) + +(#'foo bar + biz) + +(#'foo +bar + biz) + +('foo bar + biz) + +('foo +bar + biz) + + (ns my-namespace + (:require [foo :as f] + [bar :refer [x]]) + (:import + (java.io File + IOException) + [clojure.lang PersistentQueue])) + + (import '(java.io File + IOException) + '(clojure.lang PersistentQueue)) + +((if true + -) 1 + 3) + +((if true ++ + -) + 1 +3) + +(#'if (even? 1) + 2 + 3) + +(#(foo) bar + biz) + +("foo bar" biz + baz) + +(~@foo + ~bar) + +(~@foo ~bar + biz) + +(o bar + biz) + +({foo \} bar biz} foo +biz) + +('foo bar + 'biz) + +('#{foo bar} 1 + 2) + +(foo ; bar + biz) + +'(\" \b + \c) diff --git a/dev/tests/s-expr_standard/out.clj b/dev/tests/s-expr_standard/out.clj new file mode 100644 index 0000000..a3e38bd --- /dev/null +++ b/dev/tests/s-expr_standard/out.clj @@ -0,0 +1,140 @@ +(assoc {:foo 1} + :bar [2 + 3 + 4] + :biz 5) + +[:foo :bar + :biz :baz + "asdf" + 'a345r + 1234] + +{:hello "world" + :example "test" + 1234 'cake + [qwer + asdf + zxcv] #{1 2 + 3 4 :bar}} + +(qwer + [12 + 34 + 56] + xczv) + +((constantly +) + 1 + 2) + +((constantly +) 1 + 2) + +(filter + #(= 0 (mod % + 2)) + (range 1 10)) + +(#(foo) + bar) + +(#(foo + bar)) + +(#(foo bar + a)) + +(#(foo bar) + a) + +(#(foo bar) a + b) + +#_(:foo + {:foo 1}) + +(#_(foo) + bar) + +(#_(foo + bar)) + +(#_(foo bar) + a) + +(@foo bar + biz) + +(@foo + bar + biz) + +(#'foo bar + biz) + +(#'foo + bar + biz) + +('foo bar + biz) + +('foo + bar + biz) + +(ns my-namespace + (:require [foo :as f] + [bar :refer [x]]) + (:import + (java.io File + IOException) + [clojure.lang PersistentQueue])) + +(import '(java.io File + IOException) + '(clojure.lang PersistentQueue)) + +((if true + -) 1 + 3) + +((if true + + + -) + 1 + 3) + +(#'if (even? 1) + 2 + 3) + +(#(foo) bar + biz) + +("foo bar" biz + baz) + +(~@foo + ~bar) + +(~@foo ~bar + biz) + +(o bar + biz) + +({foo \} bar biz} foo + biz) + +('foo bar + 'biz) + +('#{foo bar} 1 + 2) + +(foo ; bar + biz) + +'(\" \b + \c) diff --git a/dev/tests/s-expr_traditional/in.clj b/dev/tests/s-expr_traditional/in.clj new file mode 100644 index 0000000..c0a3237 --- /dev/null +++ b/dev/tests/s-expr_traditional/in.clj @@ -0,0 +1,140 @@ + (assoc {:foo 1} + :bar [2 + 3 + 4] + :biz 5) + + [:foo :bar +:biz :baz + "asdf" + 'a345r + 1234] + + {:hello "world" + :example "test" + 1234 'cake + [qwer + asdf + zxcv] #{1 2 + 3 4 :bar}} + + (qwer + [12 + 34 +56] + xczv) + + ((constantly +) +1 + 2) + + ((constantly +) 1 + 2) + + (filter + #(= 0 (mod % + 2)) + (range 1 10)) + +(#(foo) + bar) + + (#(foo + bar)) + + (#(foo bar +a)) + +(#(foo bar) +a) + +(#(foo bar) a + b) + +#_(:foo + {:foo 1}) + +(#_(foo) + bar) + + (#_(foo + bar)) + + (#_(foo bar) + a) + +(@foo bar + biz) + +(@foo +bar + biz) + +(#'foo bar + biz) + +(#'foo +bar + biz) + +('foo bar + biz) + +('foo +bar + biz) + + (ns my-namespace + (:require [foo :as f] + [bar :refer [x]]) + (:import + (java.io File + IOException) + [clojure.lang PersistentQueue])) + + (import '(java.io File + IOException) + '(clojure.lang PersistentQueue)) + +((if true + -) 1 + 3) + +((if true ++ + -) + 1 +3) + +(#'if (even? 1) + 2 + 3) + +(#(foo) bar + biz) + +("foo bar" biz + baz) + +(~@foo + ~bar) + +(~@foo ~bar + biz) + +(o bar + biz) + +({foo \} bar biz} foo +biz) + +('foo bar + 'biz) + +('#{foo bar} 1 + 2) + +(foo ; bar + biz) + +'(\" \b + \c) diff --git a/dev/tests/s-expr_traditional/out.clj b/dev/tests/s-expr_traditional/out.clj new file mode 100644 index 0000000..98f2129 --- /dev/null +++ b/dev/tests/s-expr_traditional/out.clj @@ -0,0 +1,140 @@ +(assoc {:foo 1} + :bar [2 + 3 + 4] + :biz 5) + +[:foo :bar + :biz :baz + "asdf" + 'a345r + 1234] + +{:hello "world" + :example "test" + 1234 'cake + [qwer + asdf + zxcv] #{1 2 + 3 4 :bar}} + +(qwer + [12 + 34 + 56] + xczv) + +((constantly +) + 1 + 2) + +((constantly +) 1 + 2) + +(filter + #(= 0 (mod % + 2)) + (range 1 10)) + +(#(foo) + bar) + +(#(foo + bar)) + +(#(foo bar + a)) + +(#(foo bar) + a) + +(#(foo bar) a + b) + +#_(:foo + {:foo 1}) + +(#_(foo) + bar) + +(#_(foo + bar)) + +(#_(foo bar) + a) + +(@foo bar + biz) + +(@foo + bar + biz) + +(#'foo bar + biz) + +(#'foo + bar + biz) + +('foo bar + biz) + +('foo + bar + biz) + +(ns my-namespace + (:require [foo :as f] + [bar :refer [x]]) + (:import + (java.io File + IOException) + [clojure.lang PersistentQueue])) + +(import '(java.io File + IOException) + '(clojure.lang PersistentQueue)) + +((if true + -) 1 + 3) + +((if true + + + -) + 1 + 3) + +(#'if (even? 1) + 2 + 3) + +(#(foo) bar + biz) + +("foo bar" biz + baz) + +(~@foo + ~bar) + +(~@foo ~bar + biz) + +(o bar + biz) + +({foo \} bar biz} foo + biz) + +('foo bar + 'biz) + +('#{foo bar} 1 + 2) + +(foo ; bar + biz) + +'(\" \b + \c) diff --git a/dev/tests/s-expr_traditional/test.vim b/dev/tests/s-expr_traditional/test.vim new file mode 100644 index 0000000..b9a80a9 --- /dev/null +++ b/dev/tests/s-expr_traditional/test.vim @@ -0,0 +1,2 @@ +let g:clojure_indent_style = 'traditional' +normal! gg=G diff --git a/dev/tests/s-expr_uniform/in.clj b/dev/tests/s-expr_uniform/in.clj new file mode 100644 index 0000000..c0a3237 --- /dev/null +++ b/dev/tests/s-expr_uniform/in.clj @@ -0,0 +1,140 @@ + (assoc {:foo 1} + :bar [2 + 3 + 4] + :biz 5) + + [:foo :bar +:biz :baz + "asdf" + 'a345r + 1234] + + {:hello "world" + :example "test" + 1234 'cake + [qwer + asdf + zxcv] #{1 2 + 3 4 :bar}} + + (qwer + [12 + 34 +56] + xczv) + + ((constantly +) +1 + 2) + + ((constantly +) 1 + 2) + + (filter + #(= 0 (mod % + 2)) + (range 1 10)) + +(#(foo) + bar) + + (#(foo + bar)) + + (#(foo bar +a)) + +(#(foo bar) +a) + +(#(foo bar) a + b) + +#_(:foo + {:foo 1}) + +(#_(foo) + bar) + + (#_(foo + bar)) + + (#_(foo bar) + a) + +(@foo bar + biz) + +(@foo +bar + biz) + +(#'foo bar + biz) + +(#'foo +bar + biz) + +('foo bar + biz) + +('foo +bar + biz) + + (ns my-namespace + (:require [foo :as f] + [bar :refer [x]]) + (:import + (java.io File + IOException) + [clojure.lang PersistentQueue])) + + (import '(java.io File + IOException) + '(clojure.lang PersistentQueue)) + +((if true + -) 1 + 3) + +((if true ++ + -) + 1 +3) + +(#'if (even? 1) + 2 + 3) + +(#(foo) bar + biz) + +("foo bar" biz + baz) + +(~@foo + ~bar) + +(~@foo ~bar + biz) + +(o bar + biz) + +({foo \} bar biz} foo +biz) + +('foo bar + 'biz) + +('#{foo bar} 1 + 2) + +(foo ; bar + biz) + +'(\" \b + \c) diff --git a/dev/tests/s-expr_uniform/out.clj b/dev/tests/s-expr_uniform/out.clj new file mode 100644 index 0000000..083ddc8 --- /dev/null +++ b/dev/tests/s-expr_uniform/out.clj @@ -0,0 +1,140 @@ +(assoc {:foo 1} + :bar [2 + 3 + 4] + :biz 5) + +[:foo :bar + :biz :baz + "asdf" + 'a345r + 1234] + +{:hello "world" + :example "test" + 1234 'cake + [qwer + asdf + zxcv] #{1 2 + 3 4 :bar}} + +(qwer + [12 + 34 + 56] + xczv) + +((constantly +) + 1 + 2) + +((constantly +) 1 + 2) + +(filter + #(= 0 (mod % + 2)) + (range 1 10)) + +(#(foo) + bar) + +(#(foo + bar)) + +(#(foo bar + a)) + +(#(foo bar) + a) + +(#(foo bar) a + b) + +#_(:foo + {:foo 1}) + +(#_(foo) + bar) + +(#_(foo + bar)) + +(#_(foo bar) + a) + +(@foo bar + biz) + +(@foo + bar + biz) + +(#'foo bar + biz) + +(#'foo + bar + biz) + +('foo bar + biz) + +('foo + bar + biz) + +(ns my-namespace + (:require [foo :as f] + [bar :refer [x]]) + (:import + (java.io File + IOException) + [clojure.lang PersistentQueue])) + +(import '(java.io File + IOException) + '(clojure.lang PersistentQueue)) + +((if true + -) 1 + 3) + +((if true + + + -) + 1 + 3) + +(#'if (even? 1) + 2 + 3) + +(#(foo) bar + biz) + +("foo bar" biz + baz) + +(~@foo + ~bar) + +(~@foo ~bar + biz) + +(o bar + biz) + +({foo \} bar biz} foo + biz) + +('foo bar + 'biz) + +('#{foo bar} 1 + 2) + +(foo ; bar + biz) + +'(\" \b + \c) diff --git a/dev/tests/s-expr_uniform/test.vim b/dev/tests/s-expr_uniform/test.vim new file mode 100644 index 0000000..bc715f3 --- /dev/null +++ b/dev/tests/s-expr_uniform/test.vim @@ -0,0 +1,2 @@ +let g:clojure_indent_style = 'uniform' +normal! gg=G diff --git a/dev/tests/special_forms/in.clj b/dev/tests/special_forms/in.clj new file mode 100644 index 0000000..fda77be --- /dev/null +++ b/dev/tests/special_forms/in.clj @@ -0,0 +1,8 @@ +(try (/ 1 0) + (catch Exception e + (foo))) + +(try + (/ 1 0) + (catch Exception e + (foo))) diff --git a/dev/tests/special_forms/out.clj b/dev/tests/special_forms/out.clj new file mode 100644 index 0000000..388fb54 --- /dev/null +++ b/dev/tests/special_forms/out.clj @@ -0,0 +1,8 @@ +(try (/ 1 0) + (catch Exception e + (foo))) + +(try + (/ 1 0) + (catch Exception e + (foo))) diff --git a/dev/tests/with/in.clj b/dev/tests/with/in.clj new file mode 100644 index 0000000..993a33d --- /dev/null +++ b/dev/tests/with/in.clj @@ -0,0 +1,15 @@ +(with-open [f (io/file)] + (slurp f)) + +(with-meta obj + {:foo 1}) + +(with-meta + obj + {:foo 1}) + +(with-out-str +()) + +(with-in-str + ()) diff --git a/dev/tests/with/out.clj b/dev/tests/with/out.clj new file mode 100644 index 0000000..2738a16 --- /dev/null +++ b/dev/tests/with/out.clj @@ -0,0 +1,15 @@ +(with-open [f (io/file)] + (slurp f)) + +(with-meta obj + {:foo 1}) + +(with-meta + obj + {:foo 1}) + +(with-out-str + ()) + +(with-in-str + ()) diff --git a/doc/clojure.txt b/doc/clojure.txt index 1bd6018..583a2bb 100644 --- a/doc/clojure.txt +++ b/doc/clojure.txt @@ -5,124 +5,117 @@ INTRODUCTION *clojure-introduction* Clojure runtime files for Vim. -CLOJURE *ft-clojure-indent* *clojure-indent* +CLOJURE *ft-clojure-indent* *clojure-indent* Clojure indentation differs somewhat from traditional Lisps, due in part to the use of square and curly brackets, and otherwise by community convention. -These conventions are not universally followed, so the Clojure indent script -offers a few configuration options. +As these conventions are not universally followed, the Clojure indent script +offers ways to adjust the indentation. -(If the current Vim does not include |searchpairpos()|, the indent script falls -back to normal 'lisp' indenting, and the following options are ignored.) + *g:clojure_indent_style* + *b:clojure_indent_style* - *g:clojure_maxlines* +The `clojure_indent_style` config option controls the general indentation style +to use. Choose from several common presets: -Sets maximum scan distance of `searchpairpos()`. Larger values trade -performance for correctness when dealing with very long forms. A value of -0 will scan without limits. The default is 300. +* `standard` (default): + Conventional Clojure indentation. (Clojure Style Guide [1]) > + |(filter even? + | [1 2 3]) + | + |(filter + | even? + | [1 2 3]) +< +* `traditional`: + Indent like traditional Lisps. > + + |(filter even? + | [1 2 3]) + | + |(filter + | even? + | [1 2 3]) +< +* `uniform`: + Indent uniformly to 2 spaces with no alignment (aka Tonsky indentation [2]). +> + |(filter even? + | [1 2 3]) + | + |(filter + | even? + | [1 2 3]) +< +[1]: https://guide.clojure.style/ +[2]: https://tonsky.me/blog/clojurefmt/ - *g:clojure_fuzzy_indent* - *g:clojure_fuzzy_indent_patterns* - *g:clojure_fuzzy_indent_blacklist* -The 'lispwords' option is a list of comma-separated words that mark special -forms whose subforms should be indented with two spaces. + *g:clojure_indent_rules* + *b:clojure_indent_rules* -For example: -> - (defn bad [] - "Incorrect indentation") +TODO: add this option and write this section. - (defn good [] - "Correct indentation") -< -If you would like to specify 'lispwords' with a |pattern| instead, you can use -the fuzzy indent feature: -> - " Default - let g:clojure_fuzzy_indent = 1 - let g:clojure_fuzzy_indent_patterns = ['^with', '^def', '^let'] - let g:clojure_fuzzy_indent_blacklist = - \ ['-fn$', '\v^with-%(meta|out-str|loading-context)$'] -< -|g:clojure_fuzzy_indent_patterns| and |g:clojure_fuzzy_indent_blacklist| are -lists of patterns that will be matched against the unqualified symbol at the -head of a list. This means that a pattern like `"^foo"` will match all these -candidates: `foobar`, `my.ns/foobar`, and `#'foobar`. -Each candidate word is tested for special treatment in this order: + *g:clojure_fuzzy_indent_patterns* + *b:clojure_fuzzy_indent_patterns* - 1. Return true if word is literally in 'lispwords' - 2. Return false if word matches a pattern in - |g:clojure_fuzzy_indent_blacklist| - 3. Return true if word matches a pattern in - |g:clojure_fuzzy_indent_patterns| - 4. Return false and indent normally otherwise +TODO: add this option and write this section. - *g:clojure_special_indent_words* + *g:clojure_indent_multiline_strings* + *b:clojure_indent_multiline_strings* -Some forms in Clojure are indented such that every subform is indented by only -two spaces, regardless of 'lispwords'. If you have a custom construct that -should be indented in this idiosyncratic fashion, you can add your symbols to -the default list below. -> - " Default - let g:clojure_special_indent_words = - \ 'deftype,defrecord,reify,proxy,extend-type,extend-protocol,letfn' -< +Control alignment of new lines within Clojure multi-line strings and regular +expressions with `clojure_indent_multiline_strings`. - *g:clojure_align_multiline_strings* +NOTE: indenting with |=| will not alter the indentation within multi-line +strings, as this could break intentional formatting. -Align subsequent lines in multi-line strings to the column after the opening -quote, instead of the same column. +Pick from the following multi-line string indent styles: -For example: +* `standard` (default): + Align to the front of the `"` or `#"` delimiter. Ideal for doc-strings. > - (def default - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut - enim ad minim veniam, quis nostrud exercitation ullamco laboris - nisi ut aliquip ex ea commodo consequat.") - - (def aligned - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut - enim ad minim veniam, quis nostrud exercitation ullamco laboris - nisi ut aliquip ex ea commodo consequat.") + |(def standard + | "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do + | eiusmod tempor incididunt ut labore et dolore magna aliqua.") < - - *g:clojure_align_subforms* - -By default, parenthesized compound forms that look like function calls and -whose head subform is on its own line have subsequent subforms indented by -two spaces relative to the opening paren: +* `pretty`: + Align to the back of the `"` or `#"` delimiter. > - (foo - bar - baz) + |(def pretty + | "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do + | eiusmod tempor incididunt ut labore et dolore magna aliqua.") < -Setting this option to `1` changes this behaviour so that all subforms are -aligned to the same column, emulating the default behaviour of -clojure-mode.el: +* `traditional`: + No indent, align to left edge of the file. > - (foo - bar - baz) + |(def traditional + | "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do + |eiusmod tempor incididunt ut labore et dolore magna aliqua.") < - *g:clojure_cljfmt_compat* + *clojure-indent-deprecations* + +As part of the Clojure indentation script rebuild, the following configuration +options have been removed/replaced: -Try to be (more) compatible with `cljfmt` Clojure code formatting tool. Turns -on single space indenting for forms starting with `:keywords`, `'symbols`, -`#'variables` and `@dereferences` (it affects, for instance, `(:require ...)` -clause in Clojure `ns` form). +* *g:clojure_maxlines* -> none +* *g:clojure_cljfmt_compat* -> |g:clojure_indent_style| +* *g:clojure_align_subforms* -> |g:clojure_indent_style| +* *g:clojure_align_multiline_strings* -> |g:clojure_indent_multiline_strings| +* *g:clojure_special_indent_words* -> |g:clojure_indent_rules| +* *g:clojure_fuzzy_indent* -> none +* *g:clojure_fuzzy_indent_blacklist* -> none +* |'lispwords'| -> |g:clojure_indent_rules| CLOJURE *ft-clojure-syntax* + *g:clojure_syntax_keywords* Syntax highlighting of public vars in "clojure.core" is provided by default, @@ -170,22 +163,19 @@ ABOUT *clojure-about* This document and associated runtime files are maintained at: https://github.com/clojure-vim/clojure.vim -Distributed under the Vim license. See |license|. - -syntax/clojure.vim - - Copyright 2007-2008 (c) Toralf Wittner - Copyright 2008-2012 (c) Meikel Brandmeyer +Maintainer: Alex Vear +syntax/clojure.vim, ftdetect/clojure.vim, ftplugin/clojure.vim, indent/clojure.vim - Copyright 2008-2012 (c) Meikel Brandmeyer - -Modified and relicensed under the Vim License for distribution with Vim: + Distributed under the Vim license. See |license|. - Copyright 2013-2014 (c) Sung Pae + Copyright 2007–2008 (c) Toralf Wittner + Copyright 2008–2012 (c) Meikel Brandmeyer + Copyright 2013–2018 (c) Sung Pae + Copyright 2020–2025 (c) The clojure-vim contributors Last Change: %%RELEASE_DATE%% diff --git a/indent/clojure.vim b/indent/clojure.vim index dcbef55..09764b6 100644 --- a/indent/clojure.vim +++ b/indent/clojure.vim @@ -1,438 +1,321 @@ " Vim indent file -" Language: Clojure -" Maintainer: Alex Vear -" Former Maintainers: Sung Pae -" Meikel Brandmeyer -" URL: https://github.com/clojure-vim/clojure.vim -" License: Vim (see :h license) -" Last Change: %%RELEASE_DATE%% - -if exists("b:did_indent") - finish -endif +" Language: Clojure +" Maintainer: Alex Vear +" Former Maintainers: Sung Pae +" Meikel Brandmeyer +" Last Change: %%RELEASE_DATE%% +" License: Vim (see :h license) +" Repository: https://github.com/clojure-vim/clojure.vim + +" NOTE: To debug this code, make sure to "set debug+=msg" otherwise errors +" will occur silently. + +if exists("b:did_indent") | finish | endif let b:did_indent = 1 -let s:save_cpo = &cpo -set cpo&vim - -let b:undo_indent = 'setlocal autoindent< smartindent< expandtab< softtabstop< shiftwidth< indentexpr< indentkeys<' +let s:save_cpo = &cpoptions +set cpoptions&vim -setlocal noautoindent nosmartindent +setlocal noautoindent nosmartindent nolisp indentkeys=!,o,O setlocal softtabstop=2 shiftwidth=2 expandtab -setlocal indentkeys=!,o,O - -if exists("*searchpairpos") - - if !exists('g:clojure_maxlines') - let g:clojure_maxlines = 300 - endif - - if !exists('g:clojure_fuzzy_indent') - let g:clojure_fuzzy_indent = 1 - endif - - if !exists('g:clojure_fuzzy_indent_patterns') - let g:clojure_fuzzy_indent_patterns = ['^with', '^def', '^let'] - endif - - if !exists('g:clojure_fuzzy_indent_blacklist') - let g:clojure_fuzzy_indent_blacklist = ['-fn$', '\v^with-%(meta|out-str|loading-context)$'] - endif - - if !exists('g:clojure_special_indent_words') - let g:clojure_special_indent_words = 'deftype,defrecord,reify,proxy,extend-type,extend-protocol,letfn' +let b:undo_indent = 'setlocal autoindent< smartindent< expandtab< softtabstop< shiftwidth< indentexpr< indentkeys< lisp<' + +" Set a new configuration option with a default value. Assigns a script-local +" version too, to be used as a default fallback if the global was "unlet". +function! s:SConf(name, default) abort + let [s, g] = ['s:' . a:name, 'g:' . a:name] + exec 'let' 's:' . a:name '=' string(a:default) + if ! exists(g) | exec 'let' g '=' s | endif +endfunction + +" Get the value of a configuration option with a possible fallback. +function! s:Conf(opt, fallback) abort + return get(b:, a:opt, get(g:, a:opt, a:fallback)) +endfunction + +call s:SConf('clojure_indent_style', 'standard') +call s:SConf('clojure_indent_multiline_strings', 'standard') +call s:SConf('clojure_fuzzy_indent_patterns', ['\m^def', '\m^let', '\m^with-']) + +" FIXME: reader conditional indentation? + +" TODO: explain the different numbers. The "indent_style" option can override "0" +" -1 : Not in dictionary, follow defaults. +" 0 : Align to first argument, else 2 space indentation. +" 1+: 2 space indentation, no alignment. +" Defaults copied from: https://github.com/clojure-emacs/clojure-mode/blob/0e62583b5198f71856e4d7b80e1099789d47f2ed/clojure-mode.el#L1800-L1875 +call s:SConf('clojure_indent_rules', { +\ 'fn': 1, 'def': 1, 'defn': 1, 'bound-fn': 1, 'let': 1, 'binding': 1, 'defmethod': 1, +\ 'if': 1, 'if-not': 1, 'if-some': 1, 'if-let': 1, +\ 'when': 1, 'when-not': 1, 'when-some': 1, 'when-let': 1, 'when-first': 1, +\ 'case': 1, 'cond': 0, 'cond->': 1, 'cond->>': 1, 'condp': 2, +\ 'while': 1, 'loop': 1, 'for': 1, 'doseq': 1, 'dotimes': 1, +\ 'ns': 1, 'do': 0, 'doto': 1, 'comment': 0, 'as->': 2, +\ 'delay': 0, 'future': 0, 'locking': 1, 'try': 0, 'catch': 2, 'finally': 0, +\ 'reify': 1, 'proxy': 2, 'defrecord': 2, 'defprotocol': 1, 'definterface': 1, +\ 'extend': 1, 'extend-protocol': 1, 'extend-type': 1, +"\ [letfn] [1 [[:defn]] nil] [deftype defrecord proxy] [2 nil nil [:defn]] +"\ [defprotocol definterface extend-protocol extend-type] [1 [:defn]] +"\ ClojureScript +\ 'this-as': 1, 'specify': 1, 'specify!': 1, +"\ clojure.test +\ 'deftest': 1, 'testing': 1, 'use-fixtures': 1, 'are': 2, +"\ clojure.spec.alpha +\ 'fdef': 1, +"\ core.async +\ 'alt!': 0, 'alt!!': 0, 'go': 0, 'go-loop': 1, 'thread': 0, +"\ core.logic +\ 'run': 1, 'run*': 1, 'fresh': 1 +\ }) + +" Returns "1" if position "i_char" in "line_str" is preceded by an odd number +" of backslash characters (i.e. escaped). +function! s:IsEscaped(line_str, i_char) + let ln = a:line_str[: a:i_char - 1] + return (strlen(ln) - strlen(trim(ln, '\', 2))) % 2 +endfunction + +" Variation of "s:IsEscaped" which can be used within "search(pair)pos". +function! s:SkipIfEscaped() + let pos = getcursorcharpos() + return s:IsEscaped(getline(pos[1]), pos[2] - 1) +endfunction + +" Used during list function indentation. Returns the position of the first +" operand in the list on the first line of the form at "pos". +function! s:FirstFnArgPos(pos) + let [lnr, base_idx] = a:pos + let ln = getline(lnr) + call cursor([lnr, base_idx + 1]) + + if ln[base_idx] =~# '\m["\\,[:space:]]' | return [0, 0] | endif + + " Find first collection delimiter or char preceeding whitespace. + let pos = searchpos('\m\([{\[(]\|.[[:space:],]\)', 'cWz', lnr) + if pos == [0, 0] | return pos | endif + + " If at collection delimiter, jump to end delimiter. + let ch = ln[pos[1] - 1] + if has_key(s:pairs, ch) + let pos = searchpairpos('\V' . ch, '', '\V' . get(s:pairs, ch), 'Wz', function('s:SkipIfEscaped'), lnr) + " If end not on same line: no arg. + if pos == [0, 0] | return pos | endif endif - if !exists('g:clojure_align_multiline_strings') - let g:clojure_align_multiline_strings = 0 - endif - - if !exists('g:clojure_align_subforms') - let g:clojure_align_subforms = 0 - endif - - if !exists('g:clojure_cljfmt_compat') - let g:clojure_cljfmt_compat = 0 - endif - - function! s:syn_id_name() - return synIDattr(synID(line("."), col("."), 0), "name") - endfunction - - function! s:ignored_region() - return s:syn_id_name() =~? '\vstring|regex|comment|character' - endfunction - - function! s:current_char() - return getline('.')[col('.')-1] - endfunction - - function! s:current_word() - return getline('.')[col('.')-1 : searchpos('\v>', 'n', line('.'))[1]-2] - endfunction - - function! s:is_paren() - return s:current_char() =~# '\v[\(\)\[\]\{\}]' && !s:ignored_region() - endfunction - - " Returns 1 if string matches a pattern in 'patterns', which should be - " a list of patterns. - function! s:match_one(patterns, string) - for pat in a:patterns - if a:string =~# pat | return 1 | endif - endfor - endfunction - - function! s:match_pairs(open, close, stopat) - " Stop only on vector and map [ resp. {. Ignore the ones in strings and - " comments. - if a:stopat == 0 && g:clojure_maxlines > 0 - let stopat = max([line(".") - g:clojure_maxlines, 0]) - else - let stopat = a:stopat - endif - - let pos = searchpairpos(a:open, '', a:close, 'bWn', "!s:is_paren()", stopat) - return [pos[0], col(pos)] - endfunction - - function! s:clojure_check_for_string_worker() - " Check whether there is the last character of the previous line is - " highlighted as a string. If so, we check whether it's a ". In this - " case we have to check also the previous character. The " might be the - " closing one. In case the we are still in the string, we search for the - " opening ". If this is not found we take the indent of the line. - let nb = prevnonblank(v:lnum - 1) - - if nb == 0 - return -1 - endif - - call cursor(nb, 0) - call cursor(0, col("$") - 1) - if s:syn_id_name() !~? "string" - return -1 - endif - - " This will not work for a " in the first column... - if s:current_char() == '"' - call cursor(0, col("$") - 2) - if s:syn_id_name() !~? "string" - return -1 - endif - if s:current_char() != '\' - return -1 - endif - call cursor(0, col("$") - 1) - endif - - let p = searchpos('\(^\|[^\\]\)\zs"', 'bW') - - if p != [0, 0] - return p[1] - 1 - endif - - return indent(".") - endfunction - - function! s:check_for_string() - let pos = getpos('.') - try - let val = s:clojure_check_for_string_worker() - finally - call setpos('.', pos) - endtry - return val - endfunction - - function! s:strip_namespace_and_macro_chars(word) - return substitute(a:word, "\\v%(.*/|[#'`~@^,]*)(.*)", '\1', '') - endfunction - - function! s:clojure_is_method_special_case_worker(position) - " Find the next enclosing form. - call search('\S', 'Wb') - - " Special case: we are at a '(('. - if s:current_char() == '(' - return 0 - endif - call cursor(a:position) - - let next_paren = s:match_pairs('(', ')', 0) - - " Special case: we are now at toplevel. - if next_paren == [0, 0] - return 0 - endif - call cursor(next_paren) - - call search('\S', 'W') - let w = s:strip_namespace_and_macro_chars(s:current_word()) - - if g:clojure_special_indent_words =~# '\V\<' . w . '\>' - - " `letfn` is a special-special-case. - if w ==# 'letfn' - " Earlier code left the cursor at: - " (letfn [...] ...) - " ^ - - " Search and get coordinates of first `[` - " (letfn [...] ...) - " ^ - call search('\[', 'W') - let pos = getcurpos() - let letfn_bracket = [pos[1], pos[2]] - - " Move cursor to start of the form this function was - " initially called on. Grab the coordinates of the - " closest outer `[`. - call cursor(a:position) - let outer_bracket = s:match_pairs('\[', '\]', 0) - - " If the located square brackets are not the same, - " don't use special-case formatting. - if outer_bracket != letfn_bracket - return 0 + " Search forwards for first non-whitespace/comment char on line. + let pos = searchpos('\m[^[:space:],]', 'Wz', lnr) + return ln[pos[1] - 1] ==# ';' ? [0, 0] : pos +endfunction + +" Converts a cursor position into a characterwise cursor column position (to +" handle multibyte characters). +function! s:PosToCharCol(pos) + call cursor(a:pos) | return getcursorcharpos()[2] +endfunction + +" Repeatedly search for indentation significant Clojure tokens on a given line +" (in reverse order) building up a list of tokens and their positions. +" Ignores escaped tokens. Does not care about strings, which is handled by +" "s:InsideForm". +function! s:TokeniseLine(line_num) + let tokens = [] + let ln = getline(a:line_num) + let possible_comment = 0 + + while 1 + " We perform searches within the buffer (and move the cusor) + " for better performance than looping char by char in a line. + let token_pos = searchpos('\m[()[\]{};"]', 'bW', a:line_num) + + " No more matches, exit loop. + if token_pos == [0, 0] | break | endif + + let t_idx = token_pos[1] - 1 + + " Escaped character, ignore. + if s:IsEscaped(ln, t_idx) | continue | endif + + " Add token to the list. + let token = ln[t_idx] + call add(tokens, [token, token_pos]) + + " Early "possible comment" detection to reduce copying later. + if token ==# ';' | let possible_comment = 1 | endif + endwhile + + return [tokens, possible_comment] +endfunction + +let s:pairs = {'(': ')', '[': ']', '{': '}'} + +" This procedure is kind of like a really lightweight Clojure reader that +" analyses from the inside out. It looks at the lines above the current line, +" tokenises them (from right to left), and performs reductions to find the +" parent form and where it is. +function! s:InsideForm(lnum) + " Reset cursor to first column of the line we wish to indent. + call cursor(a:lnum, 1) + + " Token list looks like this: "[[delim, [line, col]], ...]". + let tokens = [] + let first_string_pos = [] + let in_string = 0 + + let lnum = a:lnum - 1 + while lnum > 0 + let [line_tokens, possible_comment] = s:TokeniseLine(lnum) + + " In case of comments, copy "tokens" so we can undo alterations. + if possible_comment | let prev_tokens = copy(tokens) | endif + + " Reduce tokens from line "lnum" into "tokens". + for tk in line_tokens + if tk[0] ==# '"' + if in_string + let in_string = 0 + call remove(tokens, -1) + else + let in_string = 1 + call add(tokens, tk) + + " Track the first string delimiter we + " see, as we may need it later for + " multi-line strings/regexps. + if first_string_pos == [] + let first_string_pos = tk + endif endif + elseif in_string " In string: ignore other tokens. + elseif possible_comment && tk[0] ==# ';' + " Comment: undo previous token applications on this line. + let tokens = copy(prev_tokens) + elseif ! empty(tokens) && get(s:pairs, tk[0], '') ==# tokens[-1][0] + " Matching pair: drop the last item in tokens. + call remove(tokens, -1) + else + " No match: append to token list. + call add(tokens, tk) endif + endfor - return 1 - endif - - return 0 - endfunction - - function! s:is_method_special_case(position) - let pos = getpos('.') - try - let val = s:clojure_is_method_special_case_worker(a:position) - finally - call setpos('.', pos) - endtry - return val - endfunction - - " Check if form is a reader conditional, that is, it is prefixed by #? - " or #?@ - function! s:is_reader_conditional_special_case(position) - return getline(a:position[0])[a:position[1] - 3 : a:position[1] - 2] == "#?" - \|| getline(a:position[0])[a:position[1] - 4 : a:position[1] - 2] == "#?@" - endfunction - - " Returns 1 for opening brackets, -1 for _anything else_. - function! s:bracket_type(char) - return stridx('([{', a:char) > -1 ? 1 : -1 - endfunction - - " Returns: [opening-bracket-lnum, indent] - function! s:clojure_indent_pos() - " Get rid of special case. - if line(".") == 1 - return [0, 0] - endif - - " We have to apply some heuristics here to figure out, whether to use - " normal lisp indenting or not. - let i = s:check_for_string() - if i > -1 - return [0, i + !!g:clojure_align_multiline_strings] - endif - - call cursor(0, 1) - - " Find the next enclosing [ or {. We can limit the second search - " to the line, where the [ was found. If no [ was there this is - " zero and we search for an enclosing {. - let paren = s:match_pairs('(', ')', 0) - let bracket = s:match_pairs('\[', '\]', paren[0]) - let curly = s:match_pairs('{', '}', bracket[0]) - - " In case the curly brace is on a line later then the [ or - in - " case they are on the same line - in a higher column, we take the - " curly indent. - if curly[0] > bracket[0] || curly[1] > bracket[1] - if curly[0] > paren[0] || curly[1] > paren[1] - return curly - endif - endif - - " If the curly was not chosen, we take the bracket indent - if - " there was one. - if bracket[0] > paren[0] || bracket[1] > paren[1] - return bracket - endif - - " There are neither { nor [ nor (, ie. we are at the toplevel. - if paren == [0, 0] - return paren - endif - - " Now we have to reimplement lispindent. This is surprisingly easy, as - " soon as one has access to syntax items. - " - " - Check whether we are in a special position after a word in - " g:clojure_special_indent_words. These are special cases. - " - Get the next keyword after the (. - " - If its first character is also a (, we have another sexp and align - " one column to the right of the unmatched (. - " - In case it is in lispwords, we indent the next line to the column of - " the ( + sw. - " - If not, we check whether it is last word in the line. In that case - " we again use ( + sw for indent. - " - In any other case we use the column of the end of the word + 2. - call cursor(paren) - - if s:is_method_special_case(paren) - return [paren[0], paren[1] + &shiftwidth - 1] - endif - - if s:is_reader_conditional_special_case(paren) - return paren - endif - - " In case we are at the last character, we use the paren position. - if col("$") - 1 == paren[1] - return paren + if ! empty(tokens) && has_key(s:pairs, tokens[0][0]) && ! in_string + return tokens[0] " Match found! endif - " In case after the paren is a whitespace, we search for the next word. - call cursor(0, col('.') + 1) - if s:current_char() == ' ' - call search('\v\S', 'W') - endif + let lnum -= 1 + endwhile - " If we moved to another line, there is no word after the (. We - " use the ( position for indent. - if line(".") > paren[0] - return paren - endif + " TODO: can this conditional be simplified? + if (in_string && first_string_pos != []) || (! empty(tokens) && tokens[0][0] ==# '"') + " String was not closed, must have been in a multi-line string or regex. + return first_string_pos + endif - " We still have to check, whether the keyword starts with a (, [ or {. - " In that case we use the ( position for indent. - let w = s:current_word() - if s:bracket_type(w[0]) == 1 - return paren + return ['^', [0, 0]] " Default to top-level. +endfunction + +" Returns "1" when the "=" operator is currently active, else "0". +function! s:EqualsOperatorInEffect() + return exists('*state') ? v:operator ==# '=' && state('o') ==# 'o' : 0 +endfunction + +function! s:StringIndent(delim_pos) + " Mimic multi-line string indentation behaviour in VS Code and Emacs. + let m = mode() + if m ==# 'i' || (m ==# 'n' && ! s:EqualsOperatorInEffect()) + " If in insert mode, or normal mode but "=" is not in effect. + let alignment = s:Conf('clojure_indent_multiline_strings', s:clojure_indent_multiline_strings) + if alignment ==# 'traditional' | return 0 + elseif alignment ==# 'pretty' | return s:PosToCharCol(a:delim_pos) + else " standard + let col = a:delim_pos[1] + let is_regex = col > 1 && getline(a:delim_pos[0])[col - 2] ==# '#' + return s:PosToCharCol(a:delim_pos) - (is_regex ? 2 : 1) endif - - " If the keyword begins with #, check if it is an anonymous - " function or set, in which case we indent by the shiftwidth - " (minus one if g:clojure_align_subforms = 1), or if it is - " ignored, in which case we use the ( position for indent. - if w[0] == "#" - " TODO: Handle #=() and other rare reader invocations? - if w[1] == '(' || w[1] == '{' - return [paren[0], paren[1] + (g:clojure_align_subforms ? 0 : &shiftwidth - 1)] - elseif w[1] == '_' - return paren - elseif w[1] == "'" && g:clojure_cljfmt_compat - return paren + else | return -1 " Keep existing indent. + endif +endfunction + +function! s:ListIndent(delim_pos) + " TODO: extend "s:InsideForm" to provide information about the + " subforms being formatted to avoid second parsing step. + + let indent_style = s:Conf('clojure_indent_style', s:clojure_indent_style) + let base_indent = s:PosToCharCol(a:delim_pos) + + " Uniform indentation: just indent by 2 spaces. + if indent_style ==# 'uniform' | return base_indent + 1 | endif + + let ln = getline(a:delim_pos[0]) + let ln_content = ln[a:delim_pos[1]:] + + " 1. Macro/rule indentation + " if starts with a symbol, extract it. + " - Split namespace off symbol and #'/' syntax. + " - Check against pattern rules and apply indent on match. + " - Look up in rules table and apply indent on match. + " else: not found, go to 2. + " TODO: handle complex indentation (e.g. letfn). Skip if "traditional" style was chosen? + + " TODO: simplify this. + let syms = split(ln_content, '[[:space:],;()\[\]{}@\\"^~`]', 1) + let sym_match = -1 + + if ! empty(syms) + let sym = syms[0] + if sym =~# '\v^%([a-zA-Z!$&*_+=|<>?-]|[^\x00-\x7F])' + " TODO: handle namespaced and non-namespaced variants. + if sym =~# '\m./.' + let [_namespace, name] = split(sym, '\m/') endif - endif - - " Paren indent for keywords, symbols and derefs - if g:clojure_cljfmt_compat && w[0] =~# "[:@']" - return paren - endif - " Test words without namespace qualifiers and leading reader macro - " metacharacters. - " - " e.g. clojure.core/defn and #'defn should both indent like defn. - let ww = s:strip_namespace_and_macro_chars(w) + " TODO: replace `clojure_fuzzy_indent_patterns` with `clojure_indent_patterns`? + for pat in s:Conf('clojure_fuzzy_indent_patterns', []) + if sym =~# pat | return base_indent + 1 | endif + endfor - if &lispwords =~# '\V\<' . ww . '\>' - return [paren[0], paren[1] + &shiftwidth - 1] + let rules = s:Conf('clojure_indent_rules', {}) + let sym_match = get(rules, sym, -1) + " TODO: handle 2+ differently? + if sym_match > 0 | return base_indent + 1 | endif endif + endif - if g:clojure_fuzzy_indent - \ && !s:match_one(g:clojure_fuzzy_indent_blacklist, ww) - \ && s:match_one(g:clojure_fuzzy_indent_patterns, ww) - return [paren[0], paren[1] + &shiftwidth - 1] - endif - - call search('\v\_s', 'cW') - call search('\v\S', 'W') - if paren[0] < line(".") - return [paren[0], paren[1] + (g:clojure_align_subforms ? 0 : &shiftwidth - 1)] - endif - - call search('\v\S', 'bW') - return [line('.'), col('.') + 1] - endfunction - - function! GetClojureIndent() - let lnum = line('.') - let orig_lnum = lnum - let orig_col = col('.') - let [opening_lnum, indent] = s:clojure_indent_pos() - - " Account for multibyte characters - if opening_lnum > 0 - let indent -= indent - virtcol([opening_lnum, indent]) - endif - - " Return if there are no previous lines to inherit from - if opening_lnum < 1 || opening_lnum >= lnum - 1 - call cursor(orig_lnum, orig_col) - return indent - endif - - let bracket_count = 0 - - " Take the indent of the first previous non-white line that is - " at the same sexp level. cf. src/misc1.c:get_lisp_indent() - while 1 - let lnum = prevnonblank(lnum - 1) - let col = 1 - - if lnum <= opening_lnum - break - endif - - call cursor(lnum, col) - - " Handle bracket counting edge case - if s:is_paren() - let bracket_count += s:bracket_type(s:current_char()) - endif - - while 1 - if search('\v[(\[{}\])]', '', lnum) < 1 - break - elseif !s:ignored_region() - let bracket_count += s:bracket_type(s:current_char()) - endif - endwhile - - if bracket_count == 0 - " Check if this is part of a multiline string - call cursor(lnum, 1) - if s:syn_id_name() !~? '\vstring|regex' - call cursor(orig_lnum, orig_col) - return indent(lnum) - endif - endif - endwhile - - call cursor(orig_lnum, orig_col) - return indent - endfunction - - setlocal indentexpr=GetClojureIndent() - -else - - " In case we have searchpairpos not available we fall back to - " normal lisp indenting. - setlocal indentexpr= - setlocal lisp - let b:undo_indent .= '| setlocal lisp<' + " 2. Function indentation + " if first operand is on the same line? + " - Indent subsequent lines to align with first operand. + " else: indent 1 or 2 spaces. + let pos = s:FirstFnArgPos(a:delim_pos) + if pos != [0, 0] | return s:PosToCharCol(pos) - 1 | endif + + " Fallback indentation for operands. When "clojure_indent_style" is + " "traditional", use 2 space indentation, else 1 space indentation. + " The "sym_match" check handles the case when "clojure_indent_rules" + " specified a value of "0" for "standard" style. + return base_indent + (indent_style ==# 'traditional' || sym_match == 0) +endfunction + +" TODO: improve configurability for other Clojure-like languages. +function! ClojureIndent() + " Calculate and return indent to use based on the matching form. + let [form, pos] = s:InsideForm(v:lnum) + if form ==# '^' | return 0 " At top-level, no indent. + elseif form ==# '(' | return s:ListIndent(pos) + elseif form ==# '[' | return s:PosToCharCol(pos) + elseif form ==# '{' | return s:PosToCharCol(pos) + elseif form ==# '"' | return s:StringIndent(pos) + else | return -1 " Keep existing indent. + endif +endfunction +" Connect indentation function. +if exists('&lispoptions') + setlocal lisp lispoptions=expr:1 + let b:undo_indent .= ' lispoptions<' endif +setlocal indentexpr=ClojureIndent() -let &cpo = s:save_cpo +let &cpoptions = s:save_cpo unlet! s:save_cpo " vim:sts=8:sw=8:ts=8:noet