From 7fbd8a08374289a7e6865dd3b84635091aea5af3 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 19 Jan 2026 16:44:09 +0900 Subject: [PATCH] invoke callback for vim9script, vim9cmd, def --- README.mkd | 48 +++++++++++++++++++++++++++++- autoload/vimlparser.vim | 47 ++++++++++++++++++++++++----- js/vimlfunc.js | 15 ++++++++++ js/vimlparser.js | 66 ++++++++++++++++++++++++++++++++++++----- py/vimlfunc.py | 13 ++++++++ py/vimlparser.py | 48 ++++++++++++++++++++++++++---- 6 files changed, 217 insertions(+), 20 deletions(-) diff --git a/README.mkd b/README.mkd index f516e0c0..e6ad6793 100644 --- a/README.mkd +++ b/README.mkd @@ -14,7 +14,7 @@ This parser provide same feature for following languages. * Vim script * Python -* JavaScript +* JavaScript ## Example @@ -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". diff --git a/autoload/vimlparser.vim b/autoload/vimlparser.vim index b6916595..dca26989 100644 --- a/autoload/vimlparser.vim +++ b/autoload/vimlparser.vim @@ -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 @@ -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) @@ -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() @@ -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 @@ -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 diff --git a/js/vimlfunc.js b/js/vimlfunc.js index 06a749f4..95b07db4 100644 --- a/js/vimlfunc.js +++ b/js/vimlfunc.js @@ -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() {}; +} + diff --git a/js/vimlparser.js b/js/vimlparser.js index 47e4c87f..010e6788 100644 --- a/js/vimlparser.js +++ b/js/vimlparser.js @@ -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; @@ -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 = {}; } @@ -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); @@ -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(); @@ -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) { @@ -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); } diff --git a/py/vimlfunc.py b/py/vimlfunc.py index 12cf5ce7..5764d70c 100644 --- a/py/vimlfunc.py +++ b/py/vimlfunc.py @@ -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 diff --git a/py/vimlparser.py b/py/vimlparser.py index 3a1c1ade..2108d0bd 100644 --- a/py/vimlparser.py +++ b/py/vimlparser.py @@ -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 @@ -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): @@ -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)) @@ -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() @@ -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: @@ -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):