├── dump └── .gitkeep ├── .gitignore ├── libs-testing.hxml ├── .release-please-manifest.json ├── src ├── import.hx ├── Test.hx ├── lua │ ├── Pair.hx │ └── StringMap.hx ├── vim │ ├── plugin │ │ ├── Manager.hx │ │ ├── Plugin.hx │ │ ├── types │ │ │ └── VimPlugin.hx │ │ └── PluginMacro.hx │ ├── Ui.hx │ ├── Lsp.hx │ ├── TableTools.hx │ ├── Vim.hx │ ├── Api.hx │ ├── types │ │ └── ArgComplete.hx │ ├── Vimx.hx │ └── VimTypes.hx ├── plenary │ └── Job.hx ├── Macro.hx ├── packer │ ├── Macro.hx │ └── Packer.hx ├── TableBuilder.hx ├── TableWrapper.hx ├── ApiGen.hx └── Main.hx ├── test-fail-wrong-type.hxml ├── test-fail-extra-fields.hxml ├── tools ├── Result.hx ├── MainTests.hx ├── luaParser │ ├── fixtures │ │ ├── basic_fn.lua │ │ ├── vim_iconv.lua │ │ └── filetype_getLines.lua │ ├── ParseErrorDetail.hx │ ├── ParseDump.hx │ ├── GenerateDocLexerTest.hx │ ├── Lexer.hx │ ├── ParserTest.hx │ ├── LuaDoc.hx │ └── LuaParser.hx ├── Log.hx ├── GitRepo.hx ├── FileTools.hx ├── Cmd.hx └── ReadNvimApi.hx ├── generate-test-files.hxml ├── test ├── should_fail │ ├── ExtraFields.hx │ └── WrongType.hx ├── RawTable.hx ├── Optionals.hx └── TestMacros.hx ├── tests.hxml ├── libs.hxml ├── test-base.hxml ├── .luarc.json ├── test-fail-wrong-type.hxml.stderr ├── run-fail-test.sh ├── test-fail-extra-fields.hxml.stderr ├── release-please-config.json ├── run-test-clean.sh ├── haxelib.json ├── test-macro.hxml ├── CHANGELOG.md ├── .github └── workflows │ └── publish.yml ├── hxformat.json ├── Readme.md ├── history.md └── res ├── fs.json └── lsp_buf.json /dump/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dump/* 2 | !.gitkeep 3 | -------------------------------------------------------------------------------- /libs-testing.hxml: -------------------------------------------------------------------------------- 1 | --library buddy:2.13.0 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.1.1" 3 | } 4 | -------------------------------------------------------------------------------- /src/import.hx: -------------------------------------------------------------------------------- 1 | using Lambda; 2 | using StringTools; 3 | 4 | import haxe.ds.Option; 5 | -------------------------------------------------------------------------------- /test-fail-wrong-type.hxml: -------------------------------------------------------------------------------- 1 | test-base.hxml 2 | --lua 3 | -main test.should_fail.WrongType 4 | 5 | -------------------------------------------------------------------------------- /test-fail-extra-fields.hxml: -------------------------------------------------------------------------------- 1 | test-base.hxml 2 | --lua 3 | -main test.should_fail.ExtraFields 4 | 5 | -------------------------------------------------------------------------------- /tools/Result.hx: -------------------------------------------------------------------------------- 1 | package tools; 2 | 3 | enum Result< T > { 4 | Ok(result:T); 5 | Error(message:String); 6 | } 7 | -------------------------------------------------------------------------------- /generate-test-files.hxml: -------------------------------------------------------------------------------- 1 | libs.hxml 2 | 3 | -cp tools 4 | -main tools.luaParser.GenerateDocLexerTest 5 | 6 | --interp 7 | -------------------------------------------------------------------------------- /test/should_fail/ExtraFields.hx: -------------------------------------------------------------------------------- 1 | package test.should_fail; 2 | 3 | @:keep 4 | final e = test.Optionals.test({test: true, weirdField: "null"}); 5 | -------------------------------------------------------------------------------- /test/should_fail/WrongType.hx: -------------------------------------------------------------------------------- 1 | package test.should_fail; 2 | 3 | @:keep 4 | final d = test.Optionals.test({test: true, optionalField: "null"}); 5 | -------------------------------------------------------------------------------- /tests.hxml: -------------------------------------------------------------------------------- 1 | test-base.hxml 2 | # --each 3 | # -main tools.luaParser.ParserTest 4 | # --interp 5 | 6 | # --next 7 | -main tools.MainTests 8 | 9 | --interp 10 | -------------------------------------------------------------------------------- /libs.hxml: -------------------------------------------------------------------------------- 1 | --library msgpack-haxe:1.15.1 2 | --library tink_lang:0.7.0 3 | --library safety:1.1.2 4 | --library hxparse:4.0.1 5 | # Testing libs 6 | --library buddy:2.13.0 7 | -------------------------------------------------------------------------------- /test-base.hxml: -------------------------------------------------------------------------------- 1 | libs.hxml 2 | libs-testing.hxml 3 | 4 | -D lua-vanilla 5 | -D luajit 6 | -D dump=pretty 7 | 8 | -dce std 9 | -cp src 10 | -cp tools 11 | -cp test 12 | -------------------------------------------------------------------------------- /tools/MainTests.hx: -------------------------------------------------------------------------------- 1 | package tools; 2 | 3 | import buddy.*; 4 | 5 | using buddy.Should; 6 | 7 | class MainTests implements Buddy< [ 8 | tools.luaParser.LuaDocParserTest, 9 | tools.luaParser.ParserTest 10 | ] > {} 11 | -------------------------------------------------------------------------------- /test/RawTable.hx: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import packer.Packer; 4 | import packer.Macro; 5 | 6 | function main() { 7 | final x:PluginSpec = packer.Macro.plugin({name: "rabo", cmd: "Rabo", culo: true}); 8 | trace(x); 9 | } 10 | -------------------------------------------------------------------------------- /src/Test.hx: -------------------------------------------------------------------------------- 1 | class Test { 2 | /** 3 | * Returns `value` if it is not `null`. Otherwise returns `defaultValue`. 4 | */ 5 | static public function or< T >(v:Null< T >, fallback:T):T { 6 | return v != null ? v : fallback; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lua/Pair.hx: -------------------------------------------------------------------------------- 1 | package lua; 2 | 3 | @:expose 4 | class Pair< A > { 5 | @:pure public static inline function make< A >(a:A, b:A):Pair< A > { 6 | return untyped { 7 | __lua__("{ {0}, {1} }", a, b); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.luarc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", 3 | "Lua.diagnostics.disable": [ 4 | "lowercase-global" 5 | ], 6 | "Lua.workspace.checkThirdParty": false 7 | } -------------------------------------------------------------------------------- /test-fail-wrong-type.hxml.stderr: -------------------------------------------------------------------------------- 1 | test/should_fail/WrongType.hx:4: characters 31-66 : TableWrapper<{ test : Bool, optionalField : String }> should be test.X 2 | test/should_fail/WrongType.hx:4: characters 31-66 : ... For function argument 'x' 3 | -------------------------------------------------------------------------------- /run-fail-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | # Iterate all the test-fail-*.hxml files and save its stderr output with a file of the same name but with stderr extension 4 | for f in test-fail-*.hxml; do 5 | haxe $f 2> $f.stderr 6 | done 7 | exit 0 8 | -------------------------------------------------------------------------------- /test-fail-extra-fields.hxml.stderr: -------------------------------------------------------------------------------- 1 | test/should_fail/ExtraFields.hx:4: characters 31-63 : TableWrapper<{ weirdField : String, test : Bool, optionalField : Null }> should be test.X 2 | test/should_fail/ExtraFields.hx:4: characters 31-63 : ... For function argument 'x' 3 | -------------------------------------------------------------------------------- /tools/luaParser/fixtures/basic_fn.lua: -------------------------------------------------------------------------------- 1 | -- Invokes |vim-function| or |user-function| {func} with arguments {...}. 2 | -- See also |vim.fn|. 3 | -- Equivalent to: 4 | -- ```lua 5 | -- vim.fn[func]({...}) 6 | -- ``` 7 | --- @param func fun() 8 | function vim.call(func, ...) end 9 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bootstrap-sha": "72e30c275d61e6c89091c4c1228c9956acca8804", 3 | "release-type": "simple", 4 | "packages": { 5 | ".": {} 6 | }, 7 | "extra-files": [ 8 | { 9 | "type": "json", 10 | "path": "haxelib.json", 11 | "jsonpath": "$.version" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/vim/plugin/Manager.hx: -------------------------------------------------------------------------------- 1 | package vim.plugin; 2 | 3 | enum LibraryState { 4 | Required; 5 | Installed; 6 | } 7 | 8 | private final libraries:Map< String, LibraryState > = new Map(); 9 | 10 | function registerLibrary(libraryName:String, ?commit:Null< String >) { 11 | if (!libraries.exists(libraryName)) 12 | libraries.set(libraryName, Required); 13 | } 14 | -------------------------------------------------------------------------------- /tools/Log.hx: -------------------------------------------------------------------------------- 1 | package tools; 2 | 3 | import haxe.Json; 4 | 5 | function prettyPrint(?msg, data:Dynamic) { 6 | Sys.println(msg); 7 | Sys.println(Json.stringify(data, null, " ")); 8 | } 9 | 10 | function print(data:Dynamic) { 11 | Sys.println(data); 12 | } 13 | 14 | function print2(msg, data:Dynamic) { 15 | Sys.print(msg); 16 | Sys.println(data); 17 | } 18 | -------------------------------------------------------------------------------- /tools/luaParser/ParseErrorDetail.hx: -------------------------------------------------------------------------------- 1 | package tools.luaParser; 2 | 3 | class ParseErrorDetail extends hxparse.ParserError { 4 | public final message:String; 5 | 6 | public function new(pos:hxparse.Position, msg:String) { 7 | super(pos); 8 | this.message = msg; 9 | } 10 | 11 | override function toString() { 12 | return "Parse error: " + message; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /run-test-clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Ignores successful tests and only shows failed ones. 3 | # This is useful when fixing test that fail on local, 4 | # but in CI I want the full log, so don't uese this script. 5 | set -x 6 | # Iterate all the test-fail-*.hxml files and save its stderr output with a file of the same name but with stderr extension 7 | haxe -D buddy-ignore-passing-specs tests.hxml 8 | exit 0 9 | -------------------------------------------------------------------------------- /test/Optionals.hx: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import TableWrapper; 4 | 5 | typedef X = TableWrapper< { 6 | test:Bool, 7 | ?optionalField:Bool, 8 | } >; 9 | 10 | function test(x:X) { 11 | return x; 12 | } 13 | 14 | // All these should compile and lead to the same compiled output 15 | 16 | @:keep 17 | final b = test({test: true, optionalField: true}); 18 | 19 | @:keep 20 | final c = test({test: true, optionalField: null}); 21 | 22 | @:keep 23 | final a = test({test: true,}); 24 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haxe-nvim", 3 | "url": "https://github.com/danielo515/haxe-nvim", 4 | "license": "MIT", 5 | "tags": [ 6 | "nvim", 7 | "externs" 8 | ], 9 | "description": "Create neovim plugins using Haxe", 10 | "version": "1.1.1", 11 | "classPath": "src/", 12 | "releasenote": "macro for easier plugin integration", 13 | "contributors": [ 14 | "danielo515" 15 | ], 16 | "dependencies": { 17 | "tink_macro": "", 18 | "hxparse": "4.0.1", 19 | "safety": "1.1.2" 20 | }, 21 | "main": "vim.Vim" 22 | } 23 | -------------------------------------------------------------------------------- /test-macro.hxml: -------------------------------------------------------------------------------- 1 | test-base.hxml 2 | # Generates the lua files 3 | # required to test the output of the macros 4 | # is still correct. 5 | # This is basically snapsot generation, so don't run to test things works, because that 6 | # is not the purpose 7 | --each 8 | 9 | test.TestMacros 10 | test.RawTable 11 | --lua lua/test/TestMacros.lua 12 | # Ensure it's formatted for better diffs 13 | --cmd stylua lua/test/TestMacros.lua 14 | 15 | --next 16 | 17 | test.Optionals 18 | 19 | --lua lua/test/Optionals.lua 20 | 21 | # Ensure it's formatted for better diffs 22 | --cmd stylua lua/test/ 23 | -------------------------------------------------------------------------------- /src/vim/Ui.hx: -------------------------------------------------------------------------------- 1 | package vim; 2 | 3 | import vim.VimTypes; 4 | import haxe.Constraints.Function; 5 | 6 | typedef SelectConfig = TableWrapper< { 7 | prompt:String 8 | } > 9 | 10 | typedef InputOpts = TableWrapper< { 11 | prompt:String, 12 | completion:() -> LuaArray< String > 13 | } > 14 | 15 | @:native('vim.ui') 16 | extern class Ui { 17 | static function select( 18 | options:LuaArray< String >, 19 | config:SelectConfig, 20 | onSelect:(Null< String >, Null< Int >) -> Void 21 | ):Void; 22 | static function input(options:InputOpts, on_confirm:(Null< String >) -> Void):Void; 23 | } 24 | -------------------------------------------------------------------------------- /src/vim/Lsp.hx: -------------------------------------------------------------------------------- 1 | package vim; 2 | 3 | import haxe.Constraints.Function; 4 | import vim.VimTypes; 5 | 6 | @:native("vim.lsp") 7 | @:build(ApiGen.attachApi("lsp")) 8 | extern class Lsp {} 9 | 10 | @:native("vim.lsp.buf") 11 | @:build(ApiGen.attachApi("lsp_buf")) 12 | extern class LspBuf { 13 | public static function list_workspace_folders():LuaArray< String >; 14 | public static function format(?opts:TableWrapper< {timeout_ms:Int, bufnr:Buffer, filter:Dynamic -> Bool} >):LuaArray< String >; 15 | } 16 | 17 | @:native("vim.lsp.protocol") 18 | extern class Protocol { 19 | static function make_client_capabilities():Dynamic; 20 | } 21 | -------------------------------------------------------------------------------- /src/lua/StringMap.hx: -------------------------------------------------------------------------------- 1 | package lua; 2 | 3 | class StringMap< T > { 4 | private var h:lua.Table< String, T >; 5 | 6 | static var tnull:Dynamic = lua.Table.create(); 7 | 8 | public function new():Void { 9 | h = lua.Table.create(); 10 | } 11 | 12 | public function set(key:String, value:T):Void { 13 | if (value == null) { 14 | h[untyped key] = tnull; 15 | } else { 16 | h[untyped key] = value; 17 | } 18 | } 19 | 20 | public function get(key:String):Null< T > { 21 | var ret = h[untyped key]; 22 | if (ret == tnull) { 23 | return null; 24 | } 25 | return ret; 26 | } 27 | 28 | public inline function exists(key:String):Bool { 29 | return h[untyped key] != null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/vim/plugin/Plugin.hx: -------------------------------------------------------------------------------- 1 | package vim.plugin; 2 | 3 | @:autoBuild(vim.plugin.PluginMacro.pluginInterface()) 4 | interface VimPlugin { 5 | /* 6 | This is an empty interface that is used to attach @:autoBuild 7 | to classes that implement it. 8 | The @:autoBuild macro will generate the required require function to load the plugin safely. 9 | The implementing class must have a field named `libName`, which will be used 10 | in the generated require function. 11 | 12 | Any public variable annotated with `@module` will generate a getter function 13 | to require that as a submodule of the plugin, also using the `libName` field 14 | and a safe require function that is guaranteed to not throw. 15 | */ 16 | } 17 | -------------------------------------------------------------------------------- /src/vim/plugin/types/VimPlugin.hx: -------------------------------------------------------------------------------- 1 | package vim.plugin.types; 2 | 3 | import lua.Lua; 4 | 5 | abstract VimPlugin< T >(Null< T >) { 6 | inline function new(pluginName:String) { 7 | final requireResult = Lua.pcall(Lua.require, pluginName); 8 | if (requireResult.status) 9 | this = requireResult.value; 10 | else this = null; 11 | } 12 | 13 | @:from 14 | public static inline function from< T >(pluginName:String):VimPlugin< T > { 15 | return new VimPlugin(pluginName); 16 | } 17 | 18 | public inline function map< R >(f:T -> R):Null< R > { 19 | return if (this == null) null else f(this); 20 | } 21 | 22 | public inline function call(f:T -> Void):Void { 23 | if (this != null) 24 | f(this); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/plenary/Job.hx: -------------------------------------------------------------------------------- 1 | package plenary; 2 | 3 | import vim.VimTypes.LuaArray; 4 | import lua.Table; 5 | 6 | typedef Job_opts = { 7 | final command:String; 8 | final args:LuaArray< String >; 9 | final ?cwd:Null< String >; 10 | final ?on_stdout:(Null< String >, String) -> Void; 11 | final ?on_stderr:(String, Int) -> Void; 12 | } 13 | 14 | // @:build(TableBuilder.build()) 15 | 16 | @:luaRequire("plenary.job") 17 | extern class Job { 18 | private static inline function create(args:Table< String, Dynamic >):Job { 19 | return untyped __lua__("{0}:new({1})", Job, args); 20 | } 21 | static inline function make(jobargs:Job_opts):Job { 22 | return create(Table.create(jobargs)); 23 | } 24 | function start():Void; 25 | function sync():Table< Int, String >; 26 | } 27 | -------------------------------------------------------------------------------- /tools/luaParser/fixtures/vim_iconv.lua: -------------------------------------------------------------------------------- 1 | -- The result is a String, which is the text {str} converted from 2 | -- encoding {from} to encoding {to}. When the conversion fails `nil` is 3 | -- returned. When some characters could not be converted they 4 | -- are replaced with "?". 5 | -- The encoding names are whatever the iconv() library function 6 | -- can accept, see ":Man 3 iconv". 7 | -- 8 | -- Parameters: ~ 9 | -- • {str} (string) Text to convert 10 | -- • {from} (string) Encoding of {str} 11 | -- • {to} (string) Target encoding 12 | -- 13 | -- Returns: ~ 14 | -- Converted string if conversion succeeds, `nil` otherwise. 15 | --- @param str string 16 | --- @param from number 17 | --- @param to number 18 | --- @param opts? table 19 | function vim.iconv(str, from, to, opts) end 20 | -------------------------------------------------------------------------------- /tools/luaParser/ParseDump.hx: -------------------------------------------------------------------------------- 1 | package tools.luaParser; 2 | 3 | import hxparse.Position; 4 | import byte.ByteData; 5 | 6 | using StringTools; 7 | 8 | function dumpAtCurrent(pos:Position, input:ByteData, lastToken:String) { 9 | final max:Int = input.length - 1; 10 | final inputAsString = input.readString(0, max); 11 | final lines = inputAsString.split("\n"); 12 | final linePosition = pos.getLinePosition(input); 13 | final line = lines[linePosition.lineMin - 1]; 14 | Log.print2("> Last parsed token: ", lastToken); 15 | Log.print2("> ", line); 16 | // final cursorWidth = (pos.pmax - pos.pmin) - 1; 17 | final padding = 2; 18 | final cursorPadding = linePosition.posMax + padding; 19 | // Log.print("^".lpad(" ", pos.pmin + padding) + "^".lpad(" ", cursorWidth + padding)); 20 | Log.print("^".lpad(" ", cursorPadding)); 21 | Log.print(pos.format(input)); 22 | } 23 | -------------------------------------------------------------------------------- /tools/luaParser/fixtures/filetype_getLines.lua: -------------------------------------------------------------------------------- 1 | ---@private 2 | --- Get a single line or line range from the buffer. 3 | --- If only start_lnum is specified, return a single line as a string. 4 | --- If both start_lnum and end_lnum are omitted, return all lines from the buffer. 5 | --- 6 | ---@param bufnr number|nil The buffer to get the lines from 7 | ---@param start_lnum number|nil The line number of the first line (inclusive, 1-based) 8 | ---@param end_lnum number|nil The line number of the last line (inclusive, 1-based) 9 | ---@return table|string Array of lines, or string when end_lnum is omitted 10 | function M.getlines(bufnr, start_lnum, end_lnum) 11 | if end_lnum then 12 | -- Return a line range 13 | return api.nvim_buf_get_lines(bufnr, start_lnum - 1, end_lnum, false) 14 | end 15 | if start_lnum then 16 | -- Return a single line 17 | return api.nvim_buf_get_lines(bufnr, start_lnum - 1, start_lnum, false)[1] or "" 18 | else 19 | -- Return all lines 20 | return api.nvim_buf_get_lines(bufnr, 0, -1, false) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /tools/GitRepo.hx: -------------------------------------------------------------------------------- 1 | package tools; 2 | 3 | import tools.Cmd; 4 | import sys.FileSystem; 5 | import tools.Result; 6 | 7 | class GitRepo { 8 | final path:String; 9 | final repoUrl:String; 10 | 11 | public function new(repoUrl, destinationPath) { 12 | this.path = destinationPath; 13 | this.repoUrl = repoUrl; 14 | } 15 | 16 | static function destinationExists(path):Bool { 17 | return FileSystem.isDirectory(path) && FileSystem.isDirectory(path) && FileSystem.readDirectory(path).length > 0; 18 | 19 | // return path.exists() && path.isDirectory() && path.readDirectory().length > 0; 20 | } 21 | 22 | public function toString() { 23 | return '${this.repoUrl} => ${this.path}'; 24 | } 25 | 26 | public static function clone(repo, dest):Result< GitRepo > { 27 | if (destinationExists(dest)) { 28 | Sys.println("Destination path exist, skip repo clone"); 29 | return Ok(new GitRepo(repo, dest)); 30 | } 31 | final status = executeCommand("git", ["clone", repo, dest, "--single-branch"]); 32 | return switch (status) { 33 | case Ok(_): Ok(new GitRepo(repo, dest)); 34 | case Error(err): Error(err); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/TestMacros.hx: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import TableWrapper; 4 | 5 | typedef WithNesting = { 6 | doX:Int, 7 | test:Bool, 8 | nest:{a:{renest:Int, b:{c:{meganest:Int}}}}, 9 | objWithArr:{x:Array< {y:String} >}, 10 | arrWithObjs:Array< {x:String} >, 11 | }; 12 | 13 | typedef WithLambdas = TableWrapper< { 14 | lambda1:(name:String, age:Int) -> Void, 15 | nestedLambda:{lambda2:(Int, Int) -> Int} 16 | } > 17 | 18 | extern function log(arg:Dynamic):Void; 19 | typedef W = TableWrapper< WithNesting >; 20 | extern function testMethod(x:W):Void; 21 | extern function testlambdas(x:WithLambdas):Void; 22 | 23 | function lotOfNesting() { 24 | testMethod({ 25 | doX: 99, 26 | test: true, 27 | objWithArr: {x: [{y: "obj -> array -> obj "}, {y: "second obj -> array -> obj "}]}, 28 | nest: {a: {renest: 99, b: {c: {meganest: 88}}}}, 29 | arrWithObjs: [{x: "inside array -> obj "}, {x: "second array -> obj "}], 30 | }); 31 | } 32 | 33 | function objectWithLambdas() { 34 | testlambdas({ 35 | lambda1: (name:String, age:Int) -> { 36 | Sys.println('Hello $name, good age $age'); 37 | }, 38 | nestedLambda: {lambda2: (a, b) -> (a + b)} 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /tools/FileTools.hx: -------------------------------------------------------------------------------- 1 | package tools; 2 | 3 | import sys.io.File; 4 | import sys.io.Process; 5 | import tools.Result; 6 | import haxe.io.Path; 7 | 8 | /** 9 | Given a path that can be found by the Haxe compiler, return the directory 10 | that contains it. 11 | This is useful to get the correct paths within a library. 12 | */ 13 | function getDirFromPackagePath(path) { 14 | final base = Path.directory(haxe.macro.Context.resolvePath(path)); 15 | return base; 16 | } 17 | 18 | function getNvimRuntimePath() { 19 | return switch (Cmd.executeCommand("nvim", [ 20 | "--clean", 21 | "--headless", 22 | "--cmd", 23 | "echo $VIMRUNTIME | qa " 24 | ], true)) { 25 | case Error(error): 26 | Sys.println("Failed to get VIMRUNTIME"); 27 | Sys.println(error); 28 | Error(error); 29 | case ok: ok; 30 | } 31 | } 32 | 33 | function writeTextFile(outputPath:String, data:String) { 34 | final handle = File.write(outputPath, false); 35 | handle.writeString(data); 36 | handle.close(); 37 | } 38 | 39 | function getResPath(filename:String):String { 40 | return Path.join([ 41 | getDirFromPackagePath('tools/FileTools.hx'), 42 | '../res', 43 | filename 44 | ]); 45 | } 46 | -------------------------------------------------------------------------------- /src/Macro.hx: -------------------------------------------------------------------------------- 1 | #if macro 2 | import haxe.macro.Expr; 3 | import haxe.macro.Context; 4 | 5 | using haxe.macro.Tools; 6 | using Lambda; 7 | #end 8 | 9 | class StructureCombiner { 10 | // we use an Array, because we want the macro to work on variable amount of structures 11 | public static macro function combine(rest:Expr):Expr { 12 | var pos = Context.currentPos(); 13 | var block = []; 14 | var cnt = 1; 15 | // since we want to allow duplicate field names, we use a Map. The last occurrence wins. 16 | var all = new Map< String, ObjectField >(); 17 | var trest = Context.typeof(rest); 18 | switch (trest.follow()) { 19 | case TAnonymous(_.get() => tr): 20 | // for each parameter we create a tmp var with an unique name. 21 | // we need a tmp var in the case, the parameter is the result of a complex expression. 22 | var tmp = "tmp_" + cnt; 23 | cnt++; 24 | var extVar = macro $i{tmp}; 25 | block.push(macro var $tmp = $rest); 26 | for (field in tr.fields) { 27 | var fname = field.name; 28 | all.set(fname, {field: fname, expr: macro $extVar.$fname}); 29 | } 30 | default: 31 | return Context.error("Object type expected instead of " + trest.toString(), rest.pos); 32 | } 33 | var result = {expr: EObjectDecl(all.array()), pos: pos}; 34 | block.push(macro lua.Table.create($result)); 35 | return macro $b{block}; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/packer/Macro.hx: -------------------------------------------------------------------------------- 1 | package packer; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | 6 | using haxe.macro.TypeTools; 7 | using haxe.macro.ExprTools; 8 | using Safety; 9 | using haxe.macro.ComplexTypeTools; 10 | 11 | /** 12 | Gets the values of the given object expression and 13 | turns it into an untyped lua call with the values in the right order. 14 | like this: 15 | ``` 16 | // plugin({1, 2, a=3}) 17 | untyped __lua__("{ {0}, {1}, a={2} }", 1, 2, 3) 18 | ``` 19 | */ 20 | macro function plugin(expr:Expr) { 21 | final expected = Context.getExpectedType(); 22 | switch (expected) { 23 | case TType(t, params): 24 | final values = TableWrapper.extractObjFields(expr); 25 | var idx = 0; 26 | final lala = [for (k => v in values.fieldExprs) { 27 | {str: '$k = {${idx++}}, ', expr: v}; 28 | }]; 29 | final lolo = lala.fold( 30 | (item, acc:{str:String, expr:Array< Expr >}) -> { 31 | acc.str += item.str; 32 | acc.expr.push(item.expr); 33 | return acc; 34 | }, 35 | {str: "", expr: []} 36 | ); 37 | // We need to split this last thing like this because we need to get 38 | // an array of Expr first so reification converts 39 | // the resulting array to the arguments of the __lua__ call 40 | final args = [macro $v{'{ ${lolo.str} }'}].concat(lolo.expr); 41 | // This is the actual wanted output 42 | final out = macro untyped __lua__($a{args}); 43 | trace(out.toString()); 44 | return out; 45 | case _: 46 | trace("other"); 47 | }; 48 | 49 | return null; 50 | } 51 | -------------------------------------------------------------------------------- /src/vim/TableTools.hx: -------------------------------------------------------------------------------- 1 | package vim; 2 | 3 | import vim.VimTypes.LuaArray; 4 | import lua.Table; 5 | import lua.Lua; 6 | 7 | /** 8 | This is a collection of helpers to work with lua tables 9 | in a more comfortable way. 10 | This functions has a more functional focus, avoiding mutating the 11 | tables whenever possible. 12 | It is designed to be used as a static extensions module 13 | */ 14 | /** 15 | Joins all the values of a table with the provided separator. 16 | This is an alias for the less obvious concat of plain lua 17 | */ 18 | final join = lua.Table.concat; 19 | 20 | /** 21 | Concatenates two tables creating a new table that contains the values of both 22 | */ 23 | function concat< T >(tableA:LuaArray< T >, tableB:LuaArray< T >):LuaArray< T > { 24 | final result = Vim.list_extend(Table.create(), tableA); 25 | return Vim.list_extend(result, tableB); 26 | } 27 | 28 | inline function pairs< T >(table:LuaArray< T >) { 29 | return lua.PairTools.ipairsIterator(table); 30 | } 31 | 32 | /** 33 | Given a table and a function that returns a boolean, returns the next value 34 | right after the first value that satisfies the function. 35 | It is different from the usual find function because it does not returns 36 | the value that satisfies the predicate, but the next one in the table. 37 | */ 38 | function findNext< T >(table:LuaArray< T >, fn:T -> Bool):Null< T > { 39 | final p = Lua.ipairs(table); 40 | final next = p.next; 41 | final t = p.table; 42 | function loop(next, table, nextP:NextResult< Int, T >) { 43 | return if (fn(nextP.value)) { 44 | next(table, nextP.index).value; 45 | } else loop(next, table, next(table, nextP.index)); 46 | } 47 | return loop(next, t, next(t, p.index)); 48 | } 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.1.1](https://github.com/danielo515/haxe-nvim/compare/v1.1.0...v1.1.1) (2023-04-30) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * command mode for mappins ([a1251e9](https://github.com/danielo515/haxe-nvim/commit/a1251e93ac49769dedccb341bc0155bd759564a7)) 9 | 10 | ## [1.1.0](https://github.com/danielo515/haxe-nvim/compare/v1.0.0...v1.1.0) (2023-04-30) 11 | 12 | 13 | ### Features 14 | 15 | * run all the tests ([1ce559e](https://github.com/danielo515/haxe-nvim/commit/1ce559e5843a658f920cbdb9b00d33713cfc9038)) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * file readable ([ba40b15](https://github.com/danielo515/haxe-nvim/commit/ba40b15ea8363fa5c86764c0ce355173b605ce47)) 21 | 22 | ## 1.0.0 (2023-04-04) 23 | 24 | ### Features 25 | 26 | * better mappings for user args ([422330a](https://github.com/danielo515/haxe-nvim/commit/422330abc968a4c3e0a22ab503f30b1dcf2efa65)) 27 | * copy plugin version to clipboard ([5607e1a](https://github.com/danielo515/haxe-nvim/commit/5607e1a68f47a7697a91d311de4197b72ec6451f)) 28 | * macro for easy and safe submodule access ([e5844ee](https://github.com/danielo515/haxe-nvim/commit/e5844ee76f8dc2a5256b759ba3b1255f66232395)) 29 | * setup release please ([1e8ae6a](https://github.com/danielo515/haxe-nvim/commit/1e8ae6a569dfb4dc3e50ffbb4675ffcc2e74ee1e)) 30 | * better mappings for user args ([422330a](https://github.com/danielo515/haxe-nvim/commit/422330abc968a4c3e0a22ab503f30b1dcf2efa65)) 31 | * copy plugin version to clipboard ([5607e1a](https://github.com/danielo515/haxe-nvim/commit/5607e1a68f47a7697a91d311de4197b72ec6451f)) 32 | * macro for easy and safe submodule access ([e5844ee](https://github.com/danielo515/haxe-nvim/commit/e5844ee76f8dc2a5256b759ba3b1255f66232395)) 33 | * setup release please ([1e8ae6a](https://github.com/danielo515/haxe-nvim/commit/1e8ae6a569dfb4dc3e50ffbb4675ffcc2e74ee1e)) 34 | 35 | ### Bug Fixes 36 | 37 | * file readable ([ba40b15](https://github.com/danielo515/haxe-nvim/commit/ba40b15ea8363fa5c86764c0ce355173b605ce47)) 38 | -------------------------------------------------------------------------------- /tools/Cmd.hx: -------------------------------------------------------------------------------- 1 | package tools; 2 | 3 | import haxe.io.Path; 4 | import sys.io.Process; 5 | 6 | function readStd(source:haxe.io.Input):String { 7 | return try { 8 | source.readAll().toString(); 9 | } 10 | catch (e:Dynamic) { 11 | ""; 12 | }; 13 | } 14 | 15 | function executeCommand(cmd, args, readStder = false):Result< String > { 16 | final res = new Process(cmd, args); 17 | return switch (res.exitCode(true)) { 18 | case 0: 19 | final stdOut = readStd(res.stdout); 20 | Ok(!readStder ? stdOut : readStd(res.stderr)); 21 | case _: 22 | Log.print("Error executing command: " + cmd + " " + args.join(" ")); 23 | Error(readStd(res.stderr)); 24 | } 25 | } 26 | 27 | // function getHomeFolder() { 28 | // return Sys.getEnv(if (Sys.systemName() == "Windows")"UserProfile" else "HOME"); 29 | // } 30 | 31 | function getHomeFolder():String { 32 | return switch (Sys.systemName()) { 33 | case "Windows": 34 | Sys.getEnv("USERPROFILE"); 35 | default: 36 | var home = Sys.getEnv("XDG_CONFIG_HOME"); 37 | if (home == null)Sys.getEnv("HOME"); else home; 38 | } 39 | } 40 | 41 | /* 42 | Returns a temporary folder to be used. 43 | It tries to follow the XDG spec, and if not 44 | tries to use platform specific folders reading env variables. 45 | If that is not available, fallback to the user home folder 46 | with the provided namespace. 47 | */ 48 | function getTempFolderPath(namespace:String):String { 49 | var temp = Sys.getEnv("XDG_RUNTIME_DIR"); 50 | if (temp != null) { 51 | return temp; 52 | } 53 | final fallback = Path.join([getHomeFolder(), namespace]); 54 | return switch (Sys.systemName()) { 55 | case "Windows": 56 | final tmp = Sys.getEnv("TEMP"); 57 | if (tmp != null)tmp; else Sys.getEnv("TMP"); 58 | case "Mac" | "Linux": 59 | switch (executeCommand("mktemp", ["-d", "-t", namespace])) { 60 | case Ok(path): path; 61 | case Error(_): fallback; 62 | } 63 | default: fallback; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/vim/plugin/PluginMacro.hx: -------------------------------------------------------------------------------- 1 | package vim.plugin; 2 | 3 | using haxe.macro.TypeTools; 4 | using haxe.macro.ExprTools; 5 | 6 | import haxe.macro.Context; 7 | import haxe.macro.Expr; 8 | 9 | class PluginMacro { 10 | /** 11 | helper to transform the declaration annotated with @module 12 | into a getter that calls Vimx.require with the correct path 13 | based on the library name. 14 | e.g.: 15 | ```haxe 16 | // given 17 | @module var submodule:SomeType; 18 | // becomes 19 | public static var submodule(get, null):Null< SomeType >; 20 | inline static public function get_submodule():Null< SomeType > { 21 | return vim.Vimx.require("mylib.submodule"); 22 | } 23 | ``` 24 | **/ 25 | static public function makeModule(libName, name, typeName):Array< Field > { 26 | final returnType = macro :$typeName; 27 | final getter = 'get_$name'; 28 | final requirePath = libName + '.' + name; 29 | final built = macro class { 30 | public static var $name(get, null):Null< $returnType >; 31 | 32 | inline static public function $getter():Null< $returnType > { 33 | return vim.Vimx.require($v{requirePath}); 34 | } 35 | }; 36 | return built.fields; 37 | } 38 | 39 | /** 40 | For docs about this take a look at the VimPlugin interface 41 | */ 42 | macro static public function pluginInterface():Array< Field > { 43 | final fields = Context.getBuildFields(); 44 | final localType = Context.getLocalType().toComplexType(); 45 | final returnType = macro :$localType; 46 | var libName = null; 47 | return fields.flatMap(field -> switch field { 48 | case {name: "libName", kind: FVar(_, {expr: EConst(CString(val, _))})}: 49 | final built = macro class X { 50 | inline static public function require():Null< $returnType > { 51 | return vim.Vimx.require($v{val}); 52 | } 53 | }; 54 | libName = val; 55 | built.fields; 56 | 57 | case {name: name, meta: [{name: "module"}], kind: FVar(t, e)}: 58 | if (libName == null) { 59 | Context.error("libName must be defined before any @module", field.pos); 60 | } 61 | makeModule(libName, name, t); 62 | 63 | case _: [field]; 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/TableBuilder.hx: -------------------------------------------------------------------------------- 1 | using haxe.macro.Tools; 2 | 3 | // using haxe.macro.TypeTools; 4 | import haxe.macro.ExprTools; 5 | import haxe.macro.Expr; 6 | import haxe.macro.Context; 7 | 8 | // Thanks to 9 | // https://stackoverflow.com/a/74711862/1734815 10 | class TableBuilder { 11 | macro public static function getFields(td:Expr):Array< Field > { 12 | var t = Context.getType(td.toString()).follow(); 13 | var anon = switch (t) { 14 | case TAnonymous(ref): ref.get(); 15 | case _: Context.error("Structure expected", td.pos); 16 | } 17 | for (tdf in anon.fields) { 18 | trace('generate function: resolve_${tdf.name};'); 19 | } 20 | 21 | return null; 22 | } 23 | 24 | public static macro function build():Array< Field > { 25 | var fields = Context.getBuildFields(); 26 | for (field in fields) { 27 | if (field.name != "make") continue; // ignore other methods 28 | 29 | var f = switch (field.kind) { // ... that's a function 30 | case FFun(_f): _f; 31 | default: continue; 32 | } 33 | // Locate the function call within the function body 34 | // we will inject the table creation there 35 | var val = switch (f.expr.expr) { 36 | case EBlock([{expr: EReturn({expr: ECall(_, params)})}]): params; 37 | default: continue; 38 | } 39 | 40 | var objFields:Array< ObjectField > = []; 41 | for (arg in f.args) { 42 | var argVal = arg.value; 43 | switch (arg.type) { 44 | case TPath({ 45 | pack: pack, 46 | name: x, 47 | params: params, 48 | sub: sub 49 | }): 50 | var theType = (Context.getType(x).follow()); 51 | switch (theType) { 52 | case TAnonymous(ref): 53 | final fields = ref.get(); 54 | for (field in fields.fields) { 55 | var name = field.name; 56 | trace(field.name, field.type); 57 | var plain_expr = macro($i{arg.name}).$name; 58 | var expr = switch (field.type) { 59 | case TInst(_.get().name => "Array", _): 60 | // plain_expr; 61 | macro(lua.Table.fromArray($plain_expr)); 62 | case other: plain_expr; 63 | }; 64 | objFields.push({ 65 | field: name, 66 | expr: expr, 67 | }); 68 | } 69 | case other: 70 | trace("other", other); 71 | continue; 72 | } 73 | default: 74 | continue; 75 | } 76 | } 77 | var objExpr:Expr = {expr: EObjectDecl(objFields), pos: Context.currentPos()}; 78 | val[0].expr = (macro lua.Table.create(null, $objExpr)).expr; 79 | // trace(val[0].toString()); 80 | } 81 | return fields; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/vim/Vim.hx: -------------------------------------------------------------------------------- 1 | package vim; 2 | 3 | import vim.types.WOpts; 4 | import haxe.extern.EitherType; 5 | import haxe.Rest; 6 | import vim.VimTypes; 7 | import haxe.Constraints.Function; 8 | import lua.Table; 9 | 10 | inline function comment() { 11 | untyped __lua__("---@diagnostic disable"); 12 | } 13 | 14 | @:native("vim.fs") 15 | @:build(ApiGen.attachApi("fs")) 16 | extern class Fs {} 17 | 18 | @:native("vim.fn") 19 | @:build(ApiGen.attachApi("fn")) 20 | extern class Fn { 21 | static function expand(string:ExpandString):String; 22 | static function fnamemodify(file:String, string:PathModifier):String; 23 | static function executable(binaryName:String):Int; 24 | static function json_encode(value:Dynamic):String; 25 | static function json_decode(json:String):Table< String, Dynamic >; 26 | public static function has(feature:String):Int; 27 | // static function readfile(fname:String, ?type:String, ?max:Int):LuaArray< String >; 28 | } 29 | 30 | @:native("vim.keymap") 31 | extern class Keymap { 32 | /*Set a keymap in one or more modes*/ 33 | public static function set( 34 | mode:EitherType< VimMode, LuaArray< VimMode > >, 35 | keys:String, 36 | map:EitherType< Function, String >, 37 | opts:TableWrapper< {desc:String, silent:Bool, expr:Bool} > 38 | ):Void; 39 | 40 | @:native('set') 41 | public static function setBuf( 42 | mode:VimMode, 43 | keys:String, 44 | map:EitherType< Function, String >, 45 | opts:TableWrapper< {desc:String, buffer:Buffer} > 46 | ):Void; 47 | } 48 | 49 | @:native("vim.loop") 50 | extern class Loop { 51 | /* Gives information about the host system */ 52 | public static function os_uname():Table< String, String >; 53 | } 54 | 55 | function is09(): Bool { 56 | return Fn.has('nvim-0.9') == 1; 57 | } 58 | 59 | @:native("vim") 60 | extern class Vim { 61 | public static final o:vim.types.Opt; 62 | public static final go:vim.types.GOpt; 63 | public static final g:VimGOpts; 64 | public static final wo:WOpts; 65 | // @:native("pretty_print") 66 | static function print(args:Rest< Dynamic >):Void; 67 | static inline function expand(string:ExpandString):String { 68 | return Fn.expand(string); 69 | }; 70 | public static function tbl_map< T, B >(fn:T -> B, tbl:LuaArray< T >):LuaArray< B >; 71 | public static function list_extend< T >(dest:LuaArray< T >, src:LuaArray< T >):LuaArray< T >; 72 | public static function cmd(command:String):Void; 73 | public static function notify(message:String, level:String):Void; 74 | } 75 | 76 | @:native("vim.spell") 77 | extern class Spell { 78 | public static function check(str:String):Array< Vector3< String, String, Int > >; 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build, Release & Publish 2 | on: push 3 | 4 | jobs: 5 | build: 6 | name: Checkout & Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v3 11 | - name: Setup haxe 12 | uses: krdlab/setup-haxe@v1 13 | with: 14 | haxe-version: 4.2.5 15 | - uses: danielo515/stylua-action@1.0.1 16 | name: Setup stylua so we can run it on build 17 | with: 18 | token: ${{ secrets.github_token }} 19 | version: 0.16.0 20 | - name: Run haxe tests 21 | run: | 22 | haxelib install --always libs.hxml 23 | haxe test-macro.hxml 24 | haxe tests.hxml 25 | - name: Build 26 | run: | 27 | haxe build.hxml 28 | 29 | - name: Clone neodev 30 | uses: actions/checkout@v3 31 | with: 32 | repository: folke/neodev.nvim.git 33 | path: /home/runner/.config/.haxe/nvim-api 34 | - name: Generate API 35 | run: | 36 | haxe build-api.hxml 37 | - name: Run test that should fail 38 | run: | 39 | ./run-fail-test.sh 40 | - name: Ensure lua code output doesn't change 41 | uses: tj-actions/verify-changed-files@v13 42 | id: changedfiles 43 | with: 44 | files: | 45 | lua/danielo_nvim 46 | - name: Fail if there are changed files (should not be the case) 47 | if: steps.changedfiles.outputs.files_changed == 'true' 48 | run: | 49 | echo "Changed files: ${{ steps.changedfiles.outputs.changed_files }}" 50 | git --no-pager diff "${{ steps.changedfiles.outputs.changed_files }}" 51 | exit 1 52 | 53 | release: 54 | name: Check version & Create release 55 | permissions: 56 | contents: write # to create release commit 57 | pull-requests: write # to create release PR 58 | runs-on: ubuntu-latest 59 | needs: build 60 | outputs: 61 | # expose release.outputs.released for the "publish" job 62 | released: ${{ steps.release.outputs.release_created }} 63 | steps: 64 | - name: Checkout code 65 | uses: actions/checkout@v3 66 | - uses: google-github-actions/release-please-action@v3 67 | id: release 68 | with: 69 | token: ${{ secrets.GITHUB_TOKEN }} 70 | command: manifest 71 | 72 | publish: 73 | name: Publish to Haxelib 74 | runs-on: ubuntu-latest 75 | needs: release 76 | if: needs.release.outputs.released 77 | steps: 78 | - uses: actions/checkout@v3 79 | - uses: krdlab/setup-haxe@v1 80 | with: 81 | haxe-version: 4.2.5 82 | - run: | 83 | haxe -version 84 | haxelib --debug submit . "$HAXELIB_PASSWORD" 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | HAXELIB_PASSWORD: ${{ secrets.HAXELIB_PASSWORD }} 88 | -------------------------------------------------------------------------------- /hxformat.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentation": { 3 | "character": " " 4 | }, 5 | "sameLine": { 6 | "caseBody": "next", 7 | "tryCatch": "next", 8 | "ifBody": "next", 9 | "ifElse": "same", 10 | "elseBody": "same", 11 | "forBody": "same", 12 | "whileBody": "same", 13 | "comprehensionFor": "same" 14 | }, 15 | "emptyLines": { 16 | "finalNewline": true, 17 | "lineCommentsBetweenFunctions": "none", 18 | "interfaceEmptyLines": { 19 | "beginType": 1 20 | } 21 | }, 22 | "wrapping": { 23 | "functionSignature": { 24 | "defaultWrap": "noWrap", 25 | "rules": [ 26 | { 27 | "conditions": [ 28 | { 29 | "cond": "totalItemLength >= n", 30 | "value": 80 31 | } 32 | ], 33 | "type": "onePerLine" 34 | } 35 | ] 36 | }, 37 | "arrayWrap": { 38 | "rules": [ 39 | { 40 | "conditions": [ 41 | { 42 | "cond": "totalItemLength <= n", 43 | "value": 50 44 | } 45 | ], 46 | "type": "noWrap" 47 | }, 48 | { 49 | "conditions": [ 50 | { 51 | "cond": "itemCount >= n", 52 | "value": 5 53 | } 54 | ], 55 | "type": "onePerLine" 56 | }, 57 | { 58 | "conditions": [ 59 | { 60 | "cond": "anyItemLength >= n", 61 | "value": 30 62 | } 63 | ], 64 | "type": "onePerLine" 65 | }, 66 | { 67 | "conditions": [ 68 | { 69 | "cond": "itemCount >= n", 70 | "value": 4 71 | } 72 | ], 73 | "type": "onePerLine" 74 | } 75 | ] 76 | }, 77 | "callParameter": { 78 | "defaultWrap": "noWrap", 79 | "rules": [ 80 | { 81 | "conditions": [ 82 | { 83 | "cond": "totalItemLength >= n", 84 | "value": 80 85 | } 86 | ], 87 | "type": "onePerLine" 88 | }, 89 | { 90 | "conditions": [ 91 | { 92 | "cond": "anyItemLength >= n", 93 | "value": 100 94 | } 95 | ], 96 | "type": "onePerLine" 97 | }, 98 | { 99 | "conditions": [ 100 | { 101 | "cond": "lineLength >= n", 102 | "value": 100 103 | } 104 | ], 105 | "type": "onePerLine" 106 | } 107 | ] 108 | } 109 | }, 110 | "whitespace": { 111 | "arrowFunctionsPolicy": "around", 112 | "functionTypeHaxe3Policy": "around", 113 | "functionTypeHaxe4Policy": "around", 114 | "parenConfig": { 115 | "conditionParens": { 116 | "openingPolicy": "noneAfter", 117 | "closingPolicy": "noneAfter" 118 | }, 119 | "switchParens": { 120 | "openingPolicy": "onlyAfter", 121 | "closingPolicy": "around" 122 | } 123 | }, 124 | "typeParamOpenPolicy": "onlyAfter", 125 | "typeParamClosePolicy": "onlyBefore" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/vim/Api.hx: -------------------------------------------------------------------------------- 1 | package vim; 2 | 3 | using Safety; 4 | 5 | import vim.VimTypes; 6 | import haxe.Constraints.Function; 7 | import lua.Table; 8 | 9 | typedef CommandCallbackArgs = { 10 | final args:String; 11 | final fargs:Table< String, String >; 12 | final bang:Bool; 13 | final line1:Int; 14 | final line2:Int; 15 | final count:Int; 16 | /* If the command is a range command, this is the number of range edges 0-2 */ 17 | final range:Int; 18 | final reg:String; 19 | final mods:String; 20 | } 21 | 22 | typedef UserCommandOpts = TableWrapper< { 23 | desc:String, 24 | force:Bool, 25 | ?nargs:Nargs, 26 | ?bang:Bool, 27 | // ?range:CmdRange, 28 | } > 29 | 30 | abstract AutoCmdOpts(Table< String, Dynamic >) { 31 | public inline function new( 32 | pattern:String, 33 | cb:FunctionOrString, 34 | group:Group, 35 | description:String, 36 | once = false, 37 | nested = false 38 | ) { 39 | this = Table.create(null, { 40 | pattern: pattern, 41 | group: group, 42 | desc: description, 43 | once: once, 44 | nested: nested, 45 | }); 46 | switch (cb) { 47 | case Cb(f): 48 | this.callback = f; 49 | case Str(cmd): 50 | this.command = cmd; 51 | } 52 | } 53 | } 54 | 55 | @:native("vim.api") 56 | @:build(ApiGen.attachApi("api")) 57 | extern class Api { 58 | /** 59 | Returns a list of available tabpages 60 | */ 61 | static function nvim_list_tabpages():LuaArray< Tabpage >; 62 | 63 | static function nvim_create_augroup(group:String, opts:GroupOpts):Group; 64 | static function nvim_create_autocmd(event:LuaArray< VimEvent >, opts:AutoCmdOpts):Int; 65 | static function nvim_create_user_command( 66 | command_name:String, 67 | command:LuaObj< CommandCallbackArgs > -> Void, 68 | opts:TableWrapper< { 69 | desc:String, 70 | force:Bool, 71 | ?complete:ArgComplete, 72 | ?nargs:Nargs, 73 | ?bang:Bool, 74 | ?range:CmdRange, 75 | } > 76 | ):Void; 77 | 78 | /** 79 | Same as create user command, but this is specifically typed 80 | for completion callbacks. 81 | Completion callback should take the following arguments: 82 | ArgLead, CmdLine, CursorPos 83 | Where: 84 | ArgLead: The current value of the argument being completed 85 | CmdLine: The full command line text 86 | CursorPos: The current cursor position in the command line 87 | */ 88 | @:native('nvim_create_user_command') 89 | static function nvim_create_user_command_complete_cb( 90 | command_name:String, 91 | command:LuaObj< CommandCallbackArgs > -> Void, 92 | opts:TableWrapper< { 93 | desc:String, 94 | force:Bool, 95 | complete:(String, String, Int) -> lua.Table< Int, String >, 96 | nargs:Nargs, 97 | ?bang:Bool, 98 | ?range:CmdRange, 99 | } > 100 | ):Void; 101 | 102 | static inline function create_user_command_completion( 103 | command_name:String, 104 | command:LuaObj< CommandCallbackArgs > -> Void, 105 | complete:ArgComplete, 106 | opts:{ 107 | desc:String, 108 | force:Bool, 109 | ?bang:Bool, 110 | ?range:CmdRange, 111 | } 112 | ):Void { 113 | nvim_create_user_command(command_name, command, { 114 | desc: opts.desc, 115 | force: true, 116 | complete: complete, 117 | nargs: ExactlyOne, 118 | bang: opts.bang, 119 | range: opts.range 120 | }); 121 | }; 122 | static function nvim_buf_create_user_command( 123 | bufnr:Buffer, 124 | command_name:String, 125 | command:LuaObj< CommandCallbackArgs > -> Void, 126 | opts:TableWrapper< { 127 | desc:String, 128 | force:Bool, 129 | ?nargs:Nargs, 130 | ?bang:Bool, 131 | ?range:CmdRange, 132 | } > 133 | ):Void; 134 | } 135 | -------------------------------------------------------------------------------- /src/vim/types/ArgComplete.hx: -------------------------------------------------------------------------------- 1 | package vim.types; 2 | 3 | enum ArgCompleteEnum { 4 | Custom(vimFn:String); 5 | CustomLua(luaRef:String); 6 | 7 | /** file names in argument list */ 8 | ArgList; 9 | 10 | /** autocmd groups */ 11 | Augroup; 12 | 13 | /** buffer names */ 14 | Buffer; 15 | 16 | /** :behave suboptions */ 17 | Behave; 18 | 19 | /** color schemes */ 20 | Color; 21 | 22 | /** Ex command (and arguments) */ 23 | Command; 24 | 25 | /** compilers */ 26 | Compiler; 27 | 28 | /** directory names */ 29 | Dir; 30 | 31 | /** environment variable names */ 32 | Environment; 33 | 34 | /** autocommand events */ 35 | Event; 36 | 37 | /** Vim expression */ 38 | Expression; 39 | 40 | /** file and directory names */ 41 | File; 42 | 43 | /** file and directory names in |'path'| */ 44 | File_in_path; 45 | 46 | /** filetype names |'filetype'| */ 47 | Filetype; 48 | 49 | /** function name */ 50 | Function; 51 | 52 | /** help subjects */ 53 | Help; 54 | 55 | /** highlight groups */ 56 | Highlight; 57 | 58 | /** :history suboptions */ 59 | History; 60 | 61 | /** locale names (as output of locale -a) */ 62 | Locale; 63 | 64 | /** Lua expression */ 65 | Lua; 66 | 67 | /** buffer argument */ 68 | Mapclear; 69 | 70 | /** mapping name */ 71 | Mapping; 72 | 73 | /** menus */ 74 | Menu; 75 | 76 | /** |:messages| suboptions */ 77 | Messages; 78 | 79 | /** options */ 80 | Option; 81 | 82 | /** optional package |pack-add| names */ 83 | Packadd; 84 | 85 | /** Shell command */ 86 | Shellcmd; 87 | 88 | /** |:sign| suboptions */ 89 | Sign; 90 | 91 | /** syntax file names |'syntax'| */ 92 | Syntax; 93 | 94 | /** |:syntime| suboptions */ 95 | Syntime; 96 | 97 | /** tags */ 98 | Tag; 99 | 100 | /** tags, file names are shown when CTRL-D is hit */ 101 | Tag_listfiles; 102 | 103 | /** user names */ 104 | User; 105 | 106 | /** user variables */ 107 | Var; 108 | } 109 | 110 | abstract ArgComplete(String) to String { 111 | private inline function new(arg) { 112 | this = arg; 113 | } 114 | 115 | @:from 116 | public static inline function from(enumValue:ArgCompleteEnum):ArgComplete { 117 | return switch (enumValue) { 118 | case Custom(ref): new ArgComplete('custom,$ref'); 119 | case CustomLua(ref): new ArgComplete('customlist,v:lua.$ref'); 120 | case ArgList: new ArgComplete("arglist"); 121 | case Augroup: new ArgComplete("augroup"); 122 | case Buffer: new ArgComplete("buffer"); 123 | case Behave: new ArgComplete("behave"); 124 | case Color: new ArgComplete("color"); 125 | case Command: new ArgComplete("command"); 126 | case Compiler: new ArgComplete("compiler"); 127 | case Dir: new ArgComplete("dir"); 128 | case Environment: new ArgComplete("environment"); 129 | case Event: new ArgComplete("event"); 130 | case Expression: new ArgComplete("expression"); 131 | case File: new ArgComplete("file"); 132 | case File_in_path: new ArgComplete("file_in_path"); 133 | case Filetype: new ArgComplete("filetype"); 134 | case Function: new ArgComplete("function"); 135 | case Help: new ArgComplete("help"); 136 | case Highlight: new ArgComplete("highlight"); 137 | case History: new ArgComplete("history"); 138 | case Locale: new ArgComplete("locale"); 139 | case Lua: new ArgComplete("lua"); 140 | case Mapclear: new ArgComplete("mapclear"); 141 | case Mapping: new ArgComplete("mapping"); 142 | case Menu: new ArgComplete("menu"); 143 | case Messages: new ArgComplete("messages"); 144 | case Option: new ArgComplete("option"); 145 | case Packadd: new ArgComplete("packadd"); 146 | case Shellcmd: new ArgComplete("shellcmd"); 147 | case Sign: new ArgComplete("sign"); 148 | case Syntax: new ArgComplete("syntax"); 149 | case Syntime: new ArgComplete("syntime"); 150 | case Tag: new ArgComplete("tag"); 151 | case Tag_listfiles: new ArgComplete("tag_listfiles"); 152 | case User: new ArgComplete("user"); 153 | case Var: new ArgComplete("var"); 154 | }; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/vim/Vimx.hx: -------------------------------------------------------------------------------- 1 | package vim; 2 | 3 | import haxe.Constraints.Function; 4 | import lua.Table; 5 | import vim.Vim.Fn; 6 | import vim.Vim.Loop; 7 | import vim.Api; 8 | import lua.StringMap; 9 | import vim.VimTypes; 10 | 11 | using Safety; 12 | 13 | final pathSeparator = Loop.os_uname().sysname == 'Windows' ? '\\' : '/'; 14 | 15 | @:expose("vimx") @:native("vimx") 16 | class Vimx { 17 | public static final autogroups:StringMap< Group > = new StringMap(); 18 | 19 | // internal wrapper 20 | static function acmd( 21 | groupName:String, 22 | events:LuaArray< VimEvent >, 23 | pattern:String, 24 | ?description:String, 25 | cb 26 | ) { 27 | var group:Group; 28 | switch (autogroups.get(groupName)) { 29 | case null: 30 | group = Api.nvim_create_augroup(groupName, {clear: true}); 31 | autogroups.set(groupName, group); 32 | case x: 33 | group = x; 34 | }; 35 | Api.nvim_create_autocmd( 36 | events, 37 | new AutoCmdOpts(pattern, cb, group, description.or('$groupName:[$pattern]')) 38 | ); 39 | } 40 | 41 | /** 42 | Creates a new autocommand and associates it to the given group name. 43 | If the group has not been registered before, it gets created and cached 44 | so future commands with the same group name will re-use the same group. 45 | Note that existing commands on the group created outside of this function 46 | are not checked for existence. 47 | */ 48 | static public inline function autocmd( 49 | groupName:String, 50 | events:LuaArray< VimEvent >, 51 | pattern:String, 52 | ?description:String, 53 | cb:Function 54 | ) { 55 | acmd(groupName, events, pattern, description, Cb(cb)); 56 | } 57 | 58 | static public inline function autocmdStr( 59 | groupName:String, 60 | events:LuaArray< VimEvent >, 61 | pattern:String, 62 | ?description:String, 63 | command:String 64 | ) { 65 | acmd(groupName, events, pattern, description, Str(command)); 66 | } 67 | 68 | /** 69 | Copies the given string to the system clipboard 70 | */ 71 | public static function copyToClipboard(str:String) { 72 | final cmd = 'let @* = "$str"'; 73 | Vim.cmd(cmd); 74 | Vim.notify("Copied to clipboard", "info"); 75 | } 76 | 77 | /** 78 | Returns the number of lines of the current file in the current window 79 | */ 80 | public static inline function linesInCurrentWindow():Int { 81 | return Fn.line('$', CurrentWindow); 82 | }; 83 | 84 | /** 85 | Returns the line number of the first visible line 86 | on the current window 87 | */ 88 | public static inline function firstLineVisibleCurrentWindow():Int { 89 | return Fn.line('w0', CurrentWindow); 90 | }; 91 | 92 | /** 93 | Returns the line number of the last visible line 94 | on the current window 95 | */ 96 | public static inline function lastLineVisibleCurrentWindow():Int { 97 | return Fn.line('w$', CurrentWindow); 98 | }; 99 | 100 | /** 101 | Given a list of paths as strings, joins them using the 102 | path separator. 103 | This is supposed to be used from Haxe code 104 | */ 105 | public static function join_paths(paths:Array< String >):String { 106 | return paths.join(pathSeparator); 107 | } 108 | 109 | /** 110 | Little wrapper that returns true if a file exists and is readable 111 | */ 112 | public static function file_exists(path:String):Bool { 113 | return if (Fn.filereadable(path) == 1) { 114 | true; 115 | } else { 116 | false; 117 | } 118 | } 119 | 120 | /** 121 | Reads a json file and parses it if it exists. 122 | Returns null if the file does not exist or is not readable 123 | */ 124 | public static function read_json_file(path:String):Null< lua.Table< String, Dynamic > > { 125 | if (file_exists(path)) { 126 | return Fn.json_decode(Table.concat(Fn.readfile(path))); 127 | } else { 128 | return null; 129 | } 130 | } 131 | 132 | @:native('safeRequire') 133 | public static function require< T >(name):Null< T > { 134 | final module = lua.Lua.pcall(lua.Lua.require, name); 135 | return if (module.status) { 136 | module.value; 137 | } else { 138 | null; 139 | }; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Important information!! 2 | 3 | ## WIP 4 | 5 | This is a work in progress. It is very usable, in fact I use it in my personal configs and in one Neovim plugin with great success, but the API may change. 6 | 7 | ## Not a plugin 8 | 9 | **This is not a Neovim plugin** , it is a Haxe library to help you write neovim configurations and plugins using Haxe programming language 10 | 11 | ## How to contribute 12 | 13 | I am just a newcomer to Haxe, so there are lots of places where I will appreciate contributions. 14 | Here is a list of things that are very welcome 15 | - [ ] A cool logo. If anything this project needs is a cool logo. 16 | - [x] Proper configurations to publish this to haxelib 17 | - [ ] A better lua annotations parser. I don't want to depend on anything but Haxe. Currently this library uses a very dumb parser for the Lua annotations. If you want to help there and improve it or write a new one you are welcome 18 | - [ ] Try this out. Yeah, just having this used by someone but me will probably be beneficial. 19 | - [ ] Write more neovim externs! This library tries to generate most extenrs automatically, but some manual work is unavoidable (because how wild the Neovim api is in terms of types). If you want to contribute and add new and better definitions for NeoVim methods, this is also welcome 20 | 21 | # Haxe Nvim 22 | 23 | Write your next Neovim plugin or your personal neovim configuration in Haxe and enjoy a high level language which is type safe. 24 | 25 | This library was initially intended to be named "Nvim haxelerator" (and I may rename it at some point) because it will allow you to write Neovim plugins faster and safer. 26 | Take advantage of the powerful Haxe compiler and turn frustrating runtime errors your users (probably your future self) will face and that will take you hours to debug into nice compilation time errors that will take you seconds to fix! 27 | 28 | ## Philosophy 29 | 30 | We have strong opinions about what this library should be and what should not be. Read about that below. 31 | 32 | ### Prefer enums over magic values 33 | 34 | APIs must be for humans to instruct machines, not to talk between machines. Because of that the usage of magic values that have a special meaning is avoided as much as possible. Instead Enums are used in every place it is possible. Haxe has a great feature which is called `Abstracts`, which allows you to have proper compile-time-only abstractions without any runtime overhead. 35 | An example of this is the `Buffer` type. All the neovim functions that expect a `BufferId` will not just accept any number, you must provide a proper `BufferId` (obtained from functions that return a `BufferId`) or `CurrentBuffer`. Thanks to the nature of abstract types this disappears after compilation and all what is left are plain numbers. 36 | 37 | ## Advantages over plain Lua 38 | 39 | - Proper types! You don't know how good is having proper types that will you prevent from doing silly mistakes. 40 | This library goes even further and not only prevents you from putting a `"string"` where a number is expected, it will also prevent you from putting the wrong number! 41 | - Compile time errors. Yes, rather than saving your plugin/config, reload neovim, test your feature and try to decipher the stack trace, you will get compile-time errors as you save your file, pointing out to the exact place where you did that stupid mistake because you are configuring your editor again at 2:00 am 42 | 43 | ## Limitations 44 | 45 | - Currently the output size is a bit beefy compared to plain Lua. I ported kickstart.lua, which is about 350 lines of code and the generated Lua is about ~1k lines. It is just 3 times larger, which is not that bad if you consider all the extra language features you get. 46 | - Duplicated STD. This library tries to use Neovim std as much as possible, but because the Haxe Lua output limitations there are always some Haxe STD helpers in the generated file. This paired with the inability of Haxe to output several files will make every plugin contain a copy of all the STD helper Haxe produces. 47 | 48 | # Example usage 49 | 50 | If you want an example or a bootstrap to start your own neovim plugin using haxe-nvim, take a look at the [template plugin](https://github.com/danielo515/haxe-nvim-example-plugin). 51 | If you want an example of a personal configuration using it, here is [kickstart.hx](https://github.com/danielo515/kickstart.hx) 52 | 53 | # Acknowledgments 54 | 55 | I want to thank Rudy from the Haxe discord channel for his huge help with macros to reduce the overhead 56 | of the Lua target. 57 | -------------------------------------------------------------------------------- /history.md: -------------------------------------------------------------------------------- 1 | 2 | ## Why Haxe to configure neovim? 3 | 4 | I am a former javascript developer that felt in love with static typing, well, strong and good static typing. 5 | I started my programming journey with typed languages (C, Pascal), but I soon felt in love with Javascript and how easy and fast you can prototype stuff on it. 6 | Letting projects grow and professionally working with Javascript was not as fun tough. Can't remember how many times I had this conversation with myself: 7 | 8 | > Current: Why is this (expected) string an array? 9 | > Former: do you remember that change you made 3 months ago and that didn't broke anything because 90% of the functions were just passing down their arguments and the other 10% was just checking the string length (which happens to exist also array)? Do you remember that? 10 | > Current: No, Id on't remember it 😕 11 | > Former: 😛, yeah 12 | 13 | Some developers love bug hunting, they are usually easy to fix and you can close several tickets in a day. 14 | I don't, I just feel stupid to waste 1 hour because one variable was an array instead of a string, or because a number was in milliseconds rather than in kilometers. 15 | 16 | At the same time, some hate for typed languages started to grow on me, being Java the main culprit. 17 | It is ultery verbose, the type system only gets on your way and, after all, the worst of all is that ti does not prevent the worst mistakes. 18 | Every time I tried to do something of medium complexity in Java I had to play the guess game of how many of the 5k useless classes I have to extend, and which methods I don't care I have to override... 19 | I hated Java so much that, as of today, if you ask my Alexa to destroy the world she will answer back 20 | 21 | > Ok, loading the required Jar files... 22 | 23 | That is how Evil I think Java is. 24 | 25 | I gave Typescript a go (it was version 3.x) and I also hated it. The type system was not rich enough to express half of the things you were able to do in JS, and the worst of all is that, after all that typing pain you were left with runtime errors in the same way you had with JS, bah! 26 | 27 | Things just got worse when I discovered with functional programming. 28 | You know that point-free nerds? Well, I was one of them, until I put some pipeline in production and I was not able to understand what the hell was going wrong because the lack of types and the size of the pipeline. 29 | Of course Typescript was not up to the task of having proper types in functional programming either. 30 | 31 | Thankfully, I discovered ReasonML and then felt in love with it and learnt to like Ocaml too. 32 | 33 | Why all that matters? Well, it will be relevant for the last wall of text. 34 | Almost since I started using Linux 15 years ago, I wanted to use Vim as my text-editor, but the experience was so subpar compared to any other IDE that I could just not get over it. 35 | However, it became almost a tradition to waste a couple of days every 6 months trying to make it my main editor. 36 | With every attempt I was getting further (1 week using it as my main editor, 3 weeks, 2 months!), but sooner or later I was reaching roadblocks, and I needed work to get done. 37 | On top of all the problems you get to configure Vim, using VimScript was... not fun, and who wants to waste time in something that is not fun? 38 | Then NeoVim announced they were going to add Lua as scripting language, and I thought: 39 | 40 | > I couldn't care less. 41 | 42 | But then people started to built all sort of plugins in Lua, and then I discovered LunarVim, and suddenly NeoVim was not only a capable IDE, it was an IDE that I could very easily script!. Open your `init.lua`, throw some lines of code, and boom, new functionality. Lua was simple enough and similar enough to JS to being able to use it without having to follow any tutorial or waste any time. 43 | However, as you keep adding lines and lines of code, the same problems that JS has start to arise: dumb mistakes everywhere that are hard to debug but easy to forget. 44 | As in JS, languages that try to "just add types on top of" Lua were just as poor as TS. 45 | And because the real solution to my JS problems was reasonML I thought: 46 | 47 | > Maybe is it possible to transpile ReasonML to lua in addition to JS? 48 | 49 | Well, after months of research, the answer is no. Asking in Ocaml/ReasonML/Rescript forums about transpiling to Lua someone suggested Haxe. 50 | I took a look and, I didn't liked the OOP style at first, but after getting through that barrier I started to see its potential, and I decided to give it a try, after all I can just make all my class be just namespaces and all my methods Static, right? 51 | As positive things, the language is designed to be mapped to other languages, so it will be easier to add more targets if you like it enough, which is not true for ReasonML, and the Macro system is a piece of cake compared to PPX that Ocaml has. 52 | And here we are! 53 | Hope this was not too boring! 54 | -------------------------------------------------------------------------------- /src/TableWrapper.hx: -------------------------------------------------------------------------------- 1 | // From"" https://try.haxe.org/#0D7f4371 2 | #if macro 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | 6 | using haxe.macro.TypeTools; 7 | using haxe.macro.ExprTools; 8 | using Safety; 9 | using haxe.macro.ComplexTypeTools; 10 | 11 | var uniqueCount = 1; 12 | #end 13 | 14 | /** 15 | Generates a new array that does not contain duplicate values, 16 | giving preference to the leftmos elements. 17 | */ 18 | function uniqueValues< T >(array:Array< T >, indexer:(T) -> String) { 19 | final index = new haxe.ds.StringMap< Bool >(); 20 | return [for (val in array) { 21 | final key = indexer(val); 22 | if (index.exists(key)) 23 | continue; 24 | index.set(key, true); 25 | val; 26 | }]; 27 | } 28 | 29 | // Class that transforms any Haxe object into a plain lua table 30 | // Thanks to @kLabz 31 | #if macro 32 | abstract TableWrapper< T:{} >(Dynamic) { 33 | #else 34 | abstract TableWrapper< T:{} >(lua.Table< String, Dynamic >) { 35 | #end 36 | @:pure @:noCompletion extern public static function check< T:{} >(v:T):TableWrapper< T >; 37 | 38 | #if macro 39 | public static function followTypesUp(arg:haxe.macro.Type) { 40 | return switch (arg) { 41 | case TAnonymous(_.get().fields => fields): 42 | fields; 43 | case TAbstract(_, [type]): 44 | followTypesUp(type); 45 | case TType(_, _): 46 | followTypesUp(arg.follow()); 47 | case other: 48 | trace("other", other); 49 | throw "dead end 2?"; 50 | } 51 | } 52 | 53 | public static function extractObjFields(objExpr) { 54 | return switch Context.getTypedExpr(Context.typeExpr(objExpr)).expr { 55 | case EObjectDecl(inputFields): 56 | var inputFields = inputFields.copy(); 57 | { 58 | fieldExprs: [for (f in inputFields) f.field => f.expr], 59 | inputFields: inputFields 60 | }; 61 | 62 | case _: 63 | throw "Must be called with an anonymous object"; 64 | } 65 | } 66 | 67 | static function objToTable(obj:Expr):Expr { 68 | return try { 69 | switch (obj.expr) { 70 | case EObjectDecl(fields): 71 | final objExpr:Expr = { 72 | expr: EObjectDecl([for (f in fields) { 73 | { 74 | field: f.field, 75 | expr: objToTable(f.expr) 76 | } 77 | }]), 78 | pos: obj.pos 79 | }; 80 | macro lua.Table.create(null, $objExpr); 81 | case EArrayDecl(values): 82 | macro lua.Table.create(${ExprTools.map(obj, objToTable)}, null); 83 | case EFunction(kind, f): 84 | obj; 85 | case _: 86 | ExprTools.map(obj, objToTable); 87 | } 88 | } 89 | catch (e) { 90 | trace("Failed here:", obj.toString()); 91 | obj; 92 | } 93 | } 94 | #end 95 | 96 | @:from public static macro function fromExpr(ex:Expr):Expr { 97 | var expected = Context.getExpectedType(); 98 | var complexType = expected.toComplexType(); 99 | 100 | switch expected { 101 | case TAbstract(_, [_]) | TType(_, _): 102 | final fields = followTypesUp(expected); 103 | final x = extractObjFields(ex); 104 | final inputFields = x.inputFields; 105 | final fieldExprs = x.fieldExprs; 106 | 107 | var generatedFields:Array< ObjectField > = [for (f in fields) { 108 | final currentFieldExpression = fieldExprs.get(f.name).or(macro $v{null}); 109 | 110 | // trace("currentFieldExpression", currentFieldExpression); 111 | if (currentFieldExpression == null) { 112 | continue; 113 | } 114 | switch (f.type) { 115 | case _.toComplexType() => macro :Array< String > :{ 116 | field:f.name, 117 | expr:macro 118 | lua.Table.create 119 | (${fieldExprs.get(f.name)}) 120 | }; 121 | 122 | case TAbstract(_.toString() => "TableWrapper", [_]): 123 | var ct = f.type.toComplexType(); 124 | 125 | for (inf in inputFields) if (inf.field == f.name) { 126 | inf.expr = macro(TableWrapper.check(${inf.expr}) : $ct); 127 | break; 128 | } 129 | 130 | {field: f.name, expr: macro(TableWrapper.fromExpr(${fieldExprs.get(f.name)}) : $ct)}; 131 | 132 | case TAnonymous(_): 133 | {field: f.name, expr: objToTable(currentFieldExpression)}; 134 | 135 | case TAbstract(_, _) | TFun(_, _): 136 | {field: f.name, expr: (currentFieldExpression)}; 137 | case _: 138 | {field: f.name, expr: objToTable(currentFieldExpression)}; 139 | } 140 | }]; 141 | 142 | // We merge the generated fields, which may include fields that are not present in the code 143 | // (in case of optional fields, for example) 144 | // with the real fields so the type checking is accurate and does not allow extra fields that 145 | // will be silently ignored otherwise 146 | final obj = {expr: EObjectDecl(generatedFields), pos: ex.pos}; 147 | 148 | final inputObj = { 149 | expr: EObjectDecl(uniqueValues(inputFields.concat(generatedFields), f -> f.field)), 150 | pos: ex.pos 151 | }; 152 | 153 | // trace("\n=========\n", complexType.toString()); 154 | // trace("fields", inputFields); 155 | // trace("ex", ex); 156 | // trace("fieldExprs", fieldExprs); 157 | // trace("obj", obj); 158 | 159 | final name = '_dce${uniqueCount++}'; 160 | return macro @:mergeBlock { 161 | // Type checking; should be removed by dce 162 | @:pos(ex.pos) final $name:$complexType = TableWrapper.check($inputObj); 163 | 164 | // Actual table creation 165 | (cast lua.Table.create(null, $obj) : $complexType); 166 | }; 167 | 168 | case other: 169 | trace(complexType); 170 | // trace(followTypesUp(other)); 171 | throw "TableWrapper only works with anonymous objects"; 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /tools/luaParser/GenerateDocLexerTest.hx: -------------------------------------------------------------------------------- 1 | package tools.luaParser; 2 | 3 | import sys.FileSystem; 4 | import haxe.ds.StringMap; 5 | import haxe.Json; 6 | import tools.FileTools; 7 | import haxe.io.Path; 8 | import sys.io.File; 9 | import byte.ByteData; 10 | import tools.luaParser.LuaDoc; 11 | 12 | using haxe.EnumTools; 13 | using haxe.macro.ExprTools; 14 | using StringTools; 15 | using Lambda; 16 | 17 | // Prevent generating tests for the same comment twice 18 | final globallyViewedComments = new StringMap< Bool >(); 19 | 20 | function readNeovimLuaFile(relativePath:String):Array< String > { 21 | final runtimePath = FileTools.getNvimRuntimePath(); 22 | switch (runtimePath) { 23 | case Error(e): 24 | throw e; 25 | case Ok(path): 26 | final contents = File.getContent(Path.join([path, 'lua', relativePath])); 27 | return contents.split('\n'); 28 | } 29 | } 30 | 31 | @:tink class EnuP { 32 | static public function printEnum(e:EnumValue) { 33 | final name = e.getName(); 34 | final values = EnumValueTools.getParameters(e); 35 | if (values.length == 0) { 36 | return name; 37 | } 38 | return switch values[0] { 39 | case(v : String): '$name("$v")'; 40 | default: '$name($values)'; 41 | } 42 | } 43 | } 44 | 45 | function generateTestCase(fixture, original, expected:ParamDoc) { 46 | // final expected = [${expected.map(EnuP.printEnum).join(', ')}]; 47 | final contents = ' 48 | it("$original => ${expected.name}: ${expected.type}", { 49 | final parser = new LuaDocParser(ByteData.ofString("$fixture")); 50 | final actual = parser.parse(); 51 | final expected = Json.stringify(${Json.stringify(expected)}); 52 | Json.stringify(actual).should.be(expected); 53 | });'; 54 | return contents; 55 | }; 56 | 57 | function generateTestSuite(referenceFile:String, testCases) { 58 | final contents = 'describe("$referenceFile", { 59 | ${testCases.join("\n\t")} 60 | });'; 61 | return contents; 62 | }; 63 | 64 | function generateTestFile(testSuites) { 65 | final contents = ' 66 | package tools.luaParser; 67 | 68 | import byte.ByteData; 69 | import haxe.Json; 70 | import tools.luaParser.LuaDoc; 71 | 72 | using StringTools; 73 | using buddy.Should; 74 | 75 | @colorize 76 | class LuaDocParserTest extends buddy.SingleSuite { 77 | public function new() { ${testSuites.join("\n\t")} 78 | } 79 | } 80 | '; 81 | 82 | return contents; 83 | } 84 | 85 | typedef MatchStr = {line:String, match:String}; 86 | 87 | function extractAllParamCommentsFromFile(file:String):Array< MatchStr > { 88 | final lines = readNeovimLuaFile(file); 89 | final comments = []; 90 | final commentRegex = ~/-{2,3} ?@param (.*)/; 91 | for (line in lines) { 92 | if (commentRegex.match(line)) { 93 | final matched = commentRegex.matched(1); 94 | if (globallyViewedComments.exists(matched)) { 95 | continue; 96 | } 97 | globallyViewedComments.set(matched, true); 98 | comments.push({line: line, match: matched}); 99 | } 100 | } 101 | final filteredComments = comments.filter((str) -> { 102 | // Yes, this ridiculous thing is there 103 | !str.line.contains("(context support not yet implemented)"); 104 | }); 105 | return filteredComments; 106 | } 107 | 108 | function parseParamComment(comment:MatchStr) { 109 | final parser = new LuaDocParser(ByteData.ofString(comment.match)); 110 | Log.prettyPrint("=====", comment.line); 111 | final parseResult = parser.parse(); 112 | Log.prettyPrint('', parseResult); 113 | return parseResult; 114 | } 115 | 116 | function generateTestCases(commentsAsStrings:Array< MatchStr >) { 117 | final commentsParsed = commentsAsStrings.map(parseParamComment); 118 | final testCases = [for (idx => expected in commentsParsed) { 119 | final fixture = commentsAsStrings[idx]; 120 | generateTestCase(fixture.match, fixture.line, expected); 121 | }]; 122 | return testCases; 123 | } 124 | 125 | function generateTestCasesFromRuntimeFiles() { 126 | final files = [ 127 | 'vim/filetype.lua', 128 | 'vim/fs.lua', 129 | 'vim/keymap.lua', 130 | 'vim/lsp/buf.lua' 131 | ]; 132 | final testSuites = [for (file in files) { 133 | final commentsAsStrings = extractAllParamCommentsFromFile(file); 134 | final testCases = generateTestCases(commentsAsStrings); 135 | generateTestSuite(file, testCases); 136 | }]; 137 | return testSuites; 138 | } 139 | 140 | /** 141 | Uses our existing json files already extracted to generate test 142 | against them. 143 | */ 144 | function generateTestCasesFromJsonResFiles() { 145 | final files = ['fn.json', 'api.json',]; 146 | final commentRegex = ~/ ?@param (.*)/; 147 | final testSuites = [for (file in files) { 148 | final specs:Array< {annotations:Array< String >} > = Json.parse( 149 | File.getContent(FileTools.getResPath(file)) 150 | ); 151 | final commentsAsStrings = specs.flatMap((spec) -> { 152 | spec.annotations.flatMap((line) -> { 153 | if (commentRegex.match(line)) { 154 | final match = commentRegex.matched(1); 155 | 156 | if (globallyViewedComments.exists(match)) { 157 | return []; 158 | } 159 | globallyViewedComments.set(match, true); 160 | [{line: line, match: match}]; 161 | } else { 162 | []; 163 | }; 164 | }); 165 | }); 166 | final testCases = generateTestCases(commentsAsStrings); 167 | generateTestSuite(file, testCases); 168 | }]; 169 | return testSuites; 170 | } 171 | 172 | function main() { 173 | final testSuites = generateTestCasesFromRuntimeFiles(); 174 | final testSuitesFromJson = generateTestCasesFromJsonResFiles(); 175 | final testFile = generateTestFile(testSuites.concat(testSuitesFromJson)); 176 | final destinationFile = FileSystem.absolutePath(Path.join([ 177 | FileTools.getDirFromPackagePath('tools/Cmd.hx'), 178 | '/luaParser/LuaDocParserTest.hx' 179 | ])); 180 | writeTextFile(destinationFile, testFile); 181 | Cmd.executeCommand('haxelib', ['run', 'formatter', '-s', destinationFile]); 182 | // final parsed = new LuaDocParser( 183 | // ByteData.ofString('bufnr string The buffer to get the lines from') 184 | // ).parse(); 185 | // Log.prettyPrint("parsed", parsed); 186 | }; 187 | -------------------------------------------------------------------------------- /src/ApiGen.hx: -------------------------------------------------------------------------------- 1 | import haxe.Json; 2 | import haxe.io.Path; 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import sys.io.File; 6 | 7 | var patches = [ 8 | "nvim_create_augroup" => macro :vim.Vim.Group, 9 | "nvim_buf_get_keymap" => macro :vim.VimTypes.LuaArray< vim.VimTypes.MapInfo >, 10 | "nvim_create_user_command.opts" => macro :TableWrapper< { 11 | desc:String, 12 | force:Bool 13 | } > 14 | ]; 15 | 16 | function getLibraryBase() { 17 | final base = Path.directory(haxe.macro.Context.resolvePath('vim/Vim.hx')); 18 | return base; 19 | } 20 | 21 | function getResPath(filename:String):String { 22 | return Path.join([getLibraryBase(), '../../res', filename]); 23 | } 24 | 25 | function getDumpPath(filename:String):String { 26 | return Path.join([getLibraryBase(), '../../dump', filename]); 27 | } 28 | 29 | /** 30 | Reads the neovim API from a vim dump file and generates the Haxe externs. 31 | This only covers nvim.api, not the other modules. 32 | @deprecated. We read from a different dump with more and better information 33 | */ 34 | macro function generateApi():Void { 35 | var specs = Json.parse(File.getContent(getResPath('nvim-api.json'))); 36 | 37 | Context.defineType({ 38 | pack: ["nvim"], 39 | name: "API", 40 | isExtern: true, 41 | kind: TDClass(), 42 | #if !dump 43 | meta: [meta("native", [macro "vim.api"])], 44 | #end 45 | fields: [ 46 | for (f in (specs.functions : Array< FunctionDef >)) { 47 | { 48 | name: f.name, 49 | access: [AStatic, APublic], 50 | meta: (f.deprecated_since != null) ? [meta('deprecated')] : [], 51 | kind: FFun({ 52 | args: f.parameters.map(p -> ({ 53 | name: p.name, 54 | type: resolveType(f.name, p.name, p.type) 55 | } : FunctionArg)), 56 | ret: resolveType(f.name, null, f.return_type) 57 | }), 58 | pos: (macro null).pos 59 | } 60 | } 61 | ], 62 | pos: (macro null).pos 63 | }); 64 | } 65 | 66 | function resolveType(fun:String, arg:Null< String >, t:String):ComplexType { 67 | var patch = patches.get(arg == null ? fun : '$fun.$arg'); 68 | if (patch != null) 69 | return patch; 70 | 71 | return switch (t) { 72 | case "String": macro :String; 73 | case "LuaRef": macro :haxe.Constraints.Function; 74 | case "Window": macro :vim.VimTypes.WindowId; 75 | case "Client": macro :vim.VimTypes.Client; 76 | case "Buffer": macro :vim.VimTypes.Buffer; 77 | case "Integer": macro :Int; 78 | case "Float": macro :Float; 79 | case "Tabpage": macro :vim.VimTypes.TabPage; 80 | case "Dictionary": macro :lua.Table< String, Dynamic >; 81 | case "Boolean": macro :Bool; 82 | case "Object": macro :Dynamic; 83 | case "Array": macro :vim.VimTypes.LuaArray< Dynamic >; 84 | case "void": macro :Void; 85 | 86 | case t if (StringTools.startsWith(t, "ArrayOf(")): 87 | final regexArrayArg = ~/ArrayOf\(([a-zA-Z]+),?/i; 88 | regexArrayArg.match(t); 89 | var itemType = resolveType(fun, (arg == null ? "" : arg) + "[]", regexArrayArg.matched(1)); 90 | macro :vim.VimTypes.LuaArray< $itemType >; 91 | 92 | case _: 93 | Context.warning('Cannot resolve type $t', (macro null).pos); 94 | macro :Dynamic; 95 | }; 96 | } 97 | 98 | function meta(m:String, ?params:Array< Expr >):MetadataEntry { 99 | return {name: ':$m', params: params, pos: (macro null).pos}; 100 | } 101 | 102 | abstract ParamDef(Array< String >) { 103 | public var type(get, never):String; 104 | 105 | function get_type():String 106 | return this[0]; 107 | 108 | public var name(get, never):String; 109 | 110 | function get_name():String 111 | return this[1]; 112 | } 113 | 114 | /** 115 | A function definition from the neovim API dump. 116 | */ 117 | typedef FunctionDef = { 118 | final name:String; 119 | final method:Bool; 120 | final parameters:Array< ParamDef >; 121 | final return_type:String; 122 | final since:Int; 123 | @:optional final deprecated_since:Int; 124 | } 125 | 126 | typedef FunctionWithDocs = { 127 | > FunctionDef, 128 | final parameters:Array< ParamDef >; 129 | final docs:Array< String >; 130 | } 131 | 132 | // Thanks again @rudy 133 | function parseTypeFromStr(typeString:String) { 134 | try { 135 | return switch (haxe.macro.Context.parse('(null:$typeString)', (macro null).pos).expr) { 136 | case EParenthesis({expr: ECheckType(_, ct)}): 137 | ct; 138 | 139 | case _: throw 'Unable to parse: $typeString'; 140 | } 141 | } 142 | catch (e) { 143 | // TODO;; enable this 144 | // Context.warning('bad type string: `$typeString`', (macro null).pos); 145 | throw 'Unable to parse: $typeString'; 146 | } 147 | } 148 | 149 | /** 150 | Given a namespace, which should match one of the available JSON files in the res folder, 151 | generates methods with the function definitions read from that file and attaches them to the 152 | target class. This is intended to be used as a build macro 153 | */ 154 | macro function attachApi(namespace:String):Array< Field > { 155 | var fields = Context.getBuildFields(); 156 | final existingFields = fields.map(f -> f.name); 157 | var specs:Array< FunctionWithDocs > = Json.parse(File.getContent(getResPath('$namespace.json'))); 158 | specs = specs.filter(x -> !existingFields.contains(x.name) && x.name != ""); 159 | 160 | final failures = []; 161 | 162 | final newFields:Array< Field > = [for (f in (specs)) { 163 | try { 164 | { 165 | name: f.name, 166 | doc: f.docs.join("\n"), 167 | meta: [], 168 | access: [AStatic, APublic], 169 | kind: FFun({ 170 | args: f.parameters.map(p -> ({ 171 | name: p.name.replace("?", ""), 172 | type: parseTypeFromStr(p.type), 173 | opt: p.name.charAt(0) == "?" 174 | } : FunctionArg)), 175 | ret: parseTypeFromStr(f.return_type) 176 | }), 177 | pos: Context.currentPos() 178 | } 179 | } 180 | catch (error:String) { 181 | failures.push({block: f, error: error}); 182 | continue; 183 | } 184 | }]; 185 | if (failures.length > 0) { 186 | final handle = File.write(getDumpPath(namespace + '-error.json'), false); 187 | handle.writeString(Json.stringify(failures, null, " ")); 188 | handle.close(); 189 | } 190 | return fields.concat(newFields); 191 | } 192 | -------------------------------------------------------------------------------- /res/fs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "annotations": [ 4 | "@param start (string) Initial file or directory.", 5 | "@return (function) Iterator" 6 | ], 7 | "name": "parents", 8 | "parameters": [ 9 | [ 10 | "String", 11 | "start" 12 | ] 13 | ], 14 | "fullyQualified_name": "M.parents", 15 | "docs": [ 16 | " Iterate over all the parents of the given file or directory.", 17 | " Example:", 18 | "
",
 19 |       " local root_dir",
 20 |       " for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do",
 21 |       "   if vim.fn.isdirectory(dir .. \"/.git\") == 1 then",
 22 |       "     root_dir = dir",
 23 |       "     break",
 24 |       "   end",
 25 |       " end",
 26 |       " if root_dir then",
 27 |       "   print(\"Found git repository at\", root_dir)",
 28 |       " end",
 29 |       " 
" 30 | ], 31 | "return_type": "Function" 32 | }, 33 | { 34 | "annotations": [ 35 | "@param file (string) File or directory", 36 | "@return (string) Parent directory of {file}" 37 | ], 38 | "name": "dirname", 39 | "parameters": [ 40 | [ 41 | "String", 42 | "file" 43 | ] 44 | ], 45 | "fullyQualified_name": "M.dirname", 46 | "docs": [ 47 | " Return the parent directory of the given file or directory" 48 | ], 49 | "return_type": "String" 50 | }, 51 | { 52 | "annotations": [ 53 | "@param file (string) File or directory", 54 | "@return (string) Basename of {file}" 55 | ], 56 | "name": "basename", 57 | "parameters": [ 58 | [ 59 | "String", 60 | "file" 61 | ] 62 | ], 63 | "fullyQualified_name": "M.basename", 64 | "docs": [ 65 | " Return the basename of the given file or directory" 66 | ], 67 | "return_type": "String" 68 | }, 69 | { 70 | "annotations": [ 71 | "@param path (string) An absolute or relative path to the directory to iterate", 72 | "@return Iterator over files and directories in {path}. Each iteration yields" 73 | ], 74 | "name": "dir", 75 | "parameters": [ 76 | [ 77 | "String", 78 | "path" 79 | ] 80 | ], 81 | "fullyQualified_name": "M.dir", 82 | "docs": [ 83 | " Return an iterator over the files and directories located in {path}", 84 | " over. The path is first normalized |vim.fs.normalize()|.", 85 | " two values: name and type. Each \"name\" is the basename of the file or", 86 | " directory relative to {path}. Type is one of \"file\" or \"directory\"." 87 | ], 88 | "return_type": "Function" 89 | }, 90 | { 91 | "annotations": [ 92 | "@param names (string|table|fun(name: string): boolean) Names of the files", 93 | "@param opts (table) Optional keyword arguments:", 94 | "@return (table) The paths of all matching files or directories" 95 | ], 96 | "name": "find", 97 | "parameters": [ 98 | [ 99 | "Dynamic", 100 | "names" 101 | ], 102 | [ 103 | "lua.Table", 104 | "opts" 105 | ] 106 | ], 107 | "fullyQualified_name": "M.find", 108 | "docs": [ 109 | " Find files or directories in the given path.", 110 | " Finds any files or directories given in {names} starting from {path}. If", 111 | " {upward} is \"true\" then the search traverses upward through parent", 112 | " directories; otherwise, the search traverses downward. Note that downward", 113 | " searches are recursive and may search through many directories! If {stop}", 114 | " is non-nil, then the search stops when the directory given in {stop} is", 115 | " reached. The search terminates when {limit} (default 1) matches are found.", 116 | " The search can be narrowed to find only files or or only directories by", 117 | " specifying {type} to be \"file\" or \"directory\", respectively.", 118 | " and directories to find.", 119 | " Must be base names, paths and globs are not supported.", 120 | " If a function it is called per file and dir within the", 121 | " traversed directories to test if they match.", 122 | " - path (string): Path to begin searching from. If", 123 | " omitted, the current working directory is used.", 124 | " - upward (boolean, default false): If true, search", 125 | " upward through parent directories. Otherwise,", 126 | " search through child directories", 127 | " (recursively).", 128 | " - stop (string): Stop searching when this directory is", 129 | " reached. The directory itself is not searched.", 130 | " - type (string): Find only files (\"file\") or", 131 | " directories (\"directory\"). If omitted, both", 132 | " files and directories that match {name} are", 133 | " included.", 134 | " - limit (number, default 1): Stop the search after", 135 | " finding this many matches. Use `math.huge` to", 136 | " place no limit on the number of matches." 137 | ], 138 | "return_type": "lua.Table" 139 | }, 140 | { 141 | "annotations": [], 142 | "name": "add", 143 | "parameters": [ 144 | [ 145 | "Dynamic", 146 | "match" 147 | ] 148 | ], 149 | "fullyQualified_name": "add", 150 | "docs": [], 151 | "return_type": "Void" 152 | }, 153 | { 154 | "annotations": [ 155 | "@param path (string) Path to normalize", 156 | "@return (string) Normalized path" 157 | ], 158 | "name": "normalize", 159 | "parameters": [ 160 | [ 161 | "String", 162 | "path" 163 | ] 164 | ], 165 | "fullyQualified_name": "M.normalize", 166 | "docs": [ 167 | " Normalize a path to a standard format. A tilde (~) character at the", 168 | " beginning of the path is expanded to the user's home directory and any", 169 | " backslash (\\\\) characters are converted to forward slashes (/). Environment", 170 | " variables are also expanded.", 171 | " Example:", 172 | "
",
173 |       " vim.fs.normalize('C:\\\\Users\\\\jdoe')",
174 |       " => 'C:/Users/jdoe'",
175 |       " vim.fs.normalize('~/src/neovim')",
176 |       " => '/home/jdoe/src/neovim'",
177 |       " vim.fs.normalize('$XDG_CONFIG_HOME/nvim/init.vim')",
178 |       " => '/Users/jdoe/.config/nvim/init.vim'",
179 |       " 
" 180 | ], 181 | "return_type": "String" 182 | } 183 | ] -------------------------------------------------------------------------------- /tools/luaParser/Lexer.hx: -------------------------------------------------------------------------------- 1 | package tools.luaParser; 2 | 3 | import hxparse.Lexer; 4 | import haxe.macro.Expr.Position; 5 | 6 | using StringTools; 7 | using Safety; 8 | 9 | enum LKeyword { 10 | And; 11 | Break; 12 | Do; 13 | Else; 14 | Elseif; 15 | End; 16 | False; 17 | For; 18 | Function; 19 | Goto; 20 | If; 21 | In; 22 | Local; 23 | Nil; 24 | Not; 25 | Or; 26 | Repeat; 27 | Return; 28 | Then; 29 | True; 30 | Until; 31 | While; 32 | } 33 | 34 | final luaKeywords:Map< String, LKeyword > = [ 35 | "and" => LKeyword.And, 36 | "break" => LKeyword.Break, 37 | "do" => LKeyword.Do, 38 | "else" => LKeyword.Else, 39 | "elseif" => LKeyword.Elseif, 40 | "end" => LKeyword.End, 41 | "false" => LKeyword.False, 42 | "for" => LKeyword.For, 43 | "function" => LKeyword.Function, 44 | "goto" => LKeyword.Goto, 45 | "if" => LKeyword.If, 46 | "in" => LKeyword.In, 47 | "local" => LKeyword.Local, 48 | "nil" => LKeyword.Nil, 49 | "not" => LKeyword.Not, 50 | "or" => LKeyword.Or, 51 | "repeat" => LKeyword.Repeat, 52 | "return" => LKeyword.Return, 53 | "then" => LKeyword.Then, 54 | "true" => LKeyword.True, 55 | "until" => LKeyword.Until, 56 | "while" => LKeyword.While, 57 | ]; 58 | 59 | enum TokenDef { 60 | Dot; 61 | ThreeDots; 62 | DotDot; 63 | Eof; 64 | NotEqual; 65 | LengthSharp; 66 | Comment(content:String); 67 | LuaDocParam(content:String); 68 | LuaDocReturn(content:String); 69 | LuaDocPrivate; 70 | Keyword(k:LKeyword); 71 | Identifier(name:String); 72 | Namespace(name:String); 73 | StringLiteral(content:String); 74 | IntegerLiteral(value:String); 75 | FloatLiteral(value:String); 76 | HexLiteral(value:String); 77 | Newline; 78 | OpenParen; 79 | CloseParen; 80 | CurlyOpen; 81 | CurlyClose; 82 | SquareOpen; 83 | SquareClose; 84 | Comma; 85 | Colon; 86 | Semicolon; 87 | Plus; 88 | Minus; 89 | Asterisk; 90 | Slash; 91 | Percent; 92 | Hat; 93 | Equal; 94 | Equality; 95 | LessThan; 96 | BiggerThan; 97 | LessEqual; 98 | BiggerEqual; 99 | } 100 | 101 | class Token { 102 | public var tok:TokenDef; 103 | public var pos:Position; 104 | public var space = ""; 105 | 106 | public function new(tok, pos) { 107 | this.tok = tok; 108 | this.pos = pos; 109 | } 110 | 111 | public function toString() { 112 | return switch (tok) { 113 | case Comment(content): 'Comment("$content")'; 114 | case LuaDocParam(content): 'LuaDocParam("$content")'; 115 | case LuaDocReturn(doc): 'LuaDocReturn("$doc")'; 116 | case Keyword(k): 'Keyword($k)'; 117 | case Identifier(name): 'Identifier("$name")'; 118 | case StringLiteral(content): 'StringLiteral("$content")'; 119 | case IntegerLiteral(value): 'IntegerLiteral("$value")'; 120 | case FloatLiteral(value): 'FloatLiteral("$value")'; 121 | case HexLiteral(value): 'HexLiteral("$value")'; 122 | case other: '$other'; 123 | } 124 | } 125 | } 126 | 127 | class LuaLexer extends Lexer implements hxparse.RuleBuilder { 128 | static public var ignoreComments = false; 129 | 130 | static function mkPos(p:hxparse.Position) { 131 | return { 132 | file: p.psource, 133 | min: p.pmin, 134 | max: p.pmax 135 | }; 136 | } 137 | 138 | static function mk(lexer:Lexer, td) { 139 | return new Token(td, mkPos(lexer.curPos())); 140 | } 141 | 142 | static final identifier_ = "[a-zA-Z_][a-zA-Z0-9_]*"; 143 | static final integer = "[0-9]+"; 144 | static final float = "[0-9]+\\.[0-9]([eE][-+]?[0-9]+)?"; 145 | static final hex = "0[xX][0-9a-fA-F]+"; 146 | 147 | public static var consumeLine = @:rule ["[^\n]+" => lexer.current]; 148 | // @:rule wraps the expression to the right of => with function(lexer) return 149 | public static var tok = @:rule [ 150 | "return" => mk(lexer, Keyword(LKeyword.Return)), 151 | "\\+" => mk(lexer, Plus), 152 | ";" => mk(lexer, Semicolon), 153 | "\\-" => mk(lexer, Minus), 154 | "\\*" => mk(lexer, Asterisk), 155 | "\\/" => mk(lexer, Slash), 156 | "%" => mk(lexer, Percent), 157 | "\\^" => mk(lexer, Hat), 158 | "\\.\\.\\." => mk(lexer, ThreeDots), 159 | "\\.\\." => mk(lexer, DotDot), 160 | "\n\n" => mk(lexer, Newline), 161 | "\n" => lexer.token(tok), 162 | hex => mk(lexer, HexLiteral(lexer.current)), 163 | float => mk(lexer, FloatLiteral(lexer.current)), 164 | integer => mk(lexer, IntegerLiteral(lexer.current)), 165 | "[\t ]+" => { 166 | var space = lexer.current; 167 | var token:Token = lexer.token(tok); 168 | token.space = space; 169 | token; 170 | }, 171 | "<=" => mk(lexer, LessEqual), 172 | ">=" => mk(lexer, BiggerEqual), 173 | "<" => mk(lexer, LessThan), 174 | ">" => mk(lexer, BiggerThan), 175 | "#" => mk(lexer, LengthSharp), 176 | "\\[" => mk(lexer, SquareOpen), 177 | "\\]" => mk(lexer, SquareClose), 178 | "\\{" => mk(lexer, CurlyOpen), 179 | "\\}" => mk(lexer, CurlyClose), 180 | "\\(" => mk(lexer, OpenParen), 181 | "\\)" => mk(lexer, CloseParen), 182 | "," => mk(lexer, Comma), 183 | "==" => mk(lexer, Equality), 184 | "=" => mk(lexer, Equal), 185 | "~=" => mk(lexer, NotEqual), 186 | ":" => mk(lexer, Colon), 187 | "\\[\\[" => { 188 | final content = lexer.token(longBracketString); 189 | mk(lexer, StringLiteral(content)); 190 | }, 191 | "'" => { 192 | final content = lexer.token(singleQuotedString); 193 | mk(lexer, StringLiteral(content)); 194 | }, 195 | "\"" => { 196 | final content = lexer.token(doubleQuotedString); 197 | mk(lexer, StringLiteral(content)); 198 | }, 199 | identifier_ => { 200 | final content = lexer.current; 201 | final keyword = luaKeywords.get(content); 202 | if (keyword != null) { 203 | mk(lexer, Keyword(keyword)); 204 | } else { 205 | mk(lexer, Identifier(content)); 206 | } 207 | }, 208 | "\\." => mk(lexer, Dot), 209 | "--- ?@param" => { 210 | final content = lexer.token(consumeLine); 211 | mk(lexer, LuaDocParam(content)); 212 | }, 213 | "--- ?@return" => { 214 | final content = lexer.token(consumeLine); 215 | mk(lexer, LuaDocReturn(content)); 216 | }, 217 | "--- ?@private" => { 218 | mk(lexer, LuaDocPrivate); 219 | }, 220 | "---?[^@]" => { 221 | final content = lexer.token(consumeLine); 222 | if (ignoreComments) { 223 | return lexer.token(tok); 224 | } 225 | mk(lexer, Comment(content)); 226 | }, 227 | "" => mk(lexer, Eof), 228 | ]; 229 | 230 | public static var longBracketString = @:rule ["\\]\\]" => "", "[^\\]]+" => { 231 | final content = lexer.current; 232 | content + lexer.token(longBracketString); 233 | }]; 234 | public static var singleQuotedString = @:rule ["[^']+" => { 235 | final content = lexer.current; 236 | content + lexer.token(singleQuotedString); // make sure to consume the closing quote 237 | }, "'" => ""]; 238 | public static var doubleQuotedString = @:rule ["[^\"]+" => { 239 | final content = lexer.current; 240 | content + lexer.token(doubleQuotedString); // make sure to consume the closing quote 241 | }, "\"" => ""]; 242 | } 243 | -------------------------------------------------------------------------------- /tools/luaParser/ParserTest.hx: -------------------------------------------------------------------------------- 1 | package tools.luaParser; 2 | 3 | import hxparse.Lexer; 4 | import haxe.io.Path; 5 | import haxe.Json; 6 | import sys.io.File; 7 | import byte.ByteData; 8 | import tools.luaParser.Lexer.TokenDef; 9 | 10 | using StringTools; 11 | using buddy.Should; 12 | 13 | function readFixture(path:String):ByteData { 14 | final file = File.read(Path.join([Sys.getCwd(), "tools", "luaParser", path]), false).readAll(); 15 | return byte.ByteData.ofBytes(file); 16 | } 17 | 18 | function consumeTokens< T >(lexer:Lexer, tok):Array< T > { 19 | var tokens = []; 20 | try { 21 | var token = lexer.token(tok); 22 | while (token != null) { 23 | tokens.push(token); 24 | token = lexer.token(tok); 25 | } 26 | return tokens; 27 | } 28 | catch (e:Dynamic) { 29 | trace("Error parsing", e); 30 | trace("Dumping tokens:"); 31 | return tokens; 32 | } 33 | } 34 | 35 | function dumpComments(tokens:Array< TokenDef >) { 36 | for (token in tokens) { 37 | switch (token) { 38 | case LuaDocParam(payload): 39 | Log.print('"$payload",'); 40 | case _: 41 | } 42 | } 43 | } 44 | 45 | @colorize 46 | class ParserTest extends buddy.BuddySuite { 47 | public function new() { 48 | describe("Lua Lexer", { 49 | it("should parse the basic function", { 50 | final parser = new LuaParser(readFixture("fixtures/basic_fn.lua")); 51 | final expectedDescription = [ 52 | "Invokes |vim-function| or |user-function| {func} with arguments {...}.", 53 | "See also |vim.fn|.", 54 | "Equivalent to:", 55 | "```lua", 56 | " vim.fn[func]({...})", 57 | "```", 58 | ].join('\n'); 59 | final actual = parser.parse(); 60 | switch (actual) { 61 | case FunctionWithDocs(def): 62 | def.name.should.be("call"); 63 | def.args.should.containExactly(["func", "kwargs"]); 64 | def.namespace.should.containExactly(["vim"]); 65 | Json.stringify(def.typedArgs).should.be(Json.stringify([{ 66 | name: "func", 67 | description: "", 68 | isOptional: false, 69 | type: "Function" 70 | }])); 71 | def.description.should.be(expectedDescription); 72 | case _: fail("Expected function with docs"); 73 | } 74 | }); 75 | 76 | it("should lex vim.iconv", { 77 | final parser = new LuaParser(readFixture("fixtures/vim_iconv.lua")); 78 | final expected = { 79 | name: "iconv", 80 | namespace: ["vim"], 81 | args: ["str", "from", "to", "opts"], 82 | typedArgs: [{ 83 | name: "str", 84 | description: "", 85 | isOptional: false, 86 | type: "String" 87 | }, { 88 | name: "from", 89 | description: "", 90 | isOptional: false, 91 | type: "Number" 92 | }, { 93 | name: "to", 94 | description: "", 95 | isOptional: false, 96 | type: "Number" 97 | }, { 98 | name: "?opts", 99 | description: "", 100 | isOptional: true, 101 | type: "Table" 102 | }], 103 | description: [ 104 | 'The result is a String, which is the text {str} converted from', 105 | 'encoding {from} to encoding {to}. When the conversion fails `nil` is', 106 | 'returned. When some characters could not be converted they', 107 | 'are replaced with "?".', 108 | 'The encoding names are whatever the iconv() library function', 109 | 'can accept, see ":Man 3 iconv".', 110 | '-- Parameters: ~', 111 | ' • {str} (string) Text to convert', 112 | ' • {from} (string) Encoding of {str}', 113 | ' • {to} (string) Target encoding', 114 | '-- Returns: ~', 115 | ' Converted string if conversion succeeds, `nil` otherwise.', 116 | ].join('\n'), 117 | isPrivate: false 118 | }; 119 | final actual = parser.parse(); 120 | switch (actual) { 121 | case FunctionWithDocs({ 122 | name: name, 123 | namespace: namespace, 124 | args: args, 125 | typedArgs: typedArgs, 126 | description: description, 127 | isPrivate: isPrivate 128 | }): 129 | name.should.be(expected.name); 130 | namespace.should.containExactly(expected.namespace); 131 | args.should.containExactly(expected.args); 132 | Json.stringify(typedArgs).should.be(Json.stringify(expected.typedArgs)); 133 | description.should.be(expected.description); 134 | isPrivate.should.be(expected.isPrivate); 135 | case _: fail("Expected function with docs"); 136 | } 137 | }); 138 | it("should lex filetype_getLInes", { 139 | final parser = new LuaParser(readFixture("fixtures/filetype_getLines.lua")); 140 | final expected = { 141 | name: "getlines", 142 | namespace: ["M"], 143 | args: ["bufnr", "start_lnum", "end_lnum"], 144 | typedArgs: [{ 145 | name: "start_lnum", 146 | description: "The line number of the first line (inclusive, 1-based)", 147 | isOptional: false, 148 | type: "Either" 149 | }, { 150 | name: "end_lnum", 151 | description: "The line number of the last line (inclusive, 1-based)", 152 | isOptional: false, 153 | type: "Either" 154 | }], 155 | description: [ 156 | 'Get a single line or line range from the buffer.', 157 | 'If only start_lnum is specified, return a single line as a string.', 158 | 'If both start_lnum and end_lnum are omitted, return all lines from the buffer.', 159 | '---@param bufnr number|nil The buffer to get the lines from', 160 | ].join('\n'), 161 | isPrivate: true, 162 | }; 163 | final actual = parser.parse(); 164 | switch (actual) { 165 | case FunctionWithDocs({ 166 | name: name, 167 | namespace: namespace, 168 | args: args, 169 | typedArgs: typedArgs, 170 | description: description, 171 | isPrivate: isPrivate, 172 | returnDoc: rt 173 | }): 174 | name.should.be(expected.name); 175 | namespace.should.containExactly(expected.namespace); 176 | args.should.containExactly(expected.args); 177 | Json.stringify(typedArgs).should.be(Json.stringify(expected.typedArgs)); 178 | description.should.be(expected.description); 179 | isPrivate.should.be(expected.isPrivate); 180 | rt.description.should.be("Array of lines, or string when end_lnum is omitted"); 181 | rt.type.should.be("Either, String>"); 182 | rt.description.should.be("Array of lines, or string when end_lnum is omitted"); 183 | case _: fail("Expected function with docs"); 184 | } 185 | }); 186 | }); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/packer/Packer.hx: -------------------------------------------------------------------------------- 1 | package packer; 2 | 3 | import vim.VimTypes.LuaArray; 4 | import vim.Vim; 5 | import lua.Table.create as t; 6 | import plenary.Job; 7 | 8 | /** 9 | PluginSpec type reference: 10 | { 11 | 'myusername/example', -- The plugin location string 12 | -- The following keys are all optional 13 | disable = boolean, -- Mark a plugin as inactive 14 | as = string, -- Specifies an alias under which to install the plugin 15 | installer = function, -- Specifies custom installer. See "custom installers" below. 16 | updater = function, -- Specifies custom updater. See "custom installers" below. 17 | after = string or list, -- Specifies plugins to load before this plugin. See "sequencing" below 18 | rtp = string, -- Specifies a subdirectory of the plugin to add to runtimepath. 19 | opt = boolean, -- Manually marks a plugin as optional. 20 | bufread = boolean, -- Manually specifying if a plugin needs BufRead after being loaded 21 | branch = string, -- Specifies a git branch to use 22 | tag = string, -- Specifies a git tag to use. Supports '*' for "latest tag" 23 | commit = string, -- Specifies a git commit to use 24 | lock = boolean, -- Skip updating this plugin in updates/syncs. Still cleans. 25 | run = string, function, or table, -- Post-update/install hook. See "update/install hooks". 26 | requires = string or list, -- Specifies plugin dependencies. See "dependencies". 27 | rocks = string or list, -- Specifies Luarocks dependencies for the plugin 28 | config = string or function, -- Specifies code to run after this plugin is loaded. 29 | -- The setup key implies opt = true 30 | setup = string or function, -- Specifies code to run before this plugin is loaded. The code is ran even if 31 | -- the plugin is waiting for other conditions (ft, cond...) to be met. 32 | -- The following keys all imply lazy-loading and imply opt = true 33 | cmd = string or list, -- Specifies commands which load this plugin. Can be an autocmd pattern. 34 | ft = string or list, -- Specifies filetypes which load this plugin. 35 | keys = string or list, -- Specifies maps which load this plugin. See "Keybindings". 36 | event = string or list, -- Specifies autocommand events which load this plugin. 37 | fn = string or list -- Specifies functions which load this plugin. 38 | cond = string, function, or list of strings/functions, -- Specifies a conditional test to load this plugin 39 | module = string or list -- Specifies Lua module names for require. When requiring a string which starts 40 | -- with one of these module names, the plugin will be loaded. 41 | module_pattern = string/list -- Specifies Lua pattern of Lua module names for require. When 42 | -- requiring a string which matches one of these patterns, the plugin will be loaded. 43 | } 44 | */ 45 | // PluginSpec translated to haxe 46 | typedef PluginSpec = { 47 | @idx(1) 48 | final name:String; 49 | final ?disable:Bool; 50 | final ?as:String; 51 | final ?installer:Void -> Void; 52 | final ?updater:Void -> Void; 53 | final ?after:String; 54 | final ?rtp:String; 55 | final ?opt:Bool; 56 | final ?bufread:Bool; 57 | final ?branch:String; 58 | final ?tag:String; 59 | final ?commit:String; 60 | final ?lock:Bool; 61 | final ?run:String; 62 | final ?requires:LuaArray< String >; 63 | final ?rocks:String; 64 | final ?config:Void -> Void; 65 | final ?setup:String; 66 | final ?cmd:String; 67 | final ?ft:String; 68 | final ?keys:String; 69 | final ?event:LuaArray< String >; 70 | final ?fn:String; 71 | final ?cond:String; 72 | final ?module:String; 73 | final ?module_pattern:String; 74 | } 75 | 76 | inline extern function packer_plugins():Null< lua.Table< String, {loaded:Bool, path:String, url:String} > > 77 | return untyped __lua__("_G.packer_plugins"); 78 | 79 | /** 80 | Returns the git commit hash of the plugin. 81 | It only looks for plugins that are part of the packer plugin list. 82 | If the list is not available, it returns "unknown". 83 | */ 84 | function get_plugin_version(name:String):String { 85 | return if (packer_plugins() != null) { 86 | final path = packer_plugins()[cast name].path; 87 | final job = Job.make({ 88 | command: "git", 89 | cwd: path, 90 | args: t(['rev-parse', '--short', 'HEAD']), 91 | on_stderr: (args, return_val) -> { 92 | Vim.print("Job got stderr", args, return_val); 93 | } 94 | }); 95 | job.sync()[1]; 96 | } else { 97 | "unknown"; 98 | } 99 | } 100 | 101 | /** 102 | Checks if packer is installed and installs it if not. 103 | Returns true if packer was not installed and the installation was performed. 104 | You should use this information to decide if you need to run sync() 105 | */ 106 | function ensureInstalled():Bool { 107 | final install_path = Fn.stdpath("data") + "/site/pack/packer/start/packer.nvim"; 108 | return if (vim.Fn.empty(vim.Fn.glob(install_path, null)) > 0) { 109 | vim.Fn.system(t([ 110 | "git", 111 | "clone", 112 | "--depth", 113 | "1", 114 | "https://github.com/wbthomason/packer.nvim", 115 | install_path 116 | ]), null); 117 | Vim.cmd("packadd packer.nvim"); 118 | return true; 119 | } else { 120 | return false; 121 | }; 122 | } 123 | 124 | abstract Plugin(PluginSpec) { 125 | @:from 126 | public static inline function from(spec:PluginSpec):Plugin { 127 | return untyped __lua__( 128 | "{ 129 | {0}, 130 | disable={1}, 131 | as={2}, 132 | installer={3}, 133 | updater={4}, 134 | after={5}, 135 | rtp={6}, 136 | opt={7}, 137 | bufread={8}, 138 | branch={9}, 139 | tag={10}, 140 | commit={11}, 141 | lock={12}, 142 | run={13}, 143 | requires={14}, 144 | rocks={15}, 145 | config={16}, 146 | setup={17}, 147 | cmd={18}, 148 | ft={19}, 149 | keys={20}, 150 | event={21}, 151 | fn={22}, 152 | cond={23}, 153 | module={24}, 154 | module_pattern={25} 155 | }", 156 | spec.name, 157 | spec.disable, 158 | spec.as, 159 | spec.installer, 160 | spec.updater, 161 | spec.after, 162 | spec.rtp, 163 | spec.opt, 164 | spec.bufread, 165 | spec.branch, 166 | spec.tag, 167 | spec.commit, 168 | spec.lock, 169 | spec.run, 170 | spec.requires, 171 | spec.rocks, 172 | spec.config, 173 | spec.setup, 174 | spec.cmd, 175 | spec.ft, 176 | spec.keys, 177 | spec.event, 178 | spec.fn, 179 | spec.cond, 180 | spec.module, 181 | spec.module_pattern 182 | ); 183 | }; 184 | } 185 | 186 | extern class Packer { 187 | @:luaDotMethod 188 | function startup(use:(Plugin -> Void) -> Void):Void; 189 | @:luaDotMethod 190 | function sync():Void; 191 | 192 | /** 193 | Does the packer initlization and returns true if packer was not installed and the installation was performed. 194 | Takes an array of plugin specs to install. 195 | */ 196 | inline static function init(plugins:Array< Plugin >):Bool { 197 | final is_bootstrap = ensureInstalled(); 198 | final packer:Packer = lua.Lua.require("packer"); 199 | packer.startup((use) -> { 200 | for (plugin in plugins) { 201 | use(plugin); 202 | } 203 | }); 204 | if (is_bootstrap) { 205 | packer.sync(); 206 | } 207 | return is_bootstrap; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/Main.hx: -------------------------------------------------------------------------------- 1 | import vim.Api; 2 | import vim.Vimx; 3 | import plenary.Job; 4 | import vim.Vim; 5 | import vim.VimTypes; 6 | import lua.Table.create as t; 7 | 8 | using lua.NativeStringTools; 9 | using lua.Table; 10 | using vim.TableTools; 11 | using Lambda; 12 | using Test; 13 | 14 | function createSiblingFile():Void { 15 | final path = Vim.expand(ExpandString.plus(CurentFile, Head)); 16 | final currentFileName = vim.Fn.expand(ExpandString.plus(CurentFile, Tail)); 17 | vim.Ui.input({prompt: 'newFileName'}, (filename) -> { 18 | Vim.cmd("e " + path + "/" + filename); 19 | }); 20 | } 21 | 22 | inline function command(name, description, fn, ?nargs) { 23 | vim.Api.nvim_create_user_command(name, fn, { 24 | desc: description, 25 | force: true, 26 | nargs: nargs, 27 | complete: null, 28 | bang: false, 29 | range: Yes, 30 | }); 31 | } 32 | 33 | // Derive looks like this 34 | // #[derive(Debug, Clone)] 35 | function add_missing_derive(toAdd:String) { 36 | final lines = Api.nvim_buf_get_lines(CurrentBuffer, 0, -1, false).toArray(); 37 | final deriveLines = lines.filter(x -> x.contains("#[derive(")); 38 | final derives = [for (line in deriveLines) { 39 | final content = line.split("derive(")[1].split(")")[0]; 40 | final elements = content.split(",").map(x -> x.trim()); 41 | { 42 | "line": line, 43 | "elements": elements 44 | }; 45 | }]; 46 | trace(derives); 47 | final newDeriveList = derives.filter(x -> !x.elements.contains(toAdd)).map((x) -> { 48 | { 49 | "line": x.line, 50 | "elements": x.elements.concat([toAdd]) 51 | }; 52 | }); 53 | trace(newDeriveList); 54 | final newLines = lines.map(x -> { 55 | final match = newDeriveList.find(y -> y.line == x); 56 | if (match != null) { 57 | final newDerive = "#[derive(" + match.elements.join(", ") + ")]"; 58 | trace(newDerive); 59 | return newDerive; 60 | } else { 61 | return x; 62 | } 63 | }); 64 | Api.nvim_buf_set_lines(CurrentBuffer, 0, -1, false, Table.fromArray(newLines)); 65 | } 66 | 67 | function main() { 68 | vim.Api.create_user_command_completion( 69 | "HaxeCmd", 70 | (args) -> { 71 | Vim.print(args); 72 | final spellRes = Spell.check("Hello bru! Hau are you?"); 73 | Vim.print(spellRes[1].first()); 74 | vim.Ui.select(t(["a"]), {prompt: "Pick one sexy option"}, (choice, _) -> Vim.print(choice)); 75 | }, 76 | CustomLua("require'packer'.plugin_complete"), 77 | { 78 | desc: "Testing from haxe", 79 | force: true, 80 | bang: true, 81 | range: WholeFile, 82 | } 83 | ); 84 | 85 | vim.Api.nvim_create_user_command_complete_cb("HaxeCmdCompletion", (args) -> { 86 | Vim.print(args); 87 | final spellRes = Spell.check("Hello bru! Hau are you?"); 88 | Vim.print(spellRes[1].first()); 89 | vim.Ui.select(t(["a"]), {prompt: "Pick one sexy option"}, (choice, _) -> Vim.print(choice)); 90 | }, { 91 | desc: "Testing from haxe for completion callback", 92 | force: true, 93 | bang: true, 94 | nargs: Any, 95 | range: WholeFile, 96 | complete: (lead:String, full:String, x:Int) -> { 97 | Vim.print(lead, full, x); 98 | return t(["afa", "bea", lead + "bro"]); 99 | } 100 | }); 101 | 102 | Vimx.autocmd('HaxeEvent', [BufWritePost], "*.hx", "Created from haxe", () -> { 103 | var filename = Vim.expand(ExpandString.plus(CurentFile, FullPath)); 104 | Vim.print('Hello from axe', filename); 105 | return true; 106 | }); 107 | 108 | command( 109 | "OpenInGh", 110 | "Open the current file in github", 111 | args -> openInGh(switch (args.range) { 112 | case 1: ':${args.line1}'; 113 | case 2: ':${args.line1}-${args.line2}'; 114 | case _: ""; 115 | }) 116 | ); 117 | command( 118 | "CopyGhUrl", 119 | "Copy current file github URL", 120 | args -> copyGhUrl(switch (args.range) { 121 | case 1: ':${args.line1}'; 122 | case 2: ':${args.line1}-${args.line2}'; 123 | case _: ""; 124 | }) 125 | ); 126 | 127 | command( 128 | "CopyMessagesToClipboard", 129 | "Copy the n number of messages to clipboard", 130 | (args) -> copy_messages_to_clipboard(args.args), 131 | ExactlyOne 132 | ); 133 | command( 134 | "AddMissingDerive", 135 | "Add a missing derive to the current file", 136 | (args) -> add_missing_derive(args.args), 137 | ExactlyOne 138 | 139 | ); 140 | command("RustPrettier", "Format with prettier", (_) -> Vim.cmd("!prettier % -w"), None); 141 | command( 142 | "CreateSiblingFile", 143 | "Create a file next to the current one", 144 | (_) -> createSiblingFile(), 145 | None 146 | ); 147 | command( 148 | "GetPluginVersion", 149 | "Gets the git version of a installed packer plugin", 150 | (args) -> { 151 | final version = packer.Packer.get_plugin_version(args.args); 152 | Vim.print(version); 153 | Vimx.copyToClipboard(version); 154 | }, 155 | ExactlyOne 156 | ); 157 | command("Scratch", "creates a scratch buffer", (_) -> { 158 | final buffer = Api.nvim_create_buf(false, true); 159 | Api.nvim_win_set_buf(CurrentWindow, buffer); 160 | }); 161 | // final keymaps = vim.Api.nvim_buf_get_keymap(CurrentBuffer, "n"); 162 | // Vim.print(keymaps.map(x -> '${x.lhs} -> ${x.rhs} ${x.desc}')); 163 | vim.Keymap.set(Normal, "tl", nexTab, {desc: "Go to next tab", silent: true, expr: false}); 164 | vim.Keymap.set(Command, "", "", {desc: "Home in cmd", silent: true, expr: false}); 165 | vim.Keymap.set( 166 | Normal, 167 | "", 168 | ":FzfLua lines", 169 | {desc: "Search in open files", silent: true, expr: false} 170 | ); 171 | vim.Keymap.set( 172 | Normal, 173 | "sl", 174 | ":FzfLua lines", 175 | {desc: "Search [l]ines in open files", silent: true, expr: false} 176 | ); 177 | vim.Keymap.set( 178 | Normal, 179 | "", 180 | ":%s/\\v", 181 | {desc: "Search and replace whole file", silent: true, expr: false} 182 | ); 183 | // nmap("", ":b#", "Alternate file", true) 184 | vim.Keymap.set( 185 | Normal, 186 | "", 187 | ":b#", 188 | {desc: "Alternate file", silent: true, expr: false} 189 | ); 190 | // show the effects of a search / replace in a live preview window 191 | Vim.o.inccommand = "split"; 192 | } 193 | 194 | function runGh(args):Null< lua.Table< Int, String > > { 195 | if (vim.Fn.executable("gh") != 1) 196 | return null; 197 | 198 | final job = Job.make({ 199 | command: "gh", 200 | args: args, 201 | on_stderr: (args, return_val) -> { 202 | Vim.print("Job got stderr", args, return_val); 203 | } 204 | }); 205 | return job.sync(); 206 | } 207 | 208 | function openInGh(?line) { 209 | final currentFile = vim.Fn.expand(CurentFile); 210 | final curentBranch = get_branch(); 211 | runGh([ 212 | "browse", 213 | currentFile + line, 214 | "--branch", 215 | curentBranch[1] 216 | ]); 217 | } 218 | 219 | function get_branch() { 220 | var args = lua.Table.create(["rev-parse", "--abbrev-ref", "HEAD"]); 221 | final job = Job.make({ 222 | command: "git", 223 | args: args, 224 | on_stderr: (args, return_val) -> { 225 | Vim.print("Something may have failed", args, return_val); 226 | } 227 | }); 228 | return job.sync(); 229 | } 230 | 231 | function copy_messages_to_clipboard(number:String) { 232 | final cmd = "let @* = execute('%smessages')".format(number.or("")); 233 | Vim.cmd(cmd); 234 | Vim.notify('$number :messages copied to the clipboard', "info"); 235 | } 236 | 237 | function nexTab() { 238 | final pages = vim.Api.nvim_list_tabpages(); 239 | final currentTab = vim.Api.nvim_get_current_tabpage(); 240 | final nextT = pages.findNext(id -> id == currentTab); 241 | vim.Api.nvim_set_current_tabpage(nextT.or(pages[1])); 242 | } 243 | 244 | function copyGhUrl(?line) { 245 | final currentFile = vim.Fn.expand(new ExpandString(CurentFile) + RelativePath); 246 | final curentBranch = get_branch(); 247 | var lines = runGh([ 248 | "browse", 249 | currentFile + line, 250 | "--no-browser", 251 | "--branch", 252 | curentBranch[1] 253 | ]); 254 | switch (lines) { 255 | case null: 256 | Vim.print("No URL"); 257 | case _: 258 | Vim.print(lines[1]); 259 | Vimx.copyToClipboard(lines[1]); 260 | } 261 | } 262 | 263 | @:expose("setup") 264 | function setup() { 265 | main(); 266 | Vim.print("ran setup"); 267 | } 268 | -------------------------------------------------------------------------------- /tools/luaParser/LuaDoc.hx: -------------------------------------------------------------------------------- 1 | package tools.luaParser; 2 | 3 | import haxe.Json; 4 | import byte.ByteData; 5 | import hxparse.Lexer; 6 | import hxparse.ParserError.ParserError; 7 | 8 | using StringTools; 9 | using Safety; 10 | 11 | typedef Param = { 12 | final type:String; 13 | final description:String; 14 | } 15 | 16 | enum TypeToken { 17 | TFunction; 18 | Number; 19 | String; 20 | Table; 21 | Boolean; 22 | Nil; 23 | Colon; 24 | Any; 25 | TVarArgs; 26 | TIdentifier(name:String); 27 | WindowId; 28 | BufferId; 29 | Dynamic; 30 | } 31 | 32 | enum DocToken { 33 | Identifier(name:String); 34 | Description(text:String); 35 | DocType(type:TypeToken); 36 | ArrayMod; 37 | OptionalMod; 38 | Comma; 39 | CurlyOpen; 40 | CurlyClose; 41 | SquareOpen; 42 | SquareClose; 43 | Lparen; 44 | Rparen; 45 | TypeOpen; 46 | TypeClose; 47 | Pipe; 48 | SPC; 49 | EOL; 50 | } 51 | 52 | class LuaDocLexer extends Lexer implements hxparse.RuleBuilder { 53 | static var ident = "[a-zA-Z_][a-zA-Z0-9_]*"; 54 | 55 | public static var desc = @:rule [ 56 | "[^\n]*" => Description(lexer.current.ltrim()), 57 | "" => EOL 58 | ]; 59 | public static var paramDoc = @:rule [ 60 | ident => {final name = lexer.current.ltrim().rtrim(); Identifier(name);}, 61 | " " => SPC, 62 | "" => EOL, 63 | "\\.\\.\\." => Identifier("kwargs"), 64 | "\\?" => OptionalMod, 65 | ]; 66 | public static var typeDoc = @:rule [ 67 | " " => SPC, 68 | // " " => lexer.token(typeDoc), 69 | ", ?" => Comma, 70 | "\\[\\]" => ArrayMod, 71 | "\\?" => OptionalMod, 72 | "<" => TypeOpen, 73 | ">" => TypeClose, 74 | "{" => CurlyOpen, 75 | "}" => CurlyClose, 76 | "[" => SquareOpen, 77 | "]" => SquareClose, 78 | "\\(" => Lparen, 79 | "\\)" => Rparen, 80 | "\\|" => Pipe, 81 | "number" => DocType(TypeToken.Number), 82 | "string" => DocType(TypeToken.String), 83 | "table" => DocType(TypeToken.Table), 84 | "boolean" => DocType(TypeToken.Boolean), 85 | "function" => DocType(TypeToken.TFunction), 86 | "fun" => DocType(TypeToken.TFunction), 87 | "any" => DocType(Any), 88 | "window" => DocType(WindowId), 89 | "buffer" => DocType(BufferId), 90 | "object" => DocType(TypeToken.Dynamic), 91 | // Don't judge me 92 | "fun\\(\\)" => DocType(TypeToken.TFunction), 93 | "nil" => DocType(Nil), 94 | ":" => DocType(Colon), 95 | ident => DocType(TIdentifier(lexer.current)), 96 | ", ?optional" => OptionalMod, 97 | "\\.\\.\\." => DocType(TVarArgs), 98 | "" => EOL, 99 | ]; 100 | } 101 | 102 | typedef ParamDoc = { 103 | final name:String; 104 | final type:String; 105 | final description:String; 106 | final isOptional:Bool; 107 | } 108 | 109 | typedef ReturnDoc = { 110 | final type:String; 111 | final description:String; 112 | } 113 | 114 | class LuaDocParser extends hxparse.Parser< hxparse.LexerTokenSource< DocToken >, DocToken > implements hxparse.ParserBuilder { 115 | private final inputAsString:byte.ByteData; 116 | 117 | public function new(input:byte.ByteData) { 118 | inputAsString = (input); 119 | var lexer = new LuaDocLexer(input); 120 | var ts = new hxparse.LexerTokenSource(lexer, LuaDocLexer.paramDoc); 121 | super(ts); 122 | } 123 | 124 | public function dumpAtCurrent() { 125 | final pos = stream.curPos(); 126 | final max:Int = inputAsString.length - 1; 127 | Log.print2("> Last parsed token: ", this.last); 128 | Log.print2("> ", inputAsString.readString(0, max)); 129 | final cursorWidth = (pos.pmax - pos.pmin) - 1; 130 | final padding = 2; 131 | Log.print("^".lpad(" ", pos.pmin + padding) + "^".lpad(" ", cursorWidth + padding)); 132 | // Log.print("^".lpad(" ", pos.pmax + 2)); 133 | } 134 | 135 | public function parse():ParamDoc { 136 | return switch stream { 137 | case [SPC]: parse(); 138 | case [Identifier(name)]: 139 | final isOptional = if (peek(0) == OptionalMod) { 140 | junk(); 141 | true; 142 | } else { 143 | false; 144 | }; 145 | switch stream { 146 | case [EOL]: 147 | return { 148 | name: name, 149 | type: "Any", 150 | description: "", 151 | isOptional: isOptional 152 | }; 153 | case [SPC]: 154 | stream.ruleset = LuaDocLexer.typeDoc; 155 | try { 156 | final t = parseType(); 157 | stream.ruleset = LuaDocLexer.desc; 158 | if (peek(0) == SPC) { 159 | junk(); 160 | } else { 161 | Log.print("WARNING: No space after types"); 162 | } 163 | final text = parseDesc(); 164 | return { 165 | name: (isOptional ? "?" : "") + name, 166 | type: t, 167 | description: text, 168 | isOptional: isOptional, 169 | }; 170 | } 171 | catch (e:ParserError) { 172 | Log.print2("Error parsing: \n\t", e); 173 | dumpAtCurrent(); 174 | Log.print2("line", e.pos.format(inputAsString)); 175 | throw(e); 176 | } 177 | case _: 178 | trace("Expecting identifier, dup state", null); 179 | dumpAtCurrent(); 180 | throw "Unexpected token"; 181 | } 182 | } 183 | } 184 | 185 | public function parseReturn():ReturnDoc { 186 | stream.ruleset = LuaDocLexer.typeDoc; 187 | return switch stream { 188 | case [SPC]: 189 | parseReturn(); 190 | case [t = parseType()]: 191 | if (peek(0) == SPC) { 192 | junk(); 193 | } 194 | stream.ruleset = LuaDocLexer.desc; 195 | final text = parseDesc(); 196 | return {type: t, description: text}; 197 | } 198 | } 199 | 200 | public function parseType(acc = '') { 201 | return switch stream { 202 | case [Lparen, t = parseType()]: // This is ridiculous, but neovim people think it's nice 203 | Log.print("Hey within parens"); 204 | switch stream { 205 | case [OptionalMod]: '?$t'; 206 | case [Rparen]: 207 | '$t'; 208 | case _: throw(new ParseErrorDetail(stream.curPos(), 'Unclosed parenthesis')); 209 | } 210 | case [DocType(Table)]: 211 | switch stream { 212 | case [TypeOpen, t = parseTypeArgs(), TypeClose]: 213 | parseType('Table<$t>'); 214 | case _: parseType('Table'); 215 | } 216 | case [CurlyOpen, t = parseObj(new Map())]: 217 | parseType('$t'); 218 | case [DocType(TFunction)]: 219 | switch stream { 220 | case [Lparen, t = parseFunctionArgs()]: '$t'; 221 | case _: parseType('Function'); 222 | } 223 | case [Pipe]: 224 | var e = parseEither(acc); 225 | parseType(e); 226 | case [DocType(t)]: 227 | switch stream { 228 | case [ArrayMod]: 'Array<$t>'; 229 | case [OptionalMod]: '?$t'; 230 | case [Pipe]: 231 | var e = parseEither('$t'); 232 | parseType(e); 233 | case _: 234 | '$t'; 235 | } 236 | case _: acc; 237 | } 238 | } 239 | 240 | public function parseTuple(size, arg) { 241 | return switch stream { 242 | case [Comma, t = parseType()]: 243 | parseTuple(size + 1, arg + ',' + t); 244 | case [CurlyClose]: 245 | 'Vector$size<$arg>'; 246 | } 247 | } 248 | 249 | public function parseObj(acc:Map< String, String >) { 250 | return switch stream { 251 | case [DocType(t)]: 252 | parseTuple(1, '$t'); 253 | case [Identifier(name), DocType(Colon), t = parseType()]: 254 | acc.set(name, t); 255 | switch stream { 256 | case [CurlyClose]: 'Obj(${Json.stringify(acc)})'; 257 | case [Comma]: 258 | parseObj(acc); 259 | } 260 | } 261 | } 262 | 263 | public function parseFunctionArgs() { 264 | return switch stream { 265 | case [DocType(TIdentifier(name)), DocType(Colon)]: 266 | // We should discard spaces that may be separating argument and type 267 | if (peek(0) == SPC) 268 | junk(); 269 | final t = parseType(); 270 | // account for potential return type 271 | final returnType = switch [peek(0), peek(1), peek(2)] { 272 | case [Rparen, DocType(Colon), SPC]: 273 | // I need to manually consume them because we are not in a stream switch 274 | junk(); 275 | junk(); 276 | junk(); 277 | parseType(); 278 | case [Rparen, DocType(Colon), _]: 279 | junk(); 280 | junk(); 281 | parseType(); 282 | case _: 'Void'; 283 | } 284 | 'FunctionWithArgs($name: $t):$returnType'; 285 | } 286 | } 287 | 288 | public function parseEither(left) { 289 | return switch stream { 290 | case [DocType(t)]: 291 | switch stream { 292 | case [Pipe, e = parseType()]: 'Either<$left, $e>'; 293 | case _: 294 | 'Either<$left, $t>'; 295 | } 296 | }; 297 | } 298 | 299 | public function parseTypeArgs() { 300 | Log.print("Hey table args parsing"); 301 | return switch stream { 302 | case [DocType(t)]: 303 | if (peek(0) == Comma) { 304 | junk(); 305 | t + ',' + parseTypeArgs(); 306 | } else { 307 | t + ''; 308 | } 309 | }; 310 | } 311 | 312 | public function parseDesc() { 313 | return switch stream { 314 | case [Description(text), EOL]: 315 | text; 316 | // Yeah, seems description is optional too 317 | case [EOL]: 318 | ''; 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/vim/VimTypes.hx: -------------------------------------------------------------------------------- 1 | package vim; 2 | 3 | import haxe.Constraints.Function; 4 | import lua.Table; 5 | import haxe.extern.EitherType; 6 | import lua.NativeStringTools; 7 | 8 | typedef ArgComplete = vim.types.ArgComplete; 9 | abstract Tabpage(Int) {} 10 | abstract Client(Int) {} 11 | abstract Group(Int) {} 12 | 13 | enum FunctionOrString { 14 | Cb(cb:Function); 15 | Str(cmd:String); 16 | } 17 | 18 | abstract GroupOpts(Table< String, Bool >) { 19 | public inline function new(clear:Bool) { 20 | this = Table.create(null, {clear: clear}); 21 | } 22 | 23 | @:from 24 | public inline static function fromObj(arg:{clear:Bool}) { 25 | return new GroupOpts(arg.clear); 26 | } 27 | } 28 | 29 | @:arrayAccess abstract LuaArray< T >(lua.Table< Int, T >) from lua.Table< Int, T > to lua.Table< Int, T > { 30 | // Can this be converted into a macro to avoid even calling fromArray ? 31 | @:from 32 | public static function from< T >(arr:Array< T >):LuaArray< T > { 33 | return lua.Table.fromArray(arr); 34 | } 35 | 36 | public inline function map(fn) { 37 | return Vim.tbl_map(fn, this); 38 | } 39 | } 40 | 41 | @:forward // automatically get fields from underlying type 42 | abstract LuaObj< T >(T) { 43 | @:from 44 | public static inline function fromType< T >(obj:T):LuaObj< T > { 45 | @:nullSafety(Off) untyped obj.__fields__ = null; 46 | @:nullSafety(Off) lua.Lua.setmetatable(cast obj, null); 47 | return cast obj; 48 | } 49 | 50 | // could add a @:from lua table, but that will automatically accept/convert any lua table, probably want to make it explicit 51 | @:to 52 | public function to():lua.Table< String, Dynamic > { 53 | return cast this; 54 | } 55 | } 56 | 57 | typedef MapInfo = { 58 | /** The {lhs} of the mapping as it would be typed */ 59 | final lhs:String; 60 | 61 | /** The {lhs} of the mapping as raw bytes */ 62 | final lhsraw:String; 63 | 64 | /** The {lhs} of the mapping as raw bytes, alternate form, only present when it differs from "lhsraw" */ 65 | final lhsrawalt:String; 66 | 67 | /** The {rhs} of the mapping as typed. */ 68 | final rhs:String; 69 | 70 | /** 1 for a |:map-silent| mapping, else 0. */ 71 | final silent:Int; 72 | 73 | /** 1 if the {rhs} of the mapping is not remappable. */ 74 | final noremap:Int; 75 | 76 | /** 1 if mapping was defined with