From aed8ce9da277f5ecffe968b324f242c41c3b752a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 10:50:31 +0200 Subject: Adding upstream version 2:9.0.1378. Signed-off-by: Daniel Baumann --- runtime/autoload/dist/ft.vim | 1108 ++++++++++++++++++++++++++++++ runtime/autoload/dist/man.vim | 196 ++++++ runtime/autoload/dist/script.vim | 434 ++++++++++++ runtime/autoload/dist/vimindent.vim | 1257 +++++++++++++++++++++++++++++++++++ 4 files changed, 2995 insertions(+) create mode 100644 runtime/autoload/dist/ft.vim create mode 100644 runtime/autoload/dist/man.vim create mode 100644 runtime/autoload/dist/script.vim create mode 100644 runtime/autoload/dist/vimindent.vim (limited to 'runtime/autoload/dist') diff --git a/runtime/autoload/dist/ft.vim b/runtime/autoload/dist/ft.vim new file mode 100644 index 0000000..bfc325d --- /dev/null +++ b/runtime/autoload/dist/ft.vim @@ -0,0 +1,1108 @@ +vim9script + +# Vim functions for file type detection +# +# Maintainer: Bram Moolenaar +# Last Change: 2022 Dec 14 + +# These functions are moved here from runtime/filetype.vim to make startup +# faster. + +export def Check_inp() + if getline(1) =~ '^\*' + setf abaqus + else + var n = 1 + var nmax = line("$") > 500 ? 500 : line("$") + while n <= nmax + if getline(n) =~? "^header surface data" + setf trasys + break + endif + n += 1 + endwhile + endif +enddef + +# This function checks for the kind of assembly that is wanted by the user, or +# can be detected from the first five lines of the file. +export def FTasm() + # make sure b:asmsyntax exists + if !exists("b:asmsyntax") + b:asmsyntax = "" + endif + + if b:asmsyntax == "" + FTasmsyntax() + endif + + # if b:asmsyntax still isn't set, default to asmsyntax or GNU + if b:asmsyntax == "" + if exists("g:asmsyntax") + b:asmsyntax = g:asmsyntax + else + b:asmsyntax = "asm" + endif + endif + + exe "setf " .. fnameescape(b:asmsyntax) +enddef + +export def FTasmsyntax() + # see if the file contains any asmsyntax=foo overrides. If so, change + # b:asmsyntax appropriately + var head = " " .. getline(1) .. " " .. getline(2) .. " " + .. getline(3) .. " " .. getline(4) .. " " .. getline(5) .. " " + var match = matchstr(head, '\sasmsyntax=\zs[a-zA-Z0-9]\+\ze\s') + if match != '' + b:asmsyntax = match + elseif ((head =~? '\.title') || (head =~? '\.ident') || (head =~? '\.macro') || (head =~? '\.subtitle') || (head =~? '\.library')) + b:asmsyntax = "vmasm" + endif +enddef + +var ft_visual_basic_content = '\cVB_Name\|Begin VB\.\(Form\|MDIForm\|UserControl\)' + +# See FTfrm() for Visual Basic form file detection +export def FTbas() + if exists("g:filetype_bas") + exe "setf " .. g:filetype_bas + return + endif + + # most frequent FreeBASIC-specific keywords in distro files + var fb_keywords = '\c^\s*\%(extern\|var\|enum\|private\|scope\|union\|byref\|operator\|constructor\|delete\|namespace\|public\|property\|with\|destructor\|using\)\>\%(\s*[:=(]\)\@!' + var fb_preproc = '\c^\s*\%(' .. + # preprocessor + '#\s*\a\+\|' .. + # compiler option + 'option\s\+\%(byval\|dynamic\|escape\|\%(no\)\=gosub\|nokeyword\|private\|static\)\>\|' .. + # metacommand + '\%(''\|rem\)\s*\$lang\>\|' .. + # default datatype + 'def\%(byte\|longint\|short\|ubyte\|uint\|ulongint\|ushort\)\>' .. + '\)' + var fb_comment = "^\\s*/'" + + # OPTION EXPLICIT, without the leading underscore, is common to many dialects + var qb64_preproc = '\c^\s*\%($\a\+\|option\s\+\%(_explicit\|_\=explicitarray\)\>\)' + + for lnum in range(1, min([line("$"), 100])) + var line = getline(lnum) + if line =~ ft_visual_basic_content + setf vb + return + elseif line =~ fb_preproc || line =~ fb_comment || line =~ fb_keywords + setf freebasic + return + elseif line =~ qb64_preproc + setf qb64 + return + endif + endfor + setf basic +enddef + +export def FTbtm() + if exists("g:dosbatch_syntax_for_btm") && g:dosbatch_syntax_for_btm + setf dosbatch + else + setf btm + endif +enddef + +export def BindzoneCheck(default = '') + if getline(1) .. getline(2) .. getline(3) .. getline(4) + =~ '^; <<>> DiG [0-9.]\+.* <<>>\|$ORIGIN\|$TTL\|IN\s\+SOA' + setf bindzone + elseif default != '' + exe 'setf ' .. default + endif +enddef + +# Returns true if file content looks like RAPID +def IsRapid(sChkExt: string = ""): bool + if sChkExt == "cfg" + return getline(1) =~? '\v^%(EIO|MMC|MOC|PROC|SIO|SYS):CFG' + endif + # called from FTmod, FTprg or FTsys + return getline(nextnonblank(1)) =~? '\v^\s*%(\%{3}|module\s+\k+\s*%(\(|$))' +enddef + +export def FTcfg() + if exists("g:filetype_cfg") + exe "setf " .. g:filetype_cfg + elseif IsRapid("cfg") + setf rapid + else + setf cfg + endif +enddef + +export def FTcls() + if exists("g:filetype_cls") + exe "setf " .. g:filetype_cls + return + endif + + if getline(1) =~ '^\v%(\%|\\)' + setf tex + elseif getline(1)[0] == '#' && getline(1) =~ 'rexx' + setf rexx + elseif getline(1) == 'VERSION 1.0 CLASS' + setf vb + else + setf st + endif +enddef + +export def FTlpc() + if exists("g:lpc_syntax_for_c") + var lnum = 1 + while lnum <= 12 + if getline(lnum) =~# '^\(//\|inherit\|private\|protected\|nosave\|string\|object\|mapping\|mixed\)' + setf lpc + return + endif + lnum += 1 + endwhile + endif + setf c +enddef + +export def FTheader() + if match(getline(1, min([line("$"), 200])), '^@\(interface\|end\|class\)') > -1 + if exists("g:c_syntax_for_h") + setf objc + else + setf objcpp + endif + elseif exists("g:c_syntax_for_h") + setf c + elseif exists("g:ch_syntax_for_h") + setf ch + else + setf cpp + endif +enddef + +# This function checks if one of the first ten lines start with a '@'. In +# that case it is probably a change file. +# If the first line starts with # or ! it's probably a ch file. +# If a line has "main", "include", "//" or "/*" it's probably ch. +# Otherwise CHILL is assumed. +export def FTchange() + var lnum = 1 + while lnum <= 10 + if getline(lnum)[0] == '@' + setf change + return + endif + if lnum == 1 && (getline(1)[0] == '#' || getline(1)[0] == '!') + setf ch + return + endif + if getline(lnum) =~ "MODULE" + setf chill + return + endif + if getline(lnum) =~ 'main\s*(\|#\s*include\|//' + setf ch + return + endif + lnum += 1 + endwhile + setf chill +enddef + +export def FTent() + # This function checks for valid cl syntax in the first five lines. + # Look for either an opening comment, '#', or a block start, '{'. + # If not found, assume SGML. + var lnum = 1 + while lnum < 6 + var line = getline(lnum) + if line =~ '^\s*[#{]' + setf cl + return + elseif line !~ '^\s*$' + # Not a blank line, not a comment, and not a block start, + # so doesn't look like valid cl code. + break + endif + lnum += 1 + endwhile + setf dtd +enddef + +export def ExCheck() + var lines = getline(1, min([line("$"), 100])) + if exists('g:filetype_euphoria') + exe 'setf ' .. g:filetype_euphoria + elseif match(lines, '^--\|^ifdef\>\|^include\>') > -1 + setf euphoria3 + else + setf elixir + endif +enddef + +export def EuphoriaCheck() + if exists('g:filetype_euphoria') + exe 'setf ' .. g:filetype_euphoria + else + setf euphoria3 + endif +enddef + +export def DtraceCheck() + if did_filetype() + # Filetype was already detected + return + endif + var lines = getline(1, min([line("$"), 100])) + if match(lines, '^module\>\|^import\>') > -1 + # D files often start with a module and/or import statement. + setf d + elseif match(lines, '^#!\S\+dtrace\|#pragma\s\+D\s\+option\|:\S\{-}:\S\{-}:') > -1 + setf dtrace + else + setf d + endif +enddef + +export def FTe() + if exists('g:filetype_euphoria') + exe 'setf ' .. g:filetype_euphoria + else + var n = 1 + while n < 100 && n <= line("$") + if getline(n) =~ "^\\s*\\(<'\\|'>\\)\\s*$" + setf specman + return + endif + n += 1 + endwhile + setf eiffel + endif +enddef + +export def FTfrm() + if exists("g:filetype_frm") + exe "setf " .. g:filetype_frm + return + endif + + var lines = getline(1, min([line("$"), 5])) + + if match(lines, ft_visual_basic_content) > -1 + setf vb + else + setf form + endif +enddef + +# Distinguish between Forth and F#. +# Provided by Doug Kearns. +export def FTfs() + if exists("g:filetype_fs") + exe "setf " .. g:filetype_fs + else + var line = getline(nextnonblank(1)) + # comments and colon definitions + if line =~ '^\s*\.\=( ' || line =~ '^\s*\\G\= ' || line =~ '^\\$' + \ || line =~ '^\s*: \S' + setf forth + else + setf fsharp + endif + endif +enddef + +# Distinguish between HTML, XHTML and Django +export def FThtml() + var n = 1 + while n < 10 && n <= line("$") + if getline(n) =~ '\\|{#\s\+' + setf htmldjango + return + endif + n += 1 + endwhile + setf FALLBACK html +enddef + +# Distinguish between standard IDL and MS-IDL +export def FTidl() + var n = 1 + while n < 50 && n <= line("$") + if getline(n) =~ '^\s*import\s\+"\(unknwn\|objidl\)\.idl"' + setf msidl + return + endif + n += 1 + endwhile + setf idl +enddef + +# Distinguish between "default", Prolog and Cproto prototype file. +export def ProtoCheck(default: string) + # Cproto files have a comment in the first line and a function prototype in + # the second line, it always ends in ";". Indent files may also have + # comments, thus we can't match comments to see the difference. + # IDL files can have a single ';' in the second line, require at least one + # chacter before the ';'. + if getline(2) =~ '.;$' + setf cpp + else + # recognize Prolog by specific text in the first non-empty line + # require a blank after the '%' because Perl uses "%list" and "%translate" + var l = getline(nextnonblank(1)) + if l =~ '\' || l =~ '^\s*\(%\+\(\s\|$\)\|/\*\)' || l =~ ':-' + setf prolog + else + exe 'setf ' .. default + endif + endif +enddef + +export def FTm() + if exists("g:filetype_m") + exe "setf " .. g:filetype_m + return + endif + + # excluding end(for|function|if|switch|while) common to Murphi + var octave_block_terminators = '\' + + var objc_preprocessor = '^\s*#\s*\%(import\|include\|define\|if\|ifn\=def\|undef\|line\|error\|pragma\)\>' + + var n = 1 + var saw_comment = 0 # Whether we've seen a multiline comment leader. + while n < 100 + var line = getline(n) + if line =~ '^\s*/\*' + # /* ... */ is a comment in Objective C and Murphi, so we can't conclude + # it's either of them yet, but track this as a hint in case we don't see + # anything more definitive. + saw_comment = 1 + endif + if line =~ '^\s*//' || line =~ '^\s*@import\>' || line =~ objc_preprocessor + setf objc + return + endif + if line =~ '^\s*\%(#\|%!\)' || line =~ '^\s*unwind_protect\>' || + \ line =~ '\%(^\|;\)\s*' .. octave_block_terminators + setf octave + return + endif + # TODO: could be Matlab or Octave + if line =~ '^\s*%' + setf matlab + return + endif + if line =~ '^\s*(\*' + setf mma + return + endif + if line =~ '^\c\s*\(\(type\|var\)\>\|--\)' + setf murphi + return + endif + n += 1 + endwhile + + if saw_comment + # We didn't see anything definitive, but this looks like either Objective C + # or Murphi based on the comment leader. Assume the former as it is more + # common. + setf objc + else + # Default is Matlab + setf matlab + endif +enddef + +export def FTmms() + var n = 1 + while n < 20 + var line = getline(n) + if line =~ '^\s*\(%\|//\)' || line =~ '^\*' + setf mmix + return + endif + if line =~ '^\s*#' + setf make + return + endif + n += 1 + endwhile + setf mmix +enddef + +# This function checks if one of the first five lines start with a dot. In +# that case it is probably an nroff file: 'filetype' is set and 1 is returned. +export def FTnroff(): number + if getline(1)[0] .. getline(2)[0] .. getline(3)[0] + .. getline(4)[0] .. getline(5)[0] =~ '\.' + setf nroff + return 1 + endif + return 0 +enddef + +export def FTmm() + var n = 1 + while n < 20 + if getline(n) =~ '^\s*\(#\s*\(include\|import\)\>\|@import\>\|/\*\)' + setf objcpp + return + endif + n += 1 + endwhile + setf nroff +enddef + +# Returns true if file content looks like LambdaProlog module +def IsLProlog(): bool + # skip apparent comments and blank lines, what looks like + # LambdaProlog comment may be RAPID header + var l: number = nextnonblank(1) + while l > 0 && l < line('$') && getline(l) =~ '^\s*%' # LambdaProlog comment + l = nextnonblank(l + 1) + endwhile + # this pattern must not catch a go.mod file + return getline(l) =~ '\") =~ '\' || l =~ '^\s*\(%\+\(\s\|$\)\|/\*\)' || l =~ ':-' + setf prolog + else + setf perl + endif + endif +enddef + +export def FTinc() + if exists("g:filetype_inc") + exe "setf " .. g:filetype_inc + else + var lines = getline(1) .. getline(2) .. getline(3) + if lines =~? "perlscript" + setf aspperl + elseif lines =~ "<%" + setf aspvbs + elseif lines =~ "' || lines =~# '[A-Z][A-Za-z0-9_:${}]*\s\+\%(??\|[?:+]\)\?= ' + setf bitbake + else + FTasmsyntax() + if exists("b:asmsyntax") + exe "setf " .. fnameescape(b:asmsyntax) + else + setf pov + endif + endif + endif +enddef + +export def FTprogress_cweb() + if exists("g:filetype_w") + exe "setf " .. g:filetype_w + return + endif + if getline(1) =~ '&ANALYZE' || getline(3) =~ '&GLOBAL-DEFINE' + setf progress + else + setf cweb + endif +enddef + +export def FTprogress_asm() + if exists("g:filetype_i") + exe "setf " .. g:filetype_i + return + endif + # This function checks for an assembly comment the first ten lines. + # If not found, assume Progress. + var lnum = 1 + while lnum <= 10 && lnum < line('$') + var line = getline(lnum) + if line =~ '^\s*;' || line =~ '^\*' + FTasm() + return + elseif line !~ '^\s*$' || line =~ '^/\*' + # Not an empty line: Doesn't look like valid assembly code. + # Or it looks like a Progress /* comment + break + endif + lnum += 1 + endwhile + setf progress +enddef + +var ft_pascal_comments = '^\s*\%({\|(\*\|//\)' +var ft_pascal_keywords = '^\s*\%(program\|unit\|library\|uses\|begin\|procedure\|function\|const\|type\|var\)\>' + +export def FTprogress_pascal() + if exists("g:filetype_p") + exe "setf " .. g:filetype_p + return + endif + # This function checks for valid Pascal syntax in the first ten lines. + # Look for either an opening comment or a program start. + # If not found, assume Progress. + var lnum = 1 + while lnum <= 10 && lnum < line('$') + var line = getline(lnum) + if line =~ ft_pascal_comments || line =~? ft_pascal_keywords + setf pascal + return + elseif line !~ '^\s*$' || line =~ '^/\*' + # Not an empty line: Doesn't look like valid Pascal code. + # Or it looks like a Progress /* comment + break + endif + lnum += 1 + endwhile + setf progress +enddef + +export def FTpp() + if exists("g:filetype_pp") + exe "setf " .. g:filetype_pp + else + var line = getline(nextnonblank(1)) + if line =~ ft_pascal_comments || line =~? ft_pascal_keywords + setf pascal + else + setf puppet + endif + endif +enddef + +# Determine if *.prg is ABB RAPID. Can also be Clipper, FoxPro or eviews +export def FTprg() + if exists("g:filetype_prg") + exe "setf " .. g:filetype_prg + elseif IsRapid() + setf rapid + else + # Nothing recognized, assume Clipper + setf clipper + endif +enddef + +export def FTr() + var max = line("$") > 50 ? 50 : line("$") + + for n in range(1, max) + # Rebol is easy to recognize, check for that first + if getline(n) =~? '\' + setf rebol + return + endif + endfor + + for n in range(1, max) + # R has # comments + if getline(n) =~ '^\s*#' + setf r + return + endif + # Rexx has /* comments */ + if getline(n) =~ '^\s*/\*' + setf rexx + return + endif + endfor + + # Nothing recognized, use user default or assume Rexx + if exists("g:filetype_r") + exe "setf " .. g:filetype_r + else + # Rexx used to be the default, but R appears to be much more popular. + setf r + endif +enddef + +export def McSetf() + # Rely on the file to start with a comment. + # MS message text files use ';', Sendmail files use '#' or 'dnl' + for lnum in range(1, min([line("$"), 20])) + var line = getline(lnum) + if line =~ '^\s*\(#\|dnl\)' + setf m4 # Sendmail .mc file + return + elseif line =~ '^\s*;' + setf msmessages # MS Message text file + return + endif + endfor + setf m4 # Default: Sendmail .mc file +enddef + +# Called from filetype.vim and scripts.vim. +export def SetFileTypeSH(name: string) + if did_filetype() + # Filetype was already detected + return + endif + if expand("") =~ g:ft_ignore_pat + return + endif + if name =~ '\' + # Some .sh scripts contain #!/bin/csh. + SetFileTypeShell("csh") + return + elseif name =~ '\' + # Some .sh scripts contain #!/bin/tcsh. + SetFileTypeShell("tcsh") + return + elseif name =~ '\' + # Some .sh scripts contain #!/bin/zsh. + SetFileTypeShell("zsh") + return + elseif name =~ '\' + b:is_kornshell = 1 + if exists("b:is_bash") + unlet b:is_bash + endif + if exists("b:is_sh") + unlet b:is_sh + endif + elseif exists("g:bash_is_sh") || name =~ '\' || name =~ '\' + b:is_bash = 1 + if exists("b:is_kornshell") + unlet b:is_kornshell + endif + if exists("b:is_sh") + unlet b:is_sh + endif + elseif name =~ '\' || name =~ '\' + # Ubuntu links "sh" to "dash", thus it is expected to work the same way + b:is_sh = 1 + if exists("b:is_kornshell") + unlet b:is_kornshell + endif + if exists("b:is_bash") + unlet b:is_bash + endif + endif + SetFileTypeShell("sh") +enddef + +# For shell-like file types, check for an "exec" command hidden in a comment, +# as used for Tcl. +# Also called from scripts.vim, thus can't be local to this script. +export def SetFileTypeShell(name: string) + if did_filetype() + # Filetype was already detected + return + endif + if expand("") =~ g:ft_ignore_pat + return + endif + var l = 2 + while l < 20 && l < line("$") && getline(l) =~ '^\s*\(#\|$\)' + # Skip empty and comment lines. + l += 1 + endwhile + if l < line("$") && getline(l) =~ '\s*exec\s' && getline(l - 1) =~ '^\s*#.*\\$' + # Found an "exec" line after a comment with continuation + var n = substitute(getline(l), '\s*exec\s\+\([^ ]*/\)\=', '', '') + if n =~ '\:p') + if path =~ '/\(etc/udev/\%(rules\.d/\)\=.*\.rules\|\%(usr/\)\=lib/udev/\%(rules\.d/\)\=.*\.rules\)$' + setf udevrules + return + endif + if path =~ '^/etc/ufw/' + setf conf # Better than hog + return + endif + if path =~ '^/\(etc\|usr/share\)/polkit-1/rules\.d' + setf javascript + return + endif + var config_lines: list + try + config_lines = readfile('/etc/udev/udev.conf') + catch /^Vim\%((\a\+)\)\=:E484/ + setf hog + return + endtry + var dir = expand(':p:h') + for line in config_lines + if line =~ ft_rules_udev_rules_pattern + var udev_rules = substitute(line, ft_rules_udev_rules_pattern, '\1', "") + if dir == udev_rules + setf udevrules + endif + break + endif + endfor + setf hog +enddef + +export def SQL() + if exists("g:filetype_sql") + exe "setf " .. g:filetype_sql + else + setf sql + endif +enddef + +# This function checks the first 25 lines of file extension "sc" to resolve +# detection between scala and SuperCollider. +# NOTE: We don't check for 'Class : Method', as this can easily be confused +# with valid Scala like `val x : Int = 3`. So we instead only rely on +# checks that can't be confused. +export def FTsc() + for lnum in range(1, min([line("$"), 25])) + if getline(lnum) =~# 'var\s<\|classvar\s<\|\^this.*\||\w\+|\|+\s\w*\s{\|\*ar\s' + setf supercollider + return + endif + endfor + setf scala +enddef + +# This function checks the first line of file extension "scd" to resolve +# detection between scdoc and SuperCollider +export def FTscd() + if getline(1) =~# '\%^\S\+(\d[0-9A-Za-z]*)\%(\s\+\"[^"]*\"\%(\s\+\"[^"]*\"\)\=\)\=$' + setf scdoc + else + setf supercollider + endif +enddef + +# If the file has an extension of 't' and is in a directory 't' or 'xt' then +# it is almost certainly a Perl test file. +# If the first line starts with '#' and contains 'perl' it's probably a Perl +# file. +# (Slow test) If a file contains a 'use' statement then it is almost certainly +# a Perl file. +export def FTperl(): number + var dirname = expand("%:p:h:t") + if expand("%:e") == 't' && (dirname == 't' || dirname == 'xt') + setf perl + return 1 + endif + if getline(1)[0] == '#' && getline(1) =~ 'perl' + setf perl + return 1 + endif + var save_cursor = getpos('.') + call cursor(1, 1) + var has_use = search('^use\s\s*\k', 'c', 30) > 0 + call setpos('.', save_cursor) + if has_use + setf perl + return 1 + endif + return 0 +enddef + +# LambdaProlog and Standard ML signature files +export def FTsig() + if exists("g:filetype_sig") + exe "setf " .. g:filetype_sig + return + endif + + var lprolog_comment = '^\s*\%(/\*\|%\)' + var lprolog_keyword = '^\s*sig\s\+\a' + var sml_comment = '^\s*(\*' + var sml_keyword = '^\s*\%(signature\|structure\)\s\+\a' + + var line = getline(nextnonblank(1)) + + if line =~ lprolog_comment || line =~# lprolog_keyword + setf lprolog + elseif line =~ sml_comment || line =~# sml_keyword + setf sml + endif +enddef + +# This function checks the first 100 lines of files matching "*.sil" to +# resolve detection between Swift Intermediate Language and SILE. +export def FTsil() + for lnum in range(1, [line('$'), 100]->min()) + var line: string = getline(lnum) + if line =~ '^\s*[\\%]' + setf sile + return + elseif line =~ '^\s*\S' + setf sil + return + endif + endfor + # no clue, default to "sil" + setf sil +enddef + +export def FTsys() + if exists("g:filetype_sys") + exe "setf " .. g:filetype_sys + elseif IsRapid() + setf rapid + else + setf bat + endif +enddef + +# Choose context, plaintex, or tex (LaTeX) based on these rules: +# 1. Check the first line of the file for "%&". +# 2. Check the first 1000 non-comment lines for LaTeX or ConTeXt keywords. +# 3. Default to "plain" or to g:tex_flavor, can be set in user's vimrc. +export def FTtex() + var firstline = getline(1) + var format: string + if firstline =~ '^%&\s*\a\+' + format = tolower(matchstr(firstline, '\a\+')) + format = substitute(format, 'pdf', '', '') + if format == 'tex' + format = 'latex' + elseif format == 'plaintex' + format = 'plain' + endif + elseif expand('%') =~ 'tex/context/.*/.*.tex' + format = 'context' + else + # Default value, may be changed later: + format = exists("g:tex_flavor") ? g:tex_flavor : 'plain' + # Save position, go to the top of the file, find first non-comment line. + var save_cursor = getpos('.') + call cursor(1, 1) + var firstNC = search('^\s*[^[:space:]%]', 'c', 1000) + if firstNC > 0 + # Check the next thousand lines for a LaTeX or ConTeXt keyword. + var lpat = 'documentclass\>\|usepackage\>\|begin{\|newcommand\>\|renewcommand\>' + var cpat = 'start\a\+\|setup\a\+\|usemodule\|enablemode\|enableregime\|setvariables\|useencoding\|usesymbols\|stelle\a\+\|verwende\a\+\|stel\a\+\|gebruik\a\+\|usa\a\+\|imposta\a\+\|regle\a\+\|utilisemodule\>' + var kwline = search('^\s*\\\%(' .. lpat .. '\)\|^\s*\\\(' .. cpat .. '\)', + 'cnp', firstNC + 1000) + if kwline == 1 # lpat matched + format = 'latex' + elseif kwline == 2 # cpat matched + format = 'context' + endif # If neither matched, keep default set above. + # let lline = search('^\s*\\\%(' . lpat . '\)', 'cn', firstNC + 1000) + # let cline = search('^\s*\\\%(' . cpat . '\)', 'cn', firstNC + 1000) + # if cline > 0 + # let format = 'context' + # endif + # if lline > 0 && (cline == 0 || cline > lline) + # let format = 'tex' + # endif + endif # firstNC + call setpos('.', save_cursor) + endif # firstline =~ '^%&\s*\a\+' + + # Translation from formats to file types. TODO: add AMSTeX, RevTex, others? + if format == 'plain' + setf plaintex + elseif format == 'context' + setf context + else # probably LaTeX + setf tex + endif + return +enddef + +export def FTxml() + var n = 1 + while n < 100 && n <= line("$") + var line = getline(n) + # DocBook 4 or DocBook 5. + var is_docbook4 = line =~ '\)' && getline(n) !~ '^\s*#\s*include' + setf racc + return + endif + n += 1 + endwhile + setf yacc +enddef + +export def Redif() + var lnum = 1 + while lnum <= 5 && lnum < line('$') + if getline(lnum) =~ "^\ctemplate-type:" + setf redif + return + endif + lnum += 1 + endwhile +enddef + +# This function is called for all files under */debian/patches/*, make sure not +# to non-dep3patch files, such as README and other text files. +export def Dep3patch() + if expand('%:t') ==# 'series' + return + endif + + for ln in getline(1, 100) + if ln =~# '^\%(Description\|Subject\|Origin\|Bug\|Forwarded\|Author\|From\|Reviewed-by\|Acked-by\|Last-Updated\|Applied-Upstream\):' + setf dep3patch + return + elseif ln =~# '^---' + # end of headers found. stop processing + return + endif + endfor +enddef + +# This function checks the first 15 lines for appearance of 'FoamFile' +# and then 'object' in a following line. +# In that case, it's probably an OpenFOAM file +export def FTfoam() + var ffile = 0 + var lnum = 1 + while lnum <= 15 + if getline(lnum) =~# '^FoamFile' + ffile = 1 + elseif ffile == 1 && getline(lnum) =~# '^\s*object' + setf foam + return + endif + lnum += 1 + endwhile +enddef + +# Determine if a *.tf file is TF mud client or terraform +export def FTtf() + var numberOfLines = line('$') + for i in range(1, numberOfLines) + var currentLine = trim(getline(i)) + var firstCharacter = currentLine[0] + if firstCharacter !=? ";" && firstCharacter !=? "/" && firstCharacter !=? "" + setf terraform + return + endif + endfor + setf tf +enddef + +var ft_krl_header = '\&\w+' +# Determine if a *.src file is Kuka Robot Language +export def FTsrc() + var ft_krl_def_or_deffct = '%(global\s+)?def%(fct)?>' + if exists("g:filetype_src") + exe "setf " .. g:filetype_src + elseif getline(nextnonblank(1)) =~? '\v^\s*%(' .. ft_krl_header .. '|' .. ft_krl_def_or_deffct .. ')' + setf krl + endif +enddef + +# Determine if a *.dat file is Kuka Robot Language +export def FTdat() + var ft_krl_defdat = 'defdat>' + if exists("g:filetype_dat") + exe "setf " .. g:filetype_dat + elseif getline(nextnonblank(1)) =~? '\v^\s*%(' .. ft_krl_header .. '|' .. ft_krl_defdat .. ')' + setf krl + endif +enddef + +export def FTlsl() + if exists("g:filetype_lsl") + exe "setf " .. g:filetype_lsl + endif + + var line = getline(nextnonblank(1)) + if line =~ '^\s*%' || line =~# ':\s*trait\s*$' + setf larch + else + setf lsl + endif +enddef + +# Uncomment this line to check for compilation errors early +# defcompile diff --git a/runtime/autoload/dist/man.vim b/runtime/autoload/dist/man.vim new file mode 100644 index 0000000..cd584aa --- /dev/null +++ b/runtime/autoload/dist/man.vim @@ -0,0 +1,196 @@ +" Vim filetype plugin autoload file +" Language: man +" Maintainer: Jason Franklin +" Maintainer: SungHyun Nam +" Autoload Split: Bram Moolenaar +" Last Change: 2022 Jun 18 + +let s:cpo_save = &cpo +set cpo-=C + +let s:man_tag_depth = 0 + +let s:man_sect_arg = "" +let s:man_find_arg = "-w" +try + if !has("win32") && $OSTYPE !~ 'cygwin\|linux' && system('uname -s') =~ "SunOS" && system('uname -r') =~ "^5" + let s:man_sect_arg = "-s" + let s:man_find_arg = "-l" + endif +catch /E145:/ + " Ignore the error in restricted mode +endtry + +func dist#man#PreGetPage(cnt) + if a:cnt == 0 + let old_isk = &iskeyword + if &ft == 'man' + setl iskeyword+=(,) + endif + let str = expand("") + let &l:iskeyword = old_isk + let page = substitute(str, '(*\(\k\+\).*', '\1', '') + let sect = substitute(str, '\(\k\+\)(\([^()]*\)).*', '\2', '') + if match(sect, '^[0-9 ]\+$') == -1 + let sect = "" + endif + if sect == page + let sect = "" + endif + else + let sect = a:cnt + let page = expand("") + endif + call dist#man#GetPage('', sect, page) +endfunc + +func s:GetCmdArg(sect, page) + + if empty(a:sect) + return shellescape(a:page) + endif + + return s:man_sect_arg . ' ' . shellescape(a:sect) . ' ' . shellescape(a:page) +endfunc + +func s:FindPage(sect, page) + let l:cmd = printf('man %s %s', s:man_find_arg, s:GetCmdArg(a:sect, a:page)) + call system(l:cmd) + + if v:shell_error + return 0 + endif + + return 1 +endfunc + +func dist#man#GetPage(cmdmods, ...) + if a:0 >= 2 + let sect = a:1 + let page = a:2 + elseif a:0 >= 1 + let sect = "" + let page = a:1 + else + return + endif + + " To support: nmap K :Man + if page == '' + let page = expand('') + endif + + if !exists('g:ft_man_no_sect_fallback') || (g:ft_man_no_sect_fallback == 0) + if sect != "" && s:FindPage(sect, page) == 0 + let sect = "" + endif + endif + if s:FindPage(sect, page) == 0 + let msg = 'man.vim: no manual entry for "' . page . '"' + if !empty(sect) + let msg .= ' in section ' . sect + endif + echomsg msg + return + endif + exec "let s:man_tag_buf_".s:man_tag_depth." = ".bufnr("%") + exec "let s:man_tag_lin_".s:man_tag_depth." = ".line(".") + exec "let s:man_tag_col_".s:man_tag_depth." = ".col(".") + let s:man_tag_depth = s:man_tag_depth + 1 + + let open_cmd = 'edit' + + " Use an existing "man" window if it exists, otherwise open a new one. + if &filetype != "man" + let thiswin = winnr() + exe "norm! \b" + if winnr() > 1 + exe "norm! " . thiswin . "\w" + while 1 + if &filetype == "man" + break + endif + exe "norm! \w" + if thiswin == winnr() + break + endif + endwhile + endif + if &filetype != "man" + if exists("g:ft_man_open_mode") + if g:ft_man_open_mode == 'vert' + let open_cmd = 'vsplit' + elseif g:ft_man_open_mode == 'tab' + let open_cmd = 'tabedit' + else + let open_cmd = 'split' + endif + else + let open_cmd = a:cmdmods . ' split' + endif + endif + endif + + silent execute open_cmd . " $HOME/" . page . '.' . sect . '~' + + " Avoid warning for editing the dummy file twice + setl buftype=nofile noswapfile + + setl fdc=0 ma nofen nonu nornu + %delete _ + let unsetwidth = 0 + if empty($MANWIDTH) + let $MANWIDTH = winwidth(0) + let unsetwidth = 1 + endif + + " Ensure Vim is not recursively invoked (man-db does this) when doing ctrl-[ + " on a man page reference by unsetting MANPAGER. + " Some versions of env(1) do not support the '-u' option, and in such case + " we set MANPAGER=cat. + if !exists('s:env_has_u') + call system('env -u x true') + let s:env_has_u = (v:shell_error == 0) + endif + let env_cmd = s:env_has_u ? 'env -u MANPAGER' : 'env MANPAGER=cat' + let env_cmd .= ' GROFF_NO_SGR=1' + let man_cmd = env_cmd . ' man ' . s:GetCmdArg(sect, page) . ' | col -b' + silent exec "r !" . man_cmd + + if unsetwidth + let $MANWIDTH = '' + endif + " Remove blank lines from top and bottom. + while line('$') > 1 && getline(1) =~ '^\s*$' + 1delete _ + endwhile + while line('$') > 1 && getline('$') =~ '^\s*$' + $delete _ + endwhile + 1 + setl ft=man nomod + setl bufhidden=hide + setl nobuflisted + setl noma +endfunc + +func dist#man#PopPage() + if s:man_tag_depth > 0 + let s:man_tag_depth = s:man_tag_depth - 1 + exec "let s:man_tag_buf=s:man_tag_buf_".s:man_tag_depth + exec "let s:man_tag_lin=s:man_tag_lin_".s:man_tag_depth + exec "let s:man_tag_col=s:man_tag_col_".s:man_tag_depth + exec s:man_tag_buf."b" + exec s:man_tag_lin + exec "norm! ".s:man_tag_col."|" + exec "unlet s:man_tag_buf_".s:man_tag_depth + exec "unlet s:man_tag_lin_".s:man_tag_depth + exec "unlet s:man_tag_col_".s:man_tag_depth + unlet s:man_tag_buf s:man_tag_lin s:man_tag_col + endif +endfunc + +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: set sw=2 ts=8 noet: diff --git a/runtime/autoload/dist/script.vim b/runtime/autoload/dist/script.vim new file mode 100644 index 0000000..f86c428 --- /dev/null +++ b/runtime/autoload/dist/script.vim @@ -0,0 +1,434 @@ +vim9script + +# Vim function for detecting a filetype from the file contents. +# Invoked from "scripts.vim" in 'runtimepath' +# +# Maintainer: Bram Moolenaar +# Last Change: 2022 Nov 24 + +export def DetectFiletype() + var line1 = getline(1) + if line1[0] == '#' && line1[1] == '!' + # File that starts with "#!". + DetectFromHashBang(line1) + else + # File does not start with "#!". + DetectFromText(line1) + endif +enddef + +# Called for a script that has "#!" in the first line. +def DetectFromHashBang(firstline: string) + var line1 = firstline + + # Check for a line like "#!/usr/bin/env {options} bash". Turn it into + # "#!/usr/bin/bash" to make matching easier. + # Recognize only a few {options} that are commonly used. + if line1 =~ '^#!\s*\S*\' + name = substitute(line1, '^#!.*\\s\+\(\i\+\).*', '\1', '') + elseif line1 =~ '^#!\s*[^/\\ ]*\>\([^/\\]\|$\)' + name = substitute(line1, '^#!\s*\([^/\\ ]*\>\).*', '\1', '') + else + name = substitute(line1, '^#!\s*\S*[/\\]\(\i\+\).*', '\1', '') + endif + + # tcl scripts may have #!/bin/sh in the first line and "exec wish" in the + # third line. Suggested by Steven Atkinson. + if getline(3) =~ '^exec wish' + name = 'wish' + endif + + # Bourne-like shell scripts: bash bash2 dash ksh ksh93 sh + if name =~ '^\(bash\d*\|dash\|ksh\d*\|sh\)\>' + call dist#ft#SetFileTypeSH(line1) + + # csh scripts + elseif name =~ '^csh\>' + if exists("g:filetype_csh") + call dist#ft#SetFileTypeShell(g:filetype_csh) + else + call dist#ft#SetFileTypeShell("csh") + endif + + # tcsh scripts + elseif name =~ '^tcsh\>' + call dist#ft#SetFileTypeShell("tcsh") + + # Z shell scripts + elseif name =~ '^zsh\>' + set ft=zsh + + # TCL scripts + elseif name =~ '^\(tclsh\|wish\|expectk\|itclsh\|itkwish\)\>' + set ft=tcl + + # Expect scripts + elseif name =~ '^expect\>' + set ft=expect + + # Gnuplot scripts + elseif name =~ '^gnuplot\>' + set ft=gnuplot + + # Makefiles + elseif name =~ 'make\>' + set ft=make + + # Pike + elseif name =~ '^pike\%(\>\|[0-9]\)' + set ft=pike + + # Lua + elseif name =~ 'lua' + set ft=lua + + # Perl + elseif name =~ 'perl' + set ft=perl + + # PHP + elseif name =~ 'php' + set ft=php + + # Python + elseif name =~ 'python' + set ft=python + + # Groovy + elseif name =~ '^groovy\>' + set ft=groovy + + # Raku + elseif name =~ 'raku' + set ft=raku + + # Ruby + elseif name =~ 'ruby' + set ft=ruby + + # JavaScript + elseif name =~ 'node\(js\)\=\>\|js\>' || name =~ 'rhino\>' + set ft=javascript + + # BC calculator + elseif name =~ '^bc\>' + set ft=bc + + # sed + elseif name =~ 'sed\>' + set ft=sed + + # OCaml-scripts + elseif name =~ 'ocaml' + set ft=ocaml + + # Awk scripts; also finds "gawk" + elseif name =~ 'awk\>' + set ft=awk + + # Website MetaLanguage + elseif name =~ 'wml' + set ft=wml + + # Scheme scripts + elseif name =~ 'scheme' + set ft=scheme + + # CFEngine scripts + elseif name =~ 'cfengine' + set ft=cfengine + + # Erlang scripts + elseif name =~ 'escript' + set ft=erlang + + # Haskell + elseif name =~ 'haskell' + set ft=haskell + + # Scala + elseif name =~ 'scala\>' + set ft=scala + + # Clojure + elseif name =~ 'clojure' + set ft=clojure + + # Free Pascal + elseif name =~ 'instantfpc\>' + set ft=pascal + + # Fennel + elseif name =~ 'fennel\>' + set ft=fennel + + # MikroTik RouterOS script + elseif name =~ 'rsc\>' + set ft=routeros + + # Fish shell + elseif name =~ 'fish\>' + set ft=fish + + # Gforth + elseif name =~ 'gforth\>' + set ft=forth + + # Icon + elseif name =~ 'icon\>' + set ft=icon + + # Guile + elseif name =~ 'guile' + set ft=scheme + + endif +enddef + + +# Called for a script that does not have "#!" in the first line. +def DetectFromText(line1: string) + var line2 = getline(2) + var line3 = getline(3) + var line4 = getline(4) + var line5 = getline(5) + + # Bourne-like shell scripts: sh ksh bash bash2 + if line1 =~ '^:$' + call dist#ft#SetFileTypeSH(line1) + + # Z shell scripts + elseif line1 =~ '^#compdef\>' + || line1 =~ '^#autoload\>' + || "\n" .. line1 .. "\n" .. line2 .. "\n" .. line3 .. + "\n" .. line4 .. "\n" .. line5 + =~ '\n\s*emulate\s\+\%(-[LR]\s\+\)\=[ckz]\=sh\>' + set ft=zsh + + # ELM Mail files + elseif line1 =~ '^From \([a-zA-Z][a-zA-Z_0-9\.=-]*\(@[^ ]*\)\=\|-\) .* \(19\|20\)\d\d$' + set ft=mail + + # Mason + elseif line1 =~ '^<[%&].*>' + set ft=mason + + # Vim scripts (must have '" vim' as the first line to trigger this) + elseif line1 =~ '^" *[vV]im$' + set ft=vim + + # libcxx and libstdc++ standard library headers like "iostream" do not have + # an extension, recognize the Emacs file mode. + elseif line1 =~? '-\*-.*C++.*-\*-' + set ft=cpp + + # MOO + elseif line1 =~ '^\*\* LambdaMOO Database, Format Version \%([1-3]\>\)\@!\d\+ \*\*$' + set ft=moo + + # Diff file: + # - "diff" in first line (context diff) + # - "Only in " in first line + # - "--- " in first line and "+++ " in second line (unified diff). + # - "*** " in first line and "--- " in second line (context diff). + # - "# It was generated by makepatch " in the second line (makepatch diff). + # - "Index: " in the first line (CVS file) + # - "=== ", line of "=", "---", "+++ " (SVK diff) + # - "=== ", "--- ", "+++ " (bzr diff, common case) + # - "=== (removed|added|renamed|modified)" (bzr diff, alternative) + # - "# HG changeset patch" in first line (Mercurial export format) + elseif line1 =~ '^\(diff\>\|Only in \|\d\+\(,\d\+\)\=[cda]\d\+\>\|# It was generated by makepatch \|Index:\s\+\f\+\r\=$\|===== \f\+ \d\+\.\d\+ vs edited\|==== //\f\+#\d\+\|# HG changeset patch\)' + || (line1 =~ '^--- ' && line2 =~ '^+++ ') + || (line1 =~ '^\* looking for ' && line2 =~ '^\* comparing to ') + || (line1 =~ '^\*\*\* ' && line2 =~ '^--- ') + || (line1 =~ '^=== ' && ((line2 =~ '^=\{66\}' && line3 =~ '^--- ' && line4 =~ '^+++') || (line2 =~ '^--- ' && line3 =~ '^+++ '))) + || (line1 =~ '^=== \(removed\|added\|renamed\|modified\)') + set ft=diff + + # PostScript Files (must have %!PS as the first line, like a2ps output) + elseif line1 =~ '^%![ \t]*PS' + set ft=postscr + + # M4 scripts: Guess there is a line that starts with "dnl". + elseif line1 =~ '^\s*dnl\>' + || line2 =~ '^\s*dnl\>' + || line3 =~ '^\s*dnl\>' + || line4 =~ '^\s*dnl\>' + || line5 =~ '^\s*dnl\>' + set ft=m4 + + # AmigaDos scripts + elseif $TERM == "amiga" && (line1 =~ "^;" || line1 =~? '^\.bra') + set ft=amiga + + # SiCAD scripts (must have procn or procd as the first line to trigger this) + elseif line1 =~? '^ *proc[nd] *$' + set ft=sicad + + # Purify log files start with "**** Purify" + elseif line1 =~ '^\*\*\*\* Purify' + set ft=purifylog + + # XML + elseif line1 =~ '' + set ft=xml + + # XHTML (e.g.: PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN") + elseif line1 =~ '\' + set ft=html + + # PDF + elseif line1 =~ '^%PDF-' + set ft=pdf + + # XXD output + elseif line1 =~ '^\x\{7}: \x\{2} \=\x\{2} \=\x\{2} \=\x\{2} ' + set ft=xxd + + # RCS/CVS log output + elseif line1 =~ '^RCS file:' || line2 =~ '^RCS file:' + set ft=rcslog + + # CVS commit + elseif line2 =~ '^CVS:' || getline("$") =~ '^CVS: ' + set ft=cvs + + # Prescribe + elseif line1 =~ '^!R!' + set ft=prescribe + + # Send-pr + elseif line1 =~ '^SEND-PR:' + set ft=sendpr + + # SNNS files + elseif line1 =~ '^SNNS network definition file' + set ft=snnsnet + elseif line1 =~ '^SNNS pattern definition file' + set ft=snnspat + elseif line1 =~ '^SNNS result file' + set ft=snnsres + + # Virata + elseif line1 =~ '^%.\{-}[Vv]irata' + || line2 =~ '^%.\{-}[Vv]irata' + || line3 =~ '^%.\{-}[Vv]irata' + || line4 =~ '^%.\{-}[Vv]irata' + || line5 =~ '^%.\{-}[Vv]irata' + set ft=virata + + # Strace + elseif line1 =~ '[0-9:.]* *execve(' || line1 =~ '^__libc_start_main' + set ft=strace + + # VSE JCL + elseif line1 =~ '^\* $$ JOB\>' || line1 =~ '^// *JOB\>' + set ft=vsejcl + + # TAK and SINDA + elseif line4 =~ 'K & K Associates' || line2 =~ 'TAK 2000' + set ft=takout + elseif line3 =~ 'S Y S T E M S I M P R O V E D ' + set ft=sindaout + elseif getline(6) =~ 'Run Date: ' + set ft=takcmp + elseif getline(9) =~ 'Node File 1' + set ft=sindacmp + + # DNS zone files + elseif line1 .. line2 .. line3 .. line4 =~ '^; <<>> DiG [0-9.]\+.* <<>>\|$ORIGIN\|$TTL\|IN\s\+SOA' + set ft=bindzone + + # BAAN + elseif line1 =~ '|\*\{1,80}' && line2 =~ 'VRC ' + || line2 =~ '|\*\{1,80}' && line3 =~ 'VRC ' + set ft=baan + + # Valgrind + elseif line1 =~ '^==\d\+== valgrind' || line3 =~ '^==\d\+== Using valgrind' + set ft=valgrind + + # Go docs + elseif line1 =~ '^PACKAGE DOCUMENTATION$' + set ft=godoc + + # Renderman Interface Bytestream + elseif line1 =~ '^##RenderMan' + set ft=rib + + # Scheme scripts + elseif line1 =~ 'exec\s\+\S*scheme' || line2 =~ 'exec\s\+\S*scheme' + set ft=scheme + + # Git output + elseif line1 =~ '^\(commit\|tree\|object\) \x\{40,\}\>\|^tag \S\+$' + set ft=git + + # Gprof (gnu profiler) + elseif line1 == 'Flat profile:' + && line2 == '' + && line3 =~ '^Each sample counts as .* seconds.$' + set ft=gprof + + # Erlang terms + # (See also: http://www.gnu.org/software/emacs/manual/html_node/emacs/Choosing-Modes.html#Choosing-Modes) + elseif line1 =~? '-\*-.*erlang.*-\*-' + set ft=erlang + + # YAML + elseif line1 =~ '^%YAML' + set ft=yaml + + # MikroTik RouterOS script + elseif line1 =~ '^#.*by RouterOS.*$' + set ft=routeros + + # Sed scripts + # #ncomment is allowed but most likely a false positive so require a space + # before any trailing comment text + elseif line1 =~ '^#n\%($\|\s\)' + set ft=sed + + else + var lnum = 1 + while getline(lnum) =~ "^? " && lnum < line("$") + lnum += 1 + endwhile + if getline(lnum) =~ '^Index:\s\+\f\+$' + # CVS diff + set ft=diff + + # locale input files: Formal Definitions of Cultural Conventions + # filename must be like en_US, fr_FR@euro or en_US.UTF-8 + elseif expand("%") =~ '\a\a_\a\a\($\|[.@]\)\|i18n$\|POSIX$\|translit_' + lnum = 1 + while lnum < 100 && lnum < line("$") + if getline(lnum) =~ '^LC_\(IDENTIFICATION\|CTYPE\|COLLATE\|MONETARY\|NUMERIC\|TIME\|MESSAGES\|PAPER\|TELEPHONE\|MEASUREMENT\|NAME\|ADDRESS\)$' + setf fdcc + break + endif + lnum += 1 + endwhile + endif + endif +enddef diff --git a/runtime/autoload/dist/vimindent.vim b/runtime/autoload/dist/vimindent.vim new file mode 100644 index 0000000..1306d1e --- /dev/null +++ b/runtime/autoload/dist/vimindent.vim @@ -0,0 +1,1257 @@ +vim9script + +# Language: Vim script +# Maintainer: github user lacygoill +# Last Change: 2023 Feb 01 + +# NOTE: Whenever you change the code, make sure the tests are still passing: +# +# $ cd runtime/indent/ +# $ make clean; make test || vimdiff testdir/vim.{ok,fail} + +# Config {{{1 + +const TIMEOUT: number = get(g:, 'vim_indent', {}) + ->get('searchpair_timeout', 100) + +def IndentMoreInBracketBlock(): number # {{{2 + if get(g:, 'vim_indent', {}) + ->get('more_in_bracket_block', false) + return shiftwidth() + else + return 0 + endif +enddef + +def IndentMoreLineContinuation(): number # {{{2 + var n: any = get(g:, 'vim_indent', {}) + # We inspect `g:vim_indent_cont` to stay backward compatible. + ->get('line_continuation', get(g:, 'vim_indent_cont', shiftwidth() * 3)) + + if n->typename() == 'string' + return n->eval() + else + return n + endif +enddef +# }}}2 + +# Init {{{1 +var patterns: list +# Tokens {{{2 +# BAR_SEPARATION {{{3 + +const BAR_SEPARATION: string = '[^|\\]\@1<=|' + +# OPENING_BRACKET {{{3 + +const OPENING_BRACKET: string = '[[{(]' + +# CLOSING_BRACKET {{{3 + +const CLOSING_BRACKET: string = '[]})]' + +# NON_BRACKET {{{3 + +const NON_BRACKET: string = '[^[\]{}()]' + +# LIST_OR_DICT_CLOSING_BRACKET {{{3 + +const LIST_OR_DICT_CLOSING_BRACKET: string = '[]}]' + +# LIST_OR_DICT_OPENING_BRACKET {{{3 + +const LIST_OR_DICT_OPENING_BRACKET: string = '[[{]' + +# CHARACTER_UNDER_CURSOR {{{3 + +const CHARACTER_UNDER_CURSOR: string = '\%.c.' + +# INLINE_COMMENT {{{3 + +# TODO: It is not required for an inline comment to be surrounded by whitespace. +# But it might help against false positives. +# To be more reliable, we should inspect the syntax, and only require whitespace +# before the `#` comment leader. But that might be too costly (because of +# `synstack()`). +const INLINE_COMMENT: string = '\s[#"]\%(\s\|[{}]\{3}\)' + +# INLINE_VIM9_COMMENT {{{3 + +const INLINE_VIM9_COMMENT: string = '\s#' + +# COMMENT {{{3 + +# TODO: Technically, `"\s` is wrong. +# +# First, whitespace is not required. +# Second, in Vim9, a string might appear at the start of the line. +# To be sure, we should also inspect the syntax. +# We can't use `INLINE_COMMENT` here. {{{ +# +# const COMMENT: string = $'^\s*{INLINE_COMMENT}' +# ^------------^ +# ✘ +# +# Because `INLINE_COMMENT` asserts the presence of a whitespace before the +# comment leader. This assertion is not satisfied for a comment starting at the +# start of the line. +#}}} +const COMMENT: string = '^\s*\%(#\|"\\\=\s\).*$' + +# DICT_KEY {{{3 + +const DICT_KEY: string = '^\s*\%(' + .. '\%(\w\|-\)\+' + .. '\|' + .. '"[^"]*"' + .. '\|' + .. "'[^']*'" + .. '\|' + .. '\[[^]]\+\]' + .. '\)' + .. ':\%(\s\|$\)' + +# NOT_A_DICT_KEY {{{3 + +const NOT_A_DICT_KEY: string = ':\@!' + +# END_OF_COMMAND {{{3 + +const END_OF_COMMAND: string = $'\s*\%($\|||\@!\|{INLINE_COMMENT}\)' + +# END_OF_LINE {{{3 + +const END_OF_LINE: string = $'\s*\%($\|{INLINE_COMMENT}\)' + +# END_OF_VIM9_LINE {{{3 + +const END_OF_VIM9_LINE: string = $'\s*\%($\|{INLINE_VIM9_COMMENT}\)' + +# OPERATOR {{{3 + +const OPERATOR: string = '\%(^\|\s\)\%([-+*/%]\|\.\.\|||\|&&\|??\|?\|<<\|>>\|\%([=!]=\|[<>]=\=\|[=!]\~\|is\|isnot\)[?#]\=\)\%(\s\|$\)\@=\%(\s*[|<]\)\@!' + # assignment operators + .. '\|' .. '\s\%([-+*/%]\|\.\.\)\==\%(\s\|$\)\@=' + # support `:` when used inside conditional operator `?:` + .. '\|' .. '\%(\s\|^\):\%(\s\|$\)' + +# HEREDOC_OPERATOR {{{3 + +const HEREDOC_OPERATOR: string = '\s=<<\s\@=\%(\s\+\%(trim\|eval\)\)\{,2}' + +# PATTERN_DELIMITER {{{3 + +# A better regex would be: +# +# [^-+*/%.:# \t[:alnum:]\"|]\@=.\|->\@!\%(=\s\)\@!\|[+*/%]\%(=\s\)\@! +# +# But sometimes, it can be too costly and cause `E363` to be given. +const PATTERN_DELIMITER: string = '[-+*/%]\%(=\s\)\@!' +# }}}2 +# Syntaxes {{{2 +# BLOCKS {{{3 + +const BLOCKS: list> = [ + ['if', 'el\%[se]', 'elseif\=', 'en\%[dif]'], + ['for', 'endfor\='], + ['wh\%[ile]', 'endw\%[hile]'], + ['try', 'cat\%[ch]', 'fina\|finally\=', 'endt\%[ry]'], + ['def', 'enddef'], + ['fu\%[nction](\@!', 'endf\%[unction]'], + ['class', 'endclass'], + ['interface', 'endinterface'], + ['enum', 'endenum'], + ['aug\%[roup]\%(\s\+[eE][nN][dD]\)\@!\s\+\S\+', 'aug\%[roup]\s\+[eE][nN][dD]'], +] + +# MODIFIERS {{{3 + +# some keywords can be prefixed by modifiers (e.g. `def` can be prefixed by `export`) +const MODIFIERS: dict = { + def: ['export', 'static'], + class: ['export', 'abstract', 'export abstract'], + interface: ['export'], +} +# ... +# class: ['export', 'abstract', 'export abstract'], +# ... +# → +# ... +# class: '\%(export\|abstract\|export\s\+abstract\)\s\+', +# ... +->map((_, mods: list): string => + '\%(' .. mods + ->join('\|') + ->substitute('\s\+', '\\s\\+', 'g') + .. '\)' .. '\s\+') + +# HIGHER_ORDER_COMMAND {{{3 + +patterns =<< trim eval END + argdo\>!\= + bufdo\>!\= + cdo\>!\= + folddoc\%[losed]\> + foldd\%[oopen]\> + ldo\=\>!\= + tabdo\=\> + windo\> + au\%[tocmd]\>.* + com\%[mand]\>.* + g\%[lobal]!\={PATTERN_DELIMITER}.* + v\%[global]!\={PATTERN_DELIMITER}.* +END + +const HIGHER_ORDER_COMMAND: string = $'\%(^\|{BAR_SEPARATION}\)\s*\<\%({patterns->join('\|')}\){NOT_A_DICT_KEY}' + +# START_MIDDLE_END {{{3 + +# Let's derive this constant from `BLOCKS`: +# +# [['if', 'el\%[se]', 'elseif\=', 'en\%[dif]'], +# ['for', 'endfor\='], +# ..., +# [...]] +# → +# { +# 'for': ['for', '', 'endfor\='], +# 'endfor': ['for', '', 'endfor\='], +# 'if': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], +# 'else': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], +# 'elseif': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], +# 'endif': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], +# ... +# } +var START_MIDDLE_END: dict> + +def Unshorten(kwd: string): string + return BlockStartKeyword(kwd) +enddef + +def BlockStartKeyword(line: string): string + var kwd: string = line->matchstr('\l\+') + return fullcommand(kwd, false) +enddef + +{ + for kwds: list in BLOCKS + var [start: string, middle: string, end: string] = [kwds[0], '', kwds[-1]] + if MODIFIERS->has_key(start->Unshorten()) + start = $'\%({MODIFIERS[start]}\)\={start}' + endif + if kwds->len() > 2 + middle = kwds[1 : -2]->join('\|') + endif + for kwd: string in kwds + START_MIDDLE_END->extend({[kwd->Unshorten()]: [start, middle, end]}) + endfor + endfor +} + +START_MIDDLE_END = START_MIDDLE_END + ->map((_, kwds: list) => + kwds->map((_, kwd: string) => kwd == '' + ? '' + : $'\%(^\|{BAR_SEPARATION}\|\\%(\s*{OPERATOR}\)\@!')) + +lockvar! START_MIDDLE_END + +# ENDS_BLOCK {{{3 + +const ENDS_BLOCK: string = '^\s*\%(' + .. BLOCKS + ->copy() + ->map((_, kwds: list): string => kwds[-1]) + ->join('\|') + .. '\|' .. CLOSING_BRACKET + .. $'\){END_OF_COMMAND}' + +# ENDS_BLOCK_OR_CLAUSE {{{3 + +patterns = BLOCKS + ->copy() + ->map((_, kwds: list) => kwds[1 :]) + ->flattennew() + # `catch` and `elseif` need to be handled as special cases + ->filter((_, pat: string): bool => pat->Unshorten() !~ '^\%(catch\|elseif\)\>') + +const ENDS_BLOCK_OR_CLAUSE: string = '^\s*\%(' .. patterns->join('\|') .. $'\){END_OF_COMMAND}' + .. $'\|^\s*cat\%[ch]\%(\s\+\({PATTERN_DELIMITER}\).*\1\)\={END_OF_COMMAND}' + .. $'\|^\s*elseif\=\>\%({OPERATOR}\)\@!' + +# STARTS_NAMED_BLOCK {{{3 + +patterns = [] +{ + for kwds: list in BLOCKS + for kwd: string in kwds[0 : -2] + if MODIFIERS->has_key(kwd->Unshorten()) + patterns += [$'\%({MODIFIERS[kwd]}\)\={kwd}'] + else + patterns += [kwd] + endif + endfor + endfor +} + +const STARTS_NAMED_BLOCK: string = $'^\s*\%(sil\%[ent]\s\+\)\=\%({patterns->join('\|')}\)\>{NOT_A_DICT_KEY}' + +# STARTS_CURLY_BLOCK {{{3 + +# TODO: `{` alone on a line is not necessarily the start of a block. +# It could be a dictionary if the previous line ends with a binary/ternary +# operator. This can cause an issue whenever we use `STARTS_CURLY_BLOCK` or +# `LINE_CONTINUATION_AT_EOL`. +const STARTS_CURLY_BLOCK: string = '\%(' + .. '^\s*{' + .. '\|' .. '^.*\zs\s=>\s\+{' + .. '\|' .. $'^\%(\s*\|.*{BAR_SEPARATION}\s*\)\%(com\%[mand]\|au\%[tocmd]\).*\zs\s{{' + .. '\)' .. END_OF_COMMAND + +# STARTS_FUNCTION {{{3 + +const STARTS_FUNCTION: string = $'^\s*\%({MODIFIERS.def}\)\=def\>{NOT_A_DICT_KEY}' + +# ENDS_FUNCTION {{{3 + +const ENDS_FUNCTION: string = $'^\s*enddef\>{END_OF_COMMAND}' + +# ASSIGNS_HEREDOC {{{3 + +const ASSIGNS_HEREDOC: string = $'^\%({COMMENT}\)\@!.*\%({HEREDOC_OPERATOR}\)\s\+\zs[A-Z]\+{END_OF_LINE}' + +# PLUS_MINUS_COMMAND {{{3 + +# In legacy, the `:+` and `:-` commands are not required to be preceded by a colon. +# As a result, when `+` or `-` is alone on a line, there is ambiguity. +# It might be an operator or a command. +# To not break the indentation in legacy scripts, we might need to consider such +# lines as commands. +const PLUS_MINUS_COMMAND: string = '^\s*[+-]\s*$' + +# TRICKY_COMMANDS {{{3 + +# Some commands are tricky because they accept an argument which can be +# conflated with an operator. Examples: +# +# argdelete * +# cd - +# normal! == +# nunmap ( +# +# TODO: Other commands might accept operators as argument. Handle them too. +patterns =<< trim eval END + {'\'}join('\|') +# }}}2 +# EOL {{{2 +# OPENING_BRACKET_AT_EOL {{{3 + +const OPENING_BRACKET_AT_EOL: string = OPENING_BRACKET .. END_OF_VIM9_LINE + +# CLOSING_BRACKET_AT_EOL {{{3 + +const CLOSING_BRACKET_AT_EOL: string = CLOSING_BRACKET .. END_OF_VIM9_LINE + +# COMMA_AT_EOL {{{3 + +const COMMA_AT_EOL: string = $',{END_OF_VIM9_LINE}' + +# COMMA_OR_DICT_KEY_AT_EOL {{{3 + +const COMMA_OR_DICT_KEY_AT_EOL: string = $'\%(,\|{DICT_KEY}\){END_OF_VIM9_LINE}' + +# LAMBDA_ARROW_AT_EOL {{{3 + +const LAMBDA_ARROW_AT_EOL: string = $'\s=>{END_OF_VIM9_LINE}' + +# LINE_CONTINUATION_AT_EOL {{{3 + +const LINE_CONTINUATION_AT_EOL: string = '\%(' + .. ',' + .. '\|' .. OPERATOR + .. '\|' .. '\s=>' + .. '\|' .. '[^=]\zs[[(]' + .. '\|' .. DICT_KEY + # `{` is ambiguous. + # It can be the start of a dictionary or a block. + # We only want to match the former. + .. '\|' .. $'^\%({STARTS_CURLY_BLOCK}\)\@!.*\zs{{' + .. '\)\s*\%(\s#.*\)\=$' +# }}}2 +# SOL {{{2 +# BACKSLASH_AT_SOL {{{3 + +const BACKSLASH_AT_SOL: string = '^\s*\%(\\\|[#"]\\ \)' + +# CLOSING_BRACKET_AT_SOL {{{3 + +const CLOSING_BRACKET_AT_SOL: string = $'^\s*{CLOSING_BRACKET}' + +# LINE_CONTINUATION_AT_SOL {{{3 + +const LINE_CONTINUATION_AT_SOL: string = '^\s*\%(' + .. '\\' + .. '\|' .. '[#"]\\ ' + .. '\|' .. OPERATOR + .. '\|' .. '->\s*\h' + .. '\|' .. '\.\h' # dict member + .. '\|' .. '|' + # TODO: `}` at the start of a line is not necessarily a line continuation. + # Could be the end of a block. + .. '\|' .. CLOSING_BRACKET + .. '\)' + +# RANGE_AT_SOL {{{3 + +const RANGE_AT_SOL: string = '^\s*:\S' +# }}}1 +# Interface {{{1 +export def Expr(lnum = v:lnum): number # {{{2 + # line which is indented + var line_A: dict = {text: getline(lnum), lnum: lnum} + # line above, on which we'll base the indent of line A + var line_B: dict + + if line_A->AtStartOf('HereDoc') + line_A->CacheHeredoc() + elseif line_A.lnum->IsInside('HereDoc') + return line_A.text->HereDocIndent() + elseif line_A.lnum->IsRightBelow('HereDoc') + var ind: number = b:vimindent.startindent + unlet! b:vimindent + return ind + endif + + # Don't move this block after the function header one. + # Otherwise, we might clear the cache too early if the line following the + # header is a comment. + if line_A.text =~ COMMENT + return CommentIndent() + endif + + line_B = PrevCodeLine(line_A.lnum) + if line_A.text =~ BACKSLASH_AT_SOL + if line_B.text =~ BACKSLASH_AT_SOL + return Indent(line_B.lnum) + else + return Indent(line_B.lnum) + IndentMoreLineContinuation() + endif + endif + + if line_A->AtStartOf('FuncHeader') + && !IsInInterface() + line_A.lnum->CacheFuncHeader() + elseif line_A.lnum->IsInside('FuncHeader') + return b:vimindent.startindent + 2 * shiftwidth() + elseif line_A.lnum->IsRightBelow('FuncHeader') + var startindent: number = b:vimindent.startindent + unlet! b:vimindent + if line_A.text =~ ENDS_FUNCTION + return startindent + else + return startindent + shiftwidth() + endif + endif + + var past_bracket_block: dict + if exists('b:vimindent') + && b:vimindent->has_key('is_BracketBlock') + past_bracket_block = RemovePastBracketBlock(line_A) + endif + if line_A->AtStartOf('BracketBlock') + line_A->CacheBracketBlock() + endif + if line_A.lnum->IsInside('BracketBlock') + var is_in_curly_block: bool = IsInCurlyBlock() + for block: dict in b:vimindent.block_stack + if line_A.lnum <= block.startlnum + continue + endif + if !block->has_key('startindent') + block.startindent = Indent(block.startlnum) + endif + if !is_in_curly_block + return BracketBlockIndent(line_A, block) + endif + endfor + endif + if line_A.text->ContinuesBelowBracketBlock(line_B, past_bracket_block) + && line_A.text !~ CLOSING_BRACKET_AT_SOL + return past_bracket_block.startindent + + (past_bracket_block.startline =~ STARTS_NAMED_BLOCK ? 2 * shiftwidth() : 0) + endif + + # Problem: If we press `==` on the line right below the start of a multiline + # lambda (split after its arrow `=>`), the indent is not correct. + # Solution: Indent relative to the line above. + if line_B->EndsWithLambdaArrow() + return Indent(line_B.lnum) + shiftwidth() + IndentMoreInBracketBlock() + endif + # FIXME: Similar issue here: + # + # var x = [] + # ->filter((_, _) => + # true) + # ->items() + # + # Press `==` on last line. + # Expected: The `->items()` line is indented like `->filter(...)`. + # Actual: It's indented like `true)`. + # Is it worth fixing? `=ip` gives the correct indentation, because then the + # cache is used. + + # Don't move this block before the heredoc one.{{{ + # + # A heredoc might be assigned on the very first line. + # And if it is, we need to cache some info. + #}}} + # Don't move it before the function header and bracket block ones either.{{{ + # + # You could, because these blocks of code deal with construct which can only + # appear in a Vim9 script. And in a Vim9 script, the first line is + # `vim9script`. Or maybe some legacy code/comment (see `:help vim9-mix`). + # But you can't find a Vim9 function header or Vim9 bracket block on the + # first line. + # + # Anyway, even if you could, don't. First, it would be inconsistent. + # Second, it could give unexpected results while we're trying to fix some + # failing test. + #}}} + if line_A.lnum == 1 + return 0 + endif + + # Don't do that: + # if line_A.text !~ '\S' + # return -1 + # endif + # It would prevent a line from being automatically indented when using the + # normal command `o`. + # TODO: Can we write a test for this? + + if line_B.text =~ STARTS_CURLY_BLOCK + return Indent(line_B.lnum) + shiftwidth() + IndentMoreInBracketBlock() + + elseif line_A.text =~ CLOSING_BRACKET_AT_SOL + var start: number = MatchingOpenBracket(line_A) + if start <= 0 + return -1 + endif + return Indent(start) + IndentMoreInBracketBlock() + + elseif line_A.text =~ ENDS_BLOCK_OR_CLAUSE + && !line_B->EndsWithLineContinuation() + var kwd: string = BlockStartKeyword(line_A.text) + if !START_MIDDLE_END->has_key(kwd) + return -1 + endif + + # If the cursor is after the match for the end pattern, we won't find + # the start of the block. Let's make sure that doesn't happen. + cursor(line_A.lnum, 1) + + var [start: string, middle: string, end: string] = START_MIDDLE_END[kwd] + var block_start: number = SearchPairStart(start, middle, end) + if block_start > 0 + return Indent(block_start) + else + return -1 + endif + endif + + var base_ind: number + if line_A->IsFirstLineOfCommand(line_B) + line_A.isfirst = true + line_B = line_B->FirstLinePreviousCommand() + base_ind = Indent(line_B.lnum) + + if line_B->EndsWithCurlyBlock() + && !line_A->IsInThisBlock(line_B.lnum) + return base_ind + endif + + else + line_A.isfirst = false + base_ind = Indent(line_B.lnum) + + var line_C: dict = PrevCodeLine(line_B.lnum) + if !line_B->IsFirstLineOfCommand(line_C) || line_C.lnum <= 0 + return base_ind + endif + endif + + var ind: number = base_ind + Offset(line_A, line_B) + return [ind, 0]->max() +enddef + +def g:GetVimIndent(): number # {{{2 + # for backward compatibility + return Expr() +enddef +# }}}1 +# Core {{{1 +def Offset( # {{{2 + # we indent this line ... + line_A: dict, + # ... relatively to this line + line_B: dict, + ): number + + if line_B->AtStartOf('FuncHeader') + && IsInInterface() + return 0 + + # increase indentation inside a block + elseif line_B.text =~ STARTS_NAMED_BLOCK + || line_B->EndsWithCurlyBlock() + # But don't indent if the line starting the block also closes it. + if line_B->AlsoClosesBlock() + return 0 + # Indent twice for a line continuation in the block header itself, so that + # we can easily distinguish the end of the block header from the start of + # the block body. + elseif (line_B->EndsWithLineContinuation() + && !line_A.isfirst) + || (line_A.text =~ LINE_CONTINUATION_AT_SOL + && line_A.text !~ PLUS_MINUS_COMMAND) + || line_A.text->Is_IN_KeywordForLoop(line_B.text) + return 2 * shiftwidth() + else + return shiftwidth() + endif + + # increase indentation of a line if it's the continuation of a command which + # started on a previous line + elseif !line_A.isfirst + && (line_B->EndsWithLineContinuation() + || line_A.text =~ LINE_CONTINUATION_AT_SOL) + return shiftwidth() + endif + + return 0 +enddef + +def HereDocIndent(line_A: string): number # {{{2 + # at the end of a heredoc + if line_A =~ $'^\s*{b:vimindent.endmarker}$' + # `END` must be at the very start of the line if the heredoc is not trimmed + if !b:vimindent.is_trimmed + # We can't invalidate the cache just yet. + # The indent of `END` is meaningless; it's always 0. The next line + # will need to be indented relative to the start of the heredoc. It + # must know where it starts; it needs the cache. + return 0 + else + var ind: number = b:vimindent.startindent + # invalidate the cache so that it's not used for the next heredoc + unlet! b:vimindent + return ind + endif + endif + + # In a non-trimmed heredoc, all of leading whitespace is semantic. + # Leave it alone. + if !b:vimindent.is_trimmed + # But do save the indent of the assignment line. + if !b:vimindent->has_key('startindent') + b:vimindent.startindent = b:vimindent.startlnum->Indent() + endif + return -1 + endif + + # In a trimmed heredoc, *some* of the leading whitespace is semantic. + # We want to preserve it, so we can't just indent relative to the assignment + # line. That's because we're dealing with data, not with code. + # Instead, we need to compute by how much the indent of the assignment line + # was increased or decreased. Then, we need to apply that same change to + # every line inside the body. + var offset: number + if !b:vimindent->has_key('offset') + var old_startindent: number = b:vimindent.startindent + var new_startindent: number = b:vimindent.startlnum->Indent() + offset = new_startindent - old_startindent + + # If all the non-empty lines in the body have a higher indentation relative + # to the assignment, there is no need to indent them more. + # But if at least one of them does have the same indentation level (or a + # lower one), then we want to indent it further (and the whole block with it). + # This way, we can clearly distinguish the heredoc block from the rest of + # the code. + var end: number = search($'^\s*{b:vimindent.endmarker}$', 'nW') + var should_indent_more: bool = range(v:lnum, end - 1) + ->indexof((_, lnum: number): bool => Indent(lnum) <= old_startindent && getline(lnum) != '') >= 0 + if should_indent_more + offset += shiftwidth() + endif + + b:vimindent.offset = offset + b:vimindent.startindent = new_startindent + endif + + return [0, Indent(v:lnum) + b:vimindent.offset]->max() +enddef + +def CommentIndent(): number # {{{2 + var line_B: dict + line_B.lnum = prevnonblank(v:lnum - 1) + line_B.text = getline(line_B.lnum) + if line_B.text =~ COMMENT + return Indent(line_B.lnum) + endif + + var next: number = NextCodeLine() + if next == 0 + return 0 + endif + var vimindent_save: dict = get(b:, 'vimindent', {})->deepcopy() + var ind: number = next->Expr() + # The previous `Expr()` might have set or deleted `b:vimindent`. + # This could cause issues (e.g. when indenting 2 commented lines above a + # heredoc). Let's make sure the state of the variable is not altered. + if vimindent_save->empty() + unlet! b:vimindent + else + b:vimindent = vimindent_save + endif + if getline(next) =~ ENDS_BLOCK + return ind + shiftwidth() + else + return ind + endif +enddef + +def BracketBlockIndent(line_A: dict, block: dict): number # {{{2 + var ind: number = block.startindent + + if line_A.text =~ CLOSING_BRACKET_AT_SOL + if b:vimindent.is_on_named_block_line + ind += 2 * shiftwidth() + endif + return ind + IndentMoreInBracketBlock() + endif + + var startline: dict = { + text: block.startline, + lnum: block.startlnum + } + if startline->EndsWithComma() + || startline->EndsWithLambdaArrow() + || (startline->EndsWithOpeningBracket() + # TODO: Is that reliable? + && block.startline !~ + $'^\s*{NON_BRACKET}\+{LIST_OR_DICT_CLOSING_BRACKET},\s\+{LIST_OR_DICT_OPENING_BRACKET}') + ind += shiftwidth() + IndentMoreInBracketBlock() + endif + + if b:vimindent.is_on_named_block_line + ind += shiftwidth() + endif + + if block.is_dict + && line_A.text !~ DICT_KEY + ind += shiftwidth() + endif + + return ind +enddef + +def CacheHeredoc(line_A: dict) # {{{2 + var endmarker: string = line_A.text->matchstr(ASSIGNS_HEREDOC) + var endlnum: number = search($'^\s*{endmarker}$', 'nW') + var is_trimmed: bool = line_A.text =~ $'.*\s\%(trim\%(\s\+eval\)\=\)\s\+[A-Z]\+{END_OF_LINE}' + b:vimindent = { + is_HereDoc: true, + startlnum: line_A.lnum, + endlnum: endlnum, + endmarker: endmarker, + is_trimmed: is_trimmed, + } + if is_trimmed + b:vimindent.startindent = Indent(line_A.lnum) + endif + RegisterCacheInvalidation() +enddef + +def CacheFuncHeader(startlnum: number) # {{{2 + var pos: list = getcurpos() + cursor(startlnum, 1) + if search('(', 'W', startlnum) <= 0 + return + endif + var endlnum: number = SearchPair('(', '', ')', 'nW') + setpos('.', pos) + if endlnum == startlnum + return + endif + + b:vimindent = { + is_FuncHeader: true, + startindent: startlnum->Indent(), + endlnum: endlnum, + } + RegisterCacheInvalidation() +enddef + +def CacheBracketBlock(line_A: dict) # {{{2 + var pos: list = getcurpos() + var opening: string = line_A.text->matchstr(CHARACTER_UNDER_CURSOR) + var closing: string = {'[': ']', '{': '}', '(': ')'}[opening] + var endlnum: number = SearchPair(opening, '', closing, 'nW') + setpos('.', pos) + if endlnum <= line_A.lnum + return + endif + + if !exists('b:vimindent') + b:vimindent = { + is_BracketBlock: true, + is_on_named_block_line: line_A.text =~ STARTS_NAMED_BLOCK, + block_stack: [], + } + endif + + var is_dict: bool + var is_curly_block: bool + if opening == '{' + if line_A.text =~ STARTS_CURLY_BLOCK + [is_dict, is_curly_block] = [false, true] + else + [is_dict, is_curly_block] = [true, false] + endif + endif + b:vimindent.block_stack->insert({ + is_dict: is_dict, + is_curly_block: is_curly_block, + startline: line_A.text, + startlnum: line_A.lnum, + endlnum: endlnum, + }) + + RegisterCacheInvalidation() +enddef + +def RegisterCacheInvalidation() # {{{2 + # invalidate the cache so that it's not used for the next `=` normal command + autocmd_add([{ + cmd: 'unlet! b:vimindent', + event: 'ModeChanged', + group: '__VimIndent__', + once: true, + pattern: '*:n', + replace: true, + }]) +enddef + +def RemovePastBracketBlock(line_A: dict): dict # {{{2 + var stack: list> = b:vimindent.block_stack + + var removed: dict + if line_A.lnum > stack[0].endlnum + removed = stack[0] + endif + + stack->filter((_, block: dict): bool => line_A.lnum <= block.endlnum) + if stack->empty() + unlet! b:vimindent + endif + return removed +enddef +# }}}1 +# Util {{{1 +# Get {{{2 +def Indent(lnum: number): number # {{{3 + if lnum <= 0 + # Don't return `-1`. It could cause `Expr()` to return a non-multiple of `'shiftwidth'`.{{{ + # + # It would be OK if we were always returning `Indent()` directly. But + # we don't. Most of the time, we include it in some computation + # like `Indent(...) + shiftwidth()`. If `'shiftwidth'` is `4`, and + # `Indent()` returns `-1`, `Expr()` will end up returning `3`. + #}}} + return 0 + endif + return indent(lnum) +enddef + +def MatchingOpenBracket(line: dict): number # {{{3 + var end: string = line.text->matchstr(CLOSING_BRACKET) + var start: string = {']': '[', '}': '{', ')': '('}[end] + cursor(line.lnum, 1) + return SearchPairStart(start, '', end) +enddef + +def FirstLinePreviousCommand(line: dict): dict # {{{3 + var line_B: dict = line + + while line_B.lnum > 1 + var code_line_above: dict = PrevCodeLine(line_B.lnum) + + if line_B.text =~ CLOSING_BRACKET_AT_SOL + var n: number = MatchingOpenBracket(line_B) + + if n <= 0 + break + endif + + line_B.lnum = n + line_B.text = getline(line_B.lnum) + continue + + elseif line_B->IsFirstLineOfCommand(code_line_above) + break + endif + + line_B = code_line_above + endwhile + + return line_B +enddef + +def PrevCodeLine(lnum: number): dict # {{{3 + var line: string = getline(lnum) + if line =~ '^\s*[A-Z]\+$' + var endmarker: string = line->matchstr('[A-Z]\+') + var pos: list = getcurpos() + cursor(lnum, 1) + var n: number = search(ASSIGNS_HEREDOC, 'bnW') + setpos('.', pos) + if n > 0 + line = getline(n) + if line =~ $'{HEREDOC_OPERATOR}\s\+{endmarker}' + return {lnum: n, text: line} + endif + endif + endif + + var n: number = prevnonblank(lnum - 1) + line = getline(n) + while line =~ COMMENT && n > 1 + n = prevnonblank(n - 1) + line = getline(n) + endwhile + # If we get back to the first line, we return 1 no matter what; even if it's a + # commented line. That should not cause an issue though. We just want to + # avoid a commented line above which there is a line of code which is more + # relevant. There is nothing above the first line. + return {lnum: n, text: line} +enddef + +def NextCodeLine(): number # {{{3 + var last: number = line('$') + if v:lnum == last + return 0 + endif + + var lnum: number = v:lnum + 1 + while lnum <= last + var line: string = getline(lnum) + if line != '' && line !~ COMMENT + return lnum + endif + ++lnum + endwhile + return 0 +enddef + +def SearchPair( # {{{3 + start: string, + middle: string, + end: string, + flags: string, + stopline = 0, + ): number + + var s: string = start + var e: string = end + if start == '[' || start == ']' + s = s->escape('[]') + endif + if end == '[' || end == ']' + e = e->escape('[]') + endif + return searchpair('\C' .. s, (middle == '' ? '' : '\C' .. middle), '\C' .. e, + flags, (): bool => InCommentOrString(), stopline, TIMEOUT) +enddef + +def SearchPairStart( # {{{3 + start: string, + middle: string, + end: string, + ): number + return SearchPair(start, middle, end, 'bnW') +enddef + +def SearchPairEnd( # {{{3 + start: string, + middle: string, + end: string, + stopline = 0, + ): number + return SearchPair(start, middle, end, 'nW', stopline) +enddef +# }}}2 +# Test {{{2 +def AtStartOf(line_A: dict, syntax: string): bool # {{{3 + if syntax == 'BracketBlock' + return AtStartOfBracketBlock(line_A) + endif + + var pat: string = { + HereDoc: ASSIGNS_HEREDOC, + FuncHeader: STARTS_FUNCTION + }[syntax] + return line_A.text =~ pat + && (!exists('b:vimindent') || !b:vimindent->has_key('is_HereDoc')) +enddef + +def AtStartOfBracketBlock(line_A: dict): bool # {{{3 + # We ignore bracket blocks while we're indenting a function header + # because it makes the logic simpler. It might mean that we don't + # indent correctly a multiline bracket block inside a function header, + # but that's a corner case for which it doesn't seem worth making the + # code more complex. + if exists('b:vimindent') + && !b:vimindent->has_key('is_BracketBlock') + return false + endif + + var pos: list = getcurpos() + cursor(line_A.lnum, [line_A.lnum, '$']->col()) + + if SearchPair(OPENING_BRACKET, '', CLOSING_BRACKET, 'bcW', line_A.lnum) <= 0 + setpos('.', pos) + return false + endif + # Don't restore the cursor position. + # It needs to be on a bracket for `CacheBracketBlock()` to work as intended. + + return line_A->EndsWithOpeningBracket() + || line_A->EndsWithCommaOrDictKey() + || line_A->EndsWithLambdaArrow() +enddef + +def ContinuesBelowBracketBlock( # {{{3 + line_A: string, + line_B: dict, + block: dict + ): bool + + return !block->empty() + && (line_A =~ LINE_CONTINUATION_AT_SOL + || line_B->EndsWithLineContinuation()) +enddef + +def IsInside(lnum: number, syntax: string): bool # {{{3 + if !exists('b:vimindent') + || !b:vimindent->has_key($'is_{syntax}') + return false + endif + + if syntax == 'BracketBlock' + if !b:vimindent->has_key('block_stack') + || b:vimindent.block_stack->empty() + return false + endif + return lnum <= b:vimindent.block_stack[0].endlnum + endif + + return lnum <= b:vimindent.endlnum +enddef + +def IsRightBelow(lnum: number, syntax: string): bool # {{{3 + return exists('b:vimindent') + && b:vimindent->has_key($'is_{syntax}') + && lnum > b:vimindent.endlnum +enddef + +def IsInCurlyBlock(): bool # {{{3 + return b:vimindent.block_stack + ->indexof((_, block: dict): bool => block.is_curly_block) >= 0 +enddef + +def IsInThisBlock(line_A: dict, lnum: number): bool # {{{3 + var pos: list = getcurpos() + cursor(lnum, [lnum, '$']->col()) + var end: number = SearchPairEnd('{', '', '}') + setpos('.', pos) + + return line_A.lnum <= end +enddef + +def IsInInterface(): bool # {{{3 + return SearchPair('interface', '', 'endinterface', 'nW') > 0 +enddef + +def IsFirstLineOfCommand(line_1: dict, line_2: dict): bool # {{{3 + if line_1.text->Is_IN_KeywordForLoop(line_2.text) + return false + endif + + if line_1.text =~ RANGE_AT_SOL + || line_1.text =~ PLUS_MINUS_COMMAND + return true + endif + + if line_2.text =~ DICT_KEY + && !line_1->IsInThisBlock(line_2.lnum) + return true + endif + + var line_1_is_good: bool = line_1.text !~ COMMENT + && line_1.text !~ DICT_KEY + && line_1.text !~ LINE_CONTINUATION_AT_SOL + + var line_2_is_good: bool = !line_2->EndsWithLineContinuation() + + return line_1_is_good && line_2_is_good +enddef + +def Is_IN_KeywordForLoop(line_1: string, line_2: string): bool # {{{3 + return line_2 =~ '^\s*for\s' + && line_1 =~ '^\s*in\s' +enddef + +def InCommentOrString(): bool # {{{3 + return synstack('.', col('.')) + ->indexof((_, id: number): bool => synIDattr(id, 'name') =~ '\ccomment\|string\|heredoc') >= 0 +enddef + +def AlsoClosesBlock(line_B: dict): bool # {{{3 + # We know that `line_B` opens a block. + # Let's see if it also closes that block. + var kwd: string = BlockStartKeyword(line_B.text) + if !START_MIDDLE_END->has_key(kwd) + return false + endif + + var [start: string, middle: string, end: string] = START_MIDDLE_END[kwd] + var pos: list = getcurpos() + cursor(line_B.lnum, 1) + var block_end: number = SearchPairEnd(start, middle, end, line_B.lnum) + setpos('.', pos) + + return block_end > 0 +enddef + +def EndsWithComma(line: dict): bool # {{{3 + return NonCommentedMatch(line, COMMA_AT_EOL) +enddef + +def EndsWithCommaOrDictKey(line_A: dict): bool # {{{3 + return NonCommentedMatch(line_A, COMMA_OR_DICT_KEY_AT_EOL) +enddef + +def EndsWithCurlyBlock(line_B: dict): bool # {{{3 + return NonCommentedMatch(line_B, STARTS_CURLY_BLOCK) +enddef + +def EndsWithLambdaArrow(line_A: dict): bool # {{{3 + return NonCommentedMatch(line_A, LAMBDA_ARROW_AT_EOL) +enddef + +def EndsWithLineContinuation(line_B: dict): bool # {{{3 + return NonCommentedMatch(line_B, LINE_CONTINUATION_AT_EOL) +enddef + +def EndsWithOpeningBracket(line: dict): bool # {{{3 + return NonCommentedMatch(line, OPENING_BRACKET_AT_EOL) +enddef + +def EndsWithClosingBracket(line: dict): bool # {{{3 + return NonCommentedMatch(line, CLOSING_BRACKET_AT_EOL) +enddef + +def NonCommentedMatch(line: dict, pat: string): bool # {{{3 + # Could happen if there is no code above us, and we're not on the 1st line. + # In that case, `PrevCodeLine()` returns `{lnum: 0, line: ''}`. + if line.lnum == 0 + return false + endif + + # Technically, that's wrong. A line might start with a range and end with a + # line continuation symbol. But it's unlikely. And it's useful to assume the + # opposite because it prevents us from conflating a mark with an operator or + # the start of a list: + # + # not a comparison operator + # v + # :'< mark < + # :'< mark [ + # ^ + # not the start of a list + if line.text =~ RANGE_AT_SOL + return false + endif + + # that's not an arithmetic operator + # v + # catch /pattern / + # + # When `/` is used as a pattern delimiter, it's always present twice. + # And usually, the first occurrence is in the middle of a sequence of + # non-whitespace characters. If we can find such a `/`, we assume that the + # trailing `/` is not an operator. + # Warning: Here, don't use a too complex pattern.{{{ + # + # In particular, avoid backreferences. + # For example, this would be too costly: + # + # if line.text =~ $'\%(\S*\({PATTERN_DELIMITER}\)\S\+\|\S\+\({PATTERN_DELIMITER}\)\S*\)' + # .. $'\s\+\1{END_OF_COMMAND}' + # + # Sometimes, it could even give `E363`. + #}}} + var delim: string = line.text + ->matchstr($'\s\+\zs{PATTERN_DELIMITER}\ze{END_OF_COMMAND}') + if !delim->empty() + delim = $'\V{delim}\m' + if line.text =~ $'\%(\S*{delim}\S\+\|\S\+{delim}\S*\)\s\+{delim}{END_OF_COMMAND}' + return false + endif + endif + # TODO: We might still miss some corner cases:{{{ + # + # conflated with arithmetic division + # v + # substitute/pat / rep / + # echo + # ^--^ + # ✘ + # + # A better way to handle all these corner cases, would be to inspect the top + # of the syntax stack: + # + # :echo synID('.', col('.'), v:false)->synIDattr('name') + # + # Unfortunately, the legacy syntax plugin is not accurate enough. + # For example, it doesn't highlight a slash as an operator. + # }}} + + # `%` at the end of a line is tricky. + # It might be the modulo operator or the current file (e.g. `edit %`). + # Let's assume it's the latter. + if line.text =~ $'%{END_OF_COMMAND}' + return false + endif + + if line.text =~ TRICKY_COMMANDS + return false + endif + + var pos: list = getcurpos() + cursor(line.lnum, 1) + var match_lnum: number = search(pat, 'cnW', line.lnum, TIMEOUT, (): bool => InCommentOrString()) + setpos('.', pos) + return match_lnum > 0 +enddef +# }}}1 +# vim:sw=4 -- cgit v1.2.3