mirror of
https://github.com/adambard/learnxinyminutes-docs.git
synced 2025-04-27 07:33:57 +00:00
616 lines
19 KiB
Markdown
616 lines
19 KiB
Markdown
---
|
|
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<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 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>): 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. 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:
|
|
# <SNR>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)
|