Get the code: learnvim9script.vim
Vim9 script is a modernized, TypeScript-like language to script the Vim text editor. It improves significantly on its predecessor, legacy Vim script (aka “VimL”).
In Vim9 script, Ex commands (such as :echo, :write, :substitute,
etc.) can be used inside functions and, vice-versa, you can call a function
with an Ex command, using :vim9cmd on the command line.
However, Vim9 script removes many of legacy Vim script’s unusual
and esoteric features (see
vim9-differences).
Vim9 script also enforces stricter syntax, markedly improves performance,
and supports modern programming features such as strong typing, classes,
and generic functions.
The following sections include short, complete, and sourceable scripts.
For how to source them within Vim, see
source-range.
If using the link at "Get the code:
learnvim9script.vim",
near the top of this page, each vim9script block delineates a distinct,
sourceable script.
" The `vim9script` command is required to distinguish Vim9 script from
" legacy Vim script in a `.vim` file. Before the command, legacy Vim script
" comments (starting with the " character) can be used, like these lines do.
"
" After the `vim9script` command, the # character is used for comments
vim9script
# PRIMITIVE AND COLLECTION TYPES
# Vim’s builtin function `typename()` may be used to reveal the type
# Primitive data types
echo typename(1) # number
echo typename(1.1) # float
echo typename(true) # bool
# Collection data types
echo typename("Hi") # string
echo typename(0zFE0F) # blob (a binary object)
echo typename([1]) # list<number>
echo typename((1, )) # tuple<number>
echo typename({1: 'one'}) # dict<string>
# Type casting may be used to fail early when there is a type mismatch
echo <number>3 # 3
echo <number>'3' # E1012: Type mismatch; expected number but got str
# ----------------------------------------------------------------------------
vim9script
# OPERATORS AND EXPRESSIONS
# 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
# The float type returns with the applicable number of decimal places
echo 1.25 * 6.1 # 7.625
echo 3.1405 + 0.0005 # 3.141
# An integer and float expression returns a float
echo 9 / 2.0 # 4.5
# ----------------------------------------------------------------------------
vim9script
# LOGICAL AND COMPARISON OPERATORS
# 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 operator ("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
# ----------------------------------------------------------------------------
vim9script
# NUMBERS
# Numbers may be expressed as decimal, hexadecimal (prefixed with 0x),
# octal (prefixed with 0o or 0O), or binary (prefixed with 0b)
# These are all decimal 15
echo 15 # 15
echo 0b1111 # 15
echo 0xF # 15
echo 0o17 # 15
# The maximum number is either a 32 or 64 bit signed number, which may
# be checked using Vim’s builtin variable `v:numbersize`
echo v:numbersize # either 32 or 64
# The implication is that any arithmetic where the number exceeds the
# permitted value (for 64-bit 9,223,372,036,854,775,807) will fail
echo 9223372036854775807 + 1 # -9223372036854775808
# Not-a-real-number values may be tested with `isnan()` and `isinf()` builtin
# functions - the second and third lines also illustrate method calling
echo isnan(0.0 / 0.0) # nan
echo 1.0 / 0.0 ->isinf() # inf
echo -1.0 / 0.0 ->isinf() # -inf
# ----------------------------------------------------------------------------
vim9script
# STRINGS
# String concatenation with `..`
echo "Hello" .. " " .. 'world' # Hello world
# String interpolation with `$`
echo $"Adam is {1 + 41}" # Adam is 42
# String indexing is by character (like Java/Python 3) not byte (like C/Rust)
echo 'fenêtre'[-4 : -1] # être
# Vim has dozens of builtin string functions, which can be called
# procedurally, as methods, and may be chained
echo toupper("summer") # SUMMER
echo 'metre'->strcharlen() # 5
echo "Ho"->reverse()->tolower() # oh
# Pattern matching on strings
echo 'foobar' =~ 'foo' # true (matches pattern)
echo 'foobar' !~ 'baz' # true (does not match pattern)
# ----------------------------------------------------------------------------
vim9script
# REGULAR EXPRESSIONS
# Vim uses a distinct flavor of regular expressions, though the basics
# are similar to many other languages.
# Using Vim's builtin function matchstr() to show regex matches:
echo matchstr("Hello", '.....') # '.' is any character
echo matchstr("Hello", 'Hel*o') # ('*' is zero+ times)
echo matchstr("Hello", 'H.\+') # ('\+' is one+ times)
echo matchstr("Hello", '\cheLlo') # ('\c' is case-insensitive)
echo matchstr("Hello", '^Hello$') # ('^' is start, '$' is end)
echo matchstr("Hello", 'H\(e\|x\)llo') # ('\|' is or)
# Shorthand character classes and POSIX bracket expressions may be used
# \u is uppercase, \a is alpha, \s is space or tab, \d is decimal digit
echo "Dec 2025" =~ '\u\a\a\s\d\{4\}' # true
# [[:digit:]] is a decimal digit as are [0-9] and \d
echo "2025-12-10" =~ '[[:digit:]]\{4\}-[0-9]\{2\}-\d\{2\}' # true
# Some things like "very magic" and "very nomagic" are unique to Vim:
# \v very magic means most chars are special, closer to extended regexes
# \V very nomagic, all but \ and the terminating character are literal
echo matchstr("Hello", '\v^(.*)$') # Hello
echo matchstr("Hello", '\V\^\(\.\*\)\$') # Hello
# \zs sets the start of a match and \ze sets the end of the match
var email = 'Addy [email protected] is fictitious'
echo matchstr(email, '@\zs\a\+[.]com\ze[^\w]') # example.com
# PCRE-style assertions may also be used:
# \@<= is positive lookbehind, \@= is positive lookahead
echo matchstr(email, '@\@<=\a\+[.]com\@=[^\w]') # example.com
# \@<! is negative lookbehind, \@= is negative lookahead
echo matchstr(email, '[^@]\@<!\a\+[.]com\([\w]\)\@!') # example.com
# Combined with very magic they are easier to read
echo matchstr(email, '\v[@]@<=\a+[.]com@=[^\w]') # example.com
echo matchstr(email, '\v[^@]@<!\a+[.]com([\w])@!') # example.com
# ----------------------------------------------------------------------------
vim9script
# DECLARATIONS
# `var` is used to declare a variable, which may have a type specified
var count = 10 # type inferred (number)
var name: string = 'Vim' # type declared
# When the type is a list or dict, the list’s type(s) must be declared too,
# though `<any>` may be used to allow mixed types
var a_list: list<list<number>> = [[1, 2], [3, 4]]
var a_dict: dict<any> = {a: 1, b: 'two'}
echo $"a_list is type {typename(a_list)} and a_dict is type {typename(a_dict)}"
# Constants
# `const` may be used to make both the variable and values constant
const PI: dict<float> = {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<float> = {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
# ----------------------------------------------------------------------------
vim9script
# MANIPULATING CONTAINER VARIABLES
# There are many builtin functions for variable manipulation. These are a few
# relevant to lists.
var MyList = ['a', 'b', 'c']
echo MyList[0] # a
MyList->add('d') # Adds 'd' to the end of the list
MyList->remove(2) # Removes item at index 2 (starting at 0)
echo join(MyList, ', ') # a, b, d
# String interpolation with $. (More idiomatic than `printf()`)
echo $"The first and last items in MyList are '{MyList[0]}' and '{MyList[-1]}'"
# ----------------------------------------------------------------------------
vim9script
# VARIABLES’ SCOPES
# Variables exist within scopes. When unprefixed their scope is script-local.
# Other scopes are global (prefixed with `g:`), window-local (`w:`),
# buffer-local (`b:`), and tab-local (`t:`). Vim also has many Vim-defined
# (`v:`) global variables.
var my_var: string = 'Script-local variables have no prefix in Vim9 script'
g:my_var = 'Global variables with "g:" prefixed are available everywhere'
b:my_var = 'Buffer variables ("b:") are only visible within a buffer'
echo $"{my_var}\n{g:my_var}\n{b:my_var}\n"
echo v:version # Vim variable for a Vim instance's major version (901 == 9.1)
# ----------------------------------------------------------------------------
vim9script
# REGISTERS
# Registers are a form of variable used to store string values of many
# pre-defined and user-defined items. When used in a Vim9 script, they are
# prefixed with `@`.
echo 'Current register values:'
echo "- Last command (:)\t" .. @:
echo "- Last search (/) \t" .. @/
echo "- Current file (%)\t" .. @%
@a = 'There are 26 named registers "a to "z and "A to "Z.'
echo $"- Register a equals\t{@a}"
# ----------------------------------------------------------------------------
vim9script
# BUILTIN VARIABLES AND SETTINGS
echo $MYVIMRC # (location of your .vimrc or, using Windows, _vimrc file)
echo $VIMRUNTIME # (the Vim directory of the current Vim executable)
# Vim has many settings variables. Some are global in scope, some are local,
# and others are both
echo &fileencoding # (buffer-local: the character encoding, e.g., utf-8)
echo &equalalways # (global: do/don't make newly split windows the same size)
# ----------------------------------------------------------------------------
vim9script
# CONDITIONALS AND LOOPS
const INT: number = 5
# If / Else
if INT > 5
echo 'big'
elseif INT == 5
echo 'medium'
else
echo 'small'
endif
# For loop (using builtin function range() for INT times, 0 indexed)
for j in INT->range()
echo j
endfor
# While loop (using builtin function add() to add values to the list l)
var k: number
var l: list<number>
while k < INT
l->add(k)
k += 1
endwhile
echo l
# Loop control using a tuple literal
for x in (1, 2, 3)
if x == 2
continue
elseif x == 3
echo $'Stopping at {x}'
break
endif
echo x
endfor
# ----------------------------------------------------------------------------
vim9script
# EXCEPTIONS
try
DoesNotExist() # This fails
catch
echo 'Function DoesNotExist() does not exist!'
endtry
try
var lines = readfile('nofile.txt')
catch
echo v:exception
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
# ----------------------------------------------------------------------------
vim9script
# FUNCTION BASICS
# :def functions have 0+ arguments and a return type, which may be `void`
def Add(x: number, y: number): void
echo x + y
enddef
Add(3, 4) # Echos 7
# Arguments may be defaulted
def Power(base: number, exp: number = 2): number
return float2nr(pow(base, exp))
enddef
echo Power(8) # Echos 64 because `exp` defaults to 2
# There may be a variable number of arguments
def MinMax(...args: list<number>): tuple<number, number>
return (args->min(), args->max())
enddef
echo MinMax(8, 9, 2, 4) # Echoes (2, 9)
# ----------------------------------------------------------------------------
vim9script
# GENERIC FUNCTIONS
# Return the maximum value of a list or, if it's empty, a default value
def MaxOrDefault<T, U>(lst: list<T>, default: U): any
return lst->len() > 0 ? lst->max() : default
enddef
echo MaxOrDefault<number, number>([1, 2], 0) # Echos 2
echo MaxOrDefault<number, number>([], 0) # Echos 0
echo MaxOrDefault<number, string>([], 'empty') # Echos empty
# ----------------------------------------------------------------------------
vim9script
# LAMBDAS
# Syntax is `(args) => expr`
var Divide = (val: number, by: number): number => val / by
echo Divide(420, 10) # Echos 42
# Sample list used in the following lambda examples
var nums = [1, 2, 3, 4, 5, 6]
# Using builtin function filter() to keep only the even numbers in nums list.
# This example uses the `_` argument, which ignores an argument. It is useful
# in callbacks where the argument is not needed in the expression, but an
# argument must be provided to match the called function. (Using the builtin
# function, `copy()`, avoids mutating the original list.)
var evens: list<number> = filter(copy(nums), (_, v) => v % 2 == 0)
echo evens # Echos [2, 4, 6]
# Using builtin function map() to square each number in the nums list.
# This example also uses method chaining and the argument "_".
var squares: list<number> = nums->copy()->map((_, v) => v * v)
echo squares # Echos [1, 4, 9, 16, 25, 36]
# Using builtin function `reduce()` to sum all the numbers in the nums
# list - it is called with two arguments:
# 1) the result so far (acc), and
# 2) the current item (v).
var sum: number = nums->copy()->reduce((acc, v) => acc + v, 0)
echo sum # Echos 21
# Descending sort using a lambda
var sorted: list<number> = sort(copy(nums), (a, b) => b - a)
echo sorted # Echos [6, 5, 4, 3, 2, 1]
# ----------------------------------------------------------------------------
vim9script
# CLOSURES
def Outer(): func
var x: number = 2 # Initial value captured by closure
def Inner(): number
x = x * 2 # Modifies the captured x
return x # Returns the new value
enddef
return Inner # Returns the closure (function + captured x)
enddef
var Double = Outer() # Creates a closure (with its own x = 2)
# Calling the closure
echo Double() # Echos 4
echo Double() # Echos 8
echo Double() # Echos 16
echo Double() # Echos 32
# ----------------------------------------------------------------------------
vim9script
# A CLASS EXAMPLE
# 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 = Point.new(1, 2)
p.Move(3, 4)
echo p.ToString() # => (4, 6)
# ----------------------------------------------------------------------------
vim9script
# AN ENUM EXAMPLE
enum Quad
Square('four', true),
Rhombus('opposite', false)
var es: string
var ra: bool
def QuadAbout(): string
return $"A {tolower(this.name)} has {this.es} sides of equal length; it" ..
(this.ra ? ' has only right angles' : ' has no right angles')
enddef
endenum
var q: Quad = Quad.Square
echo q.QuadAbout()
q = Quad.Rhombus
echo q.QuadAbout()
# ----------------------------------------------------------------------------
vim9script
# SOURCING ANOTHER SCRIPT
# Source the optional helptoc plugin, distributed with Vim
source $VIMRUNTIME/pack/dist/opt/helptoc/plugin/helptoc.vim
# It’s better to use `packadd` for this, though
packadd helptoc
# Using `runtime` is another way (`*` or `**` wildcards can be used)
runtime pack/**/helptoc.vim
# ----------------------------------------------------------------------------
Vim9 script provides a modular, efficient import/export system.
Explicitly exported functions, constants, and variables declared
with export are accessible from other scripts which import them.
An example...
In file MyModule.vim:
vim9script
# EXPORT-IMPORT EXAMPLE: `MyModule.vim`
# Without `export`, this constant is visible only within MyModule.vim
const LOCAL: string = 'Not exported'
# This exported constant will be available from scripts importing MyModule.vim
export const GREETING: string = 'Hello from MyModule'
# ----------------------------------------------------------------------------
In another script file (presuming it is in the same directory):
vim9script
# EXPORT-IMPORT EXAMPLE (THE SCRIPT DOING THE IMPORT)
import "./MyModule.vim"
# Using the constant from MyModule.vim
echo MyModule.GREETING
# Trying to use MyModule.LOCAL fails because it's not exported
try
echo MyModule.LOCAL
catch
echo v:exception
endtry
# ----------------------------------------------------------------------------
vim9script
# PLUGIN DEVELOPMENT
# These are some of the patterns used when creating well-behaved plugins
# (which enable others to easily use your Vim9 scripts).
# Source guard (plugin pattern), which prevents duplicate loading of plugins
if exists('g:loaded_myplugin')
finish
endif
# This would be put in the myplugin script (NB: intentionally commented here)
# g:loaded_myplugin = true
# Getting a global variable or, where it does not exist, a default value
var greeting: string = get(g:, 'myplugin_greeting', 'Hello')
# Toggling boolean settings is a common user interface pattern
def ToggleFeature()
g:myplugin_enabled = !get(g:, 'myplugin_enabled', false)
echo g:myplugin_enabled ? 'Enabled' : 'Disabled'
enddef
command! ToggleMyPlugin ToggleFeature()
# (After sourcing this script, try :ToggleMyPlugin a few times)
# ----------------------------------------------------------------------------
vim9script
# COMMAND AND MAPPING DEFINITIONS
# Commands and mappings provide the primary user interface for plugins -
# see https://vimhelp.org/usr_05.txt.html#plugin - and custom functionality.
# Basic command definition
command! Hello echo 'Hello Vim9'
execute ':Hello'
# Script-local function mapping via <Plug> - recommended plugin mapping pattern
def DoSomething()
echo 'Action triggered'
enddef
# Map function to unique <Plug> identifier, then to user keys
nnoremap <silent> <Plug>(MyPluginAction) <ScriptCmd>DoSomething()<CR>
nmap <silent> <Leader>a <Plug>(MyPluginAction)
# Command with arguments and completion
command! -nargs=1 -complete=file MyCmd edit <args>
# ----------------------------------------------------------------------------
vim9script
# EVENT HANDLING
# Autocommand groups provide systematic event management and prevent
# duplicates on re-sourcing
augroup AutoReload
autocmd!
autocmd BufWritePost $MYVIMRC source $MYVIMRC
autocmd BufReadPost *.txt echo 'Hello text file'
augroup END
# ----------------------------------------------------------------------------
vim9script
# EXECUTING NORMAL MODE COMMANDS
# Yank eight lines into register a
normal! "a8Y
# Echo register a
echo @a
# (Sourcing this script as a visual selection echos the script itself)
# ----------------------------------------------------------------------------
vim9script
# FEATURE DETECTION AND UTILITY FUNCTIONS
# Feature and existence checking
echo has('clipboard') # Check for a feature - 1 means it is present
echo exists('myVariable') # Check whether a script local variable exists
# Path and expression expansion
echo expand('%:p') # Full filepath of the current file
# Type checking with Vim `v:t_*` constants or more granularly with type names
echo type(123) == v:t_number # true
echo typename([1, 2, 3]) == 'list<number>' # true
# ----------------------------------------------------------------------------
vim9script
# DIRECT SHELL COMMAND EXECUTION
# This example should work in Linux, PowerShell, and Windows cmd.exe:
# output user info: (a) to non-interactive shell.
:!whoami
# Examples (b) and (c) would modify this buffer, so they're shown in
# a function, preventing execution.
def MoreExamples(): void
# (b) to current line in the current buffer, replacing its contents
:.!whoami
# (c) to the same buffer, appending at cursor position
:.read! whoami
enddef
# ----------------------------------------------------------------------------
system()vim9script
# CAPTURING SHELL COMMAND OUTPUT WITH `system()`
# This script creates a 4-second popup of the date, removing any line feed
# characters (U+000A), which would otherwise appear as "^@" in the popup.
silent var result = system('date')
popup_notification(result->substitute('[\xA]', '', 'g'), {time: 4000})
# ----------------------------------------------------------------------------
systemlist()vim9script
# PROCESSING COMMAND OUTPUT AS LINES WITH `systemlist()`
silent var lines = systemlist('ls *.vim')
for line in lines
echo line
endfor
# ----------------------------------------------------------------------------
vim9script
# ASYNCHRONOUS COMMAND EXECUTION WITH JOBS
# Jobs - see https://vimhelp.org/channel.txt.html#job%5Fstart%28%29 - execute
# commands asynchronously (unlike `system()` or `systemlist()`)
var mydate: list<string>
def Date(channel: channel, msg: string): void
mydate->add(msg)
enddef
var myjob = job_start([&shell, &shellcmdflag, 'date'], {out_cb: Date})
echo [myjob, myjob->job_status()]
sleep 1
echo $"The date and time is {mydate->join('')}"
echo [myjob, myjob->job_status()]
# Check exit status of the most recent shell command
echo v:shell_error
# Clear the variable; release the job's resources
myjob = null_job
# ----------------------------------------------------------------------------
vim9script
# A MORE ADVANCED EXAMPLE: GIT INTEGRATION
# The following script populates Vim’s quickfix list with files modified in
# the current Git repository when `:GitDiffQF` is executed after sourcing
# the script
def GitDiffQuickfix()
var diff_lines: list<string> = systemlist('git diff --name-only')
if v:shell_error != 0
echo 'Git not available or not a repo'
return
endif
var qf_list: list<any>
for file in diff_lines
add(qf_list, {'filename': file, 'lnum': 1, 'text': 'Modified file'})
endfor
setqflist(qf_list, 'r')
copen
enddef
command! GitDiffQF GitDiffQuickfix()
# ----------------------------------------------------------------------------
vim9script
# ASSERTIONS
# The predefined Vim variable, `v:errors`, is populated when errors occur
# in builtin `assert_*` functions, like the three in this example
v:errors = []
assert_equal(4, 2 + 1) # Expected 4 but got 3
assert_false(1 < 2) # Expected False but got true
assert_notmatch('\d\+', '123') # Pattern '\\d\\+' does match '123'
if !empty(v:errors)
echo "Test failures:\n" .. $'{v:errors->join("\n")}'
endif
# ----------------------------------------------------------------------------
vim9script
# BREAKPOINTS
# Sourcing the following script runs the function in debug mode:
# - https://vimhelp.org/repeat.txt.html#%3Adebug
# While debugging, use commands like these (at the `>` command prompt):
# - "step" to execute the command and come back to debug mode for the
# next command
# - "echo" to inspect variables
# - "cont" to continue execution until the next breakpoint is hit
# - "finish" to finish the current user function (or script)
breakdel * # clears all breakpoints
def MyFunc()
var x = 10
var y = x * 2
echo y
enddef
breakadd func 3 MyFunc # sets a breakpoint at line 3 of MyFunc
debug MyFunc() # call MyFunc() with debugging on
# ----------------------------------------------------------------------------
vim9script
# LISTING INSTRUCTIONS / BYTECODE OF A FUNCTION
# This can help understand how Vim optimizes a Vim9 script function,
# showing the instructions / bytecode generated by the function
def MyMultiply(a: number, b: number): number
return a * b
enddef
# To compile and check for syntax/type errors:
# - Using `func MyMultiply` reveals the function definition
func MyMultiply
# - Using `disassemble` shows the instructions generated for MyMultiply
disassemble MyMultiply
# Example output:
# <SNR>757_MyMultiply
# return a * b
# 0 LOAD arg[-2]
# 1 LOAD arg[-1]
# 2 OPNR *
# 3 RETURN
# ----------------------------------------------------------------------------
:h vim9 within Vim itself), kept
up-to-date automatically from the Vim source repositoryGot a suggestion? A correction, perhaps? Open an Issue on the GitHub Repo, or make a pull request yourself!
Originally contributed by Alejandro Sanchez, and updated by 1 contributor.