├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── FUNDING.yml └── workflows │ └── test.yaml ├── .gitignore ├── .ignore ├── .nfnl.fnl ├── .nrepl-port ├── .nvim.fnl ├── .nvim.lua ├── README.md ├── UNLICENSE ├── docs └── api │ └── nfnl │ ├── api.md │ ├── callback.md │ ├── compile.md │ ├── config.md │ ├── core.md │ ├── fs.md │ ├── init.md │ ├── macros.md │ ├── macros │ └── aniseed.md │ ├── module.md │ ├── notify.md │ ├── nvim.md │ ├── repl.md │ └── string.md ├── fnl ├── nfnl │ ├── api.fnl │ ├── callback.fnl │ ├── compile.fnl │ ├── config.fnl │ ├── core.fnl │ ├── fs.fnl │ ├── gc.fnl │ ├── header.fnl │ ├── init.fnl │ ├── macros.fnl │ ├── macros │ │ └── aniseed.fnl │ ├── module.fnl │ ├── notify.fnl │ ├── nvim.fnl │ ├── repl.fnl │ └── string.fnl └── spec │ └── nfnl │ ├── callback_spec.fnl │ ├── compile_spec.fnl │ ├── config_spec.fnl │ ├── core_spec.fnl │ ├── e2e_spec.fnl │ ├── fs_spec.fnl │ ├── macros │ └── aniseed_spec.fnl │ ├── macros_spec.fnl │ ├── module_spec.fnl │ ├── repl_spec.fnl │ └── string_spec.fnl ├── lua ├── nfnl │ ├── api.lua │ ├── callback.lua │ ├── compile.lua │ ├── config.lua │ ├── core.lua │ ├── fennel.lua │ ├── fs.lua │ ├── gc.lua │ ├── header.lua │ ├── init.lua │ ├── module.lua │ ├── notify.lua │ ├── nvim.lua │ ├── repl.lua │ └── string.lua └── spec │ └── nfnl │ ├── callback_spec.lua │ ├── compile_spec.lua │ ├── config_spec.lua │ ├── core_spec.lua │ ├── e2e_spec.lua │ ├── fs_spec.lua │ ├── macros │ └── aniseed_spec.lua │ ├── macros_spec.lua │ ├── module_spec.lua │ ├── repl_spec.lua │ └── string_spec.lua ├── mise.toml ├── nfnl ├── plugin ├── nfnl.fnl └── nfnl.lua └── script ├── bootstrap ├── bootstrap-dev ├── embed ├── fennel.bb ├── fennel.lua ├── fenneldoc.lua ├── fetch-fennel ├── render-api-documentation ├── setup-test-deps └── test /.gitattributes: -------------------------------------------------------------------------------- 1 | lua/**/*.lua linguist-generated 2 | lua/nfnl/fennel.lua linguist-vendored 3 | script/fennel.lua linguist-vendored 4 | script/fenneldoc.lua linguist-vendored 5 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ollie@oli.me.uk. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Olical 2 | ko_fi: olical 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macOS-latest] 10 | 11 | # TODO Reinstate "v0.10.4" once it works through mise again. 12 | neovim-version: ["v0.9.5", "stable", "nightly"] 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Mise 20 | uses: jdx/mise-action@v2 21 | 22 | - name: Cache 23 | uses: actions/cache@v4 24 | with: 25 | path: | 26 | .test-config/nvim/pack 27 | key: nfnl-${{ hashFiles('script/setup-test-deps') }} 28 | restore-keys: nfnl- 29 | 30 | - name: Test 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | run: | 34 | mise run -t neovim@${{ matrix.neovim-version }} test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.clj-kondo/ 2 | /.test-config/ 3 | -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | lua/**/*.lua 2 | api/ 3 | -------------------------------------------------------------------------------- /.nfnl.fnl: -------------------------------------------------------------------------------- 1 | {:verbose true} 2 | -------------------------------------------------------------------------------- /.nrepl-port: -------------------------------------------------------------------------------- 1 | 38253 -------------------------------------------------------------------------------- /.nvim.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload} (require :nfnl.module)) 2 | (local reload (autoload :plenary.reload)) 3 | (local notify (autoload :nfnl.notify)) 4 | 5 | (vim.api.nvim_set_keymap 6 | :n "pt" "PlenaryTestFile" {:desc "Run the current test file with plenary."}) 7 | 8 | (vim.api.nvim_set_keymap 9 | :n "pT" "PlenaryBustedDirectory lua/spec/" {:desc "Run all tests with plenary."}) 10 | 11 | (vim.api.nvim_set_keymap 12 | :n "pr" "" 13 | {:desc "Reload the nfnl modules." 14 | :callback (fn [] 15 | (notify.info "Reloading...") 16 | (reload.reload_module "nfnl") 17 | (require :nfnl) 18 | (notify.info "Done!"))}) 19 | 20 | -------------------------------------------------------------------------------- /.nvim.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] .nvim.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local reload = autoload("plenary.reload") 5 | local notify = autoload("nfnl.notify") 6 | vim.api.nvim_set_keymap("n", "pt", "PlenaryTestFile", {desc = "Run the current test file with plenary."}) 7 | vim.api.nvim_set_keymap("n", "pT", "PlenaryBustedDirectory lua/spec/", {desc = "Run all tests with plenary."}) 8 | local function _2_() 9 | notify.info("Reloading...") 10 | reload.reload_module("nfnl") 11 | require("nfnl") 12 | return notify.info("Done!") 13 | end 14 | return vim.api.nvim_set_keymap("n", "pr", "", {desc = "Reload the nfnl modules.", callback = _2_}) 15 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /docs/api/nfnl/api.md: -------------------------------------------------------------------------------- 1 | # Api.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`compile-all-files`](#compile-all-files) 6 | - [`compile-file`](#compile-file) 7 | - [`dofile`](#dofile) 8 | 9 | ## `compile-all-files` 10 | Function signature: 11 | 12 | ``` 13 | (compile-all-files dir) 14 | ``` 15 | 16 | Compiles all files in the given dir (optional), defaulting to the current working directory. Returns a sequential table with each of the files compilation result. 17 | 18 | Will do nothing if you execute it on a directory that doesn't contain an nfnl configuration file. 19 | 20 | Also displays all results via the notify system. 21 | 22 | ## `compile-file` 23 | Function signature: 24 | 25 | ``` 26 | (compile-file {:dir dir :path path}) 27 | ``` 28 | 29 | Compiles a file into the matching Lua file. Returns the compilation result. Takes an optional `dir` key that changes the working directory. 30 | 31 | Will do nothing if you execute it on a directory that doesn't contain an nfnl configuration file. 32 | 33 | Also displays all results via the notify system. 34 | 35 | ## `dofile` 36 | Function signature: 37 | 38 | ``` 39 | (dofile file) 40 | ``` 41 | 42 | Just like :luafile, takes a Fennel file path (optional, defaults to '%', runs it through expand) and executes it from disk. However! This doesn't compile the Fennel! It maps the Fennel path to the matching Lua file already in your file system and executes that with Lua's built in dofile. So you need to have already written your .fnl and had nfnl compile that to a .lua for this to work, it's just a convinience function for you to call directly or through the :NfnlFile command. 43 | 44 | 45 | 47 | -------------------------------------------------------------------------------- /docs/api/nfnl/callback.md: -------------------------------------------------------------------------------- 1 | # Callback.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`fennel-filetype-callback`](#fennel-filetype-callback) 6 | - [`supported-path?`](#supported-path) 7 | 8 | ## `fennel-filetype-callback` 9 | Function signature: 10 | 11 | ``` 12 | (fennel-filetype-callback ev) 13 | ``` 14 | 15 | Called whenever we enter a Fennel file. It walks up the tree to find a 16 | .nfnl.fnl (which can contain configuration). If found, we initialise the 17 | compiler autocmd for the directory containing the .nfnl.fnl file. 18 | 19 | This allows us to edit multiple projects in different directories with 20 | different .nfnl.fnl configuration, wonderful! 21 | 22 | ## `supported-path?` 23 | Function signature: 24 | 25 | ``` 26 | (supported-path? file-path) 27 | ``` 28 | 29 | Returns true if we can work with the given path. Right now we support a path if it's a string and it doesn't start with a protocol segment like fugitive://... 30 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /docs/api/nfnl/compile.md: -------------------------------------------------------------------------------- 1 | # Compile.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`all-files`](#all-files) 6 | - [`into-file`](#into-file) 7 | - [`into-string`](#into-string) 8 | 9 | ## `all-files` 10 | Function signature: 11 | 12 | ``` 13 | (all-files {:cfg cfg :root-dir root-dir &as opts}) 14 | ``` 15 | 16 | **Undocumented** 17 | 18 | ## `into-file` 19 | Function signature: 20 | 21 | ``` 22 | (into-file {:_root-dir _root-dir :_source _source :batch? batch? :cfg cfg :path path &as opts}) 23 | ``` 24 | 25 | **Undocumented** 26 | 27 | ## `into-string` 28 | Function signature: 29 | 30 | ``` 31 | (into-string {:batch? batch? :cfg cfg :path path :root-dir root-dir :source source &as opts}) 32 | ``` 33 | 34 | **Undocumented** 35 | 36 | 37 | 39 | -------------------------------------------------------------------------------- /docs/api/nfnl/config.md: -------------------------------------------------------------------------------- 1 | # Config.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`cfg-fn`](#cfg-fn) 6 | - [`config-file-path?`](#config-file-path) 7 | - [`default`](#default) 8 | - [`find`](#find) 9 | - [`find-and-load`](#find-and-load) 10 | - [`path-dirs`](#path-dirs) 11 | 12 | ## `cfg-fn` 13 | Function signature: 14 | 15 | ``` 16 | (cfg-fn t opts) 17 | ``` 18 | 19 | Builds a cfg fetcher for the config table t. Returns a function that takes a 20 | path sequential table, it looks up the value from the config with core.get-in 21 | and falls back to a matching value in (default) if not found. 22 | 23 | ## `config-file-path?` 24 | Function signature: 25 | 26 | ``` 27 | (config-file-path? path) 28 | ``` 29 | 30 | **Undocumented** 31 | 32 | ## `default` 33 | Function signature: 34 | 35 | ``` 36 | (default opts) 37 | ``` 38 | 39 | Returns the default configuration that you should base your custom 40 | configuration on top of. Feel free to call this with no arguments and merge 41 | your changes on top. You can override opts.root-dir (which defaults to the dir of your .nfnl.fnl project root and the CWD as a backup) to whatever you need. The defaults with no arguments should be correct for most situations though. 42 | 43 | opts.rtp-patterns is a sequential table of Lua patterns that match 44 | runtimepath directories you wish to include in your fennel-path and 45 | fennel-macro-path. It defaults to just ["/nfnl$"] which matches any 46 | runtimepath directory ending in /nfnl. Add any Neovim plugins you wish to use 47 | at compile or runtime here. You can also just replace it with a catch all 48 | pattern to include all directories. 49 | 50 | - All plugins: [".*"] 51 | - nfnl + my-cool-plugin: ["/nfnl$" "/my-cool-plugin$"] 52 | 53 | Make sure you update the README whenever you change the default 54 | configuration! 55 | 56 | ## `find` 57 | Function signature: 58 | 59 | ``` 60 | (find dir) 61 | ``` 62 | 63 | Find the nearest .nfnl.fnl file to the given directory. 64 | 65 | ## `find-and-load` 66 | Function signature: 67 | 68 | ``` 69 | (find-and-load dir) 70 | ``` 71 | 72 | Attempt to find and load the .nfnl.fnl config file relative to the given dir. 73 | Returns an empty table when there's issues or if there isn't a config file. 74 | If there's some valid config you'll get table containing config, cfg (fn) and 75 | root-dir back. 76 | 77 | ## `path-dirs` 78 | Function signature: 79 | 80 | ``` 81 | (path-dirs {:base-dirs base-dirs :rtp-patterns rtp-patterns :runtimepath runtimepath}) 82 | ``` 83 | 84 | Takes the current runtimepath and a sequential table of rtp-patterns. Those 85 | patterns are used to filter down all of the runtimepath directories. Returns 86 | the runtime path items that match at least one of the rtp-patterns. 87 | 88 | Also accepts a base-dirs table that it'll concatenate onto the end of then 89 | run through core.distinct to de-duplicate. 90 | 91 | 92 | 94 | -------------------------------------------------------------------------------- /docs/api/nfnl/core.md: -------------------------------------------------------------------------------- 1 | # Core.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`->set`](#set) 6 | - [`assoc`](#assoc) 7 | - [`assoc-in`](#assoc-in) 8 | - [`boolean?`](#boolean) 9 | - [`butlast`](#butlast) 10 | - [`clear-table!`](#clear-table) 11 | - [`complement`](#complement) 12 | - [`concat`](#concat) 13 | - [`constantly`](#constantly) 14 | - [`contains?`](#contains) 15 | - [`count`](#count) 16 | - [`dec`](#dec) 17 | - [`distinct`](#distinct) 18 | - [`drop-while`](#drop-while) 19 | - [`empty?`](#empty) 20 | - [`even?`](#even) 21 | - [`filter`](#filter) 22 | - [`first`](#first) 23 | - [`function?`](#function) 24 | - [`get`](#get) 25 | - [`get-in`](#get-in) 26 | - [`identity`](#identity) 27 | - [`inc`](#inc) 28 | - [`keys`](#keys) 29 | - [`kv-pairs`](#kv-pairs) 30 | - [`last`](#last) 31 | - [`map`](#map) 32 | - [`map-indexed`](#map-indexed) 33 | - [`mapcat`](#mapcat) 34 | - [`merge`](#merge) 35 | - [`merge!`](#merge-1) 36 | - [`nil?`](#nil) 37 | - [`number?`](#number) 38 | - [`odd?`](#odd) 39 | - [`pr`](#pr) 40 | - [`pr-str`](#pr-str) 41 | - [`println`](#println) 42 | - [`rand`](#rand) 43 | - [`reduce`](#reduce) 44 | - [`remove`](#remove) 45 | - [`rest`](#rest) 46 | - [`run!`](#run) 47 | - [`second`](#second) 48 | - [`select-keys`](#select-keys) 49 | - [`seq`](#seq) 50 | - [`sequential?`](#sequential) 51 | - [`slurp`](#slurp) 52 | - [`some`](#some) 53 | - [`sort`](#sort) 54 | - [`spit`](#spit) 55 | - [`str`](#str) 56 | - [`string?`](#string) 57 | - [`table?`](#table) 58 | - [`take-while`](#take-while) 59 | - [`update`](#update) 60 | - [`update-in`](#update-in) 61 | - [`vals`](#vals) 62 | 63 | ## `->set` 64 | Function signature: 65 | 66 | ``` 67 | (->set tbl) 68 | ``` 69 | 70 | Converts a table `tbl` to a 'set' - which means [:a :b] becomes {:a true :b true}. You can then use contains? to check membership, or just (. my-set :foo) - if that returns true, it's in your set. 71 | 72 | ## `assoc` 73 | Function signature: 74 | 75 | ``` 76 | (assoc t ...) 77 | ``` 78 | 79 | Set the key `k` in table `t` to the value `v` while safely handling `nil`. 80 | 81 | Accepts more `k` and `v` pairs as after the initial pair. This allows you 82 | to assoc multiple values in one call. 83 | 84 | Returns the table `t` once it has been mutated. 85 | 86 | ## `assoc-in` 87 | Function signature: 88 | 89 | ``` 90 | (assoc-in t ks v) 91 | ``` 92 | 93 | Set the key path `ks` in table `t` to the value `v` while safely handling `nil`. 94 | 95 | `(assoc-in {:foo {:bar 10}} [:foo :bar] 15) // => {:foo {:bar 15}}` 96 | 97 | ## `boolean?` 98 | Function signature: 99 | 100 | ``` 101 | (boolean? x) 102 | ``` 103 | 104 | True if the value is of type 'boolean'. 105 | 106 | ## `butlast` 107 | Function signature: 108 | 109 | ``` 110 | (butlast xs) 111 | ``` 112 | 113 | Return every value from the sequential table except the last one. 114 | 115 | ## `clear-table!` 116 | Function signature: 117 | 118 | ``` 119 | (clear-table! t) 120 | ``` 121 | 122 | Clear all keys from the table. 123 | 124 | ## `complement` 125 | Function signature: 126 | 127 | ``` 128 | (complement f) 129 | ``` 130 | 131 | Takes a fn `f` and returns a fn that takes the same arguments as `f`, has 132 | the same effects, if any, and returns the opposite truth value. 133 | 134 | ## `concat` 135 | Function signature: 136 | 137 | ``` 138 | (concat ...) 139 | ``` 140 | 141 | Concatenates the sequential table arguments together. 142 | 143 | ## `constantly` 144 | Function signature: 145 | 146 | ``` 147 | (constantly v) 148 | ``` 149 | 150 | Returns a function that takes any number of arguments and returns `v`. 151 | 152 | ## `contains?` 153 | Function signature: 154 | 155 | ``` 156 | (contains? tbl v) 157 | ``` 158 | 159 | Does the table `tbl` contain the value `v`? If given an associative table it'll check for membership of the key, if given a sequential table it will scan for the value. Associative is essentially O(1), sequential is O(n). You can use `->set` if you need to perform many lookups, it will turn [:a :b] into {:a true :b true} which is O(1) to check. 160 | 161 | ## `count` 162 | Function signature: 163 | 164 | ``` 165 | (count xs) 166 | ``` 167 | 168 | Count the items / characters in the input. Can handle tables, nil, strings and anything else that works with `(length xs)`. 169 | 170 | ## `dec` 171 | Function signature: 172 | 173 | ``` 174 | (dec n) 175 | ``` 176 | 177 | Decrement n by 1. 178 | 179 | ## `distinct` 180 | Function signature: 181 | 182 | ``` 183 | (distinct xs) 184 | ``` 185 | 186 | Takes a sequential table of values (xs) and returns a distinct sequential table with all duplicates removed. 187 | 188 | ## `drop-while` 189 | Function signature: 190 | 191 | ``` 192 | (drop-while f xs) 193 | ``` 194 | 195 | Drop values while (f x) returns true. 196 | 197 | ## `empty?` 198 | Function signature: 199 | 200 | ``` 201 | (empty? xs) 202 | ``` 203 | 204 | Returns true if the argument is empty, this includes empty strings, lists and nil. 205 | 206 | ## `even?` 207 | Function signature: 208 | 209 | ``` 210 | (even? n) 211 | ``` 212 | 213 | True if `n` is even. 214 | 215 | ## `filter` 216 | Function signature: 217 | 218 | ``` 219 | (filter f xs) 220 | ``` 221 | 222 | Filter xs down to a new sequential table containing every value that (f x) returned true for. 223 | 224 | ## `first` 225 | Function signature: 226 | 227 | ``` 228 | (first xs) 229 | ``` 230 | 231 | The first item of the sequential table. 232 | 233 | ## `function?` 234 | Function signature: 235 | 236 | ``` 237 | (function? value) 238 | ``` 239 | 240 | True if the value is of type 'function'. 241 | 242 | ## `get` 243 | Function signature: 244 | 245 | ``` 246 | (get t k d) 247 | ``` 248 | 249 | Get the key `k` from table `t` while safely handling `nil`. If it's not 250 | found it will return the optional default value `d`. 251 | 252 | ## `get-in` 253 | Function signature: 254 | 255 | ``` 256 | (get-in t ks d) 257 | ``` 258 | 259 | Get the key path `ks` from table `t` while safely handling `nil`. If it's 260 | not found it will return the optional default value `d`. 261 | 262 | `(get-in {:foo {:bar 10}} [:foo :bar]) // => 10` 263 | 264 | ## `identity` 265 | Function signature: 266 | 267 | ``` 268 | (identity x) 269 | ``` 270 | 271 | Returns what you pass it. 272 | 273 | ## `inc` 274 | Function signature: 275 | 276 | ``` 277 | (inc n) 278 | ``` 279 | 280 | Increment n by 1. 281 | 282 | ## `keys` 283 | Function signature: 284 | 285 | ``` 286 | (keys t) 287 | ``` 288 | 289 | Get all keys of a table. 290 | 291 | ## `kv-pairs` 292 | Function signature: 293 | 294 | ``` 295 | (kv-pairs t) 296 | ``` 297 | 298 | Get all keys and values of a table zipped up in pairs. 299 | 300 | ## `last` 301 | Function signature: 302 | 303 | ``` 304 | (last xs) 305 | ``` 306 | 307 | The last item of the sequential table. 308 | 309 | ## `map` 310 | Function signature: 311 | 312 | ``` 313 | (map f xs) 314 | ``` 315 | 316 | Map xs to a new sequential table by calling (f x) on each item. 317 | 318 | ## `map-indexed` 319 | Function signature: 320 | 321 | ``` 322 | (map-indexed f xs) 323 | ``` 324 | 325 | Map xs to a new sequential table by calling (f [k v]) on each item. 326 | 327 | ## `mapcat` 328 | Function signature: 329 | 330 | ``` 331 | (mapcat f xs) 332 | ``` 333 | 334 | The same as `map` but then `concat` all lists within the result together. 335 | 336 | ## `merge` 337 | Function signature: 338 | 339 | ``` 340 | (merge ...) 341 | ``` 342 | 343 | Merge the tables together, `nil` will be skipped safely so you can use 344 | `(when ...)` to conditionally include tables. Merges into a fresh table so 345 | no existing tables will be mutated. 346 | 347 | ## `merge!` 348 | Function signature: 349 | 350 | ``` 351 | (merge! base ...) 352 | ``` 353 | 354 | The same as `merge` above but will mutate the first argument, so all 355 | tables are merged into the first one. 356 | 357 | ## `nil?` 358 | Function signature: 359 | 360 | ``` 361 | (nil? x) 362 | ``` 363 | 364 | True if the value is equal to Lua `nil`. 365 | 366 | ## `number?` 367 | Function signature: 368 | 369 | ``` 370 | (number? x) 371 | ``` 372 | 373 | True if the value is of type 'number'. 374 | 375 | ## `odd?` 376 | Function signature: 377 | 378 | ``` 379 | (odd? n) 380 | ``` 381 | 382 | True if `n` is odd. 383 | 384 | ## `pr` 385 | Function signature: 386 | 387 | ``` 388 | (pr ...) 389 | ``` 390 | 391 | Print the arguments as data, strings will remain quoted. 392 | 393 | ## `pr-str` 394 | Function signature: 395 | 396 | ``` 397 | (pr-str ...) 398 | ``` 399 | 400 | Convert the input arguments to a string. 401 | 402 | ## `println` 403 | Function signature: 404 | 405 | ``` 406 | (println ...) 407 | ``` 408 | 409 | Convert the input arguments to a string (if required) and print them. 410 | 411 | ## `rand` 412 | Function signature: 413 | 414 | ``` 415 | (rand n) 416 | ``` 417 | 418 | Draw a random floating point number between 0 and `n`, where `n` is 1.0 if omitted. 419 | You must have a random seed set before running this: `(math.randomseed (os.time))` 420 | 421 | ## `reduce` 422 | Function signature: 423 | 424 | ``` 425 | (reduce f init xs) 426 | ``` 427 | 428 | Reduce xs into a result by passing each subsequent value into the fn with 429 | the previous value as the first arg. Starting with init. 430 | 431 | ## `remove` 432 | Function signature: 433 | 434 | ``` 435 | (remove f xs) 436 | ``` 437 | 438 | Opposite of filter, filter `xs` down to a new sequential table containing 439 | every value that `(f x)` returned falsy for. 440 | 441 | ## `rest` 442 | Function signature: 443 | 444 | ``` 445 | (rest xs) 446 | ``` 447 | 448 | Return every value from the sequential table except the first one. 449 | 450 | ## `run!` 451 | Function signature: 452 | 453 | ``` 454 | (run! f xs) 455 | ``` 456 | 457 | Execute the function (for side effects) for every xs. 458 | 459 | ## `second` 460 | Function signature: 461 | 462 | ``` 463 | (second xs) 464 | ``` 465 | 466 | The second item of the sequential table. 467 | 468 | ## `select-keys` 469 | Function signature: 470 | 471 | ``` 472 | (select-keys t ks) 473 | ``` 474 | 475 | Extract the keys listed in `ks` from `t` and return it as a new table. 476 | 477 | ## `seq` 478 | Function signature: 479 | 480 | ``` 481 | (seq x) 482 | ``` 483 | 484 | Sequential tables are just returned, associative tables are returned as [[k v]], strings are returned as sequential tables of characters and nil returns nil. Empty tables and strings also coerce to nil. 485 | 486 | ## `sequential?` 487 | Function signature: 488 | 489 | ``` 490 | (sequential? xs) 491 | ``` 492 | 493 | True if the value is a sequential table. 494 | 495 | ## `slurp` 496 | Function signature: 497 | 498 | ``` 499 | (slurp path) 500 | ``` 501 | 502 | Read the file into a string. 503 | 504 | ## `some` 505 | Function signature: 506 | 507 | ``` 508 | (some f xs) 509 | ``` 510 | 511 | Return the first truthy result from (f x) or nil. 512 | 513 | ## `sort` 514 | Function signature: 515 | 516 | ``` 517 | (sort xs) 518 | ``` 519 | 520 | Copies the sequential table xs, sorts it and returns it. 521 | 522 | ## `spit` 523 | Function signature: 524 | 525 | ``` 526 | (spit path content opts) 527 | ``` 528 | 529 | Spit the string into the file. When opts.append is true, add to the file. 530 | 531 | ## `str` 532 | Function signature: 533 | 534 | ``` 535 | (str ...) 536 | ``` 537 | 538 | Concatenate the values into one string. Converting non-string values into 539 | strings where required. 540 | 541 | ## `string?` 542 | Function signature: 543 | 544 | ``` 545 | (string? x) 546 | ``` 547 | 548 | True if the value is of type 'string'. 549 | 550 | ## `table?` 551 | Function signature: 552 | 553 | ``` 554 | (table? x) 555 | ``` 556 | 557 | True if the value is of type 'table'. 558 | 559 | ## `take-while` 560 | Function signature: 561 | 562 | ``` 563 | (take-while f xs) 564 | ``` 565 | 566 | Takes values from xs while (f x) returns true. 567 | 568 | ## `update` 569 | Function signature: 570 | 571 | ``` 572 | (update t k f) 573 | ``` 574 | 575 | Replace the key `k` in table `t` by passing the current value through the 576 | function `f`. Returns the table after the key has been mutated. 577 | 578 | ## `update-in` 579 | Function signature: 580 | 581 | ``` 582 | (update-in t ks f) 583 | ``` 584 | 585 | Same as `update` but replace the key path at `ks` with the result of 586 | passing the current value through the function `f`. 587 | 588 | ## `vals` 589 | Function signature: 590 | 591 | ``` 592 | (vals t) 593 | ``` 594 | 595 | Get all values of a table. 596 | 597 | 598 | 600 | -------------------------------------------------------------------------------- /docs/api/nfnl/fs.md: -------------------------------------------------------------------------------- 1 | # Fs.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`absglob`](#absglob) 6 | - [`basename`](#basename) 7 | - [`file-name-root`](#file-name-root) 8 | - [`filename`](#filename) 9 | - [`findfile`](#findfile) 10 | - [`fnl-path->lua-path`](#fnl-path-lua-path) 11 | - [`full-path`](#full-path) 12 | - [`glob-dir-newer?`](#glob-dir-newer) 13 | - [`glob-matches?`](#glob-matches) 14 | - [`join-path`](#join-path) 15 | - [`mkdirp`](#mkdirp) 16 | - [`path-sep`](#path-sep) 17 | - [`read-first-line`](#read-first-line) 18 | - [`relglob`](#relglob) 19 | - [`replace-dirs`](#replace-dirs) 20 | - [`replace-extension`](#replace-extension) 21 | - [`split-path`](#split-path) 22 | 23 | ## `absglob` 24 | Function signature: 25 | 26 | ``` 27 | (absglob dir expr) 28 | ``` 29 | 30 | Glob all files under dir matching the expression and return the absolute paths. 31 | 32 | ## `basename` 33 | Function signature: 34 | 35 | ``` 36 | (basename path) 37 | ``` 38 | 39 | Remove the file part of the path. 40 | 41 | ## `file-name-root` 42 | Function signature: 43 | 44 | ``` 45 | (file-name-root path) 46 | ``` 47 | 48 | Remove the suffix / extension of the file in a path. 49 | 50 | ## `filename` 51 | Function signature: 52 | 53 | ``` 54 | (filename path) 55 | ``` 56 | 57 | Just the filename / tail of a path. 58 | 59 | ## `findfile` 60 | Function signature: 61 | 62 | ``` 63 | (findfile name path) 64 | ``` 65 | 66 | Wrapper around Neovim's findfile() that returns nil 67 | instead of an empty string. 68 | 69 | ## `fnl-path->lua-path` 70 | Function signature: 71 | 72 | ``` 73 | (fnl-path->lua-path fnl-path) 74 | ``` 75 | 76 | **Undocumented** 77 | 78 | ## `full-path` 79 | Function signature: 80 | 81 | ``` 82 | (full-path path) 83 | ``` 84 | 85 | **Undocumented** 86 | 87 | ## `glob-dir-newer?` 88 | Function signature: 89 | 90 | ``` 91 | (glob-dir-newer? a-dir b-dir expr b-dir-path-fn) 92 | ``` 93 | 94 | Returns true if a-dir has newer changes than b-dir. All paths from a-dir are mapped through b-dir-path-fn before comparing to b-dir. 95 | 96 | ## `glob-matches?` 97 | Function signature: 98 | 99 | ``` 100 | (glob-matches? dir expr path) 101 | ``` 102 | 103 | Return true if path matches the glob expression. The path should be absolute and the glob should be relative to dir. 104 | 105 | ## `join-path` 106 | Function signature: 107 | 108 | ``` 109 | (join-path parts) 110 | ``` 111 | 112 | **Undocumented** 113 | 114 | ## `mkdirp` 115 | Function signature: 116 | 117 | ``` 118 | (mkdirp dir) 119 | ``` 120 | 121 | **Undocumented** 122 | 123 | ## `path-sep` 124 | Function signature: 125 | 126 | ``` 127 | (path-sep) 128 | ``` 129 | 130 | **Undocumented** 131 | 132 | ## `read-first-line` 133 | Function signature: 134 | 135 | ``` 136 | (read-first-line path) 137 | ``` 138 | 139 | **Undocumented** 140 | 141 | ## `relglob` 142 | Function signature: 143 | 144 | ``` 145 | (relglob dir expr) 146 | ``` 147 | 148 | Glob all files under dir matching the expression and return the paths 149 | relative to the dir argument. 150 | 151 | ## `replace-dirs` 152 | Function signature: 153 | 154 | ``` 155 | (replace-dirs path from to) 156 | ``` 157 | 158 | Replaces directories in `path` that match `from` with `to`. 159 | 160 | ## `replace-extension` 161 | Function signature: 162 | 163 | ``` 164 | (replace-extension path ext) 165 | ``` 166 | 167 | **Undocumented** 168 | 169 | ## `split-path` 170 | Function signature: 171 | 172 | ``` 173 | (split-path path) 174 | ``` 175 | 176 | **Undocumented** 177 | 178 | 179 | 181 | -------------------------------------------------------------------------------- /docs/api/nfnl/init.md: -------------------------------------------------------------------------------- 1 | # Init.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`setup`](#setup) 6 | 7 | ## `setup` 8 | Function signature: 9 | 10 | ``` 11 | (setup opts) 12 | ``` 13 | 14 | Sets the vim.g.nfnl#... variables in a slightly more Lua friendly way. 15 | 16 | 17 | 19 | -------------------------------------------------------------------------------- /docs/api/nfnl/macros.md: -------------------------------------------------------------------------------- 1 | # Macros.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`if-let`](#if-let) 6 | - [`time`](#time) 7 | - [`when-let`](#when-let) 8 | 9 | ## `if-let` 10 | Function signature: 11 | 12 | ``` 13 | (if-let bindings ...) 14 | ``` 15 | 16 | **Undocumented** 17 | 18 | ## `time` 19 | Function signature: 20 | 21 | ``` 22 | (time ...) 23 | ``` 24 | 25 | **Undocumented** 26 | 27 | ## `when-let` 28 | Function signature: 29 | 30 | ``` 31 | (when-let bindings ...) 32 | ``` 33 | 34 | **Undocumented** 35 | 36 | 37 | 39 | -------------------------------------------------------------------------------- /docs/api/nfnl/macros/aniseed.md: -------------------------------------------------------------------------------- 1 | # Aniseed.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`def`](#def) 6 | - [`def-`](#def-) 7 | - [`defn`](#defn) 8 | - [`defn-`](#defn-) 9 | - [`defonce`](#defonce) 10 | - [`defonce-`](#defonce-) 11 | - [`module`](#module) 12 | 13 | ## `def` 14 | Function signature: 15 | 16 | ``` 17 | (def name value) 18 | ``` 19 | 20 | **Undocumented** 21 | 22 | ## `def-` 23 | Function signature: 24 | 25 | ``` 26 | (def- name value) 27 | ``` 28 | 29 | **Undocumented** 30 | 31 | ## `defn` 32 | Function signature: 33 | 34 | ``` 35 | (defn name ...) 36 | ``` 37 | 38 | **Undocumented** 39 | 40 | ## `defn-` 41 | Function signature: 42 | 43 | ``` 44 | (defn- name ...) 45 | ``` 46 | 47 | **Undocumented** 48 | 49 | ## `defonce` 50 | Function signature: 51 | 52 | ``` 53 | (defonce name value) 54 | ``` 55 | 56 | **Undocumented** 57 | 58 | ## `defonce-` 59 | Function signature: 60 | 61 | ``` 62 | (defonce- name value) 63 | ``` 64 | 65 | **Undocumented** 66 | 67 | ## `module` 68 | Function signature: 69 | 70 | ``` 71 | (module mod-name mod-base) 72 | ``` 73 | 74 | **Undocumented** 75 | 76 | 77 | 79 | -------------------------------------------------------------------------------- /docs/api/nfnl/module.md: -------------------------------------------------------------------------------- 1 | # Module.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`autoload`](#autoload) 6 | - [`define`](#define) 7 | 8 | ## `autoload` 9 | Function signature: 10 | 11 | ``` 12 | (autoload name) 13 | ``` 14 | 15 | Like autoload from Vim Script! A replacement for require that will load the 16 | module when you first use it. 17 | 18 | (local {: autoload} (require :nfnl.module)) 19 | (local my-mod (autoload :my-mod)) 20 | (my-mod.some-fn 10) ;; Your module is required here! 21 | 22 | It's a drop in replacement for require that should speed up your Neovim 23 | startup dramatically. Only works with table modules, if the module you're 24 | requiring is a function or anything other than a table you need to use the 25 | normal require. 26 | 27 | ## `define` 28 | Function signature: 29 | 30 | ``` 31 | (define mod-name base) 32 | ``` 33 | 34 | Looks up the mod-name in package.loaded, if it's the same type as base it'll 35 | use the loaded value. If it's different it'll use base. 36 | 37 | The returned result should be used as your default value for M like so: 38 | (local M (define :my.mod {})) 39 | 40 | Then return M at the bottom of your file and define functions on M like so: 41 | (fn M.my-fn [x] (+ x 1)) 42 | 43 | This technique helps you have extremely reloadable modules through Conjure. 44 | You can reload the entire file or induvidual function definitions and the 45 | changes will be reflected in all other modules that depend on this one 46 | without having to reload the dependant modules. 47 | 48 | The base value defaults to {}, an empty table. 49 | 50 | 51 | 53 | -------------------------------------------------------------------------------- /docs/api/nfnl/notify.md: -------------------------------------------------------------------------------- 1 | # Notify.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`debug`](#debug) 6 | - [`error`](#error) 7 | - [`info`](#info) 8 | - [`trace`](#trace) 9 | - [`warn`](#warn) 10 | 11 | ## `debug` 12 | Function signature: 13 | 14 | ``` 15 | (debug ...) 16 | ``` 17 | 18 | **Undocumented** 19 | 20 | ## `error` 21 | Function signature: 22 | 23 | ``` 24 | (error ...) 25 | ``` 26 | 27 | **Undocumented** 28 | 29 | ## `info` 30 | Function signature: 31 | 32 | ``` 33 | (info ...) 34 | ``` 35 | 36 | **Undocumented** 37 | 38 | ## `trace` 39 | Function signature: 40 | 41 | ``` 42 | (trace ...) 43 | ``` 44 | 45 | **Undocumented** 46 | 47 | ## `warn` 48 | Function signature: 49 | 50 | ``` 51 | (warn ...) 52 | ``` 53 | 54 | **Undocumented** 55 | 56 | 57 | 59 | -------------------------------------------------------------------------------- /docs/api/nfnl/nvim.md: -------------------------------------------------------------------------------- 1 | # Nvim.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`get-buf-content-as-string`](#get-buf-content-as-string) 6 | 7 | ## `get-buf-content-as-string` 8 | Function signature: 9 | 10 | ``` 11 | (get-buf-content-as-string buf) 12 | ``` 13 | 14 | **Undocumented** 15 | 16 | 17 | 19 | -------------------------------------------------------------------------------- /docs/api/nfnl/repl.md: -------------------------------------------------------------------------------- 1 | # Repl.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`new`](#new) 6 | 7 | ## `new` 8 | Function signature: 9 | 10 | ``` 11 | (new opts) 12 | ``` 13 | 14 | Create a new REPL instance which is a function you repeatedly call with more 15 | code for evaluation. The results of the evaluations are returned in a table. 16 | Call with a nil instead of a string of code to close the REPL. 17 | 18 | Takes an opts table that can contain: 19 | * `on-error` - a function that will be called with the err-type, err and lua-source of any errors that occur in the REPL. 20 | * `cfg` - from cfg-fn in nfnl.config, will be used to configure the fennel instance with path strings before each evaluation. 21 | 22 | 23 | 25 | -------------------------------------------------------------------------------- /docs/api/nfnl/string.md: -------------------------------------------------------------------------------- 1 | # String.fnl 2 | 3 | **Table of contents** 4 | 5 | - [`blank?`](#blank) 6 | - [`ends-with?`](#ends-with) 7 | - [`join`](#join) 8 | - [`split`](#split) 9 | - [`trim`](#trim) 10 | - [`triml`](#triml) 11 | - [`trimr`](#trimr) 12 | 13 | ## `blank?` 14 | Function signature: 15 | 16 | ``` 17 | (blank? s) 18 | ``` 19 | 20 | Check if the string is nil, empty or only whitespace. 21 | 22 | ## `ends-with?` 23 | Function signature: 24 | 25 | ``` 26 | (ends-with? s suffix) 27 | ``` 28 | 29 | Check if the string ends with suffix. 30 | 31 | ## `join` 32 | Function signature: 33 | 34 | ``` 35 | (join ...) 36 | ``` 37 | 38 | (join xs) (join sep xs) 39 | Joins all items of a table together with an optional separator. 40 | Separator defaults to an empty string. 41 | Values that aren't a string or nil will go through nfnl.core/pr-str. 42 | 43 | ## `split` 44 | Function signature: 45 | 46 | ``` 47 | (split s pat) 48 | ``` 49 | 50 | Split the given string into a sequential table using the pattern. 51 | 52 | ## `trim` 53 | Function signature: 54 | 55 | ``` 56 | (trim s) 57 | ``` 58 | 59 | Removes whitespace from both ends of string. 60 | 61 | ## `triml` 62 | Function signature: 63 | 64 | ``` 65 | (triml s) 66 | ``` 67 | 68 | Removes whitespace from the left side of string. 69 | 70 | ## `trimr` 71 | Function signature: 72 | 73 | ``` 74 | (trimr s) 75 | ``` 76 | 77 | Removes whitespace from the right side of string. 78 | 79 | 80 | 82 | -------------------------------------------------------------------------------- /fnl/nfnl/api.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload : define} (require :nfnl.module)) 2 | (local core (autoload :nfnl.core)) 3 | (local str (autoload :nfnl.string)) 4 | (local compile (autoload :nfnl.compile)) 5 | (local config (autoload :nfnl.config)) 6 | (local notify (autoload :nfnl.notify)) 7 | (local fs (autoload :nfnl.fs)) 8 | (local gc (autoload :nfnl.gc)) 9 | 10 | (local M (define :nfnl.api)) 11 | 12 | (fn M.find-orphans [opts] 13 | "Find orphan Lua files that were compiled from a Fennel file that no longer exists. Display them with notify. Set opts.passive? to true if you don't want it to tell you that there are no orphans." 14 | (let [dir (vim.fn.getcwd) 15 | {: config : root-dir : cfg} (config.find-and-load dir)] 16 | (if config 17 | (let [orphan-files (gc.find-orphan-lua-files {: root-dir : cfg})] 18 | (if (core.empty? orphan-files) 19 | (when (not (core.get opts :passive?)) 20 | (notify.info "No orphan files detected.")) 21 | (notify.warn 22 | "Orphan files detected, delete them with :NfnlDeleteOrphans.\n" 23 | (->> orphan-files 24 | (core.map 25 | (fn [f] 26 | (.. " - " f))) 27 | (str.join "\n")))) 28 | orphan-files) 29 | (do 30 | (notify.warn "No .nfnl.fnl configuration found.") 31 | [])))) 32 | 33 | (fn M.delete-orphans [] 34 | "Delete orphan Lua files that were compiled from a Fennel file that no longer exists." 35 | (let [dir (vim.fn.getcwd) 36 | {: config : root-dir : cfg} (config.find-and-load dir)] 37 | (if config 38 | (let [orphan-files (gc.find-orphan-lua-files {: root-dir : cfg})] 39 | (if (core.empty? orphan-files) 40 | (notify.info "No orphan files detected.") 41 | (do 42 | (notify.info 43 | "Deleting orphan files:\n" 44 | (->> orphan-files 45 | (core.map 46 | (fn [f] 47 | (.. " - " f))) 48 | (str.join "\n"))) 49 | (core.map os.remove orphan-files))) 50 | orphan-files) 51 | (do 52 | (notify.warn "No .nfnl.fnl configuration found.") 53 | [])))) 54 | 55 | (fn M.compile-file [{: path : dir}] 56 | "Compiles a file into the matching Lua file. Returns the compilation result. Takes an optional `dir` key that changes the working directory. 57 | 58 | Will do nothing if you execute it on a directory that doesn't contain an nfnl configuration file. 59 | 60 | Also displays all results via the notify system." 61 | (let [dir (or dir (vim.fn.getcwd)) 62 | {: config : root-dir : cfg} (config.find-and-load dir)] 63 | (if config 64 | (let [path (fs.join-path [root-dir (vim.fn.expand (or path "%"))]) 65 | result (compile.into-file 66 | {: root-dir 67 | : cfg 68 | : path 69 | :source (core.slurp path) 70 | :batch? true})] 71 | (notify.info "Compilation complete.\n" result) 72 | result) 73 | (do 74 | (notify.warn "No .nfnl.fnl configuration found.") 75 | [])))) 76 | 77 | (fn M.compile-all-files [dir] 78 | "Compiles all files in the given dir (optional), defaulting to the current working directory. Returns a sequential table with each of the files compilation result. 79 | 80 | Will do nothing if you execute it on a directory that doesn't contain an nfnl configuration file. 81 | 82 | Also displays all results via the notify system." 83 | (let [dir (or dir (vim.fn.getcwd)) 84 | {: config : root-dir : cfg} (config.find-and-load dir)] 85 | (if config 86 | (let [results (compile.all-files {: root-dir : cfg})] 87 | (notify.info "Compilation complete.\n" results) 88 | results) 89 | (do 90 | (notify.warn "No .nfnl.fnl configuration found.") 91 | [])))) 92 | 93 | (fn M.dofile [file] 94 | "Just like :luafile, takes a Fennel file path (optional, defaults to '%', runs it through expand) and executes it from disk. However! This doesn't compile the Fennel! It maps the Fennel path to the matching Lua file already in your file system and executes that with Lua's built in dofile. So you need to have already written your .fnl and had nfnl compile that to a .lua for this to work, it's just a convinience function for you to call directly or through the :NfnlFile command." 95 | (dofile (fs.fnl-path->lua-path (vim.fn.expand (or file "%"))))) 96 | 97 | M 98 | -------------------------------------------------------------------------------- /fnl/nfnl/callback.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload} (require :nfnl.module)) 2 | (local core (autoload :nfnl.core)) 3 | (local str (autoload :nfnl.string)) 4 | (local fs (autoload :nfnl.fs)) 5 | (local nvim (autoload :nfnl.nvim)) 6 | (local compile (autoload :nfnl.compile)) 7 | (local config (autoload :nfnl.config)) 8 | (local api (autoload :nfnl.api)) 9 | (local notify (autoload :nfnl.notify)) 10 | 11 | (fn fennel-buf-write-post-callback-fn [root-dir cfg] 12 | "Builds a function to be called on buf write. Adheres to the config passed 13 | into this outer function." 14 | 15 | (fn [ev] 16 | "Called when we write a Fennel file located under a directory containing a 17 | .nfnl.fnl file. It compiles the Fennel to Lua and writes it into another 18 | file according to the .nfnl.fnl file configuration." 19 | 20 | (compile.into-file 21 | {: root-dir 22 | : cfg 23 | :path (fs.full-path (. ev :file)) 24 | :source (nvim.get-buf-content-as-string (. ev :buf))}) 25 | 26 | (when (cfg [:orphan-detection :auto?]) 27 | (api.find-orphans {:passive? true})) 28 | 29 | nil)) 30 | 31 | (fn supported-path? [file-path] 32 | "Returns true if we can work with the given path. Right now we support a path if it's a string and it doesn't start with a protocol segment like fugitive://..." 33 | (or 34 | (when (core.string? file-path) 35 | (not (file-path:find "^[%w-]+:/"))) 36 | false)) 37 | 38 | (fn fennel-filetype-callback [ev] 39 | "Called whenever we enter a Fennel file. It walks up the tree to find a 40 | .nfnl.fnl (which can contain configuration). If found, we initialise the 41 | compiler autocmd for the directory containing the .nfnl.fnl file. 42 | 43 | This allows us to edit multiple projects in different directories with 44 | different .nfnl.fnl configuration, wonderful!" 45 | 46 | (let [file-path (fs.full-path (. ev :file))] 47 | (when (supported-path? file-path) 48 | (let [file-dir (fs.basename file-path) 49 | {: config : root-dir : cfg} (config.find-and-load file-dir)] 50 | 51 | (when config 52 | (when (cfg [:verbose]) 53 | (notify.info "Found nfnl config, setting up autocmds: " root-dir)) 54 | 55 | (when (not= false vim.g.nfnl#compile_on_write) 56 | (vim.api.nvim_create_autocmd 57 | ["BufWritePost"] 58 | {:group (vim.api.nvim_create_augroup (str.join ["nfnl-on-write" root-dir ev.buf]) {}) 59 | :buffer ev.buf 60 | :callback (fennel-buf-write-post-callback-fn root-dir cfg)})) 61 | 62 | (vim.api.nvim_buf_create_user_command 63 | ev.buf :NfnlFile 64 | #(api.dofile (core.first (core.get $ :fargs))) 65 | {:desc "Run the matching Lua file for this Fennel file from disk. Does not recompile the Lua, you must use nfnl to compile your Fennel to Lua first. Calls nfnl.api/dofile under the hood." 66 | :force true 67 | :complete "file" 68 | :nargs "?"}) 69 | 70 | (vim.api.nvim_buf_create_user_command 71 | ev.buf :NfnlCompileFile 72 | #(api.compile-file {:path (core.first (core.get $ :fargs))}) 73 | {:desc "Executes (nfnl.api/compile-file) which compiles the current file or the one provided as an argumet. The output is written to the appropriate Lua file." 74 | :force true 75 | :complete "file" 76 | :nargs "?"}) 77 | 78 | (vim.api.nvim_buf_create_user_command 79 | ev.buf :NfnlCompileAllFiles 80 | #(api.compile-all-files (core.first (core.get $ :fargs))) 81 | {:desc "Executes (nfnl.api/compile-all-files) which will, you guessed it, compile all of your files." 82 | :force true 83 | :complete "file" 84 | :nargs "?"}) 85 | 86 | (vim.api.nvim_buf_create_user_command 87 | ev.buf :NfnlFindOrphans 88 | #(api.find-orphans) 89 | {:desc "Executes (nfnl.api/find-orphans) which will find and display all Lua files that no longer have a matching Fennel file." 90 | :force true}) 91 | 92 | (vim.api.nvim_buf_create_user_command 93 | ev.buf :NfnlDeleteOrphans 94 | #(api.delete-orphans) 95 | {:desc "Executes (nfnl.api/delete-orphans) deletes any orphan Lua files that no longer have their original Fennel file they were compiled from." 96 | :force true})))))) 97 | 98 | {: fennel-filetype-callback 99 | : supported-path?} 100 | -------------------------------------------------------------------------------- /fnl/nfnl/compile.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload} (require :nfnl.module)) 2 | (local core (autoload :nfnl.core)) 3 | (local fs (autoload :nfnl.fs)) 4 | (local fennel (autoload :nfnl.fennel)) 5 | (local notify (autoload :nfnl.notify)) 6 | (local config (autoload :nfnl.config)) 7 | (local header (autoload :nfnl.header)) 8 | 9 | (local mod {}) 10 | 11 | (fn safe-target? [path] 12 | "Reads the given file and checks if it contains our header marker on the 13 | first line. Returns true if it contains the marker, we're allowed to 14 | overwrite this file." 15 | (let [line (fs.read-first-line path)] 16 | (or (not line) (header.tagged? line)))) 17 | 18 | (fn macro-source? [source] 19 | (string.find source "%s*;+%s*%[nfnl%-macro%]")) 20 | 21 | (fn valid-source-files [glob-fn {: root-dir : cfg}] 22 | "Return a list of all files we're allowed to compile. These are found by 23 | performing fs.relglob calls against each :source-file-patterns value from the 24 | configuration." 25 | (core.mapcat #(glob-fn root-dir $) (cfg [:source-file-patterns]))) 26 | 27 | (fn valid-source-file? [path {: root-dir : cfg }] 28 | "Return whether we're allowed to compile the given file. This is determined by 29 | matching the given absolute path against each :source-file-patterns value from 30 | the configuration." 31 | (core.some #(fs.glob-matches? root-dir $ path) (cfg [:source-file-patterns]))) 32 | 33 | (fn mod.into-string [{: root-dir : path : cfg : source : batch? &as opts}] 34 | (let [macro? (macro-source? source)] 35 | (if 36 | (and macro? batch?) 37 | {:status :macros-are-not-compiled 38 | :source-path path} 39 | 40 | macro? 41 | (do 42 | (core.clear-table! fennel.macro-loaded) 43 | (mod.all-files {: root-dir : cfg})) 44 | 45 | (config.config-file-path? path) 46 | {:status :nfnl-config-is-not-compiled 47 | :source-path path} 48 | 49 | (not (valid-source-file? path opts)) 50 | {:status :path-is-not-in-source-file-patterns 51 | :source-path path} 52 | 53 | (let [rel-file-name (path:sub (+ 2 (root-dir:len))) 54 | (ok res) 55 | (do 56 | (set fennel.path (cfg [:fennel-path])) 57 | (set fennel.macro-path (cfg [:fennel-macro-path])) 58 | (pcall 59 | fennel.compile-string 60 | source 61 | (core.merge 62 | {:filename path 63 | :warn notify.warn} 64 | (cfg [:compiler-options]))))] 65 | (if ok 66 | (do 67 | (when (cfg [:verbose]) 68 | (notify.info "Successfully compiled: " path)) 69 | {:status :ok 70 | :source-path path 71 | :result (.. (if (cfg [:header-comment]) 72 | (header.with-header rel-file-name res) 73 | res) 74 | "\n")}) 75 | (do 76 | (when (not batch?) 77 | (notify.error res)) 78 | {:status :compilation-error 79 | :error res 80 | :source-path path})))))) 81 | 82 | (fn mod.into-file [{: _root-dir : cfg : _source : path : batch? &as opts}] 83 | (let [fnl-path->lua-path (cfg [:fnl-path->lua-path]) 84 | destination-path (fnl-path->lua-path path) 85 | {: status : source-path : result &as res} 86 | (mod.into-string opts)] 87 | (if 88 | (not= :ok status) 89 | res 90 | 91 | (or (safe-target? destination-path) 92 | (not (cfg [:header-comment]))) 93 | (do 94 | (fs.mkdirp (fs.basename destination-path)) 95 | (core.spit destination-path result) 96 | {:status :ok 97 | : source-path 98 | : destination-path}) 99 | 100 | (do 101 | (when (not batch?) 102 | (notify.warn destination-path " was not compiled by nfnl. Delete it manually if you wish to compile into this file.")) 103 | {:status :destination-exists 104 | :source-path path 105 | : destination-path})))) 106 | 107 | (fn mod.all-files [{: root-dir : cfg &as opts}] 108 | (->> (valid-source-files fs.relglob opts) 109 | (core.map #(fs.join-path [root-dir $])) 110 | (core.map 111 | (fn [path] 112 | (mod.into-file 113 | {: root-dir 114 | : path 115 | : cfg 116 | :source (core.slurp path) 117 | :batch? true}))))) 118 | 119 | mod 120 | -------------------------------------------------------------------------------- /fnl/nfnl/config.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload} (require :nfnl.module)) 2 | (local core (autoload :nfnl.core)) 3 | (local fs (autoload :nfnl.fs)) 4 | (local str (autoload :nfnl.string)) 5 | (local fennel (autoload :nfnl.fennel)) 6 | (local notify (autoload :nfnl.notify)) 7 | 8 | (local config-file-name ".nfnl.fnl") 9 | 10 | (fn find [dir] 11 | "Find the nearest .nfnl.fnl file to the given directory." 12 | (fs.findfile config-file-name (.. dir ";"))) 13 | 14 | (fn path-dirs [{: rtp-patterns : runtimepath : base-dirs}] 15 | "Takes the current runtimepath and a sequential table of rtp-patterns. Those 16 | patterns are used to filter down all of the runtimepath directories. Returns 17 | the runtime path items that match at least one of the rtp-patterns. 18 | 19 | Also accepts a base-dirs table that it'll concatenate onto the end of then 20 | run through core.distinct to de-duplicate." 21 | (->> (str.split runtimepath ",") 22 | (core.filter 23 | (fn [path] 24 | (core.some #(string.find path $) rtp-patterns))) 25 | (core.concat base-dirs) 26 | (core.distinct))) 27 | 28 | (fn default [opts] 29 | "Returns the default configuration that you should base your custom 30 | configuration on top of. Feel free to call this with no arguments and merge 31 | your changes on top. You can override opts.root-dir (which defaults to the dir of your .nfnl.fnl project root and the CWD as a backup) to whatever you need. The defaults with no arguments should be correct for most situations though. 32 | 33 | opts.rtp-patterns is a sequential table of Lua patterns that match 34 | runtimepath directories you wish to include in your fennel-path and 35 | fennel-macro-path. It defaults to just [\"/nfnl$\"] which matches any 36 | runtimepath directory ending in /nfnl. Add any Neovim plugins you wish to use 37 | at compile or runtime here. You can also just replace it with a catch all 38 | pattern to include all directories. 39 | 40 | - All plugins: [\".*\"] 41 | - nfnl + my-cool-plugin: [\"/nfnl$\" \"/my-cool-plugin$\"] 42 | 43 | Make sure you update the README whenever you change the default 44 | configuration!" 45 | 46 | (let [;; Base this config's paths on... 47 | root-dir (or 48 | ;; The given root-dir option. 49 | (core.get opts :root-dir) 50 | 51 | ;; The closest .nfnl.fnl file parent directory to the cwd. 52 | (-?> (vim.fn.getcwd) 53 | (find) ; returns nil if .nfnl.fnl is not found 54 | (fs.full-path) 55 | (fs.basename)) 56 | 57 | ;; The cwd, just in case nothing else works. 58 | (vim.fn.getcwd)) 59 | 60 | dirs (path-dirs 61 | {:runtimepath vim.o.runtimepath 62 | :rtp-patterns (core.get opts :rtp-patterns [(.. (fs.path-sep) "nfnl$")]) 63 | :base-dirs [root-dir]})] 64 | 65 | {:verbose false 66 | :header-comment true 67 | :compiler-options {:error-pinpoint false} 68 | :orphan-detection {:auto? true 69 | :ignore-patterns []} 70 | 71 | :root-dir root-dir 72 | 73 | :fennel-path 74 | (str.join 75 | ";" 76 | (core.mapcat 77 | (fn [root-dir] 78 | (core.map 79 | fs.join-path 80 | [[root-dir "?.fnl"] 81 | [root-dir "?" "init.fnl"] 82 | [root-dir "fnl" "?.fnl"] 83 | [root-dir "fnl" "?" "init.fnl"]])) 84 | dirs)) 85 | 86 | :fennel-macro-path 87 | (str.join 88 | ";" 89 | (core.mapcat 90 | (fn [root-dir] 91 | (core.map 92 | fs.join-path 93 | [[root-dir "?.fnl"] 94 | [root-dir "?" "init-macros.fnl"] 95 | [root-dir "?" "init.fnl"] 96 | [root-dir "fnl" "?.fnl"] 97 | [root-dir "fnl" "?" "init-macros.fnl"] 98 | [root-dir "fnl" "?" "init.fnl"]])) 99 | dirs)) 100 | 101 | :source-file-patterns [".*.fnl" "*.fnl" (fs.join-path ["**" "*.fnl"])] 102 | :fnl-path->lua-path fs.fnl-path->lua-path})) 103 | 104 | (fn cfg-fn [t opts] 105 | "Builds a cfg fetcher for the config table t. Returns a function that takes a 106 | path sequential table, it looks up the value from the config with core.get-in 107 | and falls back to a matching value in (default) if not found." 108 | 109 | (let [default-cfg (default opts)] 110 | (fn [path] 111 | (core.get-in 112 | t path 113 | (core.get-in default-cfg path))))) 114 | 115 | (fn config-file-path? [path] 116 | (= config-file-name (fs.filename path))) 117 | 118 | (fn find-and-load [dir] 119 | "Attempt to find and load the .nfnl.fnl config file relative to the given dir. 120 | Returns an empty table when there's issues or if there isn't a config file. 121 | If there's some valid config you'll get table containing config, cfg (fn) and 122 | root-dir back." 123 | 124 | (or 125 | (let [config-file-path (find dir)] 126 | (when config-file-path 127 | (let [root-dir (fs.basename config-file-path) 128 | config-source (vim.secure.read config-file-path) 129 | 130 | (ok config) 131 | (if 132 | (core.nil? config-source) 133 | (values false (.. config-file-path " is not trusted, refusing to compile.")) 134 | 135 | (or (str.blank? config-source) 136 | (= "{}" (str.trim config-source))) 137 | (values true {}) 138 | 139 | (pcall 140 | fennel.eval 141 | config-source 142 | {:filename config-file-path}))] 143 | (if ok 144 | {: config 145 | : root-dir 146 | :cfg (cfg-fn config {: root-dir})} 147 | (notify.error config))))) 148 | 149 | ;; Always default to an empty table for destructuring. 150 | {})) 151 | 152 | {: cfg-fn 153 | : find 154 | : find-and-load 155 | : config-file-path? 156 | : default 157 | : path-dirs} 158 | -------------------------------------------------------------------------------- /fnl/nfnl/core.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload} (require :nfnl.module)) 2 | (local fennel (autoload :nfnl.fennel)) 3 | 4 | (fn rand [n] 5 | "Draw a random floating point number between 0 and `n`, where `n` is 1.0 if omitted. 6 | You must have a random seed set before running this: `(math.randomseed (os.time))`" 7 | (* (math.random) (or n 1))) 8 | 9 | (fn nil? [x] 10 | "True if the value is equal to Lua `nil`." 11 | (= nil x)) 12 | 13 | (fn number? [x] 14 | "True if the value is of type 'number'." 15 | (= "number" (type x))) 16 | 17 | (fn boolean? [x] 18 | "True if the value is of type 'boolean'." 19 | (= "boolean" (type x))) 20 | 21 | (fn string? [x] 22 | "True if the value is of type 'string'." 23 | (= "string" (type x))) 24 | 25 | (fn table? [x] 26 | "True if the value is of type 'table'." 27 | (= "table" (type x))) 28 | 29 | (fn function? [value] 30 | "True if the value is of type 'function'." 31 | (= "function" (type value))) 32 | 33 | (fn keys [t] 34 | "Get all keys of a table." 35 | (let [result []] 36 | (when t 37 | (each [k _ (pairs t)] 38 | (table.insert result k))) 39 | result)) 40 | 41 | (fn first [xs] 42 | "The first item of the sequential table." 43 | (when xs 44 | (. xs 1))) 45 | 46 | (fn second [xs] 47 | "The second item of the sequential table." 48 | (when xs 49 | (. xs 2))) 50 | 51 | (fn count [xs] 52 | "Count the items / characters in the input. Can handle tables, nil, strings and anything else that works with `(length xs)`." 53 | (if 54 | (table? xs) (let [maxn (table.maxn xs)] 55 | ;; We only count the keys if maxn returns 0. 56 | (if (= 0 maxn) 57 | (table.maxn (keys xs)) 58 | maxn)) 59 | (not xs) 0 60 | (length xs))) 61 | 62 | (fn empty? [xs] 63 | "Returns true if the argument is empty, this includes empty strings, lists and nil." 64 | (= 0 (count xs))) 65 | 66 | (fn sequential? [xs] 67 | "True if the value is a sequential table." 68 | (and (table? xs) (or (empty? xs) (= 1 (first (keys xs)))))) 69 | 70 | (fn kv-pairs [t] 71 | "Get all keys and values of a table zipped up in pairs." 72 | (let [result []] 73 | (when t 74 | (each [k v (pairs t)] 75 | (table.insert result [k v]))) 76 | result)) 77 | 78 | (fn seq [x] 79 | "Sequential tables are just returned, associative tables are returned as [[k v]], strings are returned as sequential tables of characters and nil returns nil. Empty tables and strings also coerce to nil." 80 | (if 81 | (empty? x) nil 82 | (sequential? x) x 83 | (table? x) (kv-pairs x) 84 | 85 | (string? x) 86 | (let [acc []] 87 | (for [i 1 (count x)] 88 | (table.insert acc (x:sub i i))) 89 | (when (not (empty? acc)) 90 | acc)) 91 | 92 | nil)) 93 | 94 | (fn last [xs] 95 | "The last item of the sequential table." 96 | (when xs 97 | (. xs (count xs)))) 98 | 99 | (fn inc [n] 100 | "Increment n by 1." 101 | (+ n 1)) 102 | 103 | (fn dec [n] 104 | "Decrement n by 1." 105 | (- n 1)) 106 | 107 | (fn even? [n] 108 | "True if `n` is even." 109 | (= (% n 2) 0)) 110 | 111 | (fn odd? [n] 112 | "True if `n` is odd." 113 | (not (even? n))) 114 | 115 | (fn vals [t] 116 | "Get all values of a table." 117 | (let [result []] 118 | (when t 119 | (each [_ v (pairs t)] 120 | (table.insert result v))) 121 | result)) 122 | 123 | (fn run! [f xs] 124 | "Execute the function (for side effects) for every xs." 125 | (when xs 126 | (let [nxs (count xs)] 127 | (when (> nxs 0) 128 | (for [i 1 nxs] 129 | (f (. xs i))))))) 130 | 131 | (fn complement [f] 132 | "Takes a fn `f` and returns a fn that takes the same arguments as `f`, has 133 | the same effects, if any, and returns the opposite truth value." 134 | (fn [...] 135 | (not (f ...)))) 136 | 137 | (fn filter [f xs] 138 | "Filter xs down to a new sequential table containing every value that (f x) returned true for." 139 | (let [result []] 140 | (run! 141 | (fn [x] 142 | (when (f x) 143 | (table.insert result x))) 144 | xs) 145 | result)) 146 | 147 | (fn remove [f xs] 148 | "Opposite of filter, filter `xs` down to a new sequential table containing 149 | every value that `(f x)` returned falsy for." 150 | (filter (complement f) xs)) 151 | 152 | (fn map [f xs] 153 | "Map xs to a new sequential table by calling (f x) on each item." 154 | (let [result []] 155 | (run! 156 | (fn [x] 157 | (let [mapped (f x)] 158 | (table.insert 159 | result 160 | (if (= 0 (select "#" mapped)) 161 | nil 162 | mapped)))) 163 | xs) 164 | result)) 165 | 166 | (fn map-indexed [f xs] 167 | "Map xs to a new sequential table by calling (f [k v]) on each item. " 168 | (map f (kv-pairs xs))) 169 | 170 | (fn identity [x] 171 | "Returns what you pass it." 172 | x) 173 | 174 | (fn reduce [f init xs] 175 | "Reduce xs into a result by passing each subsequent value into the fn with 176 | the previous value as the first arg. Starting with init." 177 | (var result init) 178 | (run! 179 | (fn [x] 180 | (set result (f result x))) 181 | xs) 182 | result) 183 | 184 | (fn some [f xs] 185 | "Return the first truthy result from (f x) or nil." 186 | (var result nil) 187 | (var n 1) 188 | (while (and (nil? result) (<= n (count xs))) 189 | (let [candidate (f (. xs n))] 190 | (when candidate 191 | (set result candidate)) 192 | (set n (inc n)))) 193 | result) 194 | 195 | (fn butlast [xs] 196 | "Return every value from the sequential table except the last one." 197 | (let [total (count xs)] 198 | (->> (kv-pairs xs) 199 | (filter 200 | (fn [[n v]] 201 | (not= n total))) 202 | (map second)))) 203 | 204 | (fn rest [xs] 205 | "Return every value from the sequential table except the first one." 206 | (->> (kv-pairs xs) 207 | (filter 208 | (fn [[n v]] 209 | (not= n 1))) 210 | (map second))) 211 | 212 | (fn concat [...] 213 | "Concatenates the sequential table arguments together." 214 | (let [result []] 215 | (run! (fn [xs] 216 | (run! 217 | (fn [x] 218 | (table.insert result x)) 219 | xs)) 220 | [...]) 221 | result)) 222 | 223 | (fn mapcat [f xs] 224 | "The same as `map` but then `concat` all lists within the result together." 225 | (concat (unpack (map f xs)))) 226 | 227 | (fn pr-str [...] 228 | "Convert the input arguments to a string." 229 | (let [s (table.concat 230 | (map (fn [x] 231 | (fennel.view x {:one-line true})) 232 | [...]) 233 | " ")] 234 | (if (or (nil? s) (= "" s)) 235 | "nil" 236 | s))) 237 | 238 | (fn str [...] 239 | "Concatenate the values into one string. Converting non-string values into 240 | strings where required." 241 | (->> [...] 242 | (map 243 | (fn [s] 244 | (if (string? s) 245 | s 246 | (pr-str s)))) 247 | (reduce 248 | (fn [acc s] 249 | (.. acc s)) 250 | ""))) 251 | 252 | (fn println [...] 253 | "Convert the input arguments to a string (if required) and print them." 254 | (->> [...] 255 | (map 256 | (fn [s] 257 | (if (string? s) 258 | s 259 | (pr-str s)))) 260 | (map-indexed 261 | (fn [[i s]] 262 | (if (= 1 i) 263 | s 264 | (.. " " s)))) 265 | (reduce 266 | (fn [acc s] 267 | (.. acc s)) 268 | "") 269 | print)) 270 | 271 | (fn pr [...] 272 | "Print the arguments as data, strings will remain quoted." 273 | (println (pr-str ...))) 274 | 275 | (fn slurp [path] 276 | "Read the file into a string." 277 | (when path 278 | (match (io.open path "r") 279 | (nil _msg) nil 280 | f (let [content (f:read "*all")] 281 | (f:close) 282 | content)))) 283 | 284 | (fn get [t k d] 285 | "Get the key `k` from table `t` while safely handling `nil`. If it's not 286 | found it will return the optional default value `d`." 287 | (let [res (when (table? t) 288 | (let [val (. t k)] 289 | (when (not (nil? val)) 290 | val)))] 291 | (if (nil? res) 292 | d 293 | res))) 294 | 295 | (fn spit [path content opts] 296 | "Spit the string into the file. When opts.append is true, add to the file." 297 | (when path 298 | (match (io.open 299 | path 300 | (if (get opts :append) 301 | "a" 302 | "w")) 303 | (nil msg) (error (.. "Could not open file: " msg)) 304 | f (do 305 | (f:write content) 306 | (f:close) 307 | nil)))) 308 | 309 | (fn merge! [base ...] 310 | "The same as `merge` above but will mutate the first argument, so all 311 | tables are merged into the first one." 312 | (reduce 313 | (fn [acc m] 314 | (when m 315 | (each [k v (pairs m)] 316 | (tset acc k v))) 317 | acc) 318 | (or base {}) 319 | [...])) 320 | 321 | (fn merge [...] 322 | "Merge the tables together, `nil` will be skipped safely so you can use 323 | `(when ...)` to conditionally include tables. Merges into a fresh table so 324 | no existing tables will be mutated." 325 | (merge! {} ...)) 326 | 327 | (fn select-keys [t ks] 328 | "Extract the keys listed in `ks` from `t` and return it as a new table." 329 | (if (and t ks) 330 | (reduce 331 | (fn [acc k] 332 | (when k 333 | (tset acc k (. t k))) 334 | acc) 335 | {} 336 | ks) 337 | {})) 338 | 339 | (fn get-in [t ks d] 340 | "Get the key path `ks` from table `t` while safely handling `nil`. If it's 341 | not found it will return the optional default value `d`. 342 | 343 | `(get-in {:foo {:bar 10}} [:foo :bar]) // => 10`" 344 | (let [res (reduce 345 | (fn [acc k] 346 | (when (table? acc) 347 | (get acc k))) 348 | t ks)] 349 | (if (nil? res) 350 | d 351 | res))) 352 | 353 | (fn assoc [t ...] 354 | "Set the key `k` in table `t` to the value `v` while safely handling `nil`. 355 | 356 | Accepts more `k` and `v` pairs as after the initial pair. This allows you 357 | to assoc multiple values in one call. 358 | 359 | Returns the table `t` once it has been mutated." 360 | (let [[k v & xs] [...] 361 | rem (count xs) 362 | t (or t {})] 363 | 364 | (when (odd? rem) 365 | (error "assoc expects even number of arguments after table, found odd number")) 366 | 367 | (when (not (nil? k)) 368 | (tset t k v)) 369 | 370 | (when (> rem 0) 371 | (assoc t (unpack xs))) 372 | 373 | t)) 374 | 375 | (fn assoc-in [t ks v] 376 | "Set the key path `ks` in table `t` to the value `v` while safely handling `nil`. 377 | 378 | `(assoc-in {:foo {:bar 10}} [:foo :bar] 15) // => {:foo {:bar 15}}`" 379 | (let [path (butlast ks) 380 | final (last ks) 381 | t (or t {})] 382 | (assoc (reduce 383 | (fn [acc k] 384 | (let [step (get acc k)] 385 | (if (nil? step) 386 | (get (assoc acc k {}) k) 387 | step))) 388 | t 389 | path) 390 | final 391 | v) 392 | t)) 393 | 394 | (fn update [t k f] 395 | "Replace the key `k` in table `t` by passing the current value through the 396 | function `f`. Returns the table after the key has been mutated." 397 | (assoc t k (f (get t k)))) 398 | 399 | (fn update-in [t ks f] 400 | "Same as `update` but replace the key path at `ks` with the result of 401 | passing the current value through the function `f`." 402 | (assoc-in t ks (f (get-in t ks)))) 403 | 404 | (fn constantly [v] 405 | "Returns a function that takes any number of arguments and returns `v`." 406 | (fn [] v)) 407 | 408 | (fn distinct [xs] 409 | "Takes a sequential table of values (xs) and returns a distinct sequential table with all duplicates removed." 410 | (->> xs 411 | (reduce 412 | (fn [acc x] 413 | (tset acc x true) 414 | acc) 415 | {}) 416 | (keys))) 417 | 418 | (fn sort [xs] 419 | "Copies the sequential table xs, sorts it and returns it." 420 | (let [copy (map identity xs)] 421 | (table.sort copy) 422 | copy)) 423 | 424 | (fn clear-table! [t] 425 | "Clear all keys from the table." 426 | (when t 427 | (each [k _ (pairs t)] 428 | (tset t k nil))) 429 | nil) 430 | 431 | (fn take-while [f xs] 432 | "Takes values from xs while (f x) returns true." 433 | (local xs (seq xs)) 434 | (when xs 435 | (var acc []) 436 | (var done? false) 437 | (for [i 1 (count xs) 1] 438 | (let [v (. xs i)] 439 | (if (and (not done?) (f v)) 440 | (table.insert acc v) 441 | (set done? true)))) 442 | acc)) 443 | 444 | (fn drop-while [f xs] 445 | "Drop values while (f x) returns true." 446 | (local xs (seq xs)) 447 | (when xs 448 | (var acc []) 449 | (var done? false) 450 | (for [i 1 (count xs) 1] 451 | (let [v (. xs i)] 452 | (when (or done? (not (f v))) 453 | (table.insert acc v) 454 | (set done? true)))) 455 | acc)) 456 | 457 | (fn ->set [tbl] 458 | "Converts a table `tbl` to a 'set' - which means [:a :b] becomes {:a true :b true}. You can then use contains? to check membership, or just (. my-set :foo) - if that returns true, it's in your set." 459 | 460 | (when tbl 461 | (assert (table? tbl) "Table required as input to ->set.") 462 | (let [result {}] 463 | (each [_n v (ipairs tbl)] 464 | (tset result v true)) 465 | result))) 466 | 467 | (fn contains? [tbl v] 468 | "Does the table `tbl` contain the value `v`? If given an associative table it'll check for membership of the key, if given a sequential table it will scan for the value. Associative is essentially O(1), sequential is O(n). You can use `->set` if you need to perform many lookups, it will turn [:a :b] into {:a true :b true} which is O(1) to check." 469 | (when tbl 470 | (assert (table? tbl) "contains? expects a table") 471 | (if (sequential? tbl) 472 | (or (some #(= $ v) tbl) false) 473 | (= true (. tbl v))))) 474 | 475 | {: rand 476 | : nil? 477 | : number? 478 | : boolean? 479 | : string? 480 | : table? 481 | : function? 482 | : keys 483 | : count 484 | : empty? 485 | : first 486 | : second 487 | : last 488 | : inc 489 | : dec 490 | : even? 491 | : odd? 492 | : vals 493 | : kv-pairs 494 | : run! 495 | : complement 496 | : filter 497 | : remove 498 | : map 499 | : map-indexed 500 | : identity 501 | : reduce 502 | : some 503 | : butlast 504 | : rest 505 | : concat 506 | : mapcat 507 | : pr-str 508 | : str 509 | : println 510 | : pr 511 | : slurp 512 | : spit 513 | : merge! 514 | : merge 515 | : select-keys 516 | : get 517 | : get-in 518 | : assoc 519 | : assoc-in 520 | : update 521 | : update-in 522 | : constantly 523 | : distinct 524 | : sort 525 | : clear-table! 526 | : sequential? 527 | : seq 528 | : take-while 529 | : drop-while 530 | : ->set 531 | : contains?} 532 | -------------------------------------------------------------------------------- /fnl/nfnl/fs.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload : define} (require :nfnl.module)) 2 | (local core (autoload :nfnl.core)) 3 | (local str (autoload :nfnl.string)) 4 | 5 | (local M (define :nfnl.fs)) 6 | 7 | (fn M.basename [path] 8 | "Remove the file part of the path." 9 | (when path 10 | (vim.fn.fnamemodify path ":h"))) 11 | 12 | (fn M.filename [path] 13 | "Just the filename / tail of a path." 14 | (when path 15 | (vim.fn.fnamemodify path ":t"))) 16 | 17 | (fn M.file-name-root [path] 18 | "Remove the suffix / extension of the file in a path." 19 | (when path 20 | (vim.fn.fnamemodify path ":r"))) 21 | 22 | (fn M.full-path [path] 23 | (when path 24 | (vim.fn.fnamemodify path ":p"))) 25 | 26 | (fn M.mkdirp [dir] 27 | (when dir 28 | (vim.fn.mkdir dir "p"))) 29 | 30 | (fn M.replace-extension [path ext] 31 | (when path 32 | (.. (M.file-name-root path) (.. "." ext)))) 33 | 34 | (fn M.read-first-line [path] 35 | (let [f (io.open path "r")] 36 | (when (and f (not (core.string? f))) 37 | (let [line (f:read "*line")] 38 | (f:close) 39 | line)))) 40 | 41 | (fn M.absglob [dir expr] 42 | "Glob all files under dir matching the expression and return the absolute paths." 43 | (vim.fn.globpath dir expr true true)) 44 | 45 | (fn M.relglob [dir expr] 46 | "Glob all files under dir matching the expression and return the paths 47 | relative to the dir argument." 48 | (let [dir-len (+ 2 (string.len dir))] 49 | (->> (M.absglob dir expr) 50 | (core.map #(string.sub $1 dir-len))))) 51 | 52 | (fn M.glob-dir-newer? [a-dir b-dir expr b-dir-path-fn] 53 | "Returns true if a-dir has newer changes than b-dir. All paths from a-dir are mapped through b-dir-path-fn M.before comparing to b-dir." 54 | (var newer? false) 55 | (each [_ path (ipairs (M.relglob a-dir expr))] 56 | (when (> (vim.fn.getftime (.. a-dir path)) 57 | (vim.fn.getftime (.. b-dir (b-dir-path-fn path)))) 58 | (set newer? true))) 59 | newer?) 60 | 61 | (fn M.path-sep [] 62 | ;; https://github.com/nvim-lua/plenary.nvim/blob/8bae2c1fadc9ed5bfcfb5ecbd0c0c4d7d40cb974/lua/plenary/path.lua#L20-L31 63 | (let [os (string.lower jit.os)] 64 | (if (or (= :linux os) 65 | (= :osx os) 66 | (= :bsd os) 67 | (and (= 1 (vim.fn.exists "+shellshash")) 68 | vim.o.shellslash)) 69 | "/" 70 | "\\"))) 71 | 72 | (fn M.findfile [name path] 73 | "Wrapper around Neovim's findfile() that returns nil 74 | instead of an empty string." 75 | (let [res (vim.fn.findfile name path)] 76 | (when (not (core.empty? res)) 77 | (M.full-path res)))) 78 | 79 | (fn M.split-path [path] 80 | (str.split path (M.path-sep))) 81 | 82 | (fn M.join-path [parts] 83 | (str.join (M.path-sep) (core.concat parts))) 84 | 85 | (fn M.replace-dirs [path from to] 86 | "Replaces directories in `path` that match `from` with `to`." 87 | (->> (M.split-path path) 88 | (core.map 89 | (fn [segment] 90 | (if (= from segment) 91 | to 92 | segment))) 93 | (M.join-path))) 94 | 95 | (fn M.fnl-path->lua-path [fnl-path] 96 | (-> fnl-path 97 | (M.replace-extension "lua") 98 | (M.replace-dirs "fnl" "lua"))) 99 | 100 | (fn M.glob-matches? [dir expr path] 101 | "Return true if path matches the glob expression. The path should be absolute and the glob should be relative to dir." 102 | (let [regex (vim.regex (vim.fn.glob2regpat (M.join-path [dir expr])))] 103 | (regex:match_str path))) 104 | 105 | (local uv (or vim.uv vim.loop)) 106 | 107 | (fn M.exists? [path] 108 | (when path 109 | (= "table" (type (uv.fs_stat path))))) 110 | 111 | M 112 | -------------------------------------------------------------------------------- /fnl/nfnl/gc.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload : define} (require :nfnl.module)) 2 | (local core (autoload :nfnl.core)) 3 | (local fs (autoload :nfnl.fs)) 4 | (local header (autoload :nfnl.header)) 5 | 6 | (local M (define :nfnl.gc)) 7 | 8 | (fn M.find-orphan-lua-files [{: cfg : root-dir}] 9 | (let [fnl-path->lua-path (cfg [:fnl-path->lua-path]) 10 | ignore-patterns (cfg [:orphan-detection :ignore-patterns])] 11 | (->> (cfg [:source-file-patterns]) 12 | (core.mapcat 13 | (fn [fnl-pattern] 14 | (let [lua-pattern (fnl-path->lua-path fnl-pattern)] 15 | (fs.relglob root-dir lua-pattern)))) 16 | (core.->set) 17 | (core.keys) 18 | (core.filter 19 | (fn [path] 20 | (let [line (fs.read-first-line path)] 21 | (and (not (core.some 22 | (fn [pat] 23 | (path:find pat)) 24 | ignore-patterns)) 25 | (header.tagged? line) 26 | (not (fs.exists? (header.source-path line)))))))))) 27 | 28 | (comment 29 | (local config (require :nfnl.config)) 30 | (M.find-orphan-lua-files (config.find-and-load "."))) 31 | 32 | M 33 | -------------------------------------------------------------------------------- /fnl/nfnl/header.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload : define} (require :nfnl.module)) 2 | (local core (autoload :nfnl.core)) 3 | (local str (autoload :nfnl.string)) 4 | 5 | (local M (define :nfnl.header)) 6 | 7 | (local tag "[nfnl]") 8 | 9 | (fn M.with-header [file src] 10 | "Return the source with an nfnl header prepended." 11 | (.. "-- " tag " " file "\n" src)) 12 | 13 | (fn M.tagged? [s] 14 | "Is the line an nfnl tagged header line?" 15 | (when s 16 | (core.number? (s:find tag 1 true)))) 17 | 18 | (fn M.source-path [s] 19 | (when (M.tagged? s) 20 | (core.some 21 | (fn [part] 22 | (and (str.ends-with? part ".fnl") part)) 23 | (str.split s "%s+")))) 24 | 25 | M 26 | -------------------------------------------------------------------------------- /fnl/nfnl/init.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload} (require :nfnl.module)) 2 | (local callback (autoload :nfnl.callback)) 3 | 4 | (local minimum-neovim-version "0.9.0") 5 | 6 | (when vim 7 | (when (= 0 (vim.fn.has (.. "nvim-" minimum-neovim-version))) 8 | (error (.. "nfnl requires Neovim > v" minimum-neovim-version))) 9 | 10 | (vim.api.nvim_create_autocmd 11 | ["Filetype"] 12 | {:group (vim.api.nvim_create_augroup "nfnl-setup" {}) 13 | :pattern "fennel" 14 | :callback callback.fennel-filetype-callback}) 15 | 16 | (when (= :fennel vim.o.filetype) 17 | (callback.fennel-filetype-callback 18 | {:file (vim.fn.expand "%") 19 | :buf (vim.api.nvim_get_current_buf)}))) 20 | 21 | (fn setup [opts] 22 | "Sets the vim.g.nfnl#... variables in a slightly more Lua friendly way." 23 | 24 | (when opts 25 | (set vim.g.nfnl#compile_on_write opts.compile_on_write))) 26 | 27 | {: setup} 28 | -------------------------------------------------------------------------------- /fnl/nfnl/macros.fnl: -------------------------------------------------------------------------------- 1 | ;; [nfnl-macro] 2 | 3 | (fn time [...] 4 | `(let [start# (vim.loop.hrtime) 5 | result# (do ,...) 6 | end# (vim.loop.hrtime)] 7 | (print (.. "Elapsed time: " (/ (- end# start#) 1000000) " msecs")) 8 | result#)) 9 | 10 | (fn conditional-let [branch bindings ...] 11 | (assert (= 2 (length bindings)) "expected a single binding pair") 12 | 13 | (let [[bind-expr value-expr] bindings] 14 | (if 15 | ;; Simple symbols 16 | ;; [foo bar] 17 | (sym? bind-expr) 18 | `(let [,bind-expr ,value-expr] 19 | (,branch ,bind-expr ,...)) 20 | 21 | ;; List / values destructure 22 | ;; [(a b) c] 23 | (list? bind-expr) 24 | (do 25 | ;; Even if the user isn't using the first slot, we will. 26 | ;; [(_ val) (pcall #:foo)] 27 | ;; => [(bindGENSYM12345 val) (pcall #:foo)] 28 | (when (= `_ (. bind-expr 1)) 29 | (tset bind-expr 1 (gensym "bind"))) 30 | 31 | `(let [,bind-expr ,value-expr] 32 | (,branch ,(. bind-expr 1) ,...))) 33 | 34 | ;; Sequential and associative table destructure 35 | ;; [[a b] c] 36 | ;; [{: a : b} c] 37 | (table? bind-expr) 38 | `(let [value# ,value-expr 39 | ,bind-expr (or value# {})] 40 | (,branch value# ,...)) 41 | 42 | ;; We should never get here, but just in case. 43 | (assert (.. "unknown bind-expr type: " (type bind-expr)))))) 44 | 45 | (fn if-let [bindings ...] 46 | (assert (<= (length [...]) 2) (.. "if-let does not support more than two branches")) 47 | (conditional-let `if bindings ...)) 48 | 49 | (fn when-let [bindings ...] 50 | (conditional-let `when bindings ...)) 51 | 52 | {: time 53 | : if-let 54 | : when-let} 55 | -------------------------------------------------------------------------------- /fnl/nfnl/macros/aniseed.fnl: -------------------------------------------------------------------------------- 1 | ;; [nfnl-macro] 2 | 3 | ;; Copied over from Aniseed. Contains all of the def* module macro systems. 4 | ;; https://github.com/Olical/aniseed 5 | 6 | ;; This has been heavily slimmed down from the original implementation, the 7 | ;; `(module ...) macro can now ONLY define your module, it can not be used 8 | ;; to require dependencies. 9 | 10 | ;; We had to slim things down because the Fennel compiler no longer supports 11 | ;; the weird tricks we were using. 12 | 13 | ;; In nfnl they are not automatically required, you must use import-macros to 14 | ;; require them explicitly when migrating your Aniseed based projects. 15 | 16 | ;; Avoids the compiler complaining that we're introducing locals without gensym. 17 | (local mod-str :*module*) 18 | (local mod-sym (sym mod-str)) 19 | 20 | ;; Upserts the existence of the module for subsequent def forms. 21 | ;; 22 | ;; (module foo 23 | ;; {:some-optional-base :table-of-things 24 | ;; :to-base :the-module-off-of}) 25 | (fn module [mod-name mod-base] 26 | `(local 27 | ,mod-sym 28 | (let [pkg# (require :package)] 29 | (tset pkg#.loaded ,(tostring mod-name) ,(or mod-base {})) 30 | (. pkg#.loaded ,(tostring mod-name))))) 31 | 32 | (fn def- [name value] 33 | `(local ,name ,value)) 34 | 35 | (fn def [name value] 36 | `(local ,name (do (tset ,mod-sym ,(tostring name) ,value) (. ,mod-sym ,(tostring name))))) 37 | 38 | (fn defn- [name ...] 39 | `(fn ,name ,...)) 40 | 41 | (fn defn [name ...] 42 | `(def ,name (fn ,name ,...))) 43 | 44 | (fn defonce- [name value] 45 | `(def- ,name (or ,name ,value))) 46 | 47 | (fn defonce [name value] 48 | `(def ,name (or (. ,mod-sym ,(tostring name)) ,value))) 49 | 50 | {:module module 51 | :def- def- :def def 52 | :defn- defn- :defn defn 53 | :defonce- defonce- :defonce defonce} 54 | -------------------------------------------------------------------------------- /fnl/nfnl/module.fnl: -------------------------------------------------------------------------------- 1 | (local module-key :nfnl/autoload-module) 2 | (local enabled-key :nfnl/autoload-enabled?) 3 | 4 | (local M {}) 5 | 6 | (fn M.autoload [name] 7 | "Like autoload from Vim Script! A replacement for require that will load the 8 | module when you first use it. 9 | 10 | (local {: autoload} (require :nfnl.module)) 11 | (local my-mod (autoload :my-mod)) 12 | (my-mod.some-fn 10) ;; Your module is required here! 13 | 14 | It's a drop in replacement for require that should speed up your Neovim 15 | startup dramatically. Only works with table modules, if the module you're 16 | requiring is a function or anything other than a table you need to use the 17 | normal require." 18 | 19 | (let [res {enabled-key true 20 | module-key false} 21 | ensure (fn [] 22 | (if (. res module-key) 23 | (. res module-key) 24 | (let [m (require name)] 25 | (tset res module-key m) 26 | m)))] 27 | 28 | (setmetatable 29 | res 30 | 31 | {:__call 32 | (fn [t ...] 33 | ((ensure) ...)) 34 | 35 | :__index 36 | (fn [t k] 37 | (. (ensure) k)) 38 | 39 | :__newindex 40 | (fn [t k v] 41 | (tset (ensure) k v))}))) 42 | 43 | (fn M.define [mod-name base] 44 | "Looks up the mod-name in package.loaded, if it's the same type as base it'll 45 | use the loaded value. If it's different it'll use base. 46 | 47 | The returned result should be used as your default value for M like so: 48 | (local M (define :my.mod {})) 49 | 50 | Then return M at the bottom of your file and define functions on M like so: 51 | (fn M.my-fn [x] (+ x 1)) 52 | 53 | This technique helps you have extremely reloadable modules through Conjure. 54 | You can reload the entire file or induvidual function definitions and the 55 | changes will be reflected in all other modules that depend on this one 56 | without having to reload the dependant modules. 57 | 58 | The base value defaults to {}, an empty table." 59 | 60 | (let [loaded (. package.loaded mod-name)] 61 | (if (and (= (type loaded) (type base)) 62 | (not= nil loaded)) 63 | loaded 64 | (or base {})))) 65 | 66 | M 67 | -------------------------------------------------------------------------------- /fnl/nfnl/notify.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload} (require :nfnl.module)) 2 | (local core (autoload :nfnl.core)) 3 | 4 | (fn notify [level ...] 5 | (vim.api.nvim_notify 6 | (core.str ...) 7 | level 8 | {})) 9 | 10 | (fn debug [...] 11 | (notify vim.log.levels.DEBUG ...)) 12 | 13 | (fn error [...] 14 | (notify vim.log.levels.ERROR ...)) 15 | 16 | (fn info [...] 17 | (notify vim.log.levels.INFO ...)) 18 | 19 | (fn trace [...] 20 | (notify vim.log.levels.TRACE ...)) 21 | 22 | (fn warn [...] 23 | (notify vim.log.levels.WARN ...)) 24 | 25 | {: debug 26 | : error 27 | : info 28 | : trace 29 | : warn} 30 | -------------------------------------------------------------------------------- /fnl/nfnl/nvim.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload} (require :nfnl.module)) 2 | (local str (autoload :nfnl.string)) 3 | 4 | (fn get-buf-content-as-string [buf] 5 | (or 6 | (->> (vim.api.nvim_buf_get_lines (or buf 0) 0 -1 false) 7 | (str.join "\n")) 8 | "")) 9 | 10 | {: get-buf-content-as-string} 11 | -------------------------------------------------------------------------------- /fnl/nfnl/repl.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload} (require :nfnl.module)) 2 | (local core (autoload :nfnl.core)) 3 | (local fennel (autoload :nfnl.fennel)) 4 | (local notify (autoload :nfnl.notify)) 5 | (local str (autoload :nfnl.string)) 6 | 7 | (fn new [opts] 8 | "Create a new REPL instance which is a function you repeatedly call with more 9 | code for evaluation. The results of the evaluations are returned in a table. 10 | Call with a nil instead of a string of code to close the REPL. 11 | 12 | Takes an opts table that can contain: 13 | * `on-error` - a function that will be called with the err-type, err and lua-source of any errors that occur in the REPL. 14 | * `cfg` - from cfg-fn in nfnl.config, will be used to configure the fennel instance with path strings before each evaluation." 15 | (var results-to-return nil) 16 | (local cfg (?. opts :cfg)) 17 | 18 | (local co 19 | (coroutine.create 20 | (fn [] 21 | (fennel.repl 22 | (core.merge! 23 | {:pp core.identity 24 | :readChunk coroutine.yield 25 | 26 | ;; Possible Fennel bug? We need to clone _G or multiple REPLs interfere with each other sometimes. 27 | :env (core.merge _G) 28 | 29 | :onValues 30 | (fn [results] 31 | (set results-to-return (core.concat results-to-return results))) 32 | 33 | :onError 34 | (fn [err-type err lua-source] 35 | (if (?. opts :on-error) 36 | (opts.on-error err-type err lua-source) 37 | (notify.error (str.trim (str.join "\n\n" [(.. "[" err-type "] " err) lua-source])))))} 38 | (when cfg 39 | (cfg [:compiler-options]))))))) 40 | 41 | (coroutine.resume co) 42 | 43 | (fn [input] 44 | (when cfg 45 | (set fennel.path (cfg [:fennel-path])) 46 | (set fennel.macro-path (cfg [:fennel-macro-path]))) 47 | 48 | (coroutine.resume co input) 49 | 50 | (let [prev-eval-values results-to-return] 51 | (set results-to-return nil) 52 | prev-eval-values))) 53 | 54 | {: new} 55 | -------------------------------------------------------------------------------- /fnl/nfnl/string.fnl: -------------------------------------------------------------------------------- 1 | (local {: autoload} (require :nfnl.module)) 2 | (local core (autoload :nfnl.core)) 3 | 4 | (fn join [...] 5 | "(join xs) (join sep xs) 6 | Joins all items of a table together with an optional separator. 7 | Separator defaults to an empty string. 8 | Values that aren't a string or nil will go through nfnl.core/pr-str." 9 | (let [args [...] 10 | [sep xs] (if (= 2 (core.count args)) 11 | args 12 | ["" (core.first args)]) 13 | len (core.count xs)] 14 | 15 | (var result []) 16 | 17 | (when (> len 0) 18 | (for [i 1 len] 19 | (let [x (. xs i)] 20 | (-?>> (if 21 | (= :string (type x)) x 22 | (= nil x) x 23 | (core.pr-str x)) 24 | (table.insert result))))) 25 | 26 | (table.concat result sep))) 27 | 28 | (fn split [s pat] 29 | "Split the given string into a sequential table using the pattern." 30 | (var done? false) 31 | (var acc []) 32 | (var index 1) 33 | (while (not done?) 34 | (let [(start end) (string.find s pat index)] 35 | (if (= :nil (type start)) 36 | (do 37 | (table.insert acc (string.sub s index)) 38 | (set done? true)) 39 | (do 40 | (table.insert acc (string.sub s index (- start 1))) 41 | (set index (+ end 1)))))) 42 | acc) 43 | 44 | (fn blank? [s] 45 | "Check if the string is nil, empty or only whitespace." 46 | (or (core.empty? s) 47 | (not (string.find s "[^%s]")))) 48 | 49 | (fn triml [s] 50 | "Removes whitespace from the left side of string." 51 | (string.gsub s "^%s*(.-)" "%1")) 52 | 53 | (fn trimr [s] 54 | "Removes whitespace from the right side of string." 55 | (string.gsub s "(.-)%s*$" "%1")) 56 | 57 | (fn trim [s] 58 | "Removes whitespace from both ends of string." 59 | (string.gsub s "^%s*(.-)%s*$" "%1")) 60 | 61 | (fn ends-with? [s suffix] 62 | "Check if the string ends with suffix." 63 | (let [suffix-len (# suffix) 64 | s-len (# s)] 65 | (if (>= s-len suffix-len) 66 | (= suffix (string.sub s (- s-len suffix-len -1))) 67 | false))) 68 | 69 | {: join 70 | : split 71 | : blank? 72 | : triml 73 | : trimr 74 | : trim 75 | : ends-with?} 76 | -------------------------------------------------------------------------------- /fnl/spec/nfnl/callback_spec.fnl: -------------------------------------------------------------------------------- 1 | (local {: describe : it} (require :plenary.busted)) 2 | (local assert (require :luassert.assert)) 3 | (local callback (require :nfnl.callback)) 4 | 5 | (describe 6 | "supported-path?" 7 | (fn [] 8 | (it "rejects non-strings" 9 | (fn [] 10 | (assert.false (callback.supported-path? nil)) 11 | (assert.false (callback.supported-path? 10)))) 12 | 13 | (it "rejects fugitive or ddu-ff protcols" 14 | (fn [] 15 | (assert.false (callback.supported-path? "fugitive://foo/bar")) 16 | (assert.false (callback.supported-path? "ddu-ff:/xyz/foo/bar")))) 17 | 18 | (it "allows all other strings" 19 | (fn [] 20 | (assert.true (callback.supported-path? "/foo/bar/baz")) 21 | (assert.true (callback.supported-path? "./x/y/z.foo")) 22 | (assert.true (callback.supported-path? "henlo.world")))))) 23 | -------------------------------------------------------------------------------- /fnl/spec/nfnl/compile_spec.fnl: -------------------------------------------------------------------------------- 1 | (local {: describe : it} (require :plenary.busted)) 2 | (local assert (require :luassert.assert)) 3 | (local config (require :nfnl.config)) 4 | (local compile (require :nfnl.compile)) 5 | 6 | (describe 7 | "into-string" 8 | (fn [] 9 | (it "compiles good Fennel to Lua" 10 | (fn [] 11 | (assert.are.same 12 | {:result "-- [nfnl] bar.fnl\nreturn (10 + 20)\n" 13 | :source-path "/tmp/foo/bar.fnl" 14 | :status "ok"} 15 | (compile.into-string 16 | {:root-dir "/tmp/foo" 17 | :path "/tmp/foo/bar.fnl" 18 | :cfg (config.cfg-fn {} {:root-dir "/tmp/foo"}) 19 | :batch? true 20 | :source "(+ 10 20)"})))) 21 | 22 | (it "skips files that don't match :source-file-patterns" 23 | (fn [] 24 | (assert.are.same 25 | {:source-path "/my/dir/baz.fnl" 26 | :status "path-is-not-in-source-file-patterns"} 27 | (compile.into-string 28 | {:root-dir "/my/dir" 29 | :path "/my/dir/baz.fnl" 30 | :cfg (config.cfg-fn {:source-file-patterns ["bar.fnl"]} 31 | {:root-dir "/tmp/foo"}) 32 | :batch? true 33 | :source "(+ 10 20)"})))) 34 | 35 | (it "skips macro files" 36 | (fn [] 37 | (assert.are.same 38 | {:source-path "/my/dir/foo.fnl" 39 | :status "macros-are-not-compiled"} 40 | (compile.into-string 41 | {:root-dir "/my/dir" 42 | :path "/my/dir/foo.fnl" 43 | :cfg (config.cfg-fn {} {:root-dir "/tmp/foo"}) 44 | :batch? true 45 | :source (.. "; [nfnl" "-" "macro]\n(+ 10 20)")})))) 46 | 47 | (it "won't compile the .nfnl.fnl config file" 48 | (fn [] 49 | (assert.are.same 50 | {:source-path "/my/dir/.nfnl.fnl" 51 | :status "nfnl-config-is-not-compiled"} 52 | (compile.into-string 53 | {:root-dir "/my/dir" 54 | :path "/my/dir/.nfnl.fnl" 55 | :cfg (config.cfg-fn {} {:root-dir "/tmp/foo"}) 56 | :batch? true 57 | :source "(+ 10 20)"})))) 58 | 59 | (it "returns compilation errors" 60 | (fn [] 61 | (assert.are.same 62 | {:error "/my/dir/foo.fnl:1:3: Compile error: tried to reference a special form without calling it\n\n10 / 20\n* Try making sure to use prefix operators, not infix.\n* Try wrapping the special in a function if you need it to be first class." 63 | :source-path "/my/dir/foo.fnl" 64 | :status "compilation-error"} 65 | (compile.into-string 66 | {:root-dir "/my/dir" 67 | :path "/my/dir/foo.fnl" 68 | :cfg (config.cfg-fn {} {:root-dir "/tmp/foo"}) 69 | :batch? true 70 | :source "10 / 20"})))))) 71 | -------------------------------------------------------------------------------- /fnl/spec/nfnl/config_spec.fnl: -------------------------------------------------------------------------------- 1 | (local {: describe : it} (require :plenary.busted)) 2 | (local assert (require :luassert.assert)) 3 | (local config (require :nfnl.config)) 4 | (local fs (require :nfnl.fs)) 5 | 6 | (describe 7 | "default" 8 | (fn [] 9 | (it "is a function that returns a table" 10 | (fn [] 11 | (assert.equals :function (type config.default)) 12 | (assert.equals :table (type (config.default {:root-dir "/tmp/foo"}))) 13 | (assert.equals (vim.fn.getcwd) (. (config.default {}) :root-dir)))))) 14 | 15 | (describe 16 | "cfg-fn" 17 | (fn [] 18 | (it "builds a function that looks up values in a table falling back to defaults" 19 | (fn [] 20 | (local opts {:root-dir "/tmp/foo"}) 21 | (assert.is_string ((config.cfg-fn {} opts) [:fennel-macro-path])) 22 | (assert.is_nil ((config.cfg-fn {} opts) [:nope])) 23 | (assert.equals :yep ((config.cfg-fn {:nope :yep} opts) [:nope])) 24 | (assert.equals :yep ((config.cfg-fn {:fennel-macro-path :yep} opts) [:fennel-macro-path])))))) 25 | 26 | (describe 27 | "config-file-path?" 28 | (fn [] 29 | (it "returns true for config file paths" 30 | (fn [] 31 | (assert.is_true (config.config-file-path? "./foo/.nfnl.fnl")) 32 | (assert.is_true (config.config-file-path? ".nfnl.fnl")) 33 | (assert.is_false (config.config-file-path? ".fnl.fnl")))))) 34 | 35 | (describe 36 | "find-and-load" 37 | (fn [] 38 | (it "loads the repo config file" 39 | (fn [] 40 | (let [{: cfg : root-dir : config} 41 | (config.find-and-load ".")] 42 | (assert.are.same {:verbose true} config) 43 | (assert.equals (vim.fn.getcwd) root-dir) 44 | (assert.equals :function (type cfg))))) 45 | 46 | (it "returns an empty table if a config file isn't found" 47 | (fn [] 48 | (assert.are.same {} (config.find-and-load "/some/made/up/dir")))))) 49 | 50 | (fn sorted [xs] 51 | (table.sort xs) 52 | xs) 53 | 54 | (describe 55 | "path-dirs" 56 | (fn [] 57 | (it "builds path dirs from runtimepath, deduplicates the base-dirs" 58 | (fn [] 59 | (assert.are.same 60 | ["/foo/bar/nfnl" "/foo/baz/my-proj"] 61 | (sorted 62 | (config.path-dirs 63 | {:runtimepath "/foo/bar/nfnl,/foo/bar/other-thing" 64 | :rtp-patterns [(.. (fs.path-sep) "nfnl$")] 65 | :base-dirs ["/foo/baz/my-proj"]}))) 66 | 67 | (assert.are.same 68 | ["/foo/bar/nfnl" "/foo/baz/my-proj"] 69 | (sorted 70 | (config.path-dirs 71 | {:runtimepath "/foo/bar/nfnl,/foo/bar/other-thing" 72 | :rtp-patterns [(.. (fs.path-sep) "nfnl$")] 73 | :base-dirs ["/foo/baz/my-proj" "/foo/bar/nfnl"]}))))))) 74 | -------------------------------------------------------------------------------- /fnl/spec/nfnl/core_spec.fnl: -------------------------------------------------------------------------------- 1 | (local {: describe : it} (require :plenary.busted)) 2 | (local assert (require :luassert.assert)) 3 | (local core (require :nfnl.core)) 4 | (local fs (require :nfnl.fs)) 5 | 6 | (describe 7 | "rand" 8 | (fn [] 9 | (math.randomseed (os.time)) 10 | 11 | (it "returns a number" 12 | (fn [] 13 | (assert.is_number (core.rand)))) 14 | 15 | (it "is not the same when called twice" 16 | (fn [] 17 | (assert.are_not.equal (core.rand) (core.rand)))))) 18 | 19 | (describe 20 | "first" 21 | (fn [] 22 | (it "gets the first value" 23 | (fn [] 24 | (assert.equals 1 (core.first [1 2 3])))) 25 | 26 | (it "returns nil for empty lists or nil" 27 | (fn [] 28 | (assert.is_nil (core.first nil)) 29 | (assert.is_nil (core.first [])))))) 30 | 31 | (describe 32 | "last" 33 | (fn [] 34 | (it "gets the last value" 35 | (fn [] 36 | (assert.equals 3 (core.last [1 2 3])))) 37 | 38 | (it "returns nil for empty lists or nil" 39 | (fn [] 40 | (assert.is_nil (core.last nil)) 41 | (assert.is_nil (core.last [])))))) 42 | 43 | (describe 44 | "butlast" 45 | (fn [] 46 | (it "returns empty results where appropriate" 47 | (fn [] 48 | (assert.are.same [] (core.butlast nil)) 49 | (assert.are.same [] (core.butlast [])) 50 | (assert.are.same [] (core.butlast [1])))) 51 | 52 | (it "works when there's more than one item" 53 | (fn [] 54 | (assert.are.same [1 2] (core.butlast [1 2 3])))))) 55 | 56 | (describe 57 | "rest" 58 | (fn [] 59 | (it "returns empty where appropriate" 60 | (fn [] 61 | (assert.are.same [] (core.rest nil)) 62 | (assert.are.same [] (core.rest [])) 63 | (assert.are.same [] (core.rest [1])))) 64 | 65 | (it "returns the rest if there's more than one value" 66 | (fn [] 67 | (assert.are.same [2 3] (core.rest [1 2 3])))))) 68 | 69 | (describe 70 | "second" 71 | (fn [] 72 | (it "returns nil when required" 73 | (fn [] 74 | (assert.is_nil (core.second [])) 75 | (assert.is_nil (core.second nil)) 76 | (assert.is_nil (core.second [1 nil 3])))) 77 | 78 | (it "returns the second item if there is one" 79 | (fn [] 80 | (assert.equals 2 (core.second [1 2 3])))))) 81 | 82 | (describe 83 | "string?" 84 | (fn [] 85 | (it "returns true for strings" 86 | (fn [] 87 | (assert.is_true (core.string? "foo")) 88 | (assert.is_false (core.string? nil)) 89 | (assert.is_false (core.string? 10)))))) 90 | 91 | (describe 92 | "nil?" 93 | (fn [] 94 | (it "returns true for strings" 95 | (fn [] 96 | (assert.is_false (core.nil? "foo")) 97 | (assert.is_true (core.nil? nil)) 98 | (assert.is_false (core.nil? 10)))))) 99 | 100 | (describe 101 | "some" 102 | (fn [] 103 | (it "returns nil when nothing matches" 104 | (fn [] 105 | (assert.is_nil (core.some #(and (> $1 5) $1) [1 2 3])))) 106 | 107 | (it "returns the first match" 108 | (fn [] 109 | (assert.equals 3 (core.some #(and (> $1 2) $1) [1 2 3])))) 110 | 111 | (it "handles nils" 112 | (fn [] 113 | (assert.equals 3 (core.some #(and $1 (> $1 2) $1) [nil 1 nil 2 nil 3 nil])))))) 114 | 115 | (describe 116 | "inc" 117 | (fn [] 118 | (it "increments numbers" 119 | (fn [] 120 | (assert.equals 2 (core.inc 1)) 121 | (assert.equals -4 (core.inc -5)))))) 122 | 123 | (describe 124 | "dec" 125 | (fn [] 126 | (it "decrements numbers" 127 | (fn [] 128 | (assert.equals 1 (core.dec 2)) 129 | (assert.equals -6 (core.dec -5)))))) 130 | 131 | (describe 132 | "even?" 133 | (fn [] 134 | (it "returns true for even numbers" 135 | (fn [] 136 | (assert.is_true (core.even? 2)) 137 | (assert.is_false (core.even? 3)))))) 138 | 139 | (describe 140 | "odd?" 141 | (fn [] 142 | (it "returns true for odd numbers" 143 | (fn [] 144 | (assert.is_false (core.odd? 2)) 145 | (assert.is_true (core.odd? 3)))))) 146 | 147 | (fn sort-tables [t] 148 | (table.sort 149 | t 150 | (fn [x y] 151 | (if 152 | (and (= :table (type x)) (= :table (type y))) 153 | (< (core.first x) (core.first y)) 154 | 155 | (not= (type x) (type y)) 156 | (< (tostring x) (tostring y)) 157 | 158 | (< x y)))) 159 | t) 160 | 161 | (describe 162 | "keys" 163 | (fn [] 164 | (it "returns the keys of a map" 165 | (fn [] 166 | (assert.are.same [] (core.keys nil)) 167 | (assert.are.same [] (core.keys {})) 168 | (assert.are.same [:a :b] (sort-tables (core.keys {:a 2 :b 3}))))))) 169 | 170 | (describe 171 | "vals" 172 | (fn [] 173 | (it "returns the values of a map" 174 | (fn [] 175 | (assert.are.same [] (core.vals nil)) 176 | (assert.are.same [] (core.vals {})) 177 | (assert.are.same [2 3] (sort-tables (core.vals {:a 2 :b 3}))))))) 178 | 179 | (describe 180 | "kv-pairs" 181 | (fn [] 182 | (it "turns a map into key value pair tuples" 183 | (fn [] 184 | (assert.are.same [] (core.kv-pairs nil)) 185 | (assert.are.same [] (core.kv-pairs {})) 186 | (assert.are.same [[:a 1] [:b 2]] (sort-tables (core.kv-pairs {:a 1 :b 2}))) 187 | (assert.are.same [[1 :a] [2 :b]] (sort-tables (core.kv-pairs [:a :b]))))))) 188 | 189 | (describe 190 | "pr-str" 191 | (fn [] 192 | (it "prints a value into a string using Fennel's view function" 193 | (fn [] 194 | (assert.equals "[1 2 3]" (core.pr-str [1 2 3])) 195 | (assert.equals "1 2 3" (core.pr-str 1 2 3)) 196 | (assert.equals "nil" (core.pr-str nil)))))) 197 | 198 | (describe 199 | "str" 200 | (fn [] 201 | (it "joins many things into one string, using pr-str on the arguments" 202 | (fn [] 203 | (assert.equals "" (core.str)) 204 | (assert.equals "" (core.str "")) 205 | (assert.equals "" (core.str nil)) 206 | (assert.equals "abc" (core.str "abc")) 207 | (assert.equals "abc" (core.str "a" "b" "c")) 208 | (assert.equals "{:a \"abc\"}" (core.str {:a :abc})) 209 | (assert.equals "[1 2 3]abc" (core.str [1 2 3] "a" "bc")))))) 210 | 211 | (describe 212 | "map" 213 | (fn [] 214 | (it "maps a list to another list" 215 | (fn [] 216 | (assert.are.same [2 3 4] (core.map core.inc [1 2 3])))))) 217 | 218 | (describe 219 | "map-indexed" 220 | (fn [] 221 | (it "maps a list to another list, providing the index to the map fn" 222 | (fn [] 223 | (assert.are.same 224 | [[2 :a] [3 :b]] 225 | (core.map-indexed 226 | (fn [[k v]] 227 | [(core.inc k) v]) 228 | [:a :b])))))) 229 | 230 | (describe 231 | "complement" 232 | (fn [] 233 | (it "inverts the boolean result of a function" 234 | (fn [] 235 | (assert.is_true ((core.complement (fn [] false)))) 236 | (assert.is_false ((core.complement (fn [] true)))))))) 237 | 238 | (describe 239 | "filter" 240 | (fn [] 241 | (it "filters values out of a list" 242 | (fn [] 243 | (assert.are.same [2 4 6] (core.filter #(= 0 (% $1 2)) [1 2 3 4 5 6])) 244 | (assert.are.same [] (core.filter #(= 0 (% $1 2)) nil)))))) 245 | 246 | (describe 247 | "remove" 248 | (fn [] 249 | (it "removes matching items" 250 | (fn [] 251 | (assert.are.same [1 3 5] (core.remove #(= 0 (% $1 2)) [1 2 3 4 5 6])) 252 | (assert.are.same [] (core.remove #(= 0 (% $1 2)) nil)))))) 253 | 254 | (describe 255 | "identity" 256 | (fn [] 257 | (it "returns what you give it" 258 | (fn [] 259 | (assert.equals :hello (core.identity :hello)) 260 | (assert.is_nil (core.identity)))))) 261 | 262 | (describe 263 | "concat" 264 | (fn [] 265 | (it "concatenates tables together" 266 | (fn [] 267 | (let [orig [1 2 3]] 268 | (assert.are.same [1 2 3 4 5 6] (core.concat orig [4 5 6])) 269 | (assert.are.same [4 5 6 1 2 3] (core.concat [4 5 6] orig)) 270 | (assert.are.same [1 2 3] orig)))))) 271 | 272 | (describe 273 | "mapcat" 274 | (fn [] 275 | (it "maps and concats" 276 | (fn [] 277 | (assert.are.same [1 :x 2 :x 3 :x] (core.mapcat (fn [n] [n :x]) [1 2 3])))))) 278 | 279 | (describe 280 | "count" 281 | (fn [] 282 | (it "counts various types" 283 | (fn [] 284 | (assert.equals 3 (core.count [1 2 3]) "three values") 285 | (assert.equals 0 (core.count []) "empty") 286 | (assert.equals 0 (core.count nil) "nil") 287 | (assert.equals 0 (core.count nil) "no arg") 288 | (assert.equals 3 (core.count [1 nil 3]) "nil gap") 289 | (assert.equals 4 (core.count [nil nil nil :a]) "mostly nils") 290 | (assert.equals 3 (core.count "foo") "strings") 291 | (assert.equals 0 (core.count "") "empty strings") 292 | (assert.equals 2 (core.count {:a 1 :b 2}) "associative also works"))))) 293 | 294 | (describe 295 | "empty?" 296 | (fn [] 297 | (it "checks if tables are empty" 298 | (fn [] 299 | (assert.is_true (core.empty? []) "empty table") 300 | (assert.is_false (core.empty? [1]) "full table") 301 | (assert.is_true (core.empty? "") "empty string") 302 | (assert.is_false (core.empty? "a") "full string"))))) 303 | 304 | (describe 305 | "merge" 306 | (fn [] 307 | (it "merges tables together in a pure way returning a new table" 308 | (fn [] 309 | (assert.are.same {:a 1 :b 2} (core.merge {} {:a 1} {:b 2}) "simple maps") 310 | (assert.are.same {} (core.merge) "always start with an empty table") 311 | (assert.are.same {:a 1} (core.merge nil {:a 1}) "into nil") 312 | (assert.are.same {:a 1 :c 3} (core.merge {:a 1} nil {:c 3}) "nil in the middle"))))) 313 | 314 | (describe 315 | "merge!" 316 | (fn [] 317 | (it "merges in a side effecting way into the first table" 318 | (fn [] 319 | (let [result {:c 3}] 320 | (assert.are.same {:a 1 :b 2 :c 3} (core.merge! result {:a 1} {:b 2}) "simple maps") 321 | (assert.are.same {:a 1 :b 2 :c 3} result "the bang version side effects")))))) 322 | 323 | (describe 324 | "select-keys" 325 | (fn [] 326 | (it "pulls specific keys out of the table into a new one" 327 | (fn [] 328 | (assert.are.same {} (core.select-keys nil [:a :b]) "no table") 329 | (assert.are.same {} (core.select-keys {} [:a :b]) "empty table") 330 | (assert.are.same {} (core.select-keys {:a 1 :b 2} nil) "no keys") 331 | (assert.are.same {} (core.select-keys nil nil) "nothing") 332 | (assert.are.same {:a 1 :c 3} (core.select-keys {:a 1 :b 2 :c 3} [:a :c]) "simple table and keys"))))) 333 | 334 | (describe 335 | "get" 336 | (fn [] 337 | (it "pulls values out of tables" 338 | (fn [] 339 | (assert.equals nil (core.get nil :a) "from nothing is nothing") 340 | (assert.equals nil (core.get {:a 1} nil) "nothing from something is nothing") 341 | (assert.equals 10 (core.get nil nil 10) "just a default returns a default") 342 | (assert.equals nil (core.get {:a 1} :b) "a missing key is nothing") 343 | (assert.equals 2 (core.get {:a 1} :b 2) "defaults replace missing") 344 | (assert.equals 1 (core.get {:a 1} :a) "results match") 345 | (assert.equals 1 (core.get {:a 1} :a 2) "results match (even with default)") 346 | (assert.equals :b (core.get [:a :b] 2) "sequential tables work too"))))) 347 | 348 | (describe 349 | "get-in" 350 | (fn [] 351 | (it "works like get, but deeply using a path table" 352 | (fn [] 353 | (assert.equals nil (core.get-in nil [:a]) "something from nil is nil") 354 | (assert.are.same {:a 1} (core.get-in {:a 1} []) "empty path is idempotent") 355 | (assert.equals 10 (core.get-in {:a {:b 10 :c 20}} [:a :b]) "two levels") 356 | (assert.equals 5 (core.get-in {:a {:b 10 :c 20}} [:a :d] 5) "default"))))) 357 | 358 | (describe 359 | "assoc" 360 | (fn [] 361 | (it "puts values into tables" 362 | (fn [] 363 | (assert.are.same {} (core.assoc nil nil nil) "3x nil is an empty map") 364 | (assert.are.same {} (core.assoc nil :a nil) "setting to nil is noop") 365 | (assert.are.same {} (core.assoc nil nil :a) "nil key is noop") 366 | (assert.are.same {:a 1} (core.assoc nil :a 1) "from nothing to one key") 367 | (assert.are.same [:a] (core.assoc nil 1 :a) "sequential") 368 | (assert.are.same {:a 1 :b 2} (core.assoc {:a 1} :b 2) "adding to existing") 369 | (assert.are.same {:a 1 :b 2 :c 3} (core.assoc {:a 1} :b 2 :c 3) "multi arg") 370 | (assert.are.same {:a 1 :b 2 :c 3 :d 4} (core.assoc {:a 1} :b 2 :c 3 :d 4) "more multi arg") 371 | (let [(ok? msg) (pcall #(core.assoc {:a 1} :b 2 :c))] 372 | (assert.is_false ok? "uneven args - ok?") 373 | (assert.equals "expects even number" (msg:match "expects even number") "uneven args - msg")))))) 374 | 375 | (describe 376 | "assoc-in" 377 | (fn [] 378 | (it "works like assoc but deeply with a path table" 379 | (fn [] 380 | (assert.are.same {} (core.assoc-in nil nil nil) "empty as possible") 381 | (assert.are.same {} (core.assoc-in nil [] nil) "empty path, nothing else") 382 | (assert.are.same {} (core.assoc-in nil [] 2) "empty path and a value") 383 | (assert.are.same {:a 1} (core.assoc-in {:a 1} [] 2) "empty path, base table and a value") 384 | (assert.are.same {:a 1 :b 2} (core.assoc-in {:a 1} [:b] 2) "simple one path segment") 385 | (assert.are.same {:a 1 :b {:c 2}} (core.assoc-in {:a 1} [:b :c] 2) "two levels from base") 386 | (assert.are.same {:b {:c 2}} (core.assoc-in nil [:b :c] 2) "two levels from nothing") 387 | (assert.are.same {:a {:b [:c]}} (core.assoc-in nil [:a :b 1] :c) "mixing associative and sequential"))))) 388 | 389 | (describe 390 | "update" 391 | (fn [] 392 | (it "updates a key with a function" 393 | (fn [] 394 | (assert.are.same {:foo 2} (core.update {:foo 1} :foo core.inc) "increment a value"))))) 395 | 396 | (describe 397 | "update-in" 398 | (fn [] 399 | (it "like update but nested with a path table" 400 | (fn [] 401 | (assert.are.same 402 | {:foo {:bar 2}} 403 | (core.update-in {:foo {:bar 1}} [:foo :bar] core.inc) 404 | "increment a value"))))) 405 | 406 | (describe 407 | "constantly" 408 | (fn [] 409 | (it "builds a function that always returns the same thing" 410 | (fn [] 411 | (let [f (core.constantly :foo)] 412 | (assert.equals :foo (f)) 413 | (assert.equals :foo (f :bar))))))) 414 | 415 | (describe 416 | "spit / slurp" 417 | (fn [] 418 | (local tmp-dir (vim.fn.tempname)) 419 | (local tmp-path (fs.join-path [tmp-dir "foo.txt"])) 420 | (local expected-body "Hello, World!") 421 | (fs.mkdirp tmp-dir) 422 | 423 | (it "spits the contents into a file and can be read back with slurp" 424 | (fn [] 425 | (core.spit tmp-path expected-body) 426 | (assert.equals expected-body (core.slurp tmp-path)))) 427 | 428 | (it "returns nil if you slurp a nil or bad path" 429 | (fn [] 430 | (assert.is_nil (core.slurp "nope does not exist")) 431 | (assert.is_nil (core.slurp nil)))) 432 | 433 | (it "appends to a file if the :append option is set" 434 | (fn [] 435 | (core.spit tmp-path "\nxyz" {:append true}) 436 | (assert.equals (.. expected-body "\nxyz") (core.slurp tmp-path)))))) 437 | 438 | (describe 439 | "sort" 440 | (fn [] 441 | (it "sorts tables without modifying the original" 442 | (fn [] 443 | (assert.are.same [1 2 3] (core.sort [3 1 2])))))) 444 | 445 | (describe 446 | "distinct" 447 | (fn [] 448 | (it "does nothing to empty tables" 449 | (fn [] 450 | (assert.are.same [] (core.distinct nil)) 451 | (assert.are.same [] (core.distinct [])))) 452 | 453 | (it "does nothing to already distinct lists" 454 | (fn [] 455 | (assert.are.same [1 2 3] (sort-tables (core.distinct [1 2 3]))))) 456 | 457 | (it "removes duplicates of any type" 458 | (fn [] 459 | (assert.are.same [1 2 3] (sort-tables (core.distinct [1 2 2 3]))))) 460 | 461 | (it "removes duplicates of any type" 462 | (fn [] 463 | (assert.are.same [:a :b :c] (sort-tables (core.distinct [:a :b :c :c]))) 464 | 465 | (let [t [1 2]] 466 | (assert.are.same [:a :c t] (sort-tables (core.distinct [:a t :c t :c])))))))) 467 | 468 | (describe 469 | "clear-table!" 470 | (fn [] 471 | (it "clears a table" 472 | (fn [] 473 | (let [t {:a 1 :b 2}] 474 | (core.clear-table! t) 475 | (assert.are.same {} t)))))) 476 | 477 | (describe 478 | "sequential?" 479 | (fn [] 480 | (it "returns true for sequential tables" 481 | (fn [] 482 | (assert.is_true (core.sequential? [1 2 3])) 483 | (assert.is_true (core.sequential? [])) 484 | (assert.is_false (core.sequential? {:a 1 :b 2})) 485 | (assert.is_false (core.sequential? nil)) 486 | (assert.is_false (core.sequential? "foo")) 487 | nil)))) 488 | 489 | (describe 490 | "seq" 491 | (fn [] 492 | (it "converts appropriately as the docstring says" 493 | (fn [] 494 | (assert.are.same [1 2 3] (core.seq [1 2 3])) 495 | (assert.are.same nil (core.seq nil)) 496 | (assert.are.same nil (core.seq {})) 497 | (assert.are.same nil (core.seq "")) 498 | (assert.are.same ["f" "o" "o" " " "b" "a" "r"] (core.seq "foo bar")) 499 | (assert.are.same nil (core.seq "")) 500 | (assert.are.same [[:a :b]] (core.seq {:a :b})) 501 | nil)))) 502 | 503 | (describe 504 | "take-while" 505 | (fn [] 506 | (it "takes values while f is true" 507 | (fn [] 508 | (assert.are.same [1 2 3] (core.take-while #(> $1 0) [1 2 3 -1 -2 -3])) 509 | (assert.are.same [] (core.take-while #(> $1 0) [-1 -2 -3])) 510 | (assert.are.same nil (core.take-while #(> $1 0) nil)) 511 | (assert.are.same 512 | [[:hi :world]] 513 | (core.take-while #(= (. $1 1) :hi) {:hi :world})) 514 | nil)))) 515 | 516 | (describe 517 | "drop-while" 518 | (fn [] 519 | (it "drops values while f is true" 520 | (fn [] 521 | (assert.are.same [1 2 3] (core.drop-while #(< $1 0) [-1 -2 -3 1 2 3])) 522 | (assert.are.same [2 3] (core.drop-while #(< $1 2) [1 2 3])) 523 | (assert.are.same nil (core.drop-while #(> $1 0) nil)) 524 | (assert.are.same [] (core.drop-while #(= (. $1 1) :hi) {:hi :world})) 525 | nil)))) 526 | 527 | (describe 528 | "->set" 529 | (fn [] 530 | (it "ignores nil" 531 | (fn [] 532 | (assert.is_nil (core.->set nil)) 533 | (assert.is_nil (core.->set)) 534 | nil)) 535 | 536 | (it "it turns sequential tables into associative sets" 537 | (fn [] 538 | (assert.are.same 539 | {:a true :b true :c true} 540 | (core.->set [:a :b :c])) 541 | nil)))) 542 | 543 | (describe 544 | "contains?" 545 | (fn [] 546 | (it "ignores nil" 547 | (fn [] 548 | (assert.is_nil (core.contains? nil :foo)) 549 | nil)) 550 | 551 | (it "works on sequential tables" 552 | (fn [] 553 | (assert.is_true (core.contains? [:foo :bar] :foo)) 554 | (assert.is_false (core.contains? [:foo :bar] :baz)) 555 | nil)) 556 | 557 | (it "works on associative tables" 558 | (fn [] 559 | (assert.is_true (core.contains? (core.->set [:foo :bar]) :foo)) 560 | (assert.is_false (core.contains? (core.->set [:foo :bar]) :baz)) 561 | nil)))) 562 | -------------------------------------------------------------------------------- /fnl/spec/nfnl/e2e_spec.fnl: -------------------------------------------------------------------------------- 1 | (local {: describe : it : before_each : after_each} (require :plenary.busted)) 2 | (local assert (require :luassert.assert)) 3 | (local core (require :nfnl.core)) 4 | (local fs (require :nfnl.fs)) 5 | (local nfnl (require :nfnl)) 6 | 7 | (nfnl.setup {}) 8 | 9 | ;; These temp directories are auto deleted on Neovim exit. 10 | (local temp-dir (vim.fn.tempname)) 11 | (local unrelated-temp-dir (vim.fn.tempname)) 12 | 13 | (local fnl-dir (fs.join-path [temp-dir "fnl"])) 14 | (local lua-dir (fs.join-path [temp-dir "lua"])) 15 | (local config-path (fs.join-path [temp-dir ".nfnl.fnl"])) 16 | (local fnl-path (fs.join-path [fnl-dir "foo.fnl"])) 17 | (local macro-fnl-path (fs.join-path [fnl-dir "bar.fnl"])) 18 | (local macro-lua-path (fs.join-path [lua-dir "bar.lua"])) 19 | (local lua-path (fs.join-path [lua-dir "foo.lua"])) 20 | 21 | (fs.mkdirp fnl-dir) 22 | (fs.mkdirp unrelated-temp-dir) 23 | 24 | (fn delete-buf-file [path] 25 | (pcall vim.cmd (.. "bwipeout! " path)) 26 | (os.remove path)) 27 | 28 | (fn run-e2e-tests [] 29 | ;; Reset the files between each test run. 30 | (core.run! delete-buf-file [config-path fnl-path macro-fnl-path lua-path]) 31 | 32 | (it "does nothing when there's no .nfnl.fnl configuration" 33 | (fn [] 34 | (vim.cmd (.. "edit " fnl-path)) 35 | (set vim.o.filetype "fennel") 36 | (vim.api.nvim_buf_set_lines 0 0 -1 false ["(print \"Hello, World!\")"]) 37 | (vim.cmd "write") 38 | (assert.is_nil (core.slurp lua-path)))) 39 | 40 | (it "compiles when there's a trusted .nfnl.fnl configuration file" 41 | (fn [] 42 | (vim.cmd (.. "edit " config-path)) 43 | (vim.api.nvim_buf_set_lines 0 0 -1 false ["{}"]) 44 | (vim.cmd "write") 45 | (vim.cmd "trust") 46 | (vim.cmd (.. "edit " fnl-path)) 47 | (set vim.o.filetype "fennel") 48 | (vim.api.nvim_buf_set_lines 0 0 -1 false ["(print \"Hello, World!\")"]) 49 | (vim.cmd "write") 50 | (assert.are.equal 1 (vim.fn.isdirectory lua-dir)) 51 | 52 | (local lua-result (core.slurp lua-path)) 53 | (print "Lua result:" lua-result) 54 | 55 | (assert.are.equal 56 | "-- [nfnl] fnl/foo.fnl\nreturn print(\"Hello, World!\")\n" 57 | lua-result))) 58 | 59 | (it "can import-macros and use them, the macros aren't compiled" 60 | (fn [] 61 | (vim.cmd (.. "edit " macro-fnl-path)) 62 | (set vim.o.filetype "fennel") 63 | 64 | ;; We have to split up the macro marker otherwise this file gets marked as a macro file and won't compile. 65 | (vim.api.nvim_buf_set_lines 0 0 -1 false [(.. ";; [nfnl" "-" "macro]") "{:infix (fn [a op b] `(,op ,a ,b))}"]) 66 | (vim.cmd "write") 67 | 68 | (vim.cmd (.. "edit " fnl-path)) 69 | (set vim.o.filetype "fennel") 70 | (vim.api.nvim_buf_set_lines 0 0 -1 false ["(import-macros {: infix} :bar)" "(infix 10 + 20)"]) 71 | (vim.cmd "write") 72 | 73 | (assert.is_nil (core.slurp macro-lua-path)) 74 | 75 | (local lua-result (core.slurp lua-path)) 76 | (print "Lua result:" lua-result) 77 | 78 | (assert.are.equal 79 | "-- [nfnl] fnl/foo.fnl\nreturn (10 + 20)\n" 80 | lua-result)))) 81 | 82 | (describe 83 | "e2e file compiling from a project dir" 84 | (fn [] 85 | (var initial-cwd nil) 86 | 87 | (before_each 88 | (fn [] 89 | (set initial-cwd (vim.fn.getcwd)) 90 | (vim.api.nvim_set_current_dir temp-dir))) 91 | 92 | (after_each 93 | (fn [] 94 | (vim.api.nvim_set_current_dir initial-cwd))) 95 | 96 | (run-e2e-tests))) 97 | 98 | (describe 99 | "e2e file compiling from outside project dir" 100 | (fn [] 101 | (var initial-cwd nil) 102 | 103 | (before_each 104 | (fn [] 105 | (set initial-cwd (vim.fn.getcwd)) 106 | (vim.api.nvim_set_current_dir unrelated-temp-dir))) 107 | 108 | (after_each 109 | (fn [] 110 | (vim.api.nvim_set_current_dir initial-cwd))) 111 | 112 | (run-e2e-tests))) 113 | -------------------------------------------------------------------------------- /fnl/spec/nfnl/fs_spec.fnl: -------------------------------------------------------------------------------- 1 | (local {: describe : it} (require :plenary.busted)) 2 | (local assert (require :luassert.assert)) 3 | (local fs (require :nfnl.fs)) 4 | 5 | (describe 6 | "basename" 7 | (fn [] 8 | (it "removes the last segment of a path" 9 | (fn [] 10 | (assert.equals "foo" (fs.basename "foo/bar.fnl")) 11 | (assert.equals "foo/bar" (fs.basename "foo/bar/baz.fnl")) 12 | (assert.equals "." (fs.basename "baz.fnl")))) 13 | 14 | (it "happily lets nils flow back out" 15 | (fn [] 16 | (assert.is_nil (fs.basename nil)))))) 17 | 18 | (describe 19 | "path-sep" 20 | (fn [] 21 | (it "returns the OS path separator (test assumes Linux)" 22 | (fn [] 23 | (assert.equals "/" (fs.path-sep)))))) 24 | 25 | (describe 26 | "replace-extension" 27 | (fn [] 28 | (it "replaces extensions" 29 | (fn [] 30 | (assert.equals "foo.lua" (fs.replace-extension "foo.fnl" "lua")))))) 31 | 32 | (describe 33 | "split-path" 34 | (fn [] 35 | (it "splits a path into parts" 36 | (fn [] 37 | (assert.are.same ["foo" "bar" "baz"] (fs.split-path "foo/bar/baz")) 38 | (assert.are.same ["" "foo" "bar" "baz"] (fs.split-path "/foo/bar/baz")))))) 39 | 40 | (describe 41 | "join-path" 42 | (fn [] 43 | (it "joins a path together" 44 | (fn [] 45 | (assert.equals "foo/bar/baz" (fs.join-path ["foo" "bar" "baz"])) 46 | (assert.equals "/foo/bar/baz" (fs.join-path ["" "foo" "bar" "baz"])))))) 47 | 48 | (describe 49 | "replace-dirs" 50 | (fn [] 51 | (it "replaces directories in a path that match a string with another string" 52 | (fn [] 53 | (assert.equals "foo/lua/bar" (fs.replace-dirs "foo/fnl/bar" "fnl" "lua")) 54 | (assert.equals "/foo/lua/bar" (fs.replace-dirs "/foo/fnl/bar" "fnl" "lua")) 55 | (assert.equals "/foo/nfnl/bar" (fs.replace-dirs "/foo/nfnl/bar" "fnl" "lua")))))) 56 | -------------------------------------------------------------------------------- /fnl/spec/nfnl/macros/aniseed_spec.fnl: -------------------------------------------------------------------------------- 1 | (local {: describe : it} (require :plenary.busted)) 2 | (local assert (require :luassert.assert)) 3 | (import-macros 4 | {: module 5 | : def- : def 6 | : defn- : defn 7 | : defonce- : defonce} 8 | :nfnl.macros.aniseed) 9 | 10 | (module spec.nfnl.macros.aniseed-example) 11 | 12 | (def- private-val "this is private") 13 | (def public-val "this is public") 14 | 15 | (defn- private-fn [] "this is private fn") 16 | (defn public-fn [] "this is public fn") 17 | 18 | (defonce- private-once-val "this is private once val") 19 | (defonce- private-once-val "this is ignored") 20 | 21 | (defonce public-once-val "this is public once val") 22 | (defonce public-once-val "this is ignored") 23 | 24 | (local mod (require :spec.nfnl.macros.aniseed-example)) 25 | 26 | (describe "def-" 27 | (fn [] 28 | (it "defines private values" 29 | (fn [] 30 | (assert.equals "this is private" private-val) 31 | (assert.equals nil (. mod :private-val)))))) 32 | 33 | (describe "def" 34 | (fn [] 35 | (it "defines public values" 36 | (fn [] 37 | (assert.equals "this is public" public-val) 38 | (assert.equals "this is public" (. mod :public-val)))))) 39 | 40 | (describe "defn-" 41 | (fn [] 42 | (it "defines private functions" 43 | (fn [] 44 | (assert.equals "this is private fn" (private-fn)) 45 | (assert.equals nil (. mod :private-fn)))))) 46 | 47 | (describe "defn" 48 | (fn [] 49 | (it "defines public functions" 50 | (fn [] 51 | (assert.equals "this is public fn" (public-fn)) 52 | (assert.equals "this is public fn" ((. mod :public-fn))))))) 53 | 54 | (describe "defonce-" 55 | (fn [] 56 | (it "defines private once values" 57 | (fn [] 58 | (assert.equals "this is private once val" private-once-val) 59 | (assert.equals nil (. mod :private-once-val)))))) 60 | 61 | (describe "defonce" 62 | (fn [] 63 | (it "defines public once values" 64 | (fn [] 65 | (assert.equals "this is public once val" public-once-val) 66 | (assert.equals "this is public once val" (. mod :public-once-val)))))) 67 | -------------------------------------------------------------------------------- /fnl/spec/nfnl/macros_spec.fnl: -------------------------------------------------------------------------------- 1 | (local {: describe : it} (require :plenary.busted)) 2 | (local assert (require :luassert.assert)) 3 | (import-macros {: if-let : when-let} :nfnl.macros) 4 | 5 | (describe 6 | "if-let" 7 | (fn [] 8 | (it "works like if + let" 9 | (fn [] 10 | (assert.equals 11 | :ok 12 | (if-let [foo :ok] 13 | foo 14 | :nope)) 15 | (assert.equals 16 | :nope 17 | (if-let [foo false] 18 | :first 19 | :nope)) 20 | (assert.equals 21 | :yes 22 | (if-let [{: a} {:a 1}] 23 | :yes 24 | :no)) 25 | (assert.equals 26 | :no 27 | (if-let [{: a} nil] 28 | :yes 29 | :no)))))) 30 | 31 | (describe 32 | "when-let" 33 | (fn [] 34 | (it "works like when + let" 35 | (fn [] 36 | (assert.equals 37 | :ok 38 | (when-let [foo :ok] 39 | foo)) 40 | (assert.equals 41 | nil 42 | (when-let [foo false] 43 | :first)) 44 | (assert.equals 45 | :yarp 46 | (when-let [(ok? val) (pcall #:yarp)] 47 | val)) 48 | (assert.equals 49 | :yarp 50 | (when-let [(_ val) (pcall #:yarp)] 51 | val)) 52 | (assert.equals 53 | nil 54 | (when-let [(ok? val) (pcall #(error :narp))] 55 | val)))))) 56 | -------------------------------------------------------------------------------- /fnl/spec/nfnl/module_spec.fnl: -------------------------------------------------------------------------------- 1 | (local {: describe : it} (require :plenary.busted)) 2 | (local assert (require :luassert.assert)) 3 | (local {: autoload : define} (require :nfnl.module)) 4 | 5 | (describe 6 | "autoload" 7 | (fn [] 8 | (it "loads modules when their properties are accessed" 9 | (fn [] 10 | (local core (autoload :nfnl.core)) 11 | (assert.equals 2 (core.inc 1)) 12 | 13 | nil)))) 14 | 15 | (describe 16 | "define" 17 | (fn [] 18 | (it "returns the loaded module if it's the same type as the base" 19 | (fn [] 20 | (local m2 (define :nfnl.module {})) 21 | (assert.equals define m2.define) 22 | 23 | (local m3 (define :nfnl.module.nope {:nope true})) 24 | (assert.equals nil m3.define) 25 | (assert.equals true m3.nope) 26 | 27 | nil)))) 28 | -------------------------------------------------------------------------------- /fnl/spec/nfnl/repl_spec.fnl: -------------------------------------------------------------------------------- 1 | (local {: describe : it} (require :plenary.busted)) 2 | (local core (require :nfnl.core)) 3 | (local assert (require :luassert.assert)) 4 | (local repl (require :nfnl.repl)) 5 | 6 | (describe 7 | "repl" 8 | (fn [] 9 | (it "starts a REPL that we can confinually call with more code" 10 | (fn [] 11 | (local r (repl.new)) 12 | (assert.are.same [[10 20]] (r "[10 20]")) 13 | 14 | (assert.are.same [:foo :bar] (r ":foo :bar\n")) 15 | (assert.are.same [:foo :bar] (r "(values :foo :bar)")) 16 | 17 | ;; A nil closes the REPL and returns nil. 18 | (assert.are.same nil (r)) 19 | (assert.are.same nil (r ":foo")) 20 | 21 | nil)) 22 | 23 | (it "can handle function references, tables and multiple REPLs" 24 | (fn [] 25 | (let [r1 (repl.new) 26 | r2 (repl.new) 27 | code "(fn a []) (fn b []) {: a : b}" 28 | pat "%[#%s+#%s+{:%w # :%w #}%]"] 29 | (assert.matches pat (core.pr-str (r1 code))) 30 | (assert.matches pat (core.pr-str (r2 code))) 31 | (assert.matches pat (core.pr-str (r1 code))) 32 | (assert.matches pat (core.pr-str (r2 code)))) 33 | 34 | nil)))) 35 | -------------------------------------------------------------------------------- /fnl/spec/nfnl/string_spec.fnl: -------------------------------------------------------------------------------- 1 | (local {: describe : it} (require :plenary.busted)) 2 | (local assert (require :luassert.assert)) 3 | (local str (require :nfnl.string)) 4 | 5 | (describe 6 | "join" 7 | (fn [] 8 | (it "joins things together" 9 | (fn [] 10 | (assert.equals "foo, bar, baz" (str.join ", " ["foo" "bar" "baz"])) 11 | (assert.equals "foobarbaz" (str.join ["foo" "bar" "baz"])) 12 | (assert.equals "foobar" (str.join ["foo" nil "bar"]) "handle nils correctly"))))) 13 | 14 | (describe 15 | "split" 16 | (fn [] 17 | (it "splits strings up" 18 | (fn [] 19 | (assert.are.same [""] (str.split "" ",")) 20 | (assert.are.same ["foo"] (str.split "foo" ",")) 21 | (assert.are.same ["foo" "bar" "baz"] (str.split "foo,bar,baz" ",")) 22 | (assert.are.same ["foo" "" "bar"] (str.split "foo,,bar" ",")) 23 | (assert.are.same ["foo" "" "" "bar"] (str.split "foo,,,bar" ",")) 24 | (assert.are.same ["foo" "baz"] (str.split "foobarbaz" "bar")))))) 25 | 26 | (describe 27 | "blank?" 28 | (fn [] 29 | (it "true if the string is nil or just whitespace" 30 | (fn [] 31 | (assert.is_true (str.blank? nil)) 32 | (assert.is_true (str.blank? "")) 33 | (assert.is_true (str.blank? " ")) 34 | (assert.is_true (str.blank? " ")) 35 | (assert.is_true (str.blank? " \n \n ")) 36 | (assert.is_true (not (str.blank? " \n . \n "))))))) 37 | 38 | (describe 39 | "triml" 40 | (fn [] 41 | (it "trims all whitespace from the left of the string" 42 | (fn [] 43 | (assert.equals "" (str.triml "")) 44 | (assert.equals "foo" (str.triml "foo")) 45 | (assert.equals "foo" (str.triml " foo")) 46 | (assert.equals "foo" (str.triml " \n foo")) 47 | (assert.equals "foo " (str.triml " \n foo ")))))) 48 | 49 | (describe 50 | "trimr" 51 | (fn [] 52 | (it "trims all whitespace from the right of the string" 53 | (fn [] 54 | (assert.equals "" (str.trimr "")) 55 | (assert.equals "foo" (str.trimr "foo")) 56 | (assert.equals "foo" (str.trimr "foo ")) 57 | (assert.equals "foo" (str.trimr "foo \n ")) 58 | (assert.equals " foo" (str.trimr " foo \n ")))))) 59 | 60 | (describe 61 | "trim" 62 | (fn [] 63 | (it "trims all whitespace from start end end of a string" 64 | (fn [] 65 | (assert.equals "" (str.trim "")) 66 | (assert.equals "foo" (str.trim "foo")) 67 | (assert.equals "foo" (str.trim " \n foo \n \n ") "basic") 68 | (assert.equals "" (str.trim " ") "just whitespace"))))) 69 | 70 | (describe 71 | "ends-with?" 72 | (fn [] 73 | (it "checks if a string ends with another string" 74 | (fn [] 75 | (assert.is_true (str.ends-with? "foobarbaz" "baz")) 76 | (assert.is_true (str.ends-with? "foobarbaz" "arbaz")) 77 | (assert.is_true (str.ends-with? "foobarbaz" "foobarbaz")) 78 | (assert.is_false (str.ends-with? "foobarbaz" "foo")) 79 | (assert.is_false (str.ends-with? "foobarbaz" "barbazz")))))) 80 | -------------------------------------------------------------------------------- /lua/nfnl/api.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/api.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local define = _local_1_["define"] 5 | local core = autoload("nfnl.core") 6 | local str = autoload("nfnl.string") 7 | local compile = autoload("nfnl.compile") 8 | local config = autoload("nfnl.config") 9 | local notify = autoload("nfnl.notify") 10 | local fs = autoload("nfnl.fs") 11 | local gc = autoload("nfnl.gc") 12 | local M = define("nfnl.api") 13 | M["find-orphans"] = function(opts) 14 | local dir = vim.fn.getcwd() 15 | local _let_2_ = config["find-and-load"](dir) 16 | local config0 = _let_2_["config"] 17 | local root_dir = _let_2_["root-dir"] 18 | local cfg = _let_2_["cfg"] 19 | if config0 then 20 | local orphan_files = gc["find-orphan-lua-files"]({["root-dir"] = root_dir, cfg = cfg}) 21 | if core["empty?"](orphan_files) then 22 | if not core.get(opts, "passive?") then 23 | notify.info("No orphan files detected.") 24 | else 25 | end 26 | else 27 | local function _4_(f) 28 | return (" - " .. f) 29 | end 30 | notify.warn("Orphan files detected, delete them with :NfnlDeleteOrphans.\n", str.join("\n", core.map(_4_, orphan_files))) 31 | end 32 | return orphan_files 33 | else 34 | notify.warn("No .nfnl.fnl configuration found.") 35 | return {} 36 | end 37 | end 38 | M["delete-orphans"] = function() 39 | local dir = vim.fn.getcwd() 40 | local _let_7_ = config["find-and-load"](dir) 41 | local config0 = _let_7_["config"] 42 | local root_dir = _let_7_["root-dir"] 43 | local cfg = _let_7_["cfg"] 44 | if config0 then 45 | local orphan_files = gc["find-orphan-lua-files"]({["root-dir"] = root_dir, cfg = cfg}) 46 | if core["empty?"](orphan_files) then 47 | notify.info("No orphan files detected.") 48 | else 49 | local function _8_(f) 50 | return (" - " .. f) 51 | end 52 | notify.info("Deleting orphan files:\n", str.join("\n", core.map(_8_, orphan_files))) 53 | core.map(os.remove, orphan_files) 54 | end 55 | return orphan_files 56 | else 57 | notify.warn("No .nfnl.fnl configuration found.") 58 | return {} 59 | end 60 | end 61 | M["compile-file"] = function(_11_) 62 | local path = _11_["path"] 63 | local dir = _11_["dir"] 64 | local dir0 = (dir or vim.fn.getcwd()) 65 | local _let_12_ = config["find-and-load"](dir0) 66 | local config0 = _let_12_["config"] 67 | local root_dir = _let_12_["root-dir"] 68 | local cfg = _let_12_["cfg"] 69 | if config0 then 70 | local path0 = fs["join-path"]({root_dir, vim.fn.expand((path or "%"))}) 71 | local result = compile["into-file"]({["root-dir"] = root_dir, cfg = cfg, path = path0, source = core.slurp(path0), ["batch?"] = true}) 72 | notify.info("Compilation complete.\n", result) 73 | return result 74 | else 75 | notify.warn("No .nfnl.fnl configuration found.") 76 | return {} 77 | end 78 | end 79 | M["compile-all-files"] = function(dir) 80 | local dir0 = (dir or vim.fn.getcwd()) 81 | local _let_14_ = config["find-and-load"](dir0) 82 | local config0 = _let_14_["config"] 83 | local root_dir = _let_14_["root-dir"] 84 | local cfg = _let_14_["cfg"] 85 | if config0 then 86 | local results = compile["all-files"]({["root-dir"] = root_dir, cfg = cfg}) 87 | notify.info("Compilation complete.\n", results) 88 | return results 89 | else 90 | notify.warn("No .nfnl.fnl configuration found.") 91 | return {} 92 | end 93 | end 94 | M.dofile = function(file) 95 | return dofile(fs["fnl-path->lua-path"](vim.fn.expand((file or "%")))) 96 | end 97 | return M 98 | -------------------------------------------------------------------------------- /lua/nfnl/callback.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/callback.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local core = autoload("nfnl.core") 5 | local str = autoload("nfnl.string") 6 | local fs = autoload("nfnl.fs") 7 | local nvim = autoload("nfnl.nvim") 8 | local compile = autoload("nfnl.compile") 9 | local config = autoload("nfnl.config") 10 | local api = autoload("nfnl.api") 11 | local notify = autoload("nfnl.notify") 12 | local function fennel_buf_write_post_callback_fn(root_dir, cfg) 13 | local function _2_(ev) 14 | compile["into-file"]({["root-dir"] = root_dir, cfg = cfg, path = fs["full-path"](ev.file), source = nvim["get-buf-content-as-string"](ev.buf)}) 15 | if cfg({"orphan-detection", "auto?"}) then 16 | api["find-orphans"]({["passive?"] = true}) 17 | else 18 | end 19 | return nil 20 | end 21 | return _2_ 22 | end 23 | local function supported_path_3f(file_path) 24 | local _4_ 25 | if core["string?"](file_path) then 26 | _4_ = not file_path:find("^[%w-]+:/") 27 | else 28 | _4_ = nil 29 | end 30 | return (_4_ or false) 31 | end 32 | local function fennel_filetype_callback(ev) 33 | local file_path = fs["full-path"](ev.file) 34 | if supported_path_3f(file_path) then 35 | local file_dir = fs.basename(file_path) 36 | local _let_6_ = config["find-and-load"](file_dir) 37 | local config0 = _let_6_["config"] 38 | local root_dir = _let_6_["root-dir"] 39 | local cfg = _let_6_["cfg"] 40 | if config0 then 41 | if cfg({"verbose"}) then 42 | notify.info("Found nfnl config, setting up autocmds: ", root_dir) 43 | else 44 | end 45 | if (false ~= vim.g["nfnl#compile_on_write"]) then 46 | vim.api.nvim_create_autocmd({"BufWritePost"}, {group = vim.api.nvim_create_augroup(str.join({"nfnl-on-write", root_dir, ev.buf}), {}), buffer = ev.buf, callback = fennel_buf_write_post_callback_fn(root_dir, cfg)}) 47 | else 48 | end 49 | local function _9_(_241) 50 | return api.dofile(core.first(core.get(_241, "fargs"))) 51 | end 52 | vim.api.nvim_buf_create_user_command(ev.buf, "NfnlFile", _9_, {desc = "Run the matching Lua file for this Fennel file from disk. Does not recompile the Lua, you must use nfnl to compile your Fennel to Lua first. Calls nfnl.api/dofile under the hood.", force = true, complete = "file", nargs = "?"}) 53 | local function _10_(_241) 54 | return api["compile-file"]({path = core.first(core.get(_241, "fargs"))}) 55 | end 56 | vim.api.nvim_buf_create_user_command(ev.buf, "NfnlCompileFile", _10_, {desc = "Executes (nfnl.api/compile-file) which compiles the current file or the one provided as an argumet. The output is written to the appropriate Lua file.", force = true, complete = "file", nargs = "?"}) 57 | local function _11_(_241) 58 | return api["compile-all-files"](core.first(core.get(_241, "fargs"))) 59 | end 60 | vim.api.nvim_buf_create_user_command(ev.buf, "NfnlCompileAllFiles", _11_, {desc = "Executes (nfnl.api/compile-all-files) which will, you guessed it, compile all of your files.", force = true, complete = "file", nargs = "?"}) 61 | local function _12_() 62 | return api["find-orphans"]() 63 | end 64 | vim.api.nvim_buf_create_user_command(ev.buf, "NfnlFindOrphans", _12_, {desc = "Executes (nfnl.api/find-orphans) which will find and display all Lua files that no longer have a matching Fennel file.", force = true}) 65 | local function _13_() 66 | return api["delete-orphans"]() 67 | end 68 | return vim.api.nvim_buf_create_user_command(ev.buf, "NfnlDeleteOrphans", _13_, {desc = "Executes (nfnl.api/delete-orphans) deletes any orphan Lua files that no longer have their original Fennel file they were compiled from.", force = true}) 69 | else 70 | return nil 71 | end 72 | else 73 | return nil 74 | end 75 | end 76 | return {["fennel-filetype-callback"] = fennel_filetype_callback, ["supported-path?"] = supported_path_3f} 77 | -------------------------------------------------------------------------------- /lua/nfnl/compile.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/compile.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local core = autoload("nfnl.core") 5 | local fs = autoload("nfnl.fs") 6 | local fennel = autoload("nfnl.fennel") 7 | local notify = autoload("nfnl.notify") 8 | local config = autoload("nfnl.config") 9 | local header = autoload("nfnl.header") 10 | local mod = {} 11 | local function safe_target_3f(path) 12 | local line = fs["read-first-line"](path) 13 | return (not line or header["tagged?"](line)) 14 | end 15 | local function macro_source_3f(source) 16 | return string.find(source, "%s*;+%s*%[nfnl%-macro%]") 17 | end 18 | local function valid_source_files(glob_fn, _2_) 19 | local root_dir = _2_["root-dir"] 20 | local cfg = _2_["cfg"] 21 | local function _3_(_241) 22 | return glob_fn(root_dir, _241) 23 | end 24 | return core.mapcat(_3_, cfg({"source-file-patterns"})) 25 | end 26 | local function valid_source_file_3f(path, _4_) 27 | local root_dir = _4_["root-dir"] 28 | local cfg = _4_["cfg"] 29 | local function _5_(_241) 30 | return fs["glob-matches?"](root_dir, _241, path) 31 | end 32 | return core.some(_5_, cfg({"source-file-patterns"})) 33 | end 34 | mod["into-string"] = function(_6_) 35 | local root_dir = _6_["root-dir"] 36 | local path = _6_["path"] 37 | local cfg = _6_["cfg"] 38 | local source = _6_["source"] 39 | local batch_3f = _6_["batch?"] 40 | local opts = _6_ 41 | local macro_3f = macro_source_3f(source) 42 | if (macro_3f and batch_3f) then 43 | return {status = "macros-are-not-compiled", ["source-path"] = path} 44 | elseif macro_3f then 45 | core["clear-table!"](fennel["macro-loaded"]) 46 | return mod["all-files"]({["root-dir"] = root_dir, cfg = cfg}) 47 | elseif config["config-file-path?"](path) then 48 | return {status = "nfnl-config-is-not-compiled", ["source-path"] = path} 49 | elseif not valid_source_file_3f(path, opts) then 50 | return {status = "path-is-not-in-source-file-patterns", ["source-path"] = path} 51 | else 52 | local rel_file_name = path:sub((2 + root_dir:len())) 53 | local ok, res = nil, nil 54 | do 55 | fennel.path = cfg({"fennel-path"}) 56 | fennel["macro-path"] = cfg({"fennel-macro-path"}) 57 | ok, res = pcall(fennel["compile-string"], source, core.merge({filename = path, warn = notify.warn}, cfg({"compiler-options"}))) 58 | end 59 | if ok then 60 | if cfg({"verbose"}) then 61 | notify.info("Successfully compiled: ", path) 62 | else 63 | end 64 | local _8_ 65 | if cfg({"header-comment"}) then 66 | _8_ = header["with-header"](rel_file_name, res) 67 | else 68 | _8_ = res 69 | end 70 | return {status = "ok", ["source-path"] = path, result = (_8_ .. "\n")} 71 | else 72 | if not batch_3f then 73 | notify.error(res) 74 | else 75 | end 76 | return {status = "compilation-error", error = res, ["source-path"] = path} 77 | end 78 | end 79 | end 80 | mod["into-file"] = function(_13_) 81 | local _root_dir = _13_["_root-dir"] 82 | local cfg = _13_["cfg"] 83 | local _source = _13_["_source"] 84 | local path = _13_["path"] 85 | local batch_3f = _13_["batch?"] 86 | local opts = _13_ 87 | local fnl_path__3elua_path = cfg({"fnl-path->lua-path"}) 88 | local destination_path = fnl_path__3elua_path(path) 89 | local _let_14_ = mod["into-string"](opts) 90 | local status = _let_14_["status"] 91 | local source_path = _let_14_["source-path"] 92 | local result = _let_14_["result"] 93 | local res = _let_14_ 94 | if ("ok" ~= status) then 95 | return res 96 | elseif (safe_target_3f(destination_path) or not cfg({"header-comment"})) then 97 | fs.mkdirp(fs.basename(destination_path)) 98 | core.spit(destination_path, result) 99 | return {status = "ok", ["source-path"] = source_path, ["destination-path"] = destination_path} 100 | else 101 | if not batch_3f then 102 | notify.warn(destination_path, " was not compiled by nfnl. Delete it manually if you wish to compile into this file.") 103 | else 104 | end 105 | return {status = "destination-exists", ["source-path"] = path, ["destination-path"] = destination_path} 106 | end 107 | end 108 | mod["all-files"] = function(_17_) 109 | local root_dir = _17_["root-dir"] 110 | local cfg = _17_["cfg"] 111 | local opts = _17_ 112 | local function _18_(path) 113 | return mod["into-file"]({["root-dir"] = root_dir, path = path, cfg = cfg, source = core.slurp(path), ["batch?"] = true}) 114 | end 115 | local function _19_(_241) 116 | return fs["join-path"]({root_dir, _241}) 117 | end 118 | return core.map(_18_, core.map(_19_, valid_source_files(fs.relglob, opts))) 119 | end 120 | return mod 121 | -------------------------------------------------------------------------------- /lua/nfnl/config.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/config.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local core = autoload("nfnl.core") 5 | local fs = autoload("nfnl.fs") 6 | local str = autoload("nfnl.string") 7 | local fennel = autoload("nfnl.fennel") 8 | local notify = autoload("nfnl.notify") 9 | local config_file_name = ".nfnl.fnl" 10 | local function find(dir) 11 | return fs.findfile(config_file_name, (dir .. ";")) 12 | end 13 | local function path_dirs(_2_) 14 | local rtp_patterns = _2_["rtp-patterns"] 15 | local runtimepath = _2_["runtimepath"] 16 | local base_dirs = _2_["base-dirs"] 17 | local function _3_(path) 18 | local function _4_(_241) 19 | return string.find(path, _241) 20 | end 21 | return core.some(_4_, rtp_patterns) 22 | end 23 | return core.distinct(core.concat(base_dirs, core.filter(_3_, str.split(runtimepath, ",")))) 24 | end 25 | local function default(opts) 26 | local root_dir 27 | local or_5_ = core.get(opts, "root-dir") 28 | if not or_5_ then 29 | local tmp_3_ = vim.fn.getcwd() 30 | if (nil ~= tmp_3_) then 31 | local tmp_3_0 = find(tmp_3_) 32 | if (nil ~= tmp_3_0) then 33 | local tmp_3_1 = fs["full-path"](tmp_3_0) 34 | if (nil ~= tmp_3_1) then 35 | or_5_ = fs.basename(tmp_3_1) 36 | else 37 | or_5_ = nil 38 | end 39 | else 40 | or_5_ = nil 41 | end 42 | else 43 | or_5_ = nil 44 | end 45 | end 46 | root_dir = (or_5_ or vim.fn.getcwd()) 47 | local dirs = path_dirs({runtimepath = vim.o.runtimepath, ["rtp-patterns"] = core.get(opts, "rtp-patterns", {(fs["path-sep"]() .. "nfnl$")}), ["base-dirs"] = {root_dir}}) 48 | local function _12_(root_dir0) 49 | return core.map(fs["join-path"], {{root_dir0, "?.fnl"}, {root_dir0, "?", "init.fnl"}, {root_dir0, "fnl", "?.fnl"}, {root_dir0, "fnl", "?", "init.fnl"}}) 50 | end 51 | local function _13_(root_dir0) 52 | return core.map(fs["join-path"], {{root_dir0, "?.fnl"}, {root_dir0, "?", "init-macros.fnl"}, {root_dir0, "?", "init.fnl"}, {root_dir0, "fnl", "?.fnl"}, {root_dir0, "fnl", "?", "init-macros.fnl"}, {root_dir0, "fnl", "?", "init.fnl"}}) 53 | end 54 | return {["header-comment"] = true, ["compiler-options"] = {["error-pinpoint"] = false}, ["orphan-detection"] = {["auto?"] = true, ["ignore-patterns"] = {}}, ["root-dir"] = root_dir, ["fennel-path"] = str.join(";", core.mapcat(_12_, dirs)), ["fennel-macro-path"] = str.join(";", core.mapcat(_13_, dirs)), ["source-file-patterns"] = {".*.fnl", "*.fnl", fs["join-path"]({"**", "*.fnl"})}, ["fnl-path->lua-path"] = fs["fnl-path->lua-path"], verbose = false} 55 | end 56 | local function cfg_fn(t, opts) 57 | local default_cfg = default(opts) 58 | local function _14_(path) 59 | return core["get-in"](t, path, core["get-in"](default_cfg, path)) 60 | end 61 | return _14_ 62 | end 63 | local function config_file_path_3f(path) 64 | return (config_file_name == fs.filename(path)) 65 | end 66 | local function find_and_load(dir) 67 | local _15_ 68 | do 69 | local config_file_path = find(dir) 70 | if config_file_path then 71 | local root_dir = fs.basename(config_file_path) 72 | local config_source = vim.secure.read(config_file_path) 73 | local ok, config = nil, nil 74 | if core["nil?"](config_source) then 75 | ok, config = false, (config_file_path .. " is not trusted, refusing to compile.") 76 | elseif (str["blank?"](config_source) or ("{}" == str.trim(config_source))) then 77 | ok, config = true, {} 78 | else 79 | ok, config = pcall(fennel.eval, config_source, {filename = config_file_path}) 80 | end 81 | if ok then 82 | _15_ = {config = config, ["root-dir"] = root_dir, cfg = cfg_fn(config, {["root-dir"] = root_dir})} 83 | else 84 | _15_ = notify.error(config) 85 | end 86 | else 87 | _15_ = nil 88 | end 89 | end 90 | return (_15_ or {}) 91 | end 92 | return {["cfg-fn"] = cfg_fn, find = find, ["find-and-load"] = find_and_load, ["config-file-path?"] = config_file_path_3f, default = default, ["path-dirs"] = path_dirs} 93 | -------------------------------------------------------------------------------- /lua/nfnl/core.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/core.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local fennel = autoload("nfnl.fennel") 5 | local function rand(n) 6 | return (math.random() * (n or 1)) 7 | end 8 | local function nil_3f(x) 9 | return (nil == x) 10 | end 11 | local function number_3f(x) 12 | return ("number" == type(x)) 13 | end 14 | local function boolean_3f(x) 15 | return ("boolean" == type(x)) 16 | end 17 | local function string_3f(x) 18 | return ("string" == type(x)) 19 | end 20 | local function table_3f(x) 21 | return ("table" == type(x)) 22 | end 23 | local function function_3f(value) 24 | return ("function" == type(value)) 25 | end 26 | local function keys(t) 27 | local result = {} 28 | if t then 29 | for k, _ in pairs(t) do 30 | table.insert(result, k) 31 | end 32 | else 33 | end 34 | return result 35 | end 36 | local function first(xs) 37 | if xs then 38 | return xs[1] 39 | else 40 | return nil 41 | end 42 | end 43 | local function second(xs) 44 | if xs then 45 | return xs[2] 46 | else 47 | return nil 48 | end 49 | end 50 | local function count(xs) 51 | if table_3f(xs) then 52 | local maxn = table.maxn(xs) 53 | if (0 == maxn) then 54 | return table.maxn(keys(xs)) 55 | else 56 | return maxn 57 | end 58 | elseif not xs then 59 | return 0 60 | else 61 | return #xs 62 | end 63 | end 64 | local function empty_3f(xs) 65 | return (0 == count(xs)) 66 | end 67 | local function sequential_3f(xs) 68 | return (table_3f(xs) and (empty_3f(xs) or (1 == first(keys(xs))))) 69 | end 70 | local function kv_pairs(t) 71 | local result = {} 72 | if t then 73 | for k, v in pairs(t) do 74 | table.insert(result, {k, v}) 75 | end 76 | else 77 | end 78 | return result 79 | end 80 | local function seq(x) 81 | if empty_3f(x) then 82 | return nil 83 | elseif sequential_3f(x) then 84 | return x 85 | elseif table_3f(x) then 86 | return kv_pairs(x) 87 | elseif string_3f(x) then 88 | local acc = {} 89 | for i = 1, count(x) do 90 | table.insert(acc, x:sub(i, i)) 91 | end 92 | if not empty_3f(acc) then 93 | return acc 94 | else 95 | return nil 96 | end 97 | else 98 | return nil 99 | end 100 | end 101 | local function last(xs) 102 | if xs then 103 | return xs[count(xs)] 104 | else 105 | return nil 106 | end 107 | end 108 | local function inc(n) 109 | return (n + 1) 110 | end 111 | local function dec(n) 112 | return (n - 1) 113 | end 114 | local function even_3f(n) 115 | return ((n % 2) == 0) 116 | end 117 | local function odd_3f(n) 118 | return not even_3f(n) 119 | end 120 | local function vals(t) 121 | local result = {} 122 | if t then 123 | for _, v in pairs(t) do 124 | table.insert(result, v) 125 | end 126 | else 127 | end 128 | return result 129 | end 130 | local function run_21(f, xs) 131 | if xs then 132 | local nxs = count(xs) 133 | if (nxs > 0) then 134 | for i = 1, nxs do 135 | f(xs[i]) 136 | end 137 | return nil 138 | else 139 | return nil 140 | end 141 | else 142 | return nil 143 | end 144 | end 145 | local function complement(f) 146 | local function _14_(...) 147 | return not f(...) 148 | end 149 | return _14_ 150 | end 151 | local function filter(f, xs) 152 | local result = {} 153 | local function _15_(x) 154 | if f(x) then 155 | return table.insert(result, x) 156 | else 157 | return nil 158 | end 159 | end 160 | run_21(_15_, xs) 161 | return result 162 | end 163 | local function remove(f, xs) 164 | return filter(complement(f), xs) 165 | end 166 | local function map(f, xs) 167 | local result = {} 168 | local function _17_(x) 169 | local mapped = f(x) 170 | local function _18_() 171 | if (0 == select("#", mapped)) then 172 | return nil 173 | else 174 | return mapped 175 | end 176 | end 177 | return table.insert(result, _18_()) 178 | end 179 | run_21(_17_, xs) 180 | return result 181 | end 182 | local function map_indexed(f, xs) 183 | return map(f, kv_pairs(xs)) 184 | end 185 | local function identity(x) 186 | return x 187 | end 188 | local function reduce(f, init, xs) 189 | local result = init 190 | local function _19_(x) 191 | result = f(result, x) 192 | return nil 193 | end 194 | run_21(_19_, xs) 195 | return result 196 | end 197 | local function some(f, xs) 198 | local result = nil 199 | local n = 1 200 | while (nil_3f(result) and (n <= count(xs))) do 201 | local candidate = f(xs[n]) 202 | if candidate then 203 | result = candidate 204 | else 205 | end 206 | n = inc(n) 207 | end 208 | return result 209 | end 210 | local function butlast(xs) 211 | local total = count(xs) 212 | local function _22_(_21_) 213 | local n = _21_[1] 214 | local v = _21_[2] 215 | return (n ~= total) 216 | end 217 | return map(second, filter(_22_, kv_pairs(xs))) 218 | end 219 | local function rest(xs) 220 | local function _24_(_23_) 221 | local n = _23_[1] 222 | local v = _23_[2] 223 | return (n ~= 1) 224 | end 225 | return map(second, filter(_24_, kv_pairs(xs))) 226 | end 227 | local function concat(...) 228 | local result = {} 229 | local function _25_(xs) 230 | local function _26_(x) 231 | return table.insert(result, x) 232 | end 233 | return run_21(_26_, xs) 234 | end 235 | run_21(_25_, {...}) 236 | return result 237 | end 238 | local function mapcat(f, xs) 239 | return concat(unpack(map(f, xs))) 240 | end 241 | local function pr_str(...) 242 | local s 243 | local function _27_(x) 244 | return fennel.view(x, {["one-line"] = true}) 245 | end 246 | s = table.concat(map(_27_, {...}), " ") 247 | if (nil_3f(s) or ("" == s)) then 248 | return "nil" 249 | else 250 | return s 251 | end 252 | end 253 | local function str(...) 254 | local function _29_(acc, s) 255 | return (acc .. s) 256 | end 257 | local function _30_(s) 258 | if string_3f(s) then 259 | return s 260 | else 261 | return pr_str(s) 262 | end 263 | end 264 | return reduce(_29_, "", map(_30_, {...})) 265 | end 266 | local function println(...) 267 | local function _32_(acc, s) 268 | return (acc .. s) 269 | end 270 | local function _34_(_33_) 271 | local i = _33_[1] 272 | local s = _33_[2] 273 | if (1 == i) then 274 | return s 275 | else 276 | return (" " .. s) 277 | end 278 | end 279 | local function _36_(s) 280 | if string_3f(s) then 281 | return s 282 | else 283 | return pr_str(s) 284 | end 285 | end 286 | return print(reduce(_32_, "", map_indexed(_34_, map(_36_, {...})))) 287 | end 288 | local function pr(...) 289 | return println(pr_str(...)) 290 | end 291 | local function slurp(path) 292 | if path then 293 | local _38_, _39_ = io.open(path, "r") 294 | if ((_38_ == nil) and true) then 295 | local _msg = _39_ 296 | return nil 297 | elseif (nil ~= _38_) then 298 | local f = _38_ 299 | local content = f:read("*all") 300 | f:close() 301 | return content 302 | else 303 | return nil 304 | end 305 | else 306 | return nil 307 | end 308 | end 309 | local function get(t, k, d) 310 | local res 311 | if table_3f(t) then 312 | local val = t[k] 313 | if not nil_3f(val) then 314 | res = val 315 | else 316 | res = nil 317 | end 318 | else 319 | res = nil 320 | end 321 | if nil_3f(res) then 322 | return d 323 | else 324 | return res 325 | end 326 | end 327 | local function spit(path, content, opts) 328 | if path then 329 | local _45_, _46_ = nil, nil 330 | local function _47_() 331 | if get(opts, "append") then 332 | return "a" 333 | else 334 | return "w" 335 | end 336 | end 337 | _45_, _46_ = io.open(path, _47_()) 338 | if ((_45_ == nil) and (nil ~= _46_)) then 339 | local msg = _46_ 340 | return error(("Could not open file: " .. msg)) 341 | elseif (nil ~= _45_) then 342 | local f = _45_ 343 | f:write(content) 344 | f:close() 345 | return nil 346 | else 347 | return nil 348 | end 349 | else 350 | return nil 351 | end 352 | end 353 | local function merge_21(base, ...) 354 | local function _50_(acc, m) 355 | if m then 356 | for k, v in pairs(m) do 357 | acc[k] = v 358 | end 359 | else 360 | end 361 | return acc 362 | end 363 | return reduce(_50_, (base or {}), {...}) 364 | end 365 | local function merge(...) 366 | return merge_21({}, ...) 367 | end 368 | local function select_keys(t, ks) 369 | if (t and ks) then 370 | local function _52_(acc, k) 371 | if k then 372 | acc[k] = t[k] 373 | else 374 | end 375 | return acc 376 | end 377 | return reduce(_52_, {}, ks) 378 | else 379 | return {} 380 | end 381 | end 382 | local function get_in(t, ks, d) 383 | local res 384 | local function _55_(acc, k) 385 | if table_3f(acc) then 386 | return get(acc, k) 387 | else 388 | return nil 389 | end 390 | end 391 | res = reduce(_55_, t, ks) 392 | if nil_3f(res) then 393 | return d 394 | else 395 | return res 396 | end 397 | end 398 | local function assoc(t, ...) 399 | local _let_58_ = {...} 400 | local k = _let_58_[1] 401 | local v = _let_58_[2] 402 | local xs = (function (t, k) return ((getmetatable(t) or {}).__fennelrest or function (t, k) return {(table.unpack or unpack)(t, k)} end)(t, k) end)(_let_58_, 3) 403 | local rem = count(xs) 404 | local t0 = (t or {}) 405 | if odd_3f(rem) then 406 | error("assoc expects even number of arguments after table, found odd number") 407 | else 408 | end 409 | if not nil_3f(k) then 410 | t0[k] = v 411 | else 412 | end 413 | if (rem > 0) then 414 | assoc(t0, unpack(xs)) 415 | else 416 | end 417 | return t0 418 | end 419 | local function assoc_in(t, ks, v) 420 | local path = butlast(ks) 421 | local final = last(ks) 422 | local t0 = (t or {}) 423 | local function _62_(acc, k) 424 | local step = get(acc, k) 425 | if nil_3f(step) then 426 | return get(assoc(acc, k, {}), k) 427 | else 428 | return step 429 | end 430 | end 431 | assoc(reduce(_62_, t0, path), final, v) 432 | return t0 433 | end 434 | local function update(t, k, f) 435 | return assoc(t, k, f(get(t, k))) 436 | end 437 | local function update_in(t, ks, f) 438 | return assoc_in(t, ks, f(get_in(t, ks))) 439 | end 440 | local function constantly(v) 441 | local function _64_() 442 | return v 443 | end 444 | return _64_ 445 | end 446 | local function distinct(xs) 447 | local function _65_(acc, x) 448 | acc[x] = true 449 | return acc 450 | end 451 | return keys(reduce(_65_, {}, xs)) 452 | end 453 | local function sort(xs) 454 | local copy = map(identity, xs) 455 | table.sort(copy) 456 | return copy 457 | end 458 | local function clear_table_21(t) 459 | if t then 460 | for k, _ in pairs(t) do 461 | t[k] = nil 462 | end 463 | else 464 | end 465 | return nil 466 | end 467 | local function take_while(f, xs) 468 | local xs0 = seq(xs) 469 | if xs0 then 470 | local acc = {} 471 | local done_3f = false 472 | for i = 1, count(xs0), 1 do 473 | local v = xs0[i] 474 | if (not done_3f and f(v)) then 475 | table.insert(acc, v) 476 | else 477 | done_3f = true 478 | end 479 | end 480 | return acc 481 | else 482 | return nil 483 | end 484 | end 485 | local function drop_while(f, xs) 486 | local xs0 = seq(xs) 487 | if xs0 then 488 | local acc = {} 489 | local done_3f = false 490 | for i = 1, count(xs0), 1 do 491 | local v = xs0[i] 492 | if (done_3f or not f(v)) then 493 | table.insert(acc, v) 494 | done_3f = true 495 | else 496 | end 497 | end 498 | return acc 499 | else 500 | return nil 501 | end 502 | end 503 | local function __3eset(tbl) 504 | if tbl then 505 | assert(table_3f(tbl), "Table required as input to ->set.") 506 | local result = {} 507 | for _n, v in ipairs(tbl) do 508 | result[v] = true 509 | end 510 | return result 511 | else 512 | return nil 513 | end 514 | end 515 | local function contains_3f(tbl, v) 516 | if tbl then 517 | assert(table_3f(tbl), "contains? expects a table") 518 | if sequential_3f(tbl) then 519 | local function _72_(_241) 520 | return (_241 == v) 521 | end 522 | return (some(_72_, tbl) or false) 523 | else 524 | return (true == tbl[v]) 525 | end 526 | else 527 | return nil 528 | end 529 | end 530 | return {rand = rand, ["nil?"] = nil_3f, ["number?"] = number_3f, ["boolean?"] = boolean_3f, ["string?"] = string_3f, ["table?"] = table_3f, ["function?"] = function_3f, keys = keys, count = count, ["empty?"] = empty_3f, first = first, second = second, last = last, inc = inc, dec = dec, ["even?"] = even_3f, ["odd?"] = odd_3f, vals = vals, ["kv-pairs"] = kv_pairs, ["run!"] = run_21, complement = complement, filter = filter, remove = remove, map = map, ["map-indexed"] = map_indexed, identity = identity, reduce = reduce, some = some, butlast = butlast, rest = rest, concat = concat, mapcat = mapcat, ["pr-str"] = pr_str, str = str, println = println, pr = pr, slurp = slurp, spit = spit, ["merge!"] = merge_21, merge = merge, ["select-keys"] = select_keys, get = get, ["get-in"] = get_in, assoc = assoc, ["assoc-in"] = assoc_in, update = update, ["update-in"] = update_in, constantly = constantly, distinct = distinct, sort = sort, ["clear-table!"] = clear_table_21, ["sequential?"] = sequential_3f, seq = seq, ["take-while"] = take_while, ["drop-while"] = drop_while, ["->set"] = __3eset, ["contains?"] = contains_3f} 531 | -------------------------------------------------------------------------------- /lua/nfnl/fs.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/fs.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local define = _local_1_["define"] 5 | local core = autoload("nfnl.core") 6 | local str = autoload("nfnl.string") 7 | local M = define("nfnl.fs") 8 | M.basename = function(path) 9 | if path then 10 | return vim.fn.fnamemodify(path, ":h") 11 | else 12 | return nil 13 | end 14 | end 15 | M.filename = function(path) 16 | if path then 17 | return vim.fn.fnamemodify(path, ":t") 18 | else 19 | return nil 20 | end 21 | end 22 | M["file-name-root"] = function(path) 23 | if path then 24 | return vim.fn.fnamemodify(path, ":r") 25 | else 26 | return nil 27 | end 28 | end 29 | M["full-path"] = function(path) 30 | if path then 31 | return vim.fn.fnamemodify(path, ":p") 32 | else 33 | return nil 34 | end 35 | end 36 | M.mkdirp = function(dir) 37 | if dir then 38 | return vim.fn.mkdir(dir, "p") 39 | else 40 | return nil 41 | end 42 | end 43 | M["replace-extension"] = function(path, ext) 44 | if path then 45 | return (M["file-name-root"](path) .. ("." .. ext)) 46 | else 47 | return nil 48 | end 49 | end 50 | M["read-first-line"] = function(path) 51 | local f = io.open(path, "r") 52 | if (f and not core["string?"](f)) then 53 | local line = f:read("*line") 54 | f:close() 55 | return line 56 | else 57 | return nil 58 | end 59 | end 60 | M.absglob = function(dir, expr) 61 | return vim.fn.globpath(dir, expr, true, true) 62 | end 63 | M.relglob = function(dir, expr) 64 | local dir_len = (2 + string.len(dir)) 65 | local function _9_(_241) 66 | return string.sub(_241, dir_len) 67 | end 68 | return core.map(_9_, M.absglob(dir, expr)) 69 | end 70 | M["glob-dir-newer?"] = function(a_dir, b_dir, expr, b_dir_path_fn) 71 | local newer_3f = false 72 | for _, path in ipairs(M.relglob(a_dir, expr)) do 73 | if (vim.fn.getftime((a_dir .. path)) > vim.fn.getftime((b_dir .. b_dir_path_fn(path)))) then 74 | newer_3f = true 75 | else 76 | end 77 | end 78 | return newer_3f 79 | end 80 | M["path-sep"] = function() 81 | local os = string.lower(jit.os) 82 | if (("linux" == os) or ("osx" == os) or ("bsd" == os) or ((1 == vim.fn.exists("+shellshash")) and vim.o.shellslash)) then 83 | return "/" 84 | else 85 | return "\\" 86 | end 87 | end 88 | M.findfile = function(name, path) 89 | local res = vim.fn.findfile(name, path) 90 | if not core["empty?"](res) then 91 | return M["full-path"](res) 92 | else 93 | return nil 94 | end 95 | end 96 | M["split-path"] = function(path) 97 | return str.split(path, M["path-sep"]()) 98 | end 99 | M["join-path"] = function(parts) 100 | return str.join(M["path-sep"](), core.concat(parts)) 101 | end 102 | M["replace-dirs"] = function(path, from, to) 103 | local function _13_(segment) 104 | if (from == segment) then 105 | return to 106 | else 107 | return segment 108 | end 109 | end 110 | return M["join-path"](core.map(_13_, M["split-path"](path))) 111 | end 112 | M["fnl-path->lua-path"] = function(fnl_path) 113 | return M["replace-dirs"](M["replace-extension"](fnl_path, "lua"), "fnl", "lua") 114 | end 115 | M["glob-matches?"] = function(dir, expr, path) 116 | local regex = vim.regex(vim.fn.glob2regpat(M["join-path"]({dir, expr}))) 117 | return regex:match_str(path) 118 | end 119 | local uv = (vim.uv or vim.loop) 120 | M["exists?"] = function(path) 121 | if path then 122 | return ("table" == type(uv.fs_stat(path))) 123 | else 124 | return nil 125 | end 126 | end 127 | return M 128 | -------------------------------------------------------------------------------- /lua/nfnl/gc.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/gc.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local define = _local_1_["define"] 5 | local core = autoload("nfnl.core") 6 | local fs = autoload("nfnl.fs") 7 | local header = autoload("nfnl.header") 8 | local M = define("nfnl.gc") 9 | M["find-orphan-lua-files"] = function(_2_) 10 | local cfg = _2_["cfg"] 11 | local root_dir = _2_["root-dir"] 12 | local fnl_path__3elua_path = cfg({"fnl-path->lua-path"}) 13 | local ignore_patterns = cfg({"orphan-detection", "ignore-patterns"}) 14 | local function _3_(path) 15 | local line = fs["read-first-line"](path) 16 | local function _4_(pat) 17 | return path:find(pat) 18 | end 19 | return (not core.some(_4_, ignore_patterns) and header["tagged?"](line) and not fs["exists?"](header["source-path"](line))) 20 | end 21 | local function _5_(fnl_pattern) 22 | local lua_pattern = fnl_path__3elua_path(fnl_pattern) 23 | return fs.relglob(root_dir, lua_pattern) 24 | end 25 | return core.filter(_3_, core.keys(core["->set"](core.mapcat(_5_, cfg({"source-file-patterns"}))))) 26 | end 27 | --[[ (local config (require "nfnl.config")) (M.find-orphan-lua-files (config.find-and-load ".")) ]] 28 | return M 29 | -------------------------------------------------------------------------------- /lua/nfnl/header.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/header.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local define = _local_1_["define"] 5 | local core = autoload("nfnl.core") 6 | local str = autoload("nfnl.string") 7 | local M = define("nfnl.header") 8 | local tag = "[nfnl]" 9 | M["with-header"] = function(file, src) 10 | return ("-- " .. tag .. " " .. file .. "\n" .. src) 11 | end 12 | M["tagged?"] = function(s) 13 | if s then 14 | return core["number?"](s:find(tag, 1, true)) 15 | else 16 | return nil 17 | end 18 | end 19 | M["source-path"] = function(s) 20 | if M["tagged?"](s) then 21 | local function _3_(part) 22 | return (str["ends-with?"](part, ".fnl") and part) 23 | end 24 | return core.some(_3_, str.split(s, "%s+")) 25 | else 26 | return nil 27 | end 28 | end 29 | return M 30 | -------------------------------------------------------------------------------- /lua/nfnl/init.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/init.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local callback = autoload("nfnl.callback") 5 | local minimum_neovim_version = "0.9.0" 6 | if vim then 7 | if (0 == vim.fn.has(("nvim-" .. minimum_neovim_version))) then 8 | error(("nfnl requires Neovim > v" .. minimum_neovim_version)) 9 | else 10 | end 11 | vim.api.nvim_create_autocmd({"Filetype"}, {group = vim.api.nvim_create_augroup("nfnl-setup", {}), pattern = "fennel", callback = callback["fennel-filetype-callback"]}) 12 | if ("fennel" == vim.o.filetype) then 13 | callback["fennel-filetype-callback"]({file = vim.fn.expand("%"), buf = vim.api.nvim_get_current_buf()}) 14 | else 15 | end 16 | else 17 | end 18 | local function setup(opts) 19 | if opts then 20 | vim.g["nfnl#compile_on_write"] = opts.compile_on_write 21 | return nil 22 | else 23 | return nil 24 | end 25 | end 26 | return {setup = setup} 27 | -------------------------------------------------------------------------------- /lua/nfnl/module.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/module.fnl 2 | local module_key = "nfnl/autoload-module" 3 | local enabled_key = "nfnl/autoload-enabled?" 4 | local M = {} 5 | M.autoload = function(name) 6 | local res = {[enabled_key] = true, [module_key] = false} 7 | local ensure 8 | local function _1_() 9 | if res[module_key] then 10 | return res[module_key] 11 | else 12 | local m = require(name) 13 | res[module_key] = m 14 | return m 15 | end 16 | end 17 | ensure = _1_ 18 | local function _3_(t, ...) 19 | return ensure()(...) 20 | end 21 | local function _4_(t, k) 22 | return ensure()[k] 23 | end 24 | local function _5_(t, k, v) 25 | ensure()[k] = v 26 | return nil 27 | end 28 | return setmetatable(res, {__call = _3_, __index = _4_, __newindex = _5_}) 29 | end 30 | M.define = function(mod_name, base) 31 | local loaded = package.loaded[mod_name] 32 | if ((type(loaded) == type(base)) and (nil ~= loaded)) then 33 | return loaded 34 | else 35 | return (base or {}) 36 | end 37 | end 38 | return M 39 | -------------------------------------------------------------------------------- /lua/nfnl/notify.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/notify.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local core = autoload("nfnl.core") 5 | local function notify(level, ...) 6 | return vim.api.nvim_notify(core.str(...), level, {}) 7 | end 8 | local function debug(...) 9 | return notify(vim.log.levels.DEBUG, ...) 10 | end 11 | local function error(...) 12 | return notify(vim.log.levels.ERROR, ...) 13 | end 14 | local function info(...) 15 | return notify(vim.log.levels.INFO, ...) 16 | end 17 | local function trace(...) 18 | return notify(vim.log.levels.TRACE, ...) 19 | end 20 | local function warn(...) 21 | return notify(vim.log.levels.WARN, ...) 22 | end 23 | return {debug = debug, error = error, info = info, trace = trace, warn = warn} 24 | -------------------------------------------------------------------------------- /lua/nfnl/nvim.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/nvim.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local str = autoload("nfnl.string") 5 | local function get_buf_content_as_string(buf) 6 | return (str.join("\n", vim.api.nvim_buf_get_lines((buf or 0), 0, -1, false)) or "") 7 | end 8 | return {["get-buf-content-as-string"] = get_buf_content_as_string} 9 | -------------------------------------------------------------------------------- /lua/nfnl/repl.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/repl.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local core = autoload("nfnl.core") 5 | local fennel = autoload("nfnl.fennel") 6 | local notify = autoload("nfnl.notify") 7 | local str = autoload("nfnl.string") 8 | local function new(opts) 9 | local results_to_return = nil 10 | local cfg 11 | do 12 | local t_2_ = opts 13 | if (nil ~= t_2_) then 14 | t_2_ = t_2_.cfg 15 | else 16 | end 17 | cfg = t_2_ 18 | end 19 | local co 20 | local function _4_() 21 | local function _5_(results) 22 | results_to_return = core.concat(results_to_return, results) 23 | return nil 24 | end 25 | local function _6_(err_type, err, lua_source) 26 | local _8_ 27 | do 28 | local t_7_ = opts 29 | if (nil ~= t_7_) then 30 | t_7_ = t_7_["on-error"] 31 | else 32 | end 33 | _8_ = t_7_ 34 | end 35 | if _8_ then 36 | return opts["on-error"](err_type, err, lua_source) 37 | else 38 | return notify.error(str.trim(str.join("\n\n", {("[" .. err_type .. "] " .. err), lua_source}))) 39 | end 40 | end 41 | local function _11_() 42 | if cfg then 43 | return cfg({"compiler-options"}) 44 | else 45 | return nil 46 | end 47 | end 48 | return fennel.repl(core["merge!"]({pp = core.identity, readChunk = coroutine.yield, env = core.merge(_G), onValues = _5_, onError = _6_}, _11_())) 49 | end 50 | co = coroutine.create(_4_) 51 | coroutine.resume(co) 52 | local function _12_(input) 53 | if cfg then 54 | fennel.path = cfg({"fennel-path"}) 55 | fennel["macro-path"] = cfg({"fennel-macro-path"}) 56 | else 57 | end 58 | coroutine.resume(co, input) 59 | local prev_eval_values = results_to_return 60 | results_to_return = nil 61 | return prev_eval_values 62 | end 63 | return _12_ 64 | end 65 | return {new = new} 66 | -------------------------------------------------------------------------------- /lua/nfnl/string.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/nfnl/string.fnl 2 | local _local_1_ = require("nfnl.module") 3 | local autoload = _local_1_["autoload"] 4 | local core = autoload("nfnl.core") 5 | local function join(...) 6 | local args = {...} 7 | local function _2_(...) 8 | if (2 == core.count(args)) then 9 | return args 10 | else 11 | return {"", core.first(args)} 12 | end 13 | end 14 | local _let_3_ = _2_(...) 15 | local sep = _let_3_[1] 16 | local xs = _let_3_[2] 17 | local len = core.count(xs) 18 | local result = {} 19 | if (len > 0) then 20 | for i = 1, len do 21 | local x = xs[i] 22 | local tmp_6_ 23 | if ("string" == type(x)) then 24 | tmp_6_ = x 25 | elseif (nil == x) then 26 | tmp_6_ = x 27 | else 28 | tmp_6_ = core["pr-str"](x) 29 | end 30 | if (tmp_6_ ~= nil) then 31 | table.insert(result, tmp_6_) 32 | else 33 | end 34 | end 35 | else 36 | end 37 | return table.concat(result, sep) 38 | end 39 | local function split(s, pat) 40 | local done_3f = false 41 | local acc = {} 42 | local index = 1 43 | while not done_3f do 44 | local start, _end = string.find(s, pat, index) 45 | if ("nil" == type(start)) then 46 | table.insert(acc, string.sub(s, index)) 47 | done_3f = true 48 | else 49 | table.insert(acc, string.sub(s, index, (start - 1))) 50 | index = (_end + 1) 51 | end 52 | end 53 | return acc 54 | end 55 | local function blank_3f(s) 56 | return (core["empty?"](s) or not string.find(s, "[^%s]")) 57 | end 58 | local function triml(s) 59 | return string.gsub(s, "^%s*(.-)", "%1") 60 | end 61 | local function trimr(s) 62 | return string.gsub(s, "(.-)%s*$", "%1") 63 | end 64 | local function trim(s) 65 | return string.gsub(s, "^%s*(.-)%s*$", "%1") 66 | end 67 | local function ends_with_3f(s, suffix) 68 | local suffix_len = #suffix 69 | local s_len = #s 70 | if (s_len >= suffix_len) then 71 | return (suffix == string.sub(s, (s_len - suffix_len - -1))) 72 | else 73 | return false 74 | end 75 | end 76 | return {join = join, split = split, ["blank?"] = blank_3f, triml = triml, trimr = trimr, trim = trim, ["ends-with?"] = ends_with_3f} 77 | -------------------------------------------------------------------------------- /lua/spec/nfnl/callback_spec.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/spec/nfnl/callback_spec.fnl 2 | local _local_1_ = require("plenary.busted") 3 | local describe = _local_1_["describe"] 4 | local it = _local_1_["it"] 5 | local assert = require("luassert.assert") 6 | local callback = require("nfnl.callback") 7 | local function _2_() 8 | local function _3_() 9 | assert["false"](callback["supported-path?"](nil)) 10 | return assert["false"](callback["supported-path?"](10)) 11 | end 12 | it("rejects non-strings", _3_) 13 | local function _4_() 14 | assert["false"](callback["supported-path?"]("fugitive://foo/bar")) 15 | return assert["false"](callback["supported-path?"]("ddu-ff:/xyz/foo/bar")) 16 | end 17 | it("rejects fugitive or ddu-ff protcols", _4_) 18 | local function _5_() 19 | assert["true"](callback["supported-path?"]("/foo/bar/baz")) 20 | assert["true"](callback["supported-path?"]("./x/y/z.foo")) 21 | return assert["true"](callback["supported-path?"]("henlo.world")) 22 | end 23 | return it("allows all other strings", _5_) 24 | end 25 | return describe("supported-path?", _2_) 26 | -------------------------------------------------------------------------------- /lua/spec/nfnl/compile_spec.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/spec/nfnl/compile_spec.fnl 2 | local _local_1_ = require("plenary.busted") 3 | local describe = _local_1_["describe"] 4 | local it = _local_1_["it"] 5 | local assert = require("luassert.assert") 6 | local config = require("nfnl.config") 7 | local compile = require("nfnl.compile") 8 | local function _2_() 9 | local function _3_() 10 | return assert.are.same({result = "-- [nfnl] bar.fnl\nreturn (10 + 20)\n", ["source-path"] = "/tmp/foo/bar.fnl", status = "ok"}, compile["into-string"]({["root-dir"] = "/tmp/foo", path = "/tmp/foo/bar.fnl", cfg = config["cfg-fn"]({}, {["root-dir"] = "/tmp/foo"}), ["batch?"] = true, source = "(+ 10 20)"})) 11 | end 12 | it("compiles good Fennel to Lua", _3_) 13 | local function _4_() 14 | return assert.are.same({["source-path"] = "/my/dir/baz.fnl", status = "path-is-not-in-source-file-patterns"}, compile["into-string"]({["root-dir"] = "/my/dir", path = "/my/dir/baz.fnl", cfg = config["cfg-fn"]({["source-file-patterns"] = {"bar.fnl"}}, {["root-dir"] = "/tmp/foo"}), ["batch?"] = true, source = "(+ 10 20)"})) 15 | end 16 | it("skips files that don't match :source-file-patterns", _4_) 17 | local function _5_() 18 | return assert.are.same({["source-path"] = "/my/dir/foo.fnl", status = "macros-are-not-compiled"}, compile["into-string"]({["root-dir"] = "/my/dir", path = "/my/dir/foo.fnl", cfg = config["cfg-fn"]({}, {["root-dir"] = "/tmp/foo"}), ["batch?"] = true, source = ("; [nfnl" .. "-" .. "macro]\n(+ 10 20)")})) 19 | end 20 | it("skips macro files", _5_) 21 | local function _6_() 22 | return assert.are.same({["source-path"] = "/my/dir/.nfnl.fnl", status = "nfnl-config-is-not-compiled"}, compile["into-string"]({["root-dir"] = "/my/dir", path = "/my/dir/.nfnl.fnl", cfg = config["cfg-fn"]({}, {["root-dir"] = "/tmp/foo"}), ["batch?"] = true, source = "(+ 10 20)"})) 23 | end 24 | it("won't compile the .nfnl.fnl config file", _6_) 25 | local function _7_() 26 | return assert.are.same({error = "/my/dir/foo.fnl:1:3: Compile error: tried to reference a special form without calling it\n\n10 / 20\n* Try making sure to use prefix operators, not infix.\n* Try wrapping the special in a function if you need it to be first class.", ["source-path"] = "/my/dir/foo.fnl", status = "compilation-error"}, compile["into-string"]({["root-dir"] = "/my/dir", path = "/my/dir/foo.fnl", cfg = config["cfg-fn"]({}, {["root-dir"] = "/tmp/foo"}), ["batch?"] = true, source = "10 / 20"})) 27 | end 28 | return it("returns compilation errors", _7_) 29 | end 30 | return describe("into-string", _2_) 31 | -------------------------------------------------------------------------------- /lua/spec/nfnl/config_spec.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/spec/nfnl/config_spec.fnl 2 | local _local_1_ = require("plenary.busted") 3 | local describe = _local_1_["describe"] 4 | local it = _local_1_["it"] 5 | local assert = require("luassert.assert") 6 | local config = require("nfnl.config") 7 | local fs = require("nfnl.fs") 8 | local function _2_() 9 | local function _3_() 10 | assert.equals("function", type(config.default)) 11 | assert.equals("table", type(config.default({["root-dir"] = "/tmp/foo"}))) 12 | return assert.equals(vim.fn.getcwd(), config.default({})["root-dir"]) 13 | end 14 | return it("is a function that returns a table", _3_) 15 | end 16 | describe("default", _2_) 17 | local function _4_() 18 | local function _5_() 19 | local opts = {["root-dir"] = "/tmp/foo"} 20 | assert.is_string(config["cfg-fn"]({}, opts)({"fennel-macro-path"})) 21 | assert.is_nil(config["cfg-fn"]({}, opts)({"nope"})) 22 | assert.equals("yep", config["cfg-fn"]({nope = "yep"}, opts)({"nope"})) 23 | return assert.equals("yep", config["cfg-fn"]({["fennel-macro-path"] = "yep"}, opts)({"fennel-macro-path"})) 24 | end 25 | return it("builds a function that looks up values in a table falling back to defaults", _5_) 26 | end 27 | describe("cfg-fn", _4_) 28 | local function _6_() 29 | local function _7_() 30 | assert.is_true(config["config-file-path?"]("./foo/.nfnl.fnl")) 31 | assert.is_true(config["config-file-path?"](".nfnl.fnl")) 32 | return assert.is_false(config["config-file-path?"](".fnl.fnl")) 33 | end 34 | return it("returns true for config file paths", _7_) 35 | end 36 | describe("config-file-path?", _6_) 37 | local function _8_() 38 | local function _9_() 39 | local _let_10_ = config["find-and-load"](".") 40 | local cfg = _let_10_["cfg"] 41 | local root_dir = _let_10_["root-dir"] 42 | local config0 = _let_10_["config"] 43 | assert.are.same({verbose = true}, config0) 44 | assert.equals(vim.fn.getcwd(), root_dir) 45 | return assert.equals("function", type(cfg)) 46 | end 47 | it("loads the repo config file", _9_) 48 | local function _11_() 49 | return assert.are.same({}, config["find-and-load"]("/some/made/up/dir")) 50 | end 51 | return it("returns an empty table if a config file isn't found", _11_) 52 | end 53 | describe("find-and-load", _8_) 54 | local function sorted(xs) 55 | table.sort(xs) 56 | return xs 57 | end 58 | local function _12_() 59 | local function _13_() 60 | assert.are.same({"/foo/bar/nfnl", "/foo/baz/my-proj"}, sorted(config["path-dirs"]({runtimepath = "/foo/bar/nfnl,/foo/bar/other-thing", ["rtp-patterns"] = {(fs["path-sep"]() .. "nfnl$")}, ["base-dirs"] = {"/foo/baz/my-proj"}}))) 61 | return assert.are.same({"/foo/bar/nfnl", "/foo/baz/my-proj"}, sorted(config["path-dirs"]({runtimepath = "/foo/bar/nfnl,/foo/bar/other-thing", ["rtp-patterns"] = {(fs["path-sep"]() .. "nfnl$")}, ["base-dirs"] = {"/foo/baz/my-proj", "/foo/bar/nfnl"}}))) 62 | end 63 | return it("builds path dirs from runtimepath, deduplicates the base-dirs", _13_) 64 | end 65 | return describe("path-dirs", _12_) 66 | -------------------------------------------------------------------------------- /lua/spec/nfnl/e2e_spec.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/spec/nfnl/e2e_spec.fnl 2 | local _local_1_ = require("plenary.busted") 3 | local describe = _local_1_["describe"] 4 | local it = _local_1_["it"] 5 | local before_each = _local_1_["before_each"] 6 | local after_each = _local_1_["after_each"] 7 | local assert = require("luassert.assert") 8 | local core = require("nfnl.core") 9 | local fs = require("nfnl.fs") 10 | local nfnl = require("nfnl") 11 | nfnl.setup({}) 12 | local temp_dir = vim.fn.tempname() 13 | local unrelated_temp_dir = vim.fn.tempname() 14 | local fnl_dir = fs["join-path"]({temp_dir, "fnl"}) 15 | local lua_dir = fs["join-path"]({temp_dir, "lua"}) 16 | local config_path = fs["join-path"]({temp_dir, ".nfnl.fnl"}) 17 | local fnl_path = fs["join-path"]({fnl_dir, "foo.fnl"}) 18 | local macro_fnl_path = fs["join-path"]({fnl_dir, "bar.fnl"}) 19 | local macro_lua_path = fs["join-path"]({lua_dir, "bar.lua"}) 20 | local lua_path = fs["join-path"]({lua_dir, "foo.lua"}) 21 | fs.mkdirp(fnl_dir) 22 | fs.mkdirp(unrelated_temp_dir) 23 | local function delete_buf_file(path) 24 | pcall(vim.cmd, ("bwipeout! " .. path)) 25 | return os.remove(path) 26 | end 27 | local function run_e2e_tests() 28 | core["run!"](delete_buf_file, {config_path, fnl_path, macro_fnl_path, lua_path}) 29 | local function _2_() 30 | vim.cmd(("edit " .. fnl_path)) 31 | vim.o.filetype = "fennel" 32 | vim.api.nvim_buf_set_lines(0, 0, -1, false, {"(print \"Hello, World!\")"}) 33 | vim.cmd("write") 34 | return assert.is_nil(core.slurp(lua_path)) 35 | end 36 | it("does nothing when there's no .nfnl.fnl configuration", _2_) 37 | local function _3_() 38 | vim.cmd(("edit " .. config_path)) 39 | vim.api.nvim_buf_set_lines(0, 0, -1, false, {"{}"}) 40 | vim.cmd("write") 41 | vim.cmd("trust") 42 | vim.cmd(("edit " .. fnl_path)) 43 | vim.o.filetype = "fennel" 44 | vim.api.nvim_buf_set_lines(0, 0, -1, false, {"(print \"Hello, World!\")"}) 45 | vim.cmd("write") 46 | assert.are.equal(1, vim.fn.isdirectory(lua_dir)) 47 | local lua_result = core.slurp(lua_path) 48 | print("Lua result:", lua_result) 49 | return assert.are.equal("-- [nfnl] fnl/foo.fnl\nreturn print(\"Hello, World!\")\n", lua_result) 50 | end 51 | it("compiles when there's a trusted .nfnl.fnl configuration file", _3_) 52 | local function _4_() 53 | vim.cmd(("edit " .. macro_fnl_path)) 54 | vim.o.filetype = "fennel" 55 | vim.api.nvim_buf_set_lines(0, 0, -1, false, {(";; [nfnl" .. "-" .. "macro]"), "{:infix (fn [a op b] `(,op ,a ,b))}"}) 56 | vim.cmd("write") 57 | vim.cmd(("edit " .. fnl_path)) 58 | vim.o.filetype = "fennel" 59 | vim.api.nvim_buf_set_lines(0, 0, -1, false, {"(import-macros {: infix} :bar)", "(infix 10 + 20)"}) 60 | vim.cmd("write") 61 | assert.is_nil(core.slurp(macro_lua_path)) 62 | local lua_result = core.slurp(lua_path) 63 | print("Lua result:", lua_result) 64 | return assert.are.equal("-- [nfnl] fnl/foo.fnl\nreturn (10 + 20)\n", lua_result) 65 | end 66 | return it("can import-macros and use them, the macros aren't compiled", _4_) 67 | end 68 | local function _5_() 69 | local initial_cwd = nil 70 | local function _6_() 71 | initial_cwd = vim.fn.getcwd() 72 | return vim.api.nvim_set_current_dir(temp_dir) 73 | end 74 | before_each(_6_) 75 | local function _7_() 76 | return vim.api.nvim_set_current_dir(initial_cwd) 77 | end 78 | after_each(_7_) 79 | return run_e2e_tests() 80 | end 81 | describe("e2e file compiling from a project dir", _5_) 82 | local function _8_() 83 | local initial_cwd = nil 84 | local function _9_() 85 | initial_cwd = vim.fn.getcwd() 86 | return vim.api.nvim_set_current_dir(unrelated_temp_dir) 87 | end 88 | before_each(_9_) 89 | local function _10_() 90 | return vim.api.nvim_set_current_dir(initial_cwd) 91 | end 92 | after_each(_10_) 93 | return run_e2e_tests() 94 | end 95 | return describe("e2e file compiling from outside project dir", _8_) 96 | -------------------------------------------------------------------------------- /lua/spec/nfnl/fs_spec.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/spec/nfnl/fs_spec.fnl 2 | local _local_1_ = require("plenary.busted") 3 | local describe = _local_1_["describe"] 4 | local it = _local_1_["it"] 5 | local assert = require("luassert.assert") 6 | local fs = require("nfnl.fs") 7 | local function _2_() 8 | local function _3_() 9 | assert.equals("foo", fs.basename("foo/bar.fnl")) 10 | assert.equals("foo/bar", fs.basename("foo/bar/baz.fnl")) 11 | return assert.equals(".", fs.basename("baz.fnl")) 12 | end 13 | it("removes the last segment of a path", _3_) 14 | local function _4_() 15 | return assert.is_nil(fs.basename(nil)) 16 | end 17 | return it("happily lets nils flow back out", _4_) 18 | end 19 | describe("basename", _2_) 20 | local function _5_() 21 | local function _6_() 22 | return assert.equals("/", fs["path-sep"]()) 23 | end 24 | return it("returns the OS path separator (test assumes Linux)", _6_) 25 | end 26 | describe("path-sep", _5_) 27 | local function _7_() 28 | local function _8_() 29 | return assert.equals("foo.lua", fs["replace-extension"]("foo.fnl", "lua")) 30 | end 31 | return it("replaces extensions", _8_) 32 | end 33 | describe("replace-extension", _7_) 34 | local function _9_() 35 | local function _10_() 36 | assert.are.same({"foo", "bar", "baz"}, fs["split-path"]("foo/bar/baz")) 37 | return assert.are.same({"", "foo", "bar", "baz"}, fs["split-path"]("/foo/bar/baz")) 38 | end 39 | return it("splits a path into parts", _10_) 40 | end 41 | describe("split-path", _9_) 42 | local function _11_() 43 | local function _12_() 44 | assert.equals("foo/bar/baz", fs["join-path"]({"foo", "bar", "baz"})) 45 | return assert.equals("/foo/bar/baz", fs["join-path"]({"", "foo", "bar", "baz"})) 46 | end 47 | return it("joins a path together", _12_) 48 | end 49 | describe("join-path", _11_) 50 | local function _13_() 51 | local function _14_() 52 | assert.equals("foo/lua/bar", fs["replace-dirs"]("foo/fnl/bar", "fnl", "lua")) 53 | assert.equals("/foo/lua/bar", fs["replace-dirs"]("/foo/fnl/bar", "fnl", "lua")) 54 | return assert.equals("/foo/nfnl/bar", fs["replace-dirs"]("/foo/nfnl/bar", "fnl", "lua")) 55 | end 56 | return it("replaces directories in a path that match a string with another string", _14_) 57 | end 58 | return describe("replace-dirs", _13_) 59 | -------------------------------------------------------------------------------- /lua/spec/nfnl/macros/aniseed_spec.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/spec/nfnl/macros/aniseed_spec.fnl 2 | local _local_1_ = require("plenary.busted") 3 | local describe = _local_1_["describe"] 4 | local it = _local_1_["it"] 5 | local assert = require("luassert.assert") 6 | local _2amodule_2a 7 | do 8 | local pkg_1_auto = require("package") 9 | pkg_1_auto.loaded["spec.nfnl.macros.aniseed-example"] = {} 10 | _2amodule_2a = pkg_1_auto.loaded["spec.nfnl.macros.aniseed-example"] 11 | end 12 | local private_val = "this is private" 13 | local public_val 14 | do 15 | _2amodule_2a["public-val"] = "this is public" 16 | public_val = _2amodule_2a["public-val"] 17 | end 18 | local function private_fn() 19 | return "this is private fn" 20 | end 21 | local public_fn 22 | do 23 | local function public_fn0() 24 | return "this is public fn" 25 | end 26 | _2amodule_2a["public-fn"] = public_fn0 27 | public_fn = _2amodule_2a["public-fn"] 28 | end 29 | local private_once_val = (__fnl_global__private_2donce_2dval or "this is private once val") 30 | local private_once_val0 = (private_once_val or "this is ignored") 31 | local public_once_val 32 | do 33 | _2amodule_2a["public-once-val"] = (_2amodule_2a["public-once-val"] or "this is public once val") 34 | public_once_val = _2amodule_2a["public-once-val"] 35 | end 36 | local public_once_val0 37 | do 38 | _2amodule_2a["public-once-val"] = (_2amodule_2a["public-once-val"] or "this is ignored") 39 | public_once_val0 = _2amodule_2a["public-once-val"] 40 | end 41 | local mod = require("spec.nfnl.macros.aniseed-example") 42 | local function _2_() 43 | local function _3_() 44 | assert.equals("this is private", private_val) 45 | return assert.equals(nil, mod["private-val"]) 46 | end 47 | return it("defines private values", _3_) 48 | end 49 | describe("def-", _2_) 50 | local function _4_() 51 | local function _5_() 52 | assert.equals("this is public", public_val) 53 | return assert.equals("this is public", mod["public-val"]) 54 | end 55 | return it("defines public values", _5_) 56 | end 57 | describe("def", _4_) 58 | local function _6_() 59 | local function _7_() 60 | assert.equals("this is private fn", private_fn()) 61 | return assert.equals(nil, mod["private-fn"]) 62 | end 63 | return it("defines private functions", _7_) 64 | end 65 | describe("defn-", _6_) 66 | local function _8_() 67 | local function _9_() 68 | assert.equals("this is public fn", public_fn()) 69 | return assert.equals("this is public fn", mod["public-fn"]()) 70 | end 71 | return it("defines public functions", _9_) 72 | end 73 | describe("defn", _8_) 74 | local function _10_() 75 | local function _11_() 76 | assert.equals("this is private once val", private_once_val0) 77 | return assert.equals(nil, mod["private-once-val"]) 78 | end 79 | return it("defines private once values", _11_) 80 | end 81 | describe("defonce-", _10_) 82 | local function _12_() 83 | local function _13_() 84 | assert.equals("this is public once val", public_once_val0) 85 | return assert.equals("this is public once val", mod["public-once-val"]) 86 | end 87 | return it("defines public once values", _13_) 88 | end 89 | return describe("defonce", _12_) 90 | -------------------------------------------------------------------------------- /lua/spec/nfnl/macros_spec.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/spec/nfnl/macros_spec.fnl 2 | local _local_1_ = require("plenary.busted") 3 | local describe = _local_1_["describe"] 4 | local it = _local_1_["it"] 5 | local assert = require("luassert.assert") 6 | local function _2_() 7 | local function _3_() 8 | local function _4_() 9 | local foo = "ok" 10 | if foo then 11 | return foo 12 | else 13 | return "nope" 14 | end 15 | end 16 | assert.equals("ok", _4_()) 17 | local function _6_() 18 | local foo = false 19 | if foo then 20 | return "first" 21 | else 22 | return "nope" 23 | end 24 | end 25 | assert.equals("nope", _6_()) 26 | local function _9_() 27 | local value_5_auto = {a = 1} 28 | local _let_8_ = (value_5_auto or {}) 29 | local a = _let_8_["a"] 30 | if value_5_auto then 31 | return "yes" 32 | else 33 | return "no" 34 | end 35 | end 36 | assert.equals("yes", _9_()) 37 | local function _12_() 38 | local value_5_auto = nil 39 | local _let_11_ = (value_5_auto or {}) 40 | local a = _let_11_["a"] 41 | if value_5_auto then 42 | return "yes" 43 | else 44 | return "no" 45 | end 46 | end 47 | return assert.equals("no", _12_()) 48 | end 49 | return it("works like if + let", _3_) 50 | end 51 | describe("if-let", _2_) 52 | local function _14_() 53 | local function _15_() 54 | local function _16_() 55 | local foo = "ok" 56 | if foo then 57 | return foo 58 | else 59 | return nil 60 | end 61 | end 62 | assert.equals("ok", _16_()) 63 | local function _18_() 64 | local foo = false 65 | if foo then 66 | return "first" 67 | else 68 | return nil 69 | end 70 | end 71 | assert.equals(nil, _18_()) 72 | local function _21_() 73 | local ok_3f, val = nil, nil 74 | local function _20_() 75 | return "yarp" 76 | end 77 | ok_3f, val = pcall(_20_) 78 | if ok_3f then 79 | return val 80 | else 81 | return nil 82 | end 83 | end 84 | assert.equals("yarp", _21_()) 85 | local function _25_() 86 | local bind_23_, val = nil, nil 87 | local function _24_() 88 | return "yarp" 89 | end 90 | bind_23_, val = pcall(_24_) 91 | if bind_23_ then 92 | return val 93 | else 94 | return nil 95 | end 96 | end 97 | assert.equals("yarp", _25_()) 98 | local function _28_() 99 | local ok_3f, val = nil, nil 100 | local function _27_() 101 | return error("narp") 102 | end 103 | ok_3f, val = pcall(_27_) 104 | if ok_3f then 105 | return val 106 | else 107 | return nil 108 | end 109 | end 110 | return assert.equals(nil, _28_()) 111 | end 112 | return it("works like when + let", _15_) 113 | end 114 | return describe("when-let", _14_) 115 | -------------------------------------------------------------------------------- /lua/spec/nfnl/module_spec.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/spec/nfnl/module_spec.fnl 2 | local _local_1_ = require("plenary.busted") 3 | local describe = _local_1_["describe"] 4 | local it = _local_1_["it"] 5 | local assert = require("luassert.assert") 6 | local _local_2_ = require("nfnl.module") 7 | local autoload = _local_2_["autoload"] 8 | local define = _local_2_["define"] 9 | local function _3_() 10 | local function _4_() 11 | local core = autoload("nfnl.core") 12 | assert.equals(2, core.inc(1)) 13 | return nil 14 | end 15 | return it("loads modules when their properties are accessed", _4_) 16 | end 17 | describe("autoload", _3_) 18 | local function _5_() 19 | local function _6_() 20 | local m2 = define("nfnl.module", {}) 21 | assert.equals(define, m2.define) 22 | local m3 = define("nfnl.module.nope", {nope = true}) 23 | assert.equals(nil, m3.define) 24 | assert.equals(true, m3.nope) 25 | return nil 26 | end 27 | return it("returns the loaded module if it's the same type as the base", _6_) 28 | end 29 | return describe("define", _5_) 30 | -------------------------------------------------------------------------------- /lua/spec/nfnl/repl_spec.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/spec/nfnl/repl_spec.fnl 2 | local _local_1_ = require("plenary.busted") 3 | local describe = _local_1_["describe"] 4 | local it = _local_1_["it"] 5 | local core = require("nfnl.core") 6 | local assert = require("luassert.assert") 7 | local repl = require("nfnl.repl") 8 | local function _2_() 9 | local function _3_() 10 | local r = repl.new() 11 | assert.are.same({{10, 20}}, r("[10 20]")) 12 | assert.are.same({"foo", "bar"}, r(":foo :bar\n")) 13 | assert.are.same({"foo", "bar"}, r("(values :foo :bar)")) 14 | assert.are.same(nil, r()) 15 | assert.are.same(nil, r(":foo")) 16 | return nil 17 | end 18 | it("starts a REPL that we can confinually call with more code", _3_) 19 | local function _4_() 20 | do 21 | local r1 = repl.new() 22 | local r2 = repl.new() 23 | local code = "(fn a []) (fn b []) {: a : b}" 24 | local pat = "%[#%s+#%s+{:%w # :%w #}%]" 25 | assert.matches(pat, core["pr-str"](r1(code))) 26 | assert.matches(pat, core["pr-str"](r2(code))) 27 | assert.matches(pat, core["pr-str"](r1(code))) 28 | assert.matches(pat, core["pr-str"](r2(code))) 29 | end 30 | return nil 31 | end 32 | return it("can handle function references, tables and multiple REPLs", _4_) 33 | end 34 | return describe("repl", _2_) 35 | -------------------------------------------------------------------------------- /lua/spec/nfnl/string_spec.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] fnl/spec/nfnl/string_spec.fnl 2 | local _local_1_ = require("plenary.busted") 3 | local describe = _local_1_["describe"] 4 | local it = _local_1_["it"] 5 | local assert = require("luassert.assert") 6 | local str = require("nfnl.string") 7 | local function _2_() 8 | local function _3_() 9 | assert.equals("foo, bar, baz", str.join(", ", {"foo", "bar", "baz"})) 10 | assert.equals("foobarbaz", str.join({"foo", "bar", "baz"})) 11 | return assert.equals("foobar", str.join({"foo", nil, "bar"}), "handle nils correctly") 12 | end 13 | return it("joins things together", _3_) 14 | end 15 | describe("join", _2_) 16 | local function _4_() 17 | local function _5_() 18 | assert.are.same({""}, str.split("", ",")) 19 | assert.are.same({"foo"}, str.split("foo", ",")) 20 | assert.are.same({"foo", "bar", "baz"}, str.split("foo,bar,baz", ",")) 21 | assert.are.same({"foo", "", "bar"}, str.split("foo,,bar", ",")) 22 | assert.are.same({"foo", "", "", "bar"}, str.split("foo,,,bar", ",")) 23 | return assert.are.same({"foo", "baz"}, str.split("foobarbaz", "bar")) 24 | end 25 | return it("splits strings up", _5_) 26 | end 27 | describe("split", _4_) 28 | local function _6_() 29 | local function _7_() 30 | assert.is_true(str["blank?"](nil)) 31 | assert.is_true(str["blank?"]("")) 32 | assert.is_true(str["blank?"](" ")) 33 | assert.is_true(str["blank?"](" ")) 34 | assert.is_true(str["blank?"](" \n \n ")) 35 | return assert.is_true(not str["blank?"](" \n . \n ")) 36 | end 37 | return it("true if the string is nil or just whitespace", _7_) 38 | end 39 | describe("blank?", _6_) 40 | local function _8_() 41 | local function _9_() 42 | assert.equals("", str.triml("")) 43 | assert.equals("foo", str.triml("foo")) 44 | assert.equals("foo", str.triml(" foo")) 45 | assert.equals("foo", str.triml(" \n foo")) 46 | return assert.equals("foo ", str.triml(" \n foo ")) 47 | end 48 | return it("trims all whitespace from the left of the string", _9_) 49 | end 50 | describe("triml", _8_) 51 | local function _10_() 52 | local function _11_() 53 | assert.equals("", str.trimr("")) 54 | assert.equals("foo", str.trimr("foo")) 55 | assert.equals("foo", str.trimr("foo ")) 56 | assert.equals("foo", str.trimr("foo \n ")) 57 | return assert.equals(" foo", str.trimr(" foo \n ")) 58 | end 59 | return it("trims all whitespace from the right of the string", _11_) 60 | end 61 | describe("trimr", _10_) 62 | local function _12_() 63 | local function _13_() 64 | assert.equals("", str.trim("")) 65 | assert.equals("foo", str.trim("foo")) 66 | assert.equals("foo", str.trim(" \n foo \n \n "), "basic") 67 | return assert.equals("", str.trim(" "), "just whitespace") 68 | end 69 | return it("trims all whitespace from start end end of a string", _13_) 70 | end 71 | describe("trim", _12_) 72 | local function _14_() 73 | local function _15_() 74 | assert.is_true(str["ends-with?"]("foobarbaz", "baz")) 75 | assert.is_true(str["ends-with?"]("foobarbaz", "arbaz")) 76 | assert.is_true(str["ends-with?"]("foobarbaz", "foobarbaz")) 77 | assert.is_false(str["ends-with?"]("foobarbaz", "foo")) 78 | return assert.is_false(str["ends-with?"]("foobarbaz", "barbazz")) 79 | end 80 | return it("checks if a string ends with another string", _15_) 81 | end 82 | return describe("ends-with?", _14_) 83 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tasks.fetch-fennel] 2 | run = "bash script/fetch-fennel.sh" 3 | 4 | [tasks.render-api-documentation] 5 | run = "script/render-api-documentation" 6 | 7 | [tasks.setup-test-deps] 8 | run = "bash ./script/setup-test-deps" 9 | 10 | [tasks.test] 11 | depends = ["setup-test-deps"] 12 | run = "bash ./script/test" 13 | -------------------------------------------------------------------------------- /nfnl: -------------------------------------------------------------------------------- 1 | /home/olical/repos/Olical/nfnl -------------------------------------------------------------------------------- /plugin/nfnl.fnl: -------------------------------------------------------------------------------- 1 | (require "nfnl") 2 | -------------------------------------------------------------------------------- /plugin/nfnl.lua: -------------------------------------------------------------------------------- 1 | -- [nfnl] plugin/nfnl.fnl 2 | return require("nfnl") 3 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | script/fennel.bb --compile 4 | -------------------------------------------------------------------------------- /script/bootstrap-dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | script/fennel.bb --files | entr script/fennel.bb --compile 4 | -------------------------------------------------------------------------------- /script/embed: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Assumes your project is named the same at the root directory and under `fnl/`. 4 | # Copies the lua files and fnl macro source files into the destination project. 5 | # 6 | # Example: `conjure/fnl/conjure/**/*.fnl`. 7 | # Usage: ./embed ../conjure 8 | # Requires: rsync, fd, sd 9 | 10 | set -xe 11 | 12 | SRC_DIR=${SRC_DIR:-.} 13 | SRC_PROJECT=${SRC_PROJECT:-nfnl} 14 | SRC_PREFIX=${SRC_PREFIX:-"$SRC_PROJECT\."} 15 | 16 | DEST_DIR=$1 17 | DEST_PROJECT=${DEST_PROJECT:-$(basename $DEST_DIR)} 18 | DEST_PREFIX=${DEST_PREFIX:-"$DEST_PROJECT.$SRC_PROJECT."} 19 | 20 | rm -rf "$DEST_DIR/fnl/$DEST_PROJECT/$SRC_PROJECT" "$DEST_DIR/lua/$DEST_PROJECT/$SRC_PROJECT" 21 | rsync -av "$SRC_DIR/lua/$SRC_PROJECT" "$DEST_DIR/lua/$DEST_PROJECT/" 22 | rg -l '\[nfnl-macro\]' "$SRC_DIR/fnl/$SRC_PROJECT" --no-heading --replace '{path}' --path-separator '/' | 23 | sed "s|$SRC_DIR/fnl/$SRC_PROJECT/||" | 24 | rsync -av --files-from=- "$SRC_DIR/fnl/$SRC_PROJECT/" "$DEST_DIR/fnl/$DEST_PROJECT/$SRC_PROJECT" 25 | 26 | fd --no-ignore --extension .fnl . "$DEST_DIR/fnl/$DEST_PROJECT/$SRC_PROJECT" --exec sd " :$SRC_PREFIX" " :$DEST_PREFIX" 27 | fd --no-ignore --extension .lua . "$DEST_DIR/lua/$DEST_PROJECT/$SRC_PROJECT" --exec sd "\"$SRC_PREFIX" "\"$DEST_PREFIX" 28 | -------------------------------------------------------------------------------- /script/fennel.bb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | ;; vi: ft=clojure 3 | 4 | (ns script.fennel 5 | (:require [clojure.string :as str] 6 | [clojure.set :as set] 7 | [babashka.cli :as cli] 8 | [babashka.fs :as fs] 9 | [babashka.process :as proc])) 10 | 11 | (def cli-opts 12 | {:help {:alias :h 13 | :desc "Prints this help"} 14 | :compile {:alias :c 15 | :desc "Compile all changed Fennel files into their Lua counterparts (\"changed\" means newer modified time than their Lua counterpart)"} 16 | :dry {:alias :d 17 | :desc "When true, it won't actually modify any files"} 18 | :notify {:alias :n 19 | :desc "Use notify-send to display errors alongside printing to stderr"} 20 | :files {:alias :f 21 | :desc "Prints the Fennel files it would operate on and exits, useful for piping into entr for \"compile on change\" support"} 22 | :root {:alias :r 23 | :desc "Root directory to perform all operations under" 24 | :default "."} 25 | :prune {:alias :p 26 | :desc "Delete Lua files that lack a Fennel counterpart, prevents orphan Lua files from hanging around after a Fennel file deletion or rename, takes a list of globs that should not be included in the pruning (like `vendor/**.lua,my-cool.lua`)"}}) 27 | 28 | (defn compile-fennel-file 29 | "Compiles the given Fennel file into a Lua file. Can return an anomaly." 30 | [{:keys [src-path]}] 31 | (try 32 | (if src-path 33 | (let [{:keys [exit out err]} 34 | (proc/shell 35 | {:out :string 36 | :err :string 37 | :continue true} 38 | "lua" "script/fennel.lua" "--compile" src-path)] 39 | (if (zero? exit) 40 | out 41 | {:cognitect.anomalies/category :cognitect.anomalies/fault 42 | :cognitect.anomalies/message "Fennel process exited with a non-zero status code" 43 | ::exit exit 44 | ::err err})) 45 | {:cognitect.anomalies/category :cognitect.anomalies/incorrect 46 | :cognitect.anomalies/message "src-path must be a string"}) 47 | (catch Exception e 48 | {:cognitect.anomalies/category :cognitect.anomalies/fault 49 | :cognitect.anomalies/message "Uncaught exception" 50 | ::exeption-map (Throwable->map e)}))) 51 | 52 | (comment 53 | (compile-fennel-file {:src-path "init.fnl"})) 54 | 55 | (defn display-help! [] 56 | (let [file-name (fs/relativize (fs/cwd) *file*)] 57 | (println (str "Usage: " file-name " [FLAG]")) 58 | (println) 59 | (println "Compile changed Fennel files into Lua, intended for use with Neovim configuration.") 60 | (println) 61 | (println "To run whenever you change a file you can combine this tool with entr https://eradman.com/entrproject/") 62 | (println) 63 | (println (str "Example: " file-name " --files | entr " file-name " --compile")) 64 | (println) 65 | (println (cli/format-opts {:spec cli-opts})))) 66 | 67 | (comment 68 | (display-help!)) 69 | 70 | (defn notify! [summary msg] 71 | (proc/shell "notify-send" summary msg)) 72 | 73 | (defn last-modified-time-ms-safe 74 | "Will ALWAYS return a number. If there's an error it'll return a negative number." 75 | [path] 76 | (try 77 | (.toMillis (fs/last-modified-time path)) 78 | (catch Exception _e 79 | -1))) 80 | 81 | (defn fnl->lua-path [fnl-path] 82 | (str/replace (str (fs/strip-ext fnl-path) ".lua") #"^fnl/" "lua/")) 83 | 84 | (defn compile-changed-fennel-files! 85 | "Find changed Fennel files that are newer than their Lua counterpart and compile them." 86 | [{:keys [root prune dry notify] :or {root "."}}] 87 | (let [glob #(map str (fs/glob root %)) 88 | fnl-paths (glob "**.fnl") 89 | lua-paths (glob "**.lua")] 90 | 91 | (when prune 92 | (run! 93 | (fn [lua-path] 94 | (println "[delete]" lua-path) 95 | (when (not dry) 96 | (fs/delete lua-path))) 97 | (set/difference 98 | (set lua-paths) 99 | (set (map fnl->lua-path fnl-paths)) 100 | (set (mapcat glob prune))))) 101 | 102 | (run! 103 | (fn [fnl-path] 104 | (let [lua-path (fnl->lua-path fnl-path)] 105 | (when (> (last-modified-time-ms-safe fnl-path) 106 | (last-modified-time-ms-safe lua-path)) 107 | (println "[compile]" fnl-path) 108 | (let [lua (compile-fennel-file {:src-path fnl-path})] 109 | (if-let [category (:cognitect.anomalies/category lua)] 110 | (binding [*out* *err*] 111 | (if (= :cognitect.anomalies/fault category) 112 | (do 113 | (println (::err lua)) 114 | (when notify 115 | (notify! (str fnl-path " compilation failed") (::err lua)))) 116 | (println "[error]" lua))) 117 | (when (not dry) 118 | (spit lua-path (str "-- [nfnl] " fnl-path "\n" lua)))))))) 119 | fnl-paths))) 120 | 121 | (defn print-fennel-file-paths! [{:keys [root] :or {root "."}}] 122 | (run! 123 | println 124 | (map str (fs/glob root "**.fnl")))) 125 | 126 | (defn main 127 | "Parse the input args and work out what to do." 128 | [args] 129 | (let [opts (update (cli/parse-opts args cli-opts) :prune 130 | (fn [prune] 131 | (cond 132 | (string? prune) (str/split prune #",") 133 | (true? prune) [] 134 | (false? prune) nil 135 | :else prune)))] 136 | (cond 137 | (:help opts) (display-help!) 138 | (:files opts) (print-fennel-file-paths! opts) 139 | (:compile opts) (compile-changed-fennel-files! opts) 140 | :else (display-help!)))) 141 | 142 | (comment 143 | ;; Intended to be invoked through your editor REPL during development. 144 | (main ["--help"])) 145 | 146 | ;; Default entrypoint of the script. 147 | (main *command-line-args*) 148 | -------------------------------------------------------------------------------- /script/fetch-fennel: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | FNL_VERSION=1.5.3 4 | FNL_NAME=fennel-$FNL_VERSION 5 | 6 | # Executable binary 7 | curl https://fennel-lang.org/downloads/$FNL_NAME >script/fennel.lua 8 | chmod +x script/fennel.lua 9 | 10 | # Compiler / library 11 | curl https://fennel-lang.org/downloads/$FNL_NAME.lua >lua/nfnl/fennel.lua 12 | -------------------------------------------------------------------------------- /script/render-api-documentation: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xe 4 | 5 | cd fnl 6 | ../script/fenneldoc.lua --out-dir ../docs/api $(find nfnl -type f -name '*.fnl') 7 | cd - 8 | -------------------------------------------------------------------------------- /script/setup-test-deps: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACK_DIR=./.test-config/nvim/pack/nfnl-tests/start 4 | 5 | mkdir -p "$PACK_DIR" 6 | git clone https://github.com/nvim-lua/plenary.nvim.git "$PACK_DIR/plenary.nvim" 7 | git clone https://github.com/bakpakin/fennel.vim.git "$PACK_DIR/fennel.vim" 8 | ln -sf "${GITHUB_WORKSPACE:-$(pwd)}" "$PACK_DIR/nfnl" 9 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | XDG_CONFIG_HOME=$(pwd)/.test-config 4 | export XDG_CONFIG_HOME 5 | 6 | XDG_CONFIG_STATE=$(pwd)/.test-config/state 7 | export XDG_CONFIG_STATE 8 | 9 | nvim --headless -c 'edit .nfnl.fnl' -c trust -c qa 10 | nvim --headless -c 'PlenaryBustedDirectory lua/spec' 11 | --------------------------------------------------------------------------------