├── LICENSE ├── ReadMe.md ├── autoload └── tealmaker.vim ├── lua └── tealmaker │ ├── init.lua │ └── util.lua ├── plugin └── tealmaker.vim ├── teal ├── globals.d.tl └── tealmaker │ ├── init.tl │ └── util.tl └── tlconfig.lua /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Steve Vermeulen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | 2 | ## Nvim Teal Maker 3 | 4 | This plugin adds support for writing neovim plugins/configuration in [teal](https://github.com/teal-language/tl) (ie. strongly typed lua) instead of (or in addition to) lua 5 | 6 | ## Requirements 7 | 8 | This plugin requires that both [tl](https://github.com/teal-language/tl) and [cyan](https://github.com/teal-language/cyan) have been installed via [luarocks](https://luarocks.org/), and are both available on the PATH 9 | 10 | ## Overview 11 | 12 | This plugin follows the conventions that already exist in neovim for both lua and python. On startup, neovim will automatically modify the lua `package.path` value, so that any lua `require` statements will find any `lua` files inside any `/lua` directories on the neovim `runtimepath`. Neovim also supports a `/python` directory which works similarly. This plugin follows this same convention by adding support for a `/teal` directory on the runtimepath as well. 13 | 14 | This same approach was also done for [moonscript](https://moonscript.org/) in the [nvim-moonmaker](https://github.com/svermeulen/nvim-moonmaker) plugin 15 | 16 | ## Quick Start 17 | 18 | 1. Install the plugin using whatever neovim plugin manager you prefer. 19 | 20 | 2. Place some `tl` files inside a `/teal` directory underneath one of the directories on the neovim `runtimepath` (see `:h runtimepath` for details). If you're not making a plugin and instead want to just write some neovim configuration in `teal`, you can also just add a `/teal` directory alongside your `init.lua` / `init.vim` 21 | 22 | 3. Place a file named `tlconfig.lua` alongside the `/teal` directory with the following contents. See documentation for [tl](https://github.com/teal-language/tl) / [cyan](https://github.com/teal-language/cyan) for more details on this config file. 23 | 24 | ``` 25 | return { 26 | build_dir = "lua", 27 | source_dir = "teal", 28 | include_dir = { "teal" } 29 | } 30 | ``` 31 | 32 | 5. Execute `:TealBuild` 33 | 34 | 6. Your `tl` files inside the `/teal` directory should now have been compiled to lua and placed where neovim expects them (the `/lua` directory) 35 | 36 | Notes: 37 | 38 | * In addition to `:TealBuild`, there are several other ways to trigger a teal build: 39 | 40 | * Directly from lua/teal by importing `tealmaker`: 41 | 42 | ``` 43 | local verbose_output = false 44 | require("tealmaker").build_all(verbose_output) 45 | ``` 46 | 47 | * By calling `tealmaker#BuildAll` from VimL: 48 | 49 | ``` 50 | local verbose_output = 0 51 | call tealmaker#BuildAll(verbose_output) 52 | ``` 53 | 54 | ## init.vim example 55 | 56 | If you'd like to write your neovim configuration in teal, you might do something like this: 57 | 58 | * Create a new `init.vim` with contents: 59 | 60 | ``` 61 | call plug#begin(stdpath('data') . '/plugged') 62 | Plug 'svermeulen/nvim-teal-maker' 63 | call plug#end() 64 | ``` 65 | 66 | * Note that we assume you already have [vim-plug](https://github.com/junegunn/vim-plug) installed. Of course, any other plugin manager would be fine as well. 67 | * Add a `/teal` directory next to your `init.vim` 68 | * Place a file named `my_config.tl` inside `/teal` with some neovim configuration. As a random example: 69 | 70 | ``` 71 | require("vim") 72 | 73 | vim.o.ignorecase = true 74 | vim.o.smartcase = true 75 | vim.o.incsearch = true 76 | 77 | vim.o.hidden = true 78 | 79 | vim.o.history = 5000 80 | 81 | vim.o.tabstop = 4 82 | vim.o.shiftwidth = vim.o.tabstop 83 | vim.g.mapleader = " " 84 | 85 | vim.keymap.set('n', 'q', ':qa') 86 | vim.keymap.set('n', 'hw', function() 87 | print("hello world") 88 | end) 89 | ``` 90 | 91 | * Also add a `tlconfig.lua` file as described in quick start section above 92 | * Open neovim, run `:PlugInstall`, then execute `:TealBuild`. This should result in errors of the form: 93 | 94 | ``` 95 | Error 11 type errors in teal/my_config.tl 96 | ... teal/my_config.tl 2:1 97 | ... 2 | vim.o.ignorecase = true 98 | ... | ^^^ 99 | ... | unknown variable: vim 100 | ``` 101 | 102 | * This is because teal needs type definitions for the `vim` object that we are using. We can solve this problem by downloading the `vim.d.tl` type definition file from the [teal-types](https://github.com/teal-language/teal-types) repo [here](https://github.com/teal-language/teal-types/blob/master/types/neovim/vim.d.tl) and placing it inside our `/teal` directory. 103 | 104 | * Open neovim and execute `:TealBuild` again. The build should pass now with output: 105 | 106 | ``` 107 | Info Type checked teal/my_config.tl 108 | Info Wrote lua/my_config.lua 109 | ``` 110 | 111 | * Next, let's make sure that our teal file automatically gets built on startup. Let's change our `init.vim` to the following: 112 | 113 | ``` 114 | call plug#begin(stdpath('data') . '/plugged') 115 | Plug 'svermeulen/nvim-teal-maker' 116 | call plug#end() 117 | 118 | call tealmaker#buildAll() 119 | 120 | lua require('my_config') 121 | ``` 122 | 123 | * Note that we are calling `call tealmaker#buildAll()` before the call to `lua require('my_config')`. This is important, since otherwise the `require` would load the previously compiled version. 124 | 125 | * With the above set up, we can now directly modify our `my_config.tl` file and the corresponding lua files will be automatically built the next time neovim is started. Try changing something in `my_config.tl`, restarting neovim, and verifying that this works 126 | 127 | ## Default Options 128 | 129 | * `let g:TealMaker_Prune = 0` 130 | * Set this to `1` to automatically delete any lua files that don't have corresponding teal files. However - requires a version of [cyan](https://github.com/teal-language/cyan) that has `--prune` option (version must be > `0.1.0`). Also note that when this option is enabled, any lua files inside the `/teal` directory will be automatically copied to `/lua` as well, since you can't place lua files inside `/lua` directly with this option enabled, and it can be common to have some source files in lua. 131 | 132 | ## Tips 133 | 134 | * If you are writing a plugin, you don't need to depend on this plugin, since you can just include the compiled lua files (which is exactly what this plugin does) 135 | 136 | * If your plugin contains multiple `tl` files and you want to avoid polluting the root require path, you can put your teal files into subdirectories underneath the `teal` folder. Then you can use `require("dir1.dir2.filename")` to use them from other `tl` files 137 | 138 | -------------------------------------------------------------------------------- /autoload/tealmaker.vim: -------------------------------------------------------------------------------- 1 | 2 | function! tealmaker#BuildAll(...) 3 | let verbose = len(a:000) == 0 ? 0 : a:1 4 | if verbose 5 | lua require("tealmaker").build_all(true) 6 | else 7 | lua require("tealmaker").build_all(false) 8 | endif 9 | endfunction 10 | -------------------------------------------------------------------------------- /lua/tealmaker/init.lua: -------------------------------------------------------------------------------- 1 | local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local pcall = _tl_compat and _tl_compat.pcall or pcall; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table 2 | local util = require("tealmaker.util") 3 | 4 | local tealmaker = {} 5 | 6 | local function copy_lua_files(project_dir, verbose) 7 | local teal_dir = util.join_paths(project_dir, "teal") 8 | 9 | if vim.fn.isdirectory(teal_dir) ~= 1 then 10 | return 11 | end 12 | 13 | local output_dir = util.join_paths(project_dir, "lua") 14 | 15 | for _, source_path in ipairs(vim.fn.globpath(teal_dir, "**/*.lua", 0, 1)) do 16 | local relative_path = source_path:sub(#teal_dir + 2) 17 | local output_path = util.join_paths(output_dir, relative_path) 18 | 19 | util.make_missing_directories_in_path(output_path) 20 | vim.fn.writefile(vim.fn.readfile(source_path), output_path) 21 | 22 | if verbose then 23 | print(string.format("Unpruned '%s'", relative_path)) 24 | end 25 | end 26 | end 27 | 28 | local function should_prune_files() 29 | local ok, result = pcall(function() 30 | return vim.api.nvim_get_var("TealMaker_Prune") 31 | end) 32 | 33 | return ok and result 34 | end 35 | 36 | local function try_get_most_recent_modification_time(dir, extension) 37 | local most_recent_time = nil 38 | 39 | for _, path in ipairs(vim.fn.globpath(dir, "**/*." .. extension, 0, 1)) do 40 | local changetime = vim.fn.getftime(path) 41 | 42 | if most_recent_time == nil or changetime > most_recent_time then 43 | most_recent_time = changetime 44 | end 45 | end 46 | 47 | return most_recent_time 48 | end 49 | 50 | local function should_build_project(project_dir) 51 | local teal_dir = util.join_paths(project_dir, "teal") 52 | 53 | if vim.fn.isdirectory(teal_dir) ~= 1 then 54 | return false 55 | end 56 | 57 | local teal_change_time = try_get_most_recent_modification_time(teal_dir, "tl") 58 | 59 | if teal_change_time == nil then 60 | return false 61 | end 62 | 63 | local lua_dir = util.join_paths(project_dir, "lua") 64 | local lua_change_time = try_get_most_recent_modification_time(lua_dir, "lua") 65 | 66 | if lua_change_time == nil then 67 | return true 68 | end 69 | 70 | return teal_change_time > lua_change_time 71 | end 72 | 73 | local function build_project(project_dir, verbose) 74 | 75 | if not should_build_project(project_dir) then 76 | if verbose then 77 | print(string.format("Skipped teal project at '%s' because teal files have not changed since last build", project_dir)) 78 | end 79 | 80 | return 81 | end 82 | 83 | local all_output = {} 84 | 85 | local build_args = { "cyan", "build" } 86 | local should_prune = should_prune_files() 87 | 88 | if should_prune then 89 | table.insert(build_args, "--prune") 90 | end 91 | 92 | local job_id = vim.fn.jobstart(build_args, { 93 | cwd = project_dir, 94 | stdout_buffered = true, 95 | stderr_buffered = true, 96 | on_stdout = function(_, data) 97 | if data then 98 | for _, line in ipairs(data) do 99 | table.insert(all_output, line) 100 | end 101 | end 102 | end, 103 | on_stderr = function(_, data) 104 | if data then 105 | for _, line in ipairs(data) do 106 | table.insert(all_output, line) 107 | end 108 | end 109 | end, 110 | }) 111 | 112 | if job_id <= 0 then 113 | error("Failed to start 'cyan' to build teal files. Is it installed?") 114 | end 115 | 116 | local results = vim.fn.jobwait({ job_id }) 117 | 118 | local function get_all_output() 119 | local raw_output = table.concat(all_output, '\n') 120 | 121 | local text_output = raw_output:gsub('\x1b%[%d+;%d+;%d+;%d+;%d+m', ''): 122 | gsub('\x1b%[%d+;%d+;%d+;%d+m', ''): 123 | gsub('\x1b%[%d+;%d+;%d+m', ''): 124 | gsub('\x1b%[%d+;%d+m', ''): 125 | gsub('\x1b%[%d+m', '') 126 | return text_output 127 | end 128 | 129 | if results[1] ~= 0 then 130 | print(string.format("Build failed for project at '%s'\n%s", project_dir, get_all_output())) 131 | else 132 | if verbose then 133 | print(string.format("Successfully built project at '%s'\n%s", project_dir, get_all_output())) 134 | end 135 | 136 | if should_prune then 137 | 138 | 139 | copy_lua_files(project_dir, verbose) 140 | end 141 | end 142 | end 143 | 144 | function tealmaker.build_all(verbose) 145 | 146 | local ok, is_exe = pcall(vim.fn.executable, "cyan") 147 | 148 | if not ok or is_exe ~= 1 then 149 | error("Could not find 'cyan' on path. This is necessary to build teal files") 150 | end 151 | 152 | local plugin_paths = vim.api.nvim_list_runtime_paths() 153 | local our_plugin_name = "nvim-teal-maker" 154 | 155 | for _, plugin_path in ipairs(plugin_paths) do 156 | 157 | if plugin_path:sub(#plugin_path + 1 - #our_plugin_name) ~= our_plugin_name then 158 | local config_path = util.join_paths(plugin_path, "tlconfig.lua") 159 | 160 | if vim.fn.filereadable(config_path) == 1 then 161 | build_project(plugin_path, verbose) 162 | end 163 | end 164 | end 165 | end 166 | 167 | return tealmaker -------------------------------------------------------------------------------- /lua/tealmaker/util.lua: -------------------------------------------------------------------------------- 1 | local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local string = _tl_compat and _tl_compat.string or string 2 | local util = {} 3 | 4 | function util.join_paths(left, right) 5 | local result = left 6 | local last_char = left:sub(-1) 7 | 8 | if last_char ~= '/' and last_char ~= '\\' then 9 | result = result .. '/' 10 | end 11 | 12 | result = result .. right 13 | return result 14 | end 15 | 16 | function util.get_directory(path) 17 | return path:match('^(.*)[\\/][^\\/]*$') 18 | end 19 | 20 | function util.make_missing_directories_in_path(path) 21 | local dir_path = util.get_directory(path) 22 | vim.fn.mkdir(dir_path, 'p') 23 | end 24 | 25 | return util -------------------------------------------------------------------------------- /plugin/tealmaker.vim: -------------------------------------------------------------------------------- 1 | 2 | command! -bang -nargs=0 TealBuild call tealmaker#BuildAll(1) 3 | -------------------------------------------------------------------------------- /teal/globals.d.tl: -------------------------------------------------------------------------------- 1 | 2 | -- We assume here that vim type definitions are on the lua path 3 | -- See here: https://github.com/teal-language/teal-types 4 | require("vim") 5 | -------------------------------------------------------------------------------- /teal/tealmaker/init.tl: -------------------------------------------------------------------------------- 1 | 2 | local util = require("tealmaker.util") 3 | 4 | local tealmaker = {} 5 | 6 | local function copy_lua_files(project_dir:string, verbose:boolean) 7 | local teal_dir = util.join_paths(project_dir, "teal") 8 | 9 | if vim.fn.isdirectory(teal_dir) ~= 1 then 10 | return 11 | end 12 | 13 | local output_dir = util.join_paths(project_dir, "lua") 14 | 15 | for _, source_path in ipairs(vim.fn.globpath(teal_dir, "**/*.lua", 0, 1)) do 16 | local relative_path = source_path:sub(#teal_dir + 2) 17 | local output_path = util.join_paths(output_dir, relative_path) 18 | 19 | util.make_missing_directories_in_path(output_path) 20 | vim.fn.writefile(vim.fn.readfile(source_path), output_path) 21 | 22 | if verbose then 23 | print(string.format("Unpruned '%s'", relative_path)) 24 | end 25 | end 26 | end 27 | 28 | local function should_prune_files():boolean 29 | local ok, result = pcall(function():boolean 30 | return vim.api.nvim_get_var("TealMaker_Prune") as boolean 31 | end) 32 | 33 | return ok and result 34 | end 35 | 36 | local function try_get_most_recent_modification_time(dir:string, extension:string):integer 37 | local most_recent_time:integer = nil 38 | 39 | for _, path in ipairs(vim.fn.globpath(dir, "**/*." .. extension, 0, 1)) do 40 | local changetime = vim.fn.getftime(path) 41 | 42 | if most_recent_time == nil or changetime > most_recent_time then 43 | most_recent_time = changetime 44 | end 45 | end 46 | 47 | return most_recent_time 48 | end 49 | 50 | local function should_build_project(project_dir:string):boolean 51 | local teal_dir = util.join_paths(project_dir, "teal") 52 | 53 | if vim.fn.isdirectory(teal_dir) ~= 1 then 54 | return false 55 | end 56 | 57 | local teal_change_time = try_get_most_recent_modification_time(teal_dir, "tl") 58 | 59 | if teal_change_time == nil then 60 | return false 61 | end 62 | 63 | local lua_dir = util.join_paths(project_dir, "lua") 64 | local lua_change_time = try_get_most_recent_modification_time(lua_dir, "lua") 65 | 66 | if lua_change_time == nil then 67 | return true 68 | end 69 | 70 | return teal_change_time > lua_change_time 71 | end 72 | 73 | local function build_project(project_dir:string, verbose:boolean) 74 | 75 | if not should_build_project(project_dir) then 76 | if verbose then 77 | print(string.format("Skipped teal project at '%s' because teal files have not changed since last build", project_dir)) 78 | end 79 | 80 | return 81 | end 82 | 83 | local all_output:{string} = {} 84 | 85 | local build_args = {"cyan", "build"} 86 | local should_prune = should_prune_files() 87 | 88 | if should_prune then 89 | table.insert(build_args, "--prune") 90 | end 91 | 92 | local job_id = vim.fn.jobstart(build_args, { 93 | cwd = project_dir, 94 | stdout_buffered = true, 95 | stderr_buffered = true, 96 | on_stdout = function(_:integer, data:{string}) 97 | if data then 98 | for _, line in ipairs(data) do 99 | table.insert(all_output, line) 100 | end 101 | end 102 | end, 103 | on_stderr = function(_:integer, data:{string}) 104 | if data then 105 | for _, line in ipairs(data) do 106 | table.insert(all_output, line) 107 | end 108 | end 109 | end, 110 | }) 111 | 112 | if job_id <= 0 then 113 | error("Failed to start 'cyan' to build teal files. Is it installed?") 114 | end 115 | 116 | local results = vim.fn.jobwait({job_id}) 117 | 118 | local function get_all_output():string 119 | local raw_output = table.concat(all_output, '\n') 120 | -- Cyan adds lot of ansi color codes which vim can't display so let's remove those 121 | local text_output = raw_output:gsub('\x1b%[%d+;%d+;%d+;%d+;%d+m','') 122 | :gsub('\x1b%[%d+;%d+;%d+;%d+m','') 123 | :gsub('\x1b%[%d+;%d+;%d+m','') 124 | :gsub('\x1b%[%d+;%d+m','') 125 | :gsub('\x1b%[%d+m','') 126 | return text_output 127 | end 128 | 129 | if results[1] ~= 0 then 130 | print(string.format("Build failed for project at '%s'\n%s", project_dir, get_all_output())) 131 | else 132 | if verbose then 133 | print(string.format("Successfully built project at '%s'\n%s", project_dir, get_all_output())) 134 | end 135 | 136 | if should_prune then 137 | -- Unfortunately, cyan --prune deletes all lua files in the output directory, so we need to copy them back 138 | -- More info here: https://github.com/teal-language/cyan/issues/19 139 | copy_lua_files(project_dir, verbose) 140 | end 141 | end 142 | end 143 | 144 | function tealmaker.build_all(verbose:boolean):boolean 145 | 146 | local ok, is_exe = pcall(vim.fn.executable, "cyan") 147 | 148 | if not ok or is_exe ~= 1 then 149 | error("Could not find 'cyan' on path. This is necessary to build teal files") 150 | end 151 | 152 | local plugin_paths = vim.api.nvim_list_runtime_paths() 153 | local our_plugin_name = "nvim-teal-maker" 154 | 155 | for _, plugin_path in ipairs(plugin_paths) do 156 | -- Skip ourselves to avoid the performance hit there 157 | if plugin_path:sub(#plugin_path + 1 - #our_plugin_name) ~= our_plugin_name then 158 | local config_path = util.join_paths(plugin_path, "tlconfig.lua") 159 | 160 | if vim.fn.filereadable(config_path) == 1 then 161 | build_project(plugin_path, verbose) 162 | end 163 | end 164 | end 165 | end 166 | 167 | return tealmaker 168 | 169 | -------------------------------------------------------------------------------- /teal/tealmaker/util.tl: -------------------------------------------------------------------------------- 1 | 2 | local util = {} 3 | 4 | function util.join_paths(left:string, right:string):string 5 | local result = left 6 | local last_char = left:sub(-1) 7 | 8 | if last_char ~= '/' and last_char ~= '\\' then 9 | result = result .. '/' 10 | end 11 | 12 | result = result .. right 13 | return result 14 | end 15 | 16 | function util.get_directory(path:string):string 17 | return path:match('^(.*)[\\/][^\\/]*$') 18 | end 19 | 20 | function util.make_missing_directories_in_path(path:string) 21 | local dir_path = util.get_directory(path) 22 | vim.fn.mkdir(dir_path, 'p') 23 | end 24 | 25 | return util 26 | -------------------------------------------------------------------------------- /tlconfig.lua: -------------------------------------------------------------------------------- 1 | return { 2 | build_dir = "lua", 3 | source_dir = "teal", 4 | include_dir = { "teal" }, 5 | global_env_def = "globals" 6 | } 7 | --------------------------------------------------------------------------------