diff --git a/vim9script.md b/vim9script.md new file mode 100644 index 00000000..94cd5e99 --- /dev/null +++ b/vim9script.md @@ -0,0 +1,615 @@ +--- +name: Vim9Script +filename: learnvim9script.vim +contributors: + - ["Alejandro Sanchez", "http://hiphish.github.io/"] + - ["Yegappan Lakshmanan", "https://github.com/yegappan"] + - ["LacyGoill", "https://github.com/lacygoill"] +--- + +Vim9Script is a modern scripting language introduced in Vim 9.0. + +Vim9script, which is exclusive to Vim version 9+, improves on its predecessor, legacy Vimscript, also called VimL, which is a sequence of Ex commands enhanced with scripting constructs like variables, functions, and control flow. +(Ex commands, such as `:echo`, `:write`, `:substitute`, `:quit`, ... are commands that are part of the legacy Ex editor to execute a single action, one-liners without return values.) +Legacy Vimscript is interpreted line by line, requiring backslashes to continue lines, and commands like `let` and `call` are used to make them resemble ex-commands. + +In contrast, Vim9script supports multi-line syntax natively, without needing line continuation. +This makes it less suited for command-line usage, unlike traditional ex-commands, so that Vim9Script complements these for scripting. + +Vim9Script enforces stricter syntax, improves performance, and supports modern programming features such as strong typing, classes, lambdas, and modules. +Differences (see https://vimhelp.org/vim9.txt.html#vim9-differences) include: + +1. New syntax basics + • Comments start with `#` instead of `"`. + • Line-continuation backslashes are rarely needed --- just concatenate with `..`. + • Whitespace is significant in many places to keep things readable. + +2. Variables and constants + • Declare regular variables with `:var`, e.g. `var count = 0` + • Change them with standard operators (`count += 3`) --- no more `:let`. + • Declare immutable names with `:const` or `:final`. + +3. Typed, script-local functions + • All functions (and variables) are script-local by default. + • Use `:def` with typed params and a return type, e.g. + `def foo(x: number, y: string): bool` + • Call them like normal functions (no `:call`). + +All Ex commands can still be used inside functions and, vice-versa, you can call a function by an Ex command with `:vim9` (respectively `:call` in legacy vimscript) on the command line. +You can also define your own commands that call functions. + +Try [vim9-conversion-aid](https://github.com/ubaldot/vim9-conversion-aid) as a starting point to convert legacy Vimscript to Vim9Script. + +```vim +vim9script + +# The vim9script namespace, above, is required to distinguish a Vim9 script +# *.vim file from a legacy vimscript file. In Vim's command-line mode, +# or in a legacy script, using the command `:vim9cmd` (or just `:vim9`) before +# a command also evaluates and executes code as Vim9 script. + +# There is no distinction between single and multi-line comments. +# Use # inside a line at any position to comment out all following characters. + +# You can run this Vim9 script directly in Vim. After pasting the content +# into a Vim buffer, enter the command `so` in command-line mode (press `:` +# first to enter command-line mode). + +################################################################## +## 1. Primitive types, collection types, operators, and regex +################################################################## + +# The builtin function typename() may be used to reveal the type. +# Primitive data types are number (integer), float, and bool(ean) +echo typename(1) # number +echo typename(1.1) # float +echo typename(true) # bool + +# Collection data types are string, blob, list, tuple, and dict(ionary) +echo typename("Hello world") # string +# Blob is a binary object +echo typename(0zFE0F) # blob +echo typename([1, 2]) # list +# echo typename((1, 2)) # tuple (Yet commented as it's a recent addition) +echo typename({1: 'one', 2: 'two'}) # dict + +# Arithmetic with the number (integer) type. +echo 1 + 1 # 2 +echo 2 - 1 # 1 +echo 3 * 2 # 6 +echo 8 / 2 # 4 +# If the result is not an integer, the remainder is not returned. +echo 9 / 2 # 4 +# But modulo returns the remainder. +echo 9 % 2 # 1 + +# Similarly, for the float type. +echo 3.14 + 0.0015 # 3.1415 +echo 3.0 * 2.0 # 6.0 +# An integer and float will return a float. +echo 9 / 2.0 # 4.5 + +# Logical OR (||), AND (&&), and NOT (!). +echo true || false # true +echo true && false # false +echo !true # false + +# Equality (==) and inequality (!=) work on all three primitive types and +# comparisons (>, >=, <=, and <) on numbers and floats. +echo [1, 2] == [1, 2] # true +echo 'apples' != 'pears' # true +echo 9 > 8 # true +echo 8 >= 8 # true +echo 8 <= 9 # true +echo 8 < 9 # true + +# Ternary operator. +echo 9 > 8 ? true : false # true + +# Falsy ("null coalescing operator"). +echo 9 > 8 ?? 1 + 1 # true +echo 8 > 9 ?? 1 + 1 # 2 + +# Bitwise operators (>> and <<). +echo 1 << 2 # 4 +echo 9 >> 1 # 5 + +# String concatenation. +echo "Hello" .. " " .. 'world' # Hello world +# String indexing is at the character level (not byte level like vimscript) +echo 'fenêtre'[-4 : -1] # être +# There are dozens of builtin string functions. Examples. +echo reverse("moor") # room +echo toupper("summer") # SUMMER +echo str2list('yes') # [121, 101, 115] +echo strcharlen('length') # 6 + +# Type casting may be used to give an error for a type mismatch. +# (This example could use `try`, but that's covered later.) +echo 3 # 3 +# echo '3' # This errors, but could be caught with try/catch + +# Not-a-real-number values may be tested with isinf() and isnan() builtin +# functions. These examples also illustrate method chaining. +echo 1.0 / 0.0 ->isinf() # inf +echo -1.0 / 0.0 ->isinf() # -inf +echo 0.0 / 0.0 ->isnan() # nan + +# The maximum number is either a 32 or 64 bit signed number, which may +# be checked using: +echo v:numbersize # echos either 32 or 64 +# The implication is that any arithmatic where the number exceeds the +# permitted value, for 64-bit 9,223,372,036,854,775,807, will fail: +echo 9223372036854775807 + 1 # -9223372036854775808 + +# Numbers may be expressed as decimal, hexadecimal (prefixed with 0x), +# octal (0o or 0O), or binary (0b). These are all decimal 15: +echo 15 + 0b1111 + 0xF + 0o17 # 60 + +# Pattern matching on strings. +echo 'foobar' =~ 'foo' # true (matches pattern) +echo 'foobar' !~ 'baz' # true (does not match pattern) + +# Vim uses distinct regular expressions. The basics are the same as many +# other languages. The basics: +# `.` any character, `*` zero+ times, `\+` one+ times +# `\c` case-insensitive, `\C` case-sensitive +# `\d` digit, `\w` word char, `\s` whitespace, `^` start, `$` end, `\|` OR +# Character classes may also be used. Examples. +# [:blank:] is the same as \s, [:digit:] is the same as \d +# Some things like the "very nomagic" and "very magic" are unique. +# `\v` very magic, most chars are special, closer to extended regexes +# `\V`: very nomagic, all but \ are literal +echo 'Foobar' =~ '\c^foo' # true +echo "The year is 2025" =~ '[[:digit:]]\+$' # true +echo 'abc123' =~ '\v\d+' # true +echo 'a|b' =~ '\Va|b' # true + +# `\zs`: Sets the start of the match. +# `\ze`: Sets the end of the match. +# Match only the domain from an email: +var email = 'user@example.com' +echo matchstr(email, '@\zs[^ ]\+\ze') # Output: 'example.com' + + +#################################################### +## 2. Variables +#################################################### + +# `var` is used to declare a variable, which may have a type +var name: string = 'Vim' # type declared +var count = 10 # type inferred (number) +# When the type is declared as a list or dict, its type(s) must be also, but +# may be "". +var Alist: list> = [[1, 2], [3, 4]] +var Adict: dict = {a: 1, b: 'two'} + +# Constants +# `const` may be used to make both the variable and values constant. +const PI: dict = {2: 3.14, 4: 3.1415} # Cannot add items to this dict +echo PI[4] # 3.1415 +# `final` may be used to make the variable a constant and the values mutable. +final pi: dict = {2: 3.14, 4: 3.1415} +# Adding a key-value pair to pi. +pi[3] = 3.142 +echo pi # {2: 3.14, 3: 3.142, 4: 3.1415} +# Dictionary key-value pairs may also be accessed using dict.key +echo pi.4 # 3.1415 + +# There are many builtin functions for variable manipulation. Some have been +# illustrated, above. A selection of some more related to lists. +var MyList: list = [0, 1, 2, 3] +echo MyList[0] # 0 +MyList->add(4) +MyList->remove(0) +echo join(MyList, ', ') # 1, 2, 3, 4 +# String interpolation with $. +echo $"The first and last items in MyList are {MyList[0]} and {MyList[-1]}" + +# Variables exist within scopes. When unprefixed, as the examples are, above, +# the scope is script-local (which differs from vimscript, which uses `s:`). +# Other scopes are global (prefixed with `g:`), window-local (`w:`), +# buffer-local (`b:`), and tab-local (`t:`). +# Vim help on scopes: https://vimhelp.org/eval.txt.html#variable-scope +# Use `g:` for global variables, `b:` for buffer-local, `w:` for window-local, +# and so on. Vim also has many Vim-defined `v:` prefixed global variables. +g:global_var = 'I am global' +echo g:global_var # I am global +echo v:version # 901 (or whatever version you are running; 901 means 9.1) +echo b:current_syntax # vim (if the buffer's filetype is vim with 'syntax' on) + +# Registers are a form of variable used to store string values of many +# pre-defined or user-defined items. When used in a Vim9 script, they are +# prefixed with `@`. +echo @: # (echos the last command entered by the user) +echo @/ # (echos the last search performed by the user) +echo @1 # (echos the penultimate yanked or deleted text) +echo @% # (echos the current file path) +@z = 'There are 26 named registers, a to z' +echo @z + +# Other builtin variables. +echo $MYVIMRC # (location of your .vimrc, or _vimrc using Windows, file) +echo $VIMRUNTIME # (the Vim directory of the current Vim executable) +echo $PATH # (references the environment variable, PATH) + +# Vim has many settings, which are also variables. They may be local or +# global scoped. +echo &textwidth # (echos the buffer-local value of the textwidth option) +echo &wildmenu # (echos the boolean value of command-line wildcard expansion) + + +#################################################### +## 3. Control Flow +#################################################### + +# If / Else +if count > 5 + echo 'big' +elseif count == 5 + echo 'medium' +else + echo 'small' +endif + +# For loop +for j in range(3) + echo j +endfor + +# While loop +var k = 0 +while k < 3 + echo k + k += 1 +endwhile + +# Loop control +for x in [1, 2, 3] + if x == 2 + continue + endif + if x == 3 + break + endif + echo x +endfor + +# Exceptions +try + DoesNotExist() # This fails +catch + echo 'Function DoesNotExist() does not exist!' +endtry + +try + var lines = readfile('nofile.txt') +catch /E484:/ + echo 'File not found' +finally + echo 'Done' +endtry + +try + if !filereadable('file.txt') + throw 'MyError' + else + # Read + var lines = readfile('file.txt') + # Append + writefile(['line3'], 'file.txt', 'a') + echo lines + endif +catch /MyError/ + echo 'File not found' +endtry + + +#################################################### +## 4. Functions and Lambdas +#################################################### + +# Basic function +def Add(x: number, y: number): number + return x + y +enddef + +echo Add(3, 4) + +# Default arguments +def Power(base: number, exp: number = 2): number + return float2nr(pow(base, exp)) +enddef + +# Variable arguments +def Sum(...args: list): number + return reduce(args, (x, y) => x + y) +enddef + +# Define a function that returns a closure capturing a local variable +def MakeAdder(x: number): func + # Return a reference to the inner function + return (y: number) => x + y +enddef + +# Create a closure that adds 5 +var Add5 = MakeAdder(5) + +# Call the closure with 3; result is 8 +echo Add5(3) + +# Lambdas in Vim9Script use `(args) => expr` syntax: +var Divide = (val: number, by: number): number => val / by +echo Divide(420, 10) # 42 + +# Sample list +var nums = [1, 2, 3, 4, 5, 6] + +# map(): Square each number +var squares = map(copy(nums), (i, v) => v * v) +echo squares # [1, 4, 9, 16, 25, 36] + +# filter(): Keep even numbers +var evens = filter(copy(nums), (i, v) => v % 2 == 0) +echo evens # [2, 4, 6] + +# reduce(): Sum all numbers +var sum = reduce(copy(nums), (acc, v) => acc + v, 0) +echo sum # 21 + +# sort(): Sort descending using lambda +# Use `copy()` to avoid mutating the original list. +var sorted = sort(copy(nums), (a, b) => b - a) +echo sorted # [6, 5, 4, 3, 2, 1] + +#################################################### +## 5. Classes and enums +#################################################### + +# Class +class Point + var x: number + var y: number + + def new(x: number, y: number) + this.x = x + this.y = y + enddef + + def Move(dx: number, dy: number) + this.x += dx + this.y += dy + enddef + + def ToString(): string + return $"({this.x}, {this.y})" + enddef +endclass + +var p = Point.new(1, 2) +p.Move(3, 4) +echo p.ToString() # => (4, 6) + +# Enum +enum Quad + Square('four', true), + Rectangle('opposite', true), + Rhombus('opposite', false) + var es: string + var ra: bool + def QuadProps(): list + return [this.es, this.ra] + enddef +endenum +var q: Quad = Quad.Square +# result using string interpolation and showing multiple lines without `\`, +# which is required in vimscript but not in Vim9 script. +var result = $"A {tolower(q.name)} has {q.QuadProps()[0]} sides of equal " .. + "length and " .. + (q.QuadProps()[1] ? 'has only right angles' : 'has no right angle') +echo result + + +#################################################### +## 6. Modules and Imports +#################################################### + +# Sourcing another script file. +# In this case, sourcing the optional matchit plugin, distributed with Vim. +source $VIMRUNTIME/pack/dist/opt/matchit/plugin/matchit.vim +# That's not an ideal way though. It's better to use `packadd`. +packadd helptoc +# Using `runtime` is another way (which can be used with wildcards) +runtime pack/**/**/**/comment.vim + +# Importing functions from other files is achieved with `export` in the script +# file exporting the function/constant/variable and `import` in the +# script using the exported function/constant/variable. +# If this is in file, `MyFile.vim` in the same directory as the script file. +export const THEEND: string = 'The end.' +# The script importing THEEND would include (try used here to prevent error) +try + import "MyFile.vim" +catch + # Do something if the import fails +endtry +# And using the constant would be (try again used to prevent error) +try + echo MyFile.THEEND +catch + echo "THE END" +endtry + +#################################################### +## 7. Vim Idioms +#################################################### + +# Source guard (plugin pattern) +if exists('g:loaded_myplugin') | finish | endif +# Set to true to avoid sourcing the following code multiple times. +# g:loaded_myplugin = true + +# Default value +var greeting = get(g:, 'myplugin_greeting', 'Hello') + +# Command definition +command! Hello echo 'Hello Vim9' +# You can specify attributes like `-nargs`, `-range`, `-complete`; +# see https://vimhelp.org/usr_40.txt.html#40.2 +command! -nargs=1 -complete=file MyCmd edit +# Toggle a boolean setting +def ToggleFeature() + g:myplugin_enabled = !get(g:, 'myplugin_enabled', false) + echo g:myplugin_enabled ? 'Enabled' : 'Disabled' +enddef +command! ToggleMyPlugin ToggleFeature() + +# Define a script-local function and map it via +def DoSomething() + echo 'Action triggered' +enddef + +nnoremap (MyPluginAction) DoSomething() +nmap a (MyPluginAction) + +# Group autocommands to manage them systematically +# to prevent duplicate autocommands on re-sourcing. +augroup AutoReload + autocmd! + autocmd BufWritePost $MYVIMRC source $MYVIMRC + autocmd BufReadPost *.txt echo 'Hello text file' +augroup END + +# You can run normal commands from Vim9Script: +# This executes like pressing `ggddGp` in normal mode. +normal! ggddGp + +# `exists({name})` checks if a variable, function, or command is defined. +echo exists(':myVariable') == 2 +# `has({feature})` checks if Vim has a specific feature (e.g. `has('unix')`). +echo has('unix') +if has('nvim') + echo 'Running in Neovim' +endif +# `expand({expr})` expands filenames, ``, etc. +echo expand('%:p') +# `printf({fmt}, ...)` formats strings. +echo printf('Hello, %s!', 'world') +# `type()`, along with `v:t_*` constants, indicates object types. +echo type(123) == v:t_number + + +#################################################### +## 8. External Commands +#################################################### + +# Run a shell command and capture output +silent result = system('ls') +echo result + +# Run and split into lines +silent var lines = systemlist('ls') +for line in lines + echo line +endfor + +# output files in folder of current file +# ... to non-interactive shell +!ls %:h:S +# ... to current buffer, replacing its contents +:%!ls %:h:S +# ... to the same buffer, appending at cursor position +:.read! ls %:h:S + +# Using job_start() callback +var shell_job: job +def GotOutput(channel: channel, msg: string) + echo msg +enddef +# Start a shell in the background. +shell_job = job_start(["/bin/sh", "-c", "ls"], { + out_cb: GotOutput, + err_cb: GotOutput + }) +# Check exit status +echo v:shell_error + +# Create a quickfix list from Git diff +def GitDiffQuickfix() + var diff_lines = systemlist('git diff --name-only') + if v:shell_error != 0 + echo 'Git not available or not a repo' + return + endif + + var qf_list = [] + for file in diff_lines + add(qf_list, {'filename': file, 'lnum': 1, 'text': 'Modified file'}) + endfor + + call setqflist(qf_list, 'r') + copen +enddef +command! GitDiffQF call GitDiffQuickfix() + +############################################################ +## 9. Testing, Debugging, Compiling and Inspecting Bytecode +############################################################ + +v:errors = [] + +assert_equal(4, 2 + 2) +assert_false(2 < 1) +assert_notmatch('\d\+', 'abc') + +if !empty(v:errors) + echo $"Test failures: {v:errors}" +endif + +def MyFunc() + var x = 10 + var y = x * 2 + echo y +enddef + +# To debug this function: +# Add a breakpoint at line 3 of the function (line numbers are relative to the function) +# :breakadd func MyFunc 3 + +# Then run the function in debug mode +# :debug call MyFunc() + +# While debugging, use commands like: +# - :step to step into +# - :next to step over +# - :finish to run to end +# - :cont to continue +# - :echo to inspect variables + +## Listing Bytecode of a Function, +## Useful to understand how Vim optimizes Vim9Script functions + +def MyMultiply(a: number, b: number): number + return a * b +enddef + +# To compile and check for syntax/type errors: +# Use :func MyMultiply to view the function definition + +# To inspect the compiled bytecode: +disassemble MyMultiply + +# Example output: +# 757_MyMultiply +# return a * b +# 0 LOAD arg[-2] +# 1 LOAD arg[-1] +# 2 OPNR * +# 3 RETURN +``` + +### Additional Resources + +- [Vim9 Script Reference](https://vimhelp.org/vim9.txt.html) +- [Yegappan's Vim9 for Python Developers](https://github.com/yegappan/Vim9ScriptForPythonDevelopers) +- [Lacygoill's Vim9 Notes](https://github.com/jessepav/lacygoill-wiki-backup/blob/master/vim/vim9.md)