learnxinyminutes-docs/vim9script.md

17 KiB
Raw Blame History

name filename contributors
Vim9Script learnvim9script.vim
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. It is designed to replace legacy Vimscript (also called VimL), which is a sequence of ex-commands enhanced with scripting constructs like variables, functions, and control flow.

Unlike legacy Vimscript, Vim9Script enforces stricter syntax, improves performance, and supports modern programming features such as strong typing, classes, lambdas, and modules.

Try vim9-conversion-aid as a starting point to convert legacy Vimscript to Vim9Script.

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.

##################################################################
## 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<number>
# echo typename((1, 2))  # tuple (Yet commented as it's a recent addition)
echo typename({1: 'one', 2: 'two'})  # dict<string>

# 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 <number>3  # 3
# echo <number>'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 "<any>".
var Alist: list<list<number>> = [[1, 2], [3, 4]]
var Adict: dict<any> = {a: 1, b: 'two'}

# 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

# There are many builtin functions for variable manipulation.  Some have been
# illustrated, above.  A selection of some more related to lists.
var MyList: list<number> = [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 bufferlocal 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>): 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<any>
    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 <args>
# 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 <Plug>
def DoSomething()
  echo 'Action triggered'
enddef

nnoremap <silent> <Plug>(MyPluginAction) <ScriptCmd>DoSomething()<CR>
nmap <silent> <Leader>a <Plug>(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, `<cword>`, 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. Debugging, Compiling and Inspecting Bytecode
####################################################

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:
# <SNR>757_MyMultiply
#   return a * b
#    0 LOAD arg[-2]
#    1 LOAD arg[-1]
#    2 OPNR *
#    3 RETURN

Additional Resources