guns / vim-clojure-static

Meikel Brandmeyer's excellent Clojure runtime files
Other
420 stars 50 forks source link

Performance issue related indent/clojure.vim #88

Open hovsater opened 5 years ago

hovsater commented 5 years ago

I've just got my hands into Clojure recently and noticed that Vim was lagging pretty hard. It seems like the culprit is indent/clojure.vim.

Given the following Clojure file, adding newlines at the end of the file will result in noticeable lag.

core.clojure

(ns introduction-to-clojure.core
  (:require [bakery.core :refer :all]))

(defn error [& args]
  (apply println args)
  :error)

(def baking  {:recipes {:cake {:ingredients {:egg   2
                                             :flour 2
                                             :sugar 1
                                             :milk  1}
                               :steps [[:add :all]
                                       [:mix]
                                       [:pour]
                                       [:bake 25]
                                       [:cool]]}
                        :cookies {:ingredients {:egg 1
                                                :flour 1
                                                :butter 1
                                                :sugar 1}
                                  :steps [[:add :all]
                                          [:mix]
                                          [:pour]
                                          [:bake 30]
                                          [:cool]]}
                        :brownies {:ingredients {:egg 2
                                                 :flour 2
                                                 :butter 2
                                                 :cocoa 2
                                                 :sugar 1
                                                 :milk 1}
                                   :steps [[:add :butter]
                                           [:add :cocoa]
                                           [:add :sugar]
                                           [:mix]
                                           [:add :egg]
                                           [:add :flour]
                                           [:add :milk]
                                           [:mix]
                                           [:pour]
                                           [:bake 35]
                                           [:cool]]}}
              :ingredients {:egg {:storage :fridge
                                  :usage :squeezed}
                            :milk {:storage :fridge
                                   :usage :scooped}
                            :flour {:storage :pantry
                                    :usage :scooped}
                            :butter {:storage :fridge
                                     :usage :simple}
                            :sugar {:storage :pantry
                                    :usage :scooped}
                            :cocoa {:storage :pantry
                                    :usage :scooped}}})

(def usage {:squeezed (fn [ingredient amount]
                        (dotimes [i amount]
                          (grab ingredient)
                          (squeeze)
                          (add-to-bowl)))
            :simple (fn [ingredient amount]
                      (dotimes [i amount]
                        (grab ingredient)
                        (add-to-bowl)))
            :scooped (fn [ingredient amount]
                       (grab :cup)
                       (dotimes [i amount]
                         (scoop ingredient)
                         (add-to-bowl))
                       (release))})

(defn usage-type [ingredient]
  (let [ingredients (get baking :ingredients)
        info (get ingredients ingredient)]
    (get info :usage)))

(defn add
  ([ingredient]
    (add ingredient 1))
  ([ingredient amount]
    (let [ingredient-type (usage-type ingredient)]
      (if (contains? usage ingredient-type)
        (let [f (get usage ingredient-type)]
          (f ingredient amount))
        (error "I do not know the ingredient" ingredient)))))

(def actions {:cool (fn [ingredients step]
                      (cool-pan))
              :mix  (fn [ingredients step]
                      (mix))
              :pour (fn [ingredients step]
                      (pour-into-pan))
              :bake (fn [ingredients step]
                      (bake-pan (second step)))
              :add  (fn [ingredients step]
                      (cond
                        (and (= 2 (count step))
                             (= :all (second step)))
                          (doseq [kv ingredients]
                            (add (first kv) (second kv)))
                        (and (= 2 (count step))
                             (contains? ingredients (second step)))
                        (add (second step) (get ingredients (second step)))
                        (= 3 (count step))
                        (add (second step) (get step 2))
                        :else
                        (error "I don't know how to add" (second step) (get step 2))))})

(defn perform [ingredients step]
  (let [f (get actions (first step) (fn [ingredients step]
                                      (println "I do not know how to" (first step))))]
    (f ingredients step)))

(defn bake-recipe [recipe]
  (last
    (for [step (get recipe :steps)]
      (perform (get recipe :ingredients) step))))

(defn load-up-amount [ingredient amount]
  (dotimes [i amount]
    (load-up ingredient)))

(defn unload-amount [ingredient amount]
  (dotimes [i amount]
    (unload ingredient)))

(defn fetch-ingredient
  ([ingredient]
    (fetch-ingredient ingredient 1))
  ([ingredient amount]
    (let [ingredients (get baking :ingredients)
          info (get ingredients ingredient)]
      (if (contains? ingredients ingredient)
        (do
          (go-to (get info :storage))
          (load-up-amount ingredient amount)
          (go-to :prep-area)
          (unload-amount ingredient amount))
        (error "I don't know the ingredient" ingredient)))))

(defn storage-location [ingredient]
  (let [ingredients (get baking :ingredients)
        info (get ingredients ingredient)]
    (get info :storage)))

(defn fetch-list [shopping]
  (let [by-location (group-by (fn [item-amount]
                                (storage-location (first item-amount)))
                              shopping)]
    (doseq [loc by-location]
      (go-to (first loc))
      (doseq [item-amount (second loc)]
        (load-up-amount (first item-amount) (second item-amount)))))

  (go-to :prep-area)
  (doseq [item-amount shopping]
    (unload-amount (first item-amount) (second item-amount))))

(defn add-ingredients [a b]
  (merge-with + a b))

(defn multiply-ingredients [n ingredients]
  (into {}
    (for [kv ingredients]
      [(first kv) (* n (second kv))])))

(defn order->ingredients [order]
  (let [recipes (get baking :recipes)
        items (get order :items)]
    (reduce add-ingredients {}
            (for [kv items]
              (let [recipe (get recipes (first kv))
                    ingredients (get recipe :ingredients)]
                (multiply-ingredients (second kv) ingredients))))))

(defn orders->ingredients [orders]
  (reduce add-ingredients {}
    (for [order orders]
      (order->ingredients order))))

(defn bake [item]
  (let [recipes (get baking :recipes)]
    (bake-recipe (get recipes item))))

(defn day-at-the-bakery []
  (let [orders (get-morning-orders-day3)
        ingredients (orders->ingredients orders)]
    (fetch-list ingredients)
    (doseq [order orders]
      (let [items (get order :items)
            racks (for [kv items
                        i (range (second kv))]
                    (bake (first kv)))
            receipt {:orderid (get order :orderid)
                     :address (get order :address)
                     :rackids racks}]
        (delivery receipt)))))

(defn -main []
  (bake-cake)
  (bake-cookies))

profile.log

FUNCTION  <SNR>8_match_pairs()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 87
Called 63 times
Total time:   4.081067
 Self time:   0.245977

count  total (s)   self (s)
                                    " Stop only on vector and map [ resp. {. Ignore the ones in strings and
                                    " comments.
   63              0.000099         if a:stopat == 0
   63              0.000279             let stopat = max([line(".") - g:clojure_maxlines, 0])
                                    else
                                        let stopat = a:stopat
   63              0.000036         endif

   63   4.080176   0.245086         let pos = searchpairpos(a:open, '', a:close, 'bWn', "!s:is_paren()", stopat)
   63              0.000211         return [pos[0], col(pos)]

FUNCTION  <SNR>8_clojure_indent_pos()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 206
Called 21 times
Total time:   4.092601
 Self time:   0.001783

count  total (s)   self (s)
                                    " Get rid of special case.
   21              0.000048         if line(".") == 1
                                        return [0, 0]
   21              0.000018         endif

                                    " We have to apply some heuristics here to figure out, whether to use
                                    " normal lisp indenting or not.
   21   0.010045   0.000294         let i = s:check_for_string()
   21              0.000020         if i > -1
                                        return [0, i + !!g:clojure_align_multiline_strings]
   21              0.000014         endif

   21              0.000048         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 {.
   21   3.064751   0.000319         let paren = s:match_pairs('(', ')', 0)
   21   0.895804   0.000275         let bracket = s:match_pairs('\[', '\]', paren[0])
   21   0.121386   0.000280         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.
   21              0.000055         if curly[0] > bracket[0] || curly[1] > bracket[1]
                                        if curly[0] > paren[0] || curly[1] > paren[1]
                                            return curly
                                        endif
   21              0.000014         endif

                                    " If the curly was not chosen, we take the bracket indent - if
                                    " there was one.
   21              0.000042         if bracket[0] > paren[0] || bracket[1] > paren[1]
                                        return bracket
   21              0.000012         endif

                                    " There are neither { nor [ nor (, ie. we are at the toplevel.
   21              0.000035         if paren == [0, 0]
   21              0.000018             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
                                    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

                                    " 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

                                    " 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
                                    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)

                                    if &lispwords =~# '\V\<' . ww . '\>'
                                        return [paren[0], paren[1] + shiftwidth() - 1]
                                    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]

FUNCTION  <SNR>8_clojure_check_for_string_worker()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 100
Called 21 times
Total time:   0.009117
 Self time:   0.000672

count  total (s)   self (s)
                                    " 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.
   21              0.000086         let nb = prevnonblank(v:lnum - 1)

   21              0.000019         if nb == 0
                                        return -1
   21              0.000012         endif

   21              0.000047         call cursor(nb, 0)
   21              0.000065         call cursor(0, col("$") - 1)
   21   0.008778   0.000333         if s:syn_id_name() !~? "string"
   21              0.000022             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(".")

FUNCTION  <SNR>8_current_char()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 63
Called 6050 times
Total time:   0.022916
 Self time:   0.022916

count  total (s)   self (s)
 6050              0.021053         return getline('.')[col('.')-1]

FUNCTION  <SNR>8_check_for_string()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 139
Called 21 times
Total time:   0.009751
 Self time:   0.000634

count  total (s)   self (s)
   21              0.000072         let pos = getpos('.')
   21              0.000020         try
   21   0.009506   0.000389             let val = s:clojure_check_for_string_worker()
   21              0.000023         finally
   21              0.000060             call setpos('.', pos)
   21              0.000018         endtry
   21              0.000032         return val

FUNCTION  <SNR>8_is_paren()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 71
Called 6050 times
Total time:   3.835090
 Self time:   0.143374

count  total (s)   self (s)
 6050   3.833106   0.141390         return s:current_char() =~# '\v[\(\)\[\]\{\}]' && !s:ignored_region()

FUNCTION  GetClojureIndent()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 321
Called 21 times
Total time:   4.093354
 Self time:   0.000753

count  total (s)   self (s)
   21              0.000089         let lnum = line('.')
   21              0.000028         let orig_lnum = lnum
   21              0.000043         let orig_col = col('.')
   21   4.092957   0.000356         let [opening_lnum, indent] = s:clojure_indent_pos()

                                    " Account for multibyte characters
   21              0.000018         if opening_lnum > 0
                                        let indent -= indent - virtcol([opening_lnum, indent])
   21              0.000011         endif

                                    " Return if there are no previous lines to inherit from
   21              0.000025         if opening_lnum < 1 || opening_lnum >= lnum - 1
   21              0.000064             call cursor(orig_lnum, orig_col)
   21              0.000019             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

FUNCTION  <SNR>8_ignored_region()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 59
Called 6050 times
Total time:   3.668800
 Self time:   0.127724

count  total (s)   self (s)
 6050   3.666821   0.125745         return s:syn_id_name() =~? '\vstring|regex|comment|character'

FUNCTION  <SNR>8_syn_id_name()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 55
Called 6071 times
Total time:   3.549521
 Self time:   3.549521

count  total (s)   self (s)
 6071              3.547512         return synIDattr(synID(line("."), col("."), 0), "name")

FUNCTIONS SORTED ON TOTAL TIME
count  total (s)   self (s)  function
   21   4.093354   0.000753  GetClojureIndent()
   21   4.092601   0.001783  <SNR>8_clojure_indent_pos()
   63   4.081067   0.245977  <SNR>8_match_pairs()
 6050   3.835090   0.143374  <SNR>8_is_paren()
 6050   3.668800   0.127724  <SNR>8_ignored_region()
 6071   3.549521             <SNR>8_syn_id_name()
 6050   0.022916             <SNR>8_current_char()
   21   0.009751   0.000634  <SNR>8_check_for_string()
   21   0.009117   0.000672  <SNR>8_clojure_check_for_string_worker()

FUNCTIONS SORTED ON SELF TIME
count  total (s)   self (s)  function
 6071              3.549521  <SNR>8_syn_id_name()
   63   4.081067   0.245977  <SNR>8_match_pairs()
 6050   3.835090   0.143374  <SNR>8_is_paren()
 6050   3.668800   0.127724  <SNR>8_ignored_region()
 6050              0.022916  <SNR>8_current_char()
   21   4.092601   0.001783  <SNR>8_clojure_indent_pos()
   21   4.093354   0.000753  GetClojureIndent()
   21   0.009117   0.000672  <SNR>8_clojure_check_for_string_worker()
   21   0.009751   0.000634  <SNR>8_check_for_string()

vim --version

VIM - Vi IMproved 8.1 (2018 May 18, compiled Dec 28 2018 20:12:37)
macOS version
Included patches: 1-650
Compiled by Homebrew
Huge version without GUI.  Features included (+) or not (-):
+acl               +extra_search      +mouse_netterm     +tag_old_static
+arabic            +farsi             +mouse_sgr         -tag_any_white
+autocmd           +file_in_path      -mouse_sysmouse    -tcl
+autochdir         +find_in_path      +mouse_urxvt       +termguicolors
-autoservername    +float             +mouse_xterm       +terminal
-balloon_eval      +folding           +multi_byte        +terminfo
+balloon_eval_term -footer            +multi_lang        +termresponse
-browse            +fork()            -mzscheme          +textobjects
++builtin_terms    +gettext           +netbeans_intg     +textprop
+byte_offset       -hangul_input      +num64             +timers
+channel           +iconv             +packages          +title
+cindent           +insert_expand     +path_extra        -toolbar
-clientserver      +job               +perl              +user_commands
+clipboard         +jumplist          +persistent_undo   +vartabs
+cmdline_compl     +keymap            +postscript        +vertsplit
+cmdline_hist      +lambda            +printer           +virtualedit
+cmdline_info      +langmap           +profile           +visual
+comments          +libcall           -python            +visualextra
+conceal           +linebreak         +python3           +viminfo
+cryptv            +lispindent        +quickfix          +vreplace
+cscope            +listcmds          +reltime           +wildignore
+cursorbind        +localmap          +rightleft         +wildmenu
+cursorshape       +lua               +ruby              +windows
+dialog_con        +menu              +scrollbind        +writebackup
+diff              +mksession         +signs             -X11
+digraphs          +modify_fname      +smartindent       -xfontset
-dnd               +mouse             +startuptime       -xim
-ebcdic            -mouseshape        +statusline        -xpm
+emacs_tags        +mouse_dec         -sun_workshop      -xsmp
+eval              -mouse_gpm         +syntax            -xterm_clipboard
+ex_extra          -mouse_jsbterm     +tag_binary        -xterm_save
   system vimrc file: "$VIM/vimrc"
     user vimrc file: "$HOME/.vimrc"
 2nd user vimrc file: "~/.vim/vimrc"
      user exrc file: "$HOME/.exrc"
       defaults file: "$VIMRUNTIME/defaults.vim"
  fall-back for $VIM: "/usr/local/share/vim"
Compilation: clang -c -I. -Iproto -DHAVE_CONFIG_H   -DMACOS_X -DMACOS_X_DARWIN  -g -O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1
Linking: clang   -L. -fstack-protector-strong -L/usr/local/lib -L/usr/local/opt/libyaml/lib -L/usr/local/opt/openssl/lib -L/usr/local/opt/readline/lib  -L/usr/local/lib -o vim        -lncurses -liconv -lintl -framework AppKit  -L/usr/local/opt/lua/lib -llua5.3 -mmacosx-version-min=10.14 -fstack-protector-strong -L/usr/local/lib  -L/usr/local/Cellar/perl/5.28.1/lib/perl5/5.28.1/darwin-thread-multi-2level/CORE -lperl -lm -lutil -lc  -L/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/config-3.7m-darwin -lpython3.7m -framework CoreFoundation  -lruby.2.6

MacOS

MacOS Mojave
Version 10.14.2 (18C54)
tvirolai commented 5 years ago

Setting the let g:clojure_maxlines parameter to a lower value (200 currently) fixed this for me.

benknoble commented 4 years ago

@tvirolai are you sure 200? The default was 100... I was considering using 20.