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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy