Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion README.mkd
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This parser provide same feature for following languages.

* Vim script
* Python
* JavaScript
* JavaScript

## Example

Expand All @@ -37,6 +37,52 @@ This above code output following.
(let = s:message (printf "hello %d" (+ 1 (* 2 3))))
```

## Parsing Vim9 Script

VimL parser can detect `vim9script`, `vim9cmd`, and `def` commands via callbacks. This allows delegating Vim9 code parsing to a dedicated Vim9 parser.

### Using with vim-vim9parser

```javascript
const { VimLParser, StringReader } = require('vimlparser.js');
const { Vim9Parser } = require('vim9parser.js');

const code = [
'vim9script',
'var x: number = 10',
'def Add(a: number, b: number): number',
' return a + b',
'enddef'
].join('\n');

const reader = new StringReader(code);
const vim9parser = new Vim9Parser();

const callbacks = {
vim9script_callback: (node, content) => {
// vim9script found - remaining code is Vim9
console.log('Vim9 script detected at line', node.pos.lnum);
// Can pass to vim9parser for detailed analysis
},

def_callback: (node, content) => {
// def found - parse the function definition
console.log('Function definition at line', node.pos.lnum);
const vim9ast = vim9parser.parse(content);
},

vim9cmd_callback: (node, content) => {
// vim9cmd found - parse the command
console.log('Vim9 command at line', node.pos.lnum);
}
};

const parser = new VimLParser(false, callbacks);
const ast = parser.parse(reader);
```

This approach enables language servers and tools to support Vim9 syntax while maintaining full VimL compatibility.

## About project name

We know a name "VimL" is not the common short form of "Vim scripting language".
Expand Down
47 changes: 40 additions & 7 deletions autoload/vimlparser.vim
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,12 @@ function! s:VimLParser.__init__(...) abort
let self.neovim = 0
endif

if len(a:000) > 1 && type(a:000[1]) ==# type({})
let self.callbacks = a:000[1]
else
let self.callbacks = {}
endif

let self.find_command_cache = {}
endfunction

Expand All @@ -472,6 +478,12 @@ function! s:VimLParser.add_node(node) abort
call add(self.context[0].body, a:node)
endfunction

function! s:VimLParser.invoke_callback(name, ...) abort
if has_key(self.callbacks, a:name) && type(self.callbacks[a:name]) ==# type(function('tr'))
call call(self.callbacks[a:name], a:000)
endif
endfunction

function! s:VimLParser.check_missing_endfunction(ends, pos) abort
if self.context[0].type ==# s:NODE_FUNCTION
throw s:Err(printf('E126: Missing :endfunction: %s', a:ends), a:pos)
Expand Down Expand Up @@ -940,6 +952,8 @@ function! s:VimLParser.find_command() abort
let name = c
elseif self.reader.peekn(2) ==# 'py'
let name = self.reader.read_alnum()
elseif self.reader.peekn(4) ==# 'vim9'
let name = self.reader.read_alnum()
else
let pos = self.reader.tell()
let name = self.reader.read_alpha()
Expand All @@ -959,13 +973,22 @@ function! s:VimLParser.find_command() abort

let cmd = s:NIL

for x in self.builtin_commands
if stridx(x.name, name) ==# 0 && len(name) >= x.minlen
unlet cmd
let cmd = x
break
endif
endfor
" Special case for vim9script and vim9cmd to avoid matching vimgrep
if name !=# 'vim9script' && name !=# 'vim9cmd'
for x in self.builtin_commands
if stridx(x.name, name) ==# 0 && len(name) >= x.minlen
unlet cmd
let cmd = x
break
endif
endfor
elseif name ==# 'vim9script'
unlet cmd
let cmd = {'name': 'vim9script', 'minlen': 5, 'flags': 'WORD1|CMDWIN|LOCK_OK', 'parser': 'parse_cmd_common'}
elseif name ==# 'vim9cmd'
unlet cmd
let cmd = {'name': 'vim9cmd', 'minlen': 4, 'flags': 'NEEDARG|EXTRA|NOTRLCOM|CMDWIN|LOCK_OK', 'parser': 'parse_cmd_common'}
endif

if self.neovim
for x in self.neovim_additional_commands
Expand Down Expand Up @@ -1137,6 +1160,16 @@ function! s:VimLParser.parse_cmd_common() abort
let node.pos = self.ea.cmdpos
let node.ea = self.ea
let node.str = self.reader.getstr(self.ea.linepos, end)

" Invoke callback for vim9 script commands
if self.ea.cmd.name ==# 'vim9script'
call self.invoke_callback('vim9script_callback', node, node.str)
elseif self.ea.cmd.name ==# 'vim9cmd'
call self.invoke_callback('vim9cmd_callback', node, node.str)
elseif self.ea.cmd.name ==# 'def'
call self.invoke_callback('def_callback', node, node.str)
endif

call self.add_node(node)
endfunction

Expand Down
15 changes: 15 additions & 0 deletions js/vimlfunc.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,18 @@ function viml_stridx(a, b) {
return a.indexOf(b);
}

function viml_type(obj) {
if (obj === null || obj === undefined) return 0;
if (typeof obj === 'number') return 0;
if (typeof obj === 'string') return 1;
if (Array.isArray(obj)) return 3;
if (typeof obj === 'object') return 4;
if (typeof obj === 'function') return 2;
return 0;
}

function viml_function(name) {
// Return a dummy function for type comparison
return function() {};
}

66 changes: 59 additions & 7 deletions js/vimlparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,21 @@ function viml_stridx(a, b) {
return a.indexOf(b);
}

function viml_type(obj) {
if (obj === null || obj === undefined) return 0;
if (typeof obj === 'number') return 0;
if (typeof obj === 'string') return 1;
if (Array.isArray(obj)) return 3;
if (typeof obj === 'object') return 4;
if (typeof obj === 'function') return 2;
return 0;
}

function viml_function(name) {
// Return a dummy function for type comparison
return function() {};
}

var NIL = [];
var TRUE = 1;
var FALSE = 0;
Expand Down Expand Up @@ -618,6 +633,12 @@ VimLParser.prototype.__init__ = function() {
else {
this.neovim = 0;
}
if (viml_len(a000) > 1 && viml_type(a000[1]) == viml_type({})) {
this.callbacks = a000[1];
}
else {
this.callbacks = {};
}
this.find_command_cache = {};
}

Expand Down Expand Up @@ -646,6 +667,13 @@ VimLParser.prototype.add_node = function(node) {
viml_add(this.context[0].body, node);
}

VimLParser.prototype.invoke_callback = function(name) {
var a000 = Array.prototype.slice.call(arguments, 1);
if (viml_has_key(this.callbacks, name) && viml_type(this.callbacks[name]) == viml_type(viml_function("tr"))) {
viml_call(this.callbacks[name], a000);
}
}

VimLParser.prototype.check_missing_endfunction = function(ends, pos) {
if (this.context[0].type == NODE_FUNCTION) {
throw Err(viml_printf("E126: Missing :endfunction: %s", ends), pos);
Expand Down Expand Up @@ -1207,6 +1235,9 @@ VimLParser.prototype.find_command = function() {
else if (this.reader.peekn(2) == "py") {
var name = this.reader.read_alnum();
}
else if (this.reader.peekn(4) == "vim9") {
var name = this.reader.read_alnum();
}
else {
var pos = this.reader.tell();
var name = this.reader.read_alpha();
Expand All @@ -1222,15 +1253,26 @@ VimLParser.prototype.find_command = function() {
return this.find_command_cache[name];
}
var cmd = NIL;
var __c4 = this.builtin_commands;
for (var __i4 = 0; __i4 < __c4.length; ++__i4) {
var x = __c4[__i4];
if (viml_stridx(x.name, name) == 0 && viml_len(name) >= x.minlen) {
delete cmd;
var cmd = x;
break;
// Special case for vim9script and vim9cmd to avoid matching vimgrep
if (name != "vim9script" && name != "vim9cmd") {
var __c4 = this.builtin_commands;
for (var __i4 = 0; __i4 < __c4.length; ++__i4) {
var x = __c4[__i4];
if (viml_stridx(x.name, name) == 0 && viml_len(name) >= x.minlen) {
delete cmd;
var cmd = x;
break;
}
}
}
else if (name == "vim9script") {
delete cmd;
var cmd = {"name":"vim9script", "minlen":5, "flags":"WORD1|CMDWIN|LOCK_OK", "parser":"parse_cmd_common"};
}
else if (name == "vim9cmd") {
delete cmd;
var cmd = {"name":"vim9cmd", "minlen":4, "flags":"NEEDARG|EXTRA|NOTRLCOM|CMDWIN|LOCK_OK", "parser":"parse_cmd_common"};
}
if (this.neovim) {
var __c5 = this.neovim_additional_commands;
for (var __i5 = 0; __i5 < __c5.length; ++__i5) {
Expand Down Expand Up @@ -1419,6 +1461,16 @@ VimLParser.prototype.parse_cmd_common = function() {
node.pos = this.ea.cmdpos;
node.ea = this.ea;
node.str = this.reader.getstr(this.ea.linepos, end);
// Invoke callback for vim9 script commands
if (this.ea.cmd.name == "vim9script") {
this.invoke_callback("vim9script_callback", node, node.str);
}
else if (this.ea.cmd.name == "vim9cmd") {
this.invoke_callback("vim9cmd_callback", node, node.str);
}
else if (this.ea.cmd.name == "def") {
this.invoke_callback("def_callback", node, node.str);
}
this.add_node(node);
}

Expand Down
13 changes: 13 additions & 0 deletions py/vimlfunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,16 @@ def viml_has_key(obj, key):

def viml_stridx(a, b):
return a.find(b)

def viml_type(obj):
if obj is None: return 0
if isinstance(obj, (int, float)): return 0
if isinstance(obj, str): return 1
if isinstance(obj, list): return 3
if isinstance(obj, dict): return 4
if callable(obj): return 2
return 0

def viml_function(name):
# Return a dummy function for type comparison
return lambda: None
48 changes: 43 additions & 5 deletions py/vimlparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,19 @@ def viml_has_key(obj, key):
def viml_stridx(a, b):
return a.find(b)

def viml_type(obj):
if obj is None: return 0
if isinstance(obj, (int, float)): return 0
if isinstance(obj, str): return 1
if isinstance(obj, list): return 3
if isinstance(obj, dict): return 4
if callable(obj): return 2
return 0

def viml_function(name):
# Return a dummy function for type comparison
return lambda: None


NIL = []
TRUE = 1
Expand Down Expand Up @@ -605,6 +618,10 @@ def __init__(self, *a000):
self.neovim = a000[0]
else:
self.neovim = 0
if viml_len(a000) > 1 and viml_type(a000[1]) == viml_type(AttributeDict({})):
self.callbacks = a000[1]
else:
self.callbacks = AttributeDict({})
self.find_command_cache = AttributeDict({})

def push_context(self, node):
Expand All @@ -624,6 +641,10 @@ def find_context(self, type):
def add_node(self, node):
viml_add(self.context[0].body, node)

def invoke_callback(self, name, *a000):
if viml_has_key(self.callbacks, name) and viml_type(self.callbacks[name]) == viml_type(viml_function("tr")):
viml_call(self.callbacks[name], a000)

def check_missing_endfunction(self, ends, pos):
if self.context[0].type == NODE_FUNCTION:
raise VimLParserException(Err(viml_printf("E126: Missing :endfunction: %s", ends), pos))
Expand Down Expand Up @@ -1028,6 +1049,8 @@ def find_command(self):
name = c
elif self.reader.peekn(2) == "py":
name = self.reader.read_alnum()
elif self.reader.peekn(4) == "vim9":
name = self.reader.read_alnum()
else:
pos = self.reader.tell()
name = self.reader.read_alpha()
Expand All @@ -1039,11 +1062,19 @@ def find_command(self):
if viml_has_key(self.find_command_cache, name):
return self.find_command_cache[name]
cmd = NIL
for x in self.builtin_commands:
if viml_stridx(x.name, name) == 0 and viml_len(name) >= x.minlen:
del cmd
cmd = x
break
# Special case for vim9script and vim9cmd to avoid matching vimgrep
if name != "vim9script" and name != "vim9cmd":
for x in self.builtin_commands:
if viml_stridx(x.name, name) == 0 and viml_len(name) >= x.minlen:
del cmd
cmd = x
break
elif name == "vim9script":
del cmd
cmd = AttributeDict({"name": "vim9script", "minlen": 5, "flags": "WORD1|CMDWIN|LOCK_OK", "parser": "parse_cmd_common"})
elif name == "vim9cmd":
del cmd
cmd = AttributeDict({"name": "vim9cmd", "minlen": 4, "flags": "NEEDARG|EXTRA|NOTRLCOM|CMDWIN|LOCK_OK", "parser": "parse_cmd_common"})
if self.neovim:
for x in self.neovim_additional_commands:
if viml_stridx(x.name, name) == 0 and viml_len(name) >= x.minlen:
Expand Down Expand Up @@ -1182,6 +1213,13 @@ def parse_cmd_common(self):
node.pos = self.ea.cmdpos
node.ea = self.ea
node.str = self.reader.getstr(self.ea.linepos, end)
# Invoke callback for vim9 script commands
if self.ea.cmd.name == "vim9script":
self.invoke_callback("vim9script_callback", node, node.str)
elif self.ea.cmd.name == "vim9cmd":
self.invoke_callback("vim9cmd_callback", node, node.str)
elif self.ea.cmd.name == "def":
self.invoke_callback("def_callback", node, node.str)
self.add_node(node)

def separate_nextcmd(self):
Expand Down
Loading