Repository: nvim-neorg/neorg Branch: main Commit: 30e723dd578c Files: 122 Total size: 1.0 MB Directory structure: gitextract_dcaeq6cv/ ├── .busted ├── .envrc ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ └── workflows/ │ ├── checks.yml │ ├── gendoc.yml │ ├── integration_tests.yml │ ├── luarocks.yml │ ├── semver.yml │ ├── sponsors.yml │ ├── stylua.yml │ ├── tests.yml │ └── version_in_code.yml ├── .gitignore ├── .luacheckrc ├── .styluaignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── ROADMAP.md ├── doc/ │ ├── cheatsheet.norg │ ├── neorg.norg │ └── tags ├── docgen/ │ ├── docgen.lua │ ├── fileio.lua │ ├── init.lua │ └── minimal_init.vim ├── docs/ │ ├── CONTRIBUTING.md │ └── README.md ├── flake.nix ├── lua/ │ └── neorg/ │ ├── core/ │ │ ├── callbacks.lua │ │ ├── config.lua │ │ ├── init.lua │ │ ├── log.lua │ │ ├── modules.lua │ │ └── utils.lua │ ├── external/ │ │ └── log.lua │ ├── health.lua │ ├── init.lua │ ├── modules/ │ │ └── core/ │ │ ├── autocommands/ │ │ │ └── module.lua │ │ ├── clipboard/ │ │ │ ├── code-blocks/ │ │ │ │ └── module.lua │ │ │ └── module.lua │ │ ├── completion/ │ │ │ └── module.lua │ │ ├── concealer/ │ │ │ ├── module.lua │ │ │ ├── preset_basic/ │ │ │ │ └── module.lua │ │ │ ├── preset_diamond/ │ │ │ │ └── module.lua │ │ │ └── preset_varied/ │ │ │ └── module.lua │ │ ├── defaults/ │ │ │ └── module.lua │ │ ├── dirman/ │ │ │ ├── module.lua │ │ │ ├── tests.lua │ │ │ └── utils/ │ │ │ └── module.lua │ │ ├── esupports/ │ │ │ ├── hop/ │ │ │ │ └── module.lua │ │ │ ├── indent/ │ │ │ │ └── module.lua │ │ │ └── metagen/ │ │ │ └── module.lua │ │ ├── export/ │ │ │ ├── html/ │ │ │ │ └── module.lua │ │ │ ├── markdown/ │ │ │ │ └── module.lua │ │ │ └── module.lua │ │ ├── fs/ │ │ │ └── module.lua │ │ ├── highlights/ │ │ │ └── module.lua │ │ ├── integrations/ │ │ │ ├── coq_nvim/ │ │ │ │ └── module.lua │ │ │ ├── image/ │ │ │ │ └── module.lua │ │ │ ├── nvim-cmp/ │ │ │ │ └── module.lua │ │ │ ├── nvim-compe/ │ │ │ │ └── module.lua │ │ │ ├── otter/ │ │ │ │ └── module.lua │ │ │ ├── treesitter/ │ │ │ │ └── module.lua │ │ │ ├── truezen/ │ │ │ │ └── module.lua │ │ │ └── zen_mode/ │ │ │ └── module.lua │ │ ├── itero/ │ │ │ └── module.lua │ │ ├── journal/ │ │ │ └── module.lua │ │ ├── keybinds/ │ │ │ └── module.lua │ │ ├── latex/ │ │ │ └── renderer/ │ │ │ └── module.lua │ │ ├── links/ │ │ │ └── module.lua │ │ ├── looking-glass/ │ │ │ └── module.lua │ │ ├── neorgcmd/ │ │ │ ├── commands/ │ │ │ │ └── return/ │ │ │ │ └── module.lua │ │ │ └── module.lua │ │ ├── pivot/ │ │ │ └── module.lua │ │ ├── presenter/ │ │ │ └── module.lua │ │ ├── promo/ │ │ │ └── module.lua │ │ ├── qol/ │ │ │ ├── toc/ │ │ │ │ └── module.lua │ │ │ └── todo_items/ │ │ │ └── module.lua │ │ ├── queries/ │ │ │ └── native/ │ │ │ └── module.lua │ │ ├── scanner/ │ │ │ └── module.lua │ │ ├── storage/ │ │ │ └── module.lua │ │ ├── summary/ │ │ │ └── module.lua │ │ ├── syntax/ │ │ │ └── module.lua │ │ ├── tangle/ │ │ │ └── module.lua │ │ ├── tempus/ │ │ │ └── module.lua │ │ ├── text-objects/ │ │ │ └── module.lua │ │ ├── todo-introspector/ │ │ │ └── module.lua │ │ ├── ui/ │ │ │ ├── calendar/ │ │ │ │ ├── module.lua │ │ │ │ └── views/ │ │ │ │ └── monthly/ │ │ │ │ └── module.lua │ │ │ ├── module.lua │ │ │ ├── selection_popup/ │ │ │ │ └── module.lua │ │ │ └── text_popup/ │ │ │ └── module.lua │ │ └── upgrade/ │ │ └── module.lua │ └── tests/ │ └── init.lua ├── neorg-scm-1.rockspec ├── nix/ │ ├── checks/ │ │ └── default.nix │ ├── overlays/ │ │ ├── default.nix │ │ ├── installed-dependencies.nix │ │ ├── lib.nix │ │ └── luarc.nix │ ├── packages/ │ │ ├── default.nix │ │ └── integration-test.nix │ └── shells/ │ └── default.nix ├── queries/ │ ├── norg/ │ │ ├── folds.scm │ │ ├── highlights.scm │ │ └── injections.scm │ └── norg_meta/ │ ├── highlights.scm │ └── indents.scm ├── res/ │ ├── deps.json │ └── wiki/ │ └── static/ │ ├── Cookbook.md │ ├── Dependencies.md │ ├── Kickstart.md │ ├── Philosophy.md │ ├── Setup-Guide.md │ └── Tutorial.md └── stylua.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .busted ================================================ return { _all = { coverage = false, lpath = "lua/?.lua;lua/?/init.lua", pattern = "tests%.lua$", lua = "nlua", ROOT = { "lua/" }, }, default = { verbose = true, }, tests = { verbose = true, }, } ================================================ FILE: .envrc ================================================ use flake . -Lv ================================================ FILE: .github/CODEOWNERS ================================================ # global rule * @vhyrro @benlubas ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms open_collective: 'neorg' # Replace with a single Open Collective username github: [ 'vhyrro', 'danymat' ] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: [ 'https://buymeacoffee.com/vhyrro' ] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug Report description: Report a problem in Neorg labels: [bug] body: - type: checkboxes id: faq-prerequisite attributes: label: Prerequisites options: - label: I am using the latest stable release of Neovim required: true - label: I am using the latest version of the plugin required: true - type: input attributes: label: "Neovim Version" description: "`nvim --version`:" validations: required: true - type: textarea attributes: label: "Neorg setup" description: | - Copy your entire "require("neorg").setup" function validations: required: true - type: textarea attributes: label: "Actual behavior" description: "A description of actual behavior. Extra points if it includes images or videos." validations: required: true - type: textarea attributes: label: "Expected behavior" description: "A description of the behavior you expected." validations: required: true - type: textarea attributes: label: "Steps to reproduce" description: "Please describe how we can reproduce the issue." validations: required: true - type: textarea attributes: label: "Potentially conflicting plugins" description: "Other plugins you are using which you think could potentially be conflicting with neorg." - type: textarea attributes: label: "Other information" description: "Other information that could be helpful with debugging." - type: dropdown id: help attributes: label: "Help" description: "Would you be able to resolve this issue by submitting a pull request?" options: - "Yes" - "Yes, but I don't know how to start. I would need guidance (check question below)" - "No" - type: textarea attributes: label: "Implementation help" description: "If you selected yes in the last question please specify what you would need help with in order to resolve the issue." ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - name: Ask a question url: https://github.com/vhyrro/neorg/discussions about: If you need help with configuration or something else ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature request description: Request a feature for Neorg labels: [feature] body: - type: checkboxes id: issue-prerequisite attributes: label: Issues options: - label: I have checked [existing issues](https://github.com/vhyrro/neorg/issues?q=is%3Aissue) and there are no existing ones with the same request. required: true - type: textarea attributes: label: "Feature description" validations: required: true - type: dropdown id: help attributes: label: "Help" description: "Would you be able to implement this by submitting a pull request?" options: - "Yes" - "Yes, but I don't know how to start. I would need guidance" - "No" validations: required: true - type: textarea attributes: label: "Implementation help" description: "If you selected yes in the last question please specify in detail what you would need help with in order to implement this." validations: required: false ================================================ FILE: .github/workflows/checks.yml ================================================ name: "Codebase Checks" # This workflow runs various checks (type checking, code checks) # to ensure that Neorg is not breaking. on: push: pull_request: workflow_dispatch: jobs: integration-test: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: DeterminateSystems/nix-installer-action@v17 - name: Run Checks run: nix flake check -L ================================================ FILE: .github/workflows/gendoc.yml ================================================ name: Automatically Generate Documentation on: push: branches: - main jobs: generate-documentation: strategy: fail-fast: false name: Generate Markdown Docs runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Install Plenary uses: actions/checkout@v4 with: repository: nvim-lua/plenary.nvim path: plenary.nvim - name: Install Treesitter uses: actions/checkout@v4 with: repository: nvim-treesitter/nvim-treesitter path: nvim-treesitter ref: v0.9.3 - name: Install Neovim uses: rhysd/action-setup-vim@v1 id: neovim with: neovim: true version: v0.10.4 - uses: luarocks/gh-actions-lua@v10 with: luaVersion: "luajit-2.1.0-beta3" - uses: luarocks/gh-actions-luarocks@v5 with: luarocksVersion: "3.12.0" - name: Install all required modules run: | luarocks config lua_version 5.1 luarocks install lua-utils.nvim 1.0.2 luarocks install nui.nvim 0.3.0 luarocks install nvim-nio 1.10.1 luarocks install pathlib.nvim 2.2.3 luarocks install plenary.nvim 0.1.4 - name: Clone Wiki uses: actions/checkout@v4 with: repository: "nvim-neorg/neorg.wiki" path: wiki/ - name: Run Documentation Generator run: | if ls wiki/*.md 1> /dev/null 2>&1; then rm wiki/*.md fi make documentation - name: Commit Wiki Changes run: | cd wiki/ git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" if [[ ! -z $(git status -s) ]]; then git add . git commit -m "chore: autoupdate github wiki" fi - name: Push Wiki to Github uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} repository: "nvim-neorg/neorg.wiki" branch: master force: true directory: ./wiki ================================================ FILE: .github/workflows/integration_tests.yml ================================================ name: "Nix-based Integration Test" # This workflow attempts to install Neorg with a kickstart config on all major OSes. # Uses Nix for reproducibility. on: push: pull_request: workflow_dispatch: jobs: integration-test: runs-on: ${{ matrix.os.host }} strategy: matrix: os: - host: ubuntu-latest # - host: windows-2019 - host: macos-26-intel # x86_64 - host: macos-latest # aarch64 steps: - uses: actions/checkout@v4 - uses: DeterminateSystems/nix-installer-action@v17 - name: Run Checks run: nix run .#integration-test ================================================ FILE: .github/workflows/luarocks.yml ================================================ name: Push to Luarocks on: push: branches: - main jobs: luarocks-upload: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Required to count the commits - name: Get Version run: echo "LUAROCKS_VERSION=$(git describe --abbrev=0 --tags)" >> $GITHUB_ENV - name: LuaRocks Upload uses: nvim-neorocks/luarocks-tag-release@v7 env: LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} with: version: ${{ env.LUAROCKS_VERSION }} test_interpreters: "" dependencies: | nvim-nio ~> 1.7 lua-utils.nvim == 1.0.2 plenary.nvim == 0.1.4 nui.nvim == 0.3.0 pathlib.nvim ~> 2.2 nvim-treesitter-legacy-api == 0.9.2 ================================================ FILE: .github/workflows/semver.yml ================================================ name: Release Please Automatic Semver on: push: branches: - main jobs: release: name: release runs-on: ubuntu-24.04 steps: - uses: googleapis/release-please-action@v4 with: release-type: simple package-name: neorg ================================================ FILE: .github/workflows/sponsors.yml ================================================ name: Generate Sponsors README on: workflow_dispatch: schedule: - cron: 30 15 * * 0-6 jobs: deploy: runs-on: ubuntu-24.04 steps: - name: Checkout 🛎️ uses: actions/checkout@v4 - name: Generate Sponsors 💖 uses: JamesIves/github-sponsors-readme-action@v1 with: token: ${{ secrets.GH_SPONSORS_TOKEN }} file: 'README.md' template: '{{{ login }}}   ' - name: Commit to repository uses: stefanzweifel/git-auto-commit-action@v6 with: commit_message: "docs(README): update sponsors list" ================================================ FILE: .github/workflows/stylua.yml ================================================ name: Formatting on: push: branches: [ "main" ] paths-ignore: - ".github/**" - "**.md" - "**.norg" jobs: format-with-stylua: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Cache cargo modules id: cache-cargo uses: actions/cache@v4 env: cache-name: cache-node-modules with: path: ~/.cargo key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.toml') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- - name: Install cargo run: curl https://sh.rustup.rs -sSf | sh -s -- -y - name: Install stylua run: cargo install stylua --features lua52 - name: Run formatting run: stylua -v --verify . - uses: stefanzweifel/git-auto-commit-action@v6 with: commit_message: "chore: autoformat with stylua" branch: ${{ github.ref }} - name: Push changes uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: ${{ github.ref }} ================================================ FILE: .github/workflows/tests.yml ================================================ name: Run tests on: pull_request: ~ push: branches: - main jobs: build: name: Run tests runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Run tests uses: nvim-neorocks/nvim-busted-action@v1 with: nvim_version: stable ================================================ FILE: .github/workflows/version_in_code.yml ================================================ name: Update Version String in Neorg Code on: workflow_run: workflows: ["Release Please Automatic Semver"] branches: [main] types: - completed jobs: release: name: release runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Update Version String run: | latest_tag=$(git describe --abbrev=0 --tags) echo "Updating Neorg Version to $latest_tag" sed -ri "s/\sversion\s*=\s*\"[0-9]+\.[0-9]+\.[0-9]+\",$/ version = \"${latest_tag:1}\",/" lua/neorg/core/config.lua - name: Commit Changes run: | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" if [[ ! -z $(git status -s) ]]; then git add . git commit -m "chore(config.lua): update version variable" fi - name: Push to Github uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} repository: "nvim-neorg/neorg" branch: main ================================================ FILE: .gitignore ================================================ /wiki/ /luarocks /lua_modules /.luarocks /.luarc.json test.norg # direnv .direnv # pre-commit .pre-commit-config.yaml ================================================ FILE: .luacheckrc ================================================ -- Global objects globals = { "_", "vim", "neorg", "log", "_neorgcmd_generate_completions", "_neorg_indent_expr", "_neorg_module_autocommand_triggered" } std = "max+busted" ignore = { "631", -- max_line_length } -- Don't report unused self arguments of methods. self = false ================================================ FILE: .styluaignore ================================================ ldoc/ ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [9.4.0](https://github.com/nvim-neorg/neorg/compare/v9.3.0...v9.4.0) (2025-12-03) ### Features * export ranged table tag correctly in core.export.markdown ([#1697](https://github.com/nvim-neorg/neorg/issues/1697)) ([233fe1d](https://github.com/nvim-neorg/neorg/commit/233fe1d85754c6cf59245257354afc9a2429911e)) * foldable lists and ranged verbatim ([#1690](https://github.com/nvim-neorg/neorg/issues/1690)) ([431de0a](https://github.com/nvim-neorg/neorg/commit/431de0a233ce5ab86da4b4e03786d6fe568daf62)) * **todo_items:** configurable parent update behavior ([91db472](https://github.com/nvim-neorg/neorg/commit/91db472c7f65a1a8f7fdc461e0fb040f565a95c4)) * winfixwidth for toc ([1dd99ed](https://github.com/nvim-neorg/neorg/commit/1dd99edd73c48196f7fa9ec763089fb774cf5499)) ### Bug Fixes * **ci:** typo in gendoc workflow ([9d75ae8](https://github.com/nvim-neorg/neorg/commit/9d75ae8e2625db240636870dd7f029c9a82ab9ef)) * **ci:** update actions version ([7cac123](https://github.com/nvim-neorg/neorg/commit/7cac12395b06f549b434e9f829ff6389da598e0c)) * **ci:** update Ubuntu runner version ([f720f55](https://github.com/nvim-neorg/neorg/commit/f720f55ba2057956f585f3c19e5dedd639982d53)) * **ci:** use `macos-13` to run integration tests on Intel macOS, `macos-11` has been deprecated a long time ago ([a849204](https://github.com/nvim-neorg/neorg/commit/a8492049c2cfb9fcabe511eb2a883a602777acb0)) * **clipboard:** yank to use correct regtype instead of always 'line-wise' ([de0bf99](https://github.com/nvim-neorg/neorg/commit/de0bf99a95f4053862f0e63c8083b5e14f3ad6ec)) * compatibility with nvim-treesitter main branch ([87242d4](https://github.com/nvim-neorg/neorg/commit/87242d458d7d0bb50532b791892fb1251a4e7293)) * **esupports.indent:** keep indentation of verbatim blocks without an injected TS parser ([#1685](https://github.com/nvim-neorg/neorg/issues/1685)) ([aa35325](https://github.com/nvim-neorg/neorg/commit/aa353253aeacf01464a8214373b64ec500571a45)) * **journal:** journal index generation ([#1680](https://github.com/nvim-neorg/neorg/issues/1680)) ([35da593](https://github.com/nvim-neorg/neorg/commit/35da593c55d78086a3203ee3e6d749fafe2e4e73)) * load buffer before parsing with TS ([#1689](https://github.com/nvim-neorg/neorg/issues/1689)) ([8fdd9b2](https://github.com/nvim-neorg/neorg/commit/8fdd9b2986acfb4ce310bebfc338111793862f00)) * replace deprecated `vim.tbl_islist` with `vim.islist` ([#1723](https://github.com/nvim-neorg/neorg/issues/1723)) ([1ebab96](https://github.com/nvim-neorg/neorg/commit/1ebab969b45520b680516ffffced1b7719355ed8)) * **rockspec:** add `nvim-treesitter-legacy-api` dependency ([208f7ca](https://github.com/nvim-neorg/neorg/commit/208f7ca7f6a9fa97fa049ae17cc1046ae509811f)) * **todo_items:** update parent node when child state is changed ([4b8e64a](https://github.com/nvim-neorg/neorg/commit/4b8e64a1dd919c934a200f28873eb5be84c5d9fa)) * **todo-instrospector:** use combine hl_mode ([#1733](https://github.com/nvim-neorg/neorg/issues/1733)) ([c429af2](https://github.com/nvim-neorg/neorg/commit/c429af26352fc5194b24a4aa88fc5ecc406e23f9)) * ts module bug ([1f40253](https://github.com/nvim-neorg/neorg/commit/1f40253cda954f71a3c2a8f5a6f428a950e36cc8)) ## [9.3.0](https://github.com/nvim-neorg/neorg/compare/v9.2.0...v9.3.0) (2025-03-29) ### Features * **todo_items:** todo-changed event ([#1651](https://github.com/nvim-neorg/neorg/issues/1651)) ([5f0195d](https://github.com/nvim-neorg/neorg/commit/5f0195d99e21fc1db3d544f7473b34bf41740c16)) ### Bug Fixes * **docgen:** load buffers after opening them ([#1657](https://github.com/nvim-neorg/neorg/issues/1657)) ([18d1a18](https://github.com/nvim-neorg/neorg/commit/18d1a182720fba71ea286d9ce6c3e9970bf7ed51)) * expand tangle paths ([#1622](https://github.com/nvim-neorg/neorg/issues/1622)) ([10bf607](https://github.com/nvim-neorg/neorg/commit/10bf607f11ed94151fb5496e6127d8823d162a7e)) * iter_matches returns list of nodes now ([#1674](https://github.com/nvim-neorg/neorg/issues/1674)) ([a48166d](https://github.com/nvim-neorg/neorg/commit/a48166d3f5729d424ad39254351288944994f097)) * pin to 0.10.4 for doc gen ([#1676](https://github.com/nvim-neorg/neorg/issues/1676)) ([79ffd34](https://github.com/nvim-neorg/neorg/commit/79ffd346ca19af49197d9c1b45d0b902c32c3e14)) * Tangle should close fd for files ([#1618](https://github.com/nvim-neorg/neorg/issues/1618)) ([cd3056e](https://github.com/nvim-neorg/neorg/commit/cd3056eaf39a838b0a26438ff203bb3123c94aec)) * **tangle:** create missing dirs ([#1644](https://github.com/nvim-neorg/neorg/issues/1644)) ([e1b4bb4](https://github.com/nvim-neorg/neorg/commit/e1b4bb4ff144a953f802f665afdd9a7f7532014f)) * **tangle:** proper error handling for fs_close ([#1647](https://github.com/nvim-neorg/neorg/issues/1647)) ([1a4c20a](https://github.com/nvim-neorg/neorg/commit/1a4c20a5ed3dea4abae20e85d66b5f0f94c96f5b)) * tree-sitter-norg parser builds on MacOS ([#1587](https://github.com/nvim-neorg/neorg/issues/1587)) ([71cd029](https://github.com/nvim-neorg/neorg/commit/71cd0291a80be9b5258f5ec16fd6846d60623c29)) ## [9.2.0](https://github.com/nvim-neorg/neorg/compare/v9.1.1...v9.2.0) (2025-01-22) ### Features * automatically typed `module.required` dependencies ([#1537](https://github.com/nvim-neorg/neorg/issues/1537)) ([1985f2d](https://github.com/nvim-neorg/neorg/commit/1985f2d6f152622b0066f48ba8e39d157635dd38)) * **dirman:** dynamically set default workspace ([#1623](https://github.com/nvim-neorg/neorg/issues/1623)) ([29993a7](https://github.com/nvim-neorg/neorg/commit/29993a7bb8279ffa0ba473a3f393daa28c645825)) * **dirman:** in_workspace function ([#1615](https://github.com/nvim-neorg/neorg/issues/1615)) ([62671a7](https://github.com/nvim-neorg/neorg/commit/62671a7b03a1f38a6e5e03b006a9b6f8d804be0e)) * **esupports.hop:** Add tab drop as option for open_mode ([#1580](https://github.com/nvim-neorg/neorg/issues/1580)) ([c7ada78](https://github.com/nvim-neorg/neorg/commit/c7ada7881d7076a235b6323edcd81ae260affb41)) * **export:** copy to clipboard ([#1627](https://github.com/nvim-neorg/neorg/issues/1627)) ([1783928](https://github.com/nvim-neorg/neorg/commit/178392822c8c2ef0911458b7f43c980667784011)) * **introspector:** implement introspector customizability and improvements ([#1539](https://github.com/nvim-neorg/neorg/issues/1539)) ([fd11950](https://github.com/nvim-neorg/neorg/commit/fd11950048d111b837b9f615c5d023e79bd1af9a)) * **keybinds:** add back keybind "gO" for ToC ([#1633](https://github.com/nvim-neorg/neorg/issues/1633)) ([bed58f8](https://github.com/nvim-neorg/neorg/commit/bed58f884ecf9b0b5e855cebe26760bfadbc8f38)) * make journal methods public ([8b59db7](https://github.com/nvim-neorg/neorg/commit/8b59db79307037032c5f83fc79dff5741d6da869)) * support ranges in Neorg command, ([c04bd96](https://github.com/nvim-neorg/neorg/commit/c04bd96eeb3bed19f23d394bebb6193bcd5271da)) ### Bug Fixes * coq_nvim completion integration ([#1597](https://github.com/nvim-neorg/neorg/issues/1597)) ([488507b](https://github.com/nvim-neorg/neorg/commit/488507bb996f75ee29073f50dec32fa220867ca5)) * don't suggest leading chars for file completions ([ba35900](https://github.com/nvim-neorg/neorg/commit/ba35900b21921c439e676b063a79c8fad914eac9)) * intuitive default undone icon ([#1578](https://github.com/nvim-neorg/neorg/issues/1578)) ([13d1d54](https://github.com/nvim-neorg/neorg/commit/13d1d546684c83ba464adbf463a8a272c884e1e8)) * Neorg return to most recent file ([e5e797e](https://github.com/nvim-neorg/neorg/commit/e5e797e6eddcb6efb1d2c3fc2612b31ad9a76cef)) * **summary:** unpack summary category list ([#1637](https://github.com/nvim-neorg/neorg/issues/1637)) ([41aa380](https://github.com/nvim-neorg/neorg/commit/41aa3800cf5d30a5f90520c2a31b34727b443219)) * **toc:** only capture first detached modifier ([#1631](https://github.com/nvim-neorg/neorg/issues/1631)) ([399832e](https://github.com/nvim-neorg/neorg/commit/399832e5437de0cea5efb1d5428de03adc42cc79)) * **ToC:** only try to open when still in norg buffer ([#1549](https://github.com/nvim-neorg/neorg/issues/1549)) ([88dbab5](https://github.com/nvim-neorg/neorg/commit/88dbab5325ce07092ab7a38b160bc8e988830524)) * weird tangle file path logic ([#1604](https://github.com/nvim-neorg/neorg/issues/1604)) ([993f077](https://github.com/nvim-neorg/neorg/commit/993f077f0bff8faa68dbdb89ad95f67116b8007a)) ## [9.1.1](https://github.com/nvim-neorg/neorg/compare/v9.1.0...v9.1.1) (2024-07-24) ### Bug Fixes * non-functional toggling of sublists ([#1492](https://github.com/nvim-neorg/neorg/issues/1492)) ([169495c](https://github.com/nvim-neorg/neorg/commit/169495ca3ed67f919589499d20fa89bfea9e4de4)) ## [9.1.0](https://github.com/nvim-neorg/neorg/compare/v9.0.3...v9.1.0) (2024-07-23) ### Features * add `<LocalLeader>cm` keybind for code block magnification ([c52c00f](https://github.com/nvim-neorg/neorg/commit/c52c00f72c85d3ca258de623e40e64c3e552185a)) * **concealer:** properly display blockquotes, allow entities to set their own clear functions ([28bbefc](https://github.com/nvim-neorg/neorg/commit/28bbefcfce10a51d7d834c6b6c0f7229bc6b38e1)) ### Bug Fixes * **`:h neorg`:** remove `|example` blocks until parser is fixed ([a93190d](https://github.com/nvim-neorg/neorg/commit/a93190db5b804c7db7d2f8f4d1c7edf6414cbe39)) * `insert-date` not working in insert mode ([7af14b3](https://github.com/nvim-neorg/neorg/commit/7af14b30b6ce9321e5a399eb6eb491ad111b0cda)) * **concealer:** don't overflow folds into other, non-norg files ([19e4bea](https://github.com/nvim-neorg/neorg/commit/19e4beabdcd6080ececbd5ffcb7ecb50b1590461)) * **concealer:** properly render quotes ([08277bb](https://github.com/nvim-neorg/neorg/commit/08277bb5cf1b310d2527adcb0d613a8625c7b3d3)) * **concealer:** quote and list rendering bugs ([e292cf5](https://github.com/nvim-neorg/neorg/commit/e292cf5f3d19a8066e13928ff00efece10915124)) ## [9.0.3](https://github.com/nvim-neorg/neorg/compare/v9.0.2...v9.0.3) (2024-07-19) ### Bug Fixes * **keybinds:** fixed tempus insert date insert mode default keybind command ([87e55f3](https://github.com/nvim-neorg/neorg/commit/87e55f3727ad84ecc261f7236892f4dbec82efc1)) ## [9.0.2](https://github.com/nvim-neorg/neorg/compare/v9.0.1...v9.0.2) (2024-07-17) ### Bug Fixes * **keybinds:** correctly get keymap opts ([4a2f5b2](https://github.com/nvim-neorg/neorg/commit/4a2f5b2104169883131646f27ffaeb6af324b05a)) ## [9.0.1](https://github.com/nvim-neorg/neorg/compare/v9.0.0...v9.0.1) (2024-07-16) ### Bug Fixes * **metagen:** proper iteration on metadata fields ([#1518](https://github.com/nvim-neorg/neorg/issues/1518)) ([0306887](https://github.com/nvim-neorg/neorg/commit/0306887c0c6ea302e3a3fecbb36998793f4c27d5)) ## [9.0.0](https://github.com/nvim-neorg/neorg/compare/v8.9.0...v9.0.0) (2024-07-16) ### ⚠ BREAKING CHANGES * tangle files relative to norg file ([#1415](https://github.com/nvim-neorg/neorg/issues/1415)) * remove `traverse-link` and `traverse-heading` modes * remove all references to `core.mode` * remove `core.mode` * move rest of modules to new keybind system * move core.pivot to new keybind system * migrate core.promo to new keybind system * move esupports.hop to the new keybind schema * move qol.todo_items to new keybind API * keybind refactor, update core.itero to new changes * make Neovim 0.10 a requirement for Neorg * simplify code in preparation of lazy.nvim luarocks support * remove `ftdetect` file as Neorg is now natively recognized by Neovim ([#1083](https://github.com/nvim-neorg/neorg/issues/1083)) ### Features * add `core.ui.calendar` to the default list of modules ([52a37e0](https://github.com/nvim-neorg/neorg/commit/52a37e01abe059c6431f744eab9f22626fb275c5)) * add descriptions to all Neorg keybinds ([a042515](https://github.com/nvim-neorg/neorg/commit/a042515bc832ecab6d47a18ffc2976ee72f4bc1b)) * add dotrepeat to all common commands ([6b49397](https://github.com/nvim-neorg/neorg/commit/6b49397f1e8bea2f19064012e392cd4b36e00d18)) * auto tangle ([#1413](https://github.com/nvim-neorg/neorg/issues/1413)) ([0f24e4a](https://github.com/nvim-neorg/neorg/commit/0f24e4a53b05328a0ae6496be9867ea5df7b7f40)) * **calendar:** unify exit keybind to always be `q` instead of a mix of `q`/`<Esc>` ([582d7b6](https://github.com/nvim-neorg/neorg/commit/582d7b616d9bdb5e2bbcba4ebd0e82f7fb9130e0)) * **completion:** allow custom completion engines ([04d201d](https://github.com/nvim-neorg/neorg/commit/04d201d56857073efecf79a8be29fae45b57ebeb)) * **docgen:** add default keybinds list ([d762f6d](https://github.com/nvim-neorg/neorg/commit/d762f6dd4cfc7f8337272582abf0459b4c85fe3b)) * **docgen:** add more links to sidebar ([5bef42a](https://github.com/nvim-neorg/neorg/commit/5bef42ab385e0d2da9e68a60e4ba484c583b9aa7)) * **health:** check for keybind clashes in checkhealth ([bbe4243](https://github.com/nvim-neorg/neorg/commit/bbe42438a90afd25a5d52b843ebbcc19d8476cef)) * **intergrations.otter:** update to use otter 2.0 ([1347eeb](https://github.com/nvim-neorg/neorg/commit/1347eebc8a0116524f17a7c33240ae782efb974e)) * keybind refactor, update core.itero to new changes ([3dd946a](https://github.com/nvim-neorg/neorg/commit/3dd946ae976ee45147a60eeb5174f0f951f04f94)) * **keybinds:** add `extend_preset` function ([4f09926](https://github.com/nvim-neorg/neorg/commit/4f0992643b42d544a442f6e1928bd5838e355bcd)) * **keybinds:** don't try to forcefully override user keys ([591b883](https://github.com/nvim-neorg/neorg/commit/591b8831587895b95cbce30ad5a30c53f01b882c)) * **keybinds:** split presets into norg and non-norg, move to buffer-local mappings instead ([113c21b](https://github.com/nvim-neorg/neorg/commit/113c21b2de4f68c150a5778ff754cdbbec04758f)) * support multi-line values in metagen ([#1514](https://github.com/nvim-neorg/neorg/issues/1514)) ([321c435](https://github.com/nvim-neorg/neorg/commit/321c435e96a738a32ba2376f7f8f27b401759236)) ### Bug Fixes * better formatting, properly handle complex keybind descriptions ([c087db0](https://github.com/nvim-neorg/neorg/commit/c087db0473b3d8363e31135ef42d1290994075e6)) * calendar not working with the latest breakages ([c659b09](https://github.com/nvim-neorg/neorg/commit/c659b0901bea4143667489ee2af4c78762fabc5c)) * **docgen:** beautify keybind output in <details> tag ([7a9d54c](https://github.com/nvim-neorg/neorg/commit/7a9d54c3c81bb1c403b3591cbc3b0cf27949fc6b)) * **docgen:** better mnemonic rendering ([b3bf963](https://github.com/nvim-neorg/neorg/commit/b3bf9639d2ebc905f7a99197844bf6da0547a2c9)) * **docgen:** broken wiki ([e23d0d3](https://github.com/nvim-neorg/neorg/commit/e23d0d32ea11d13c9da67b841a809b6cfda02887)) * **docgen:** display keybind data in the form of a dropdown ([572de72](https://github.com/nvim-neorg/neorg/commit/572de724159fcf929f3feb125da72b25ccad7bd2)) * **docgen:** invalid github markdown ([75edcdc](https://github.com/nvim-neorg/neorg/commit/75edcdc68ba9ce4aae5f0df6543f9818c55c5206)) * error in hasmapto() ([dfcc78a](https://github.com/nvim-neorg/neorg/commit/dfcc78a110051aaedef8c19b48fda486960e1089)) * feed keys without remaps ([ecf5f41](https://github.com/nvim-neorg/neorg/commit/ecf5f415c96cf7a12b74a8233b8f6d4ecc3779c4)) * improve startup time by removing pcall on every module load ([7d9bd33](https://github.com/nvim-neorg/neorg/commit/7d9bd33a176fa86c65030776eb9b45cdb729250b)) * itero keybind not functioning, add fallback functions to core.keybinds ([cd950aa](https://github.com/nvim-neorg/neorg/commit/cd950aa89ae2125882b235b8e79afde13c90e1b1)) * make core.promo keybinds functional again ([8a48172](https://github.com/nvim-neorg/neorg/commit/8a48172e94854d364b3cb3ecd2940cbe84b2f7bd)) * properly error when failing to load module using `:Neorg module load` ([721fd28](https://github.com/nvim-neorg/neorg/commit/721fd28f39ba2cb0978e410bd9a7668f8c74ccca)) * remove all references to `core.mode` ([53429c4](https://github.com/nvim-neorg/neorg/commit/53429c497bda64671c7161b3f59d4640415bf145)) * set global keybinds only once, set keys on filetype ([e00042a](https://github.com/nvim-neorg/neorg/commit/e00042af322802f4db38706c5eeee8e77145fe65)) * set keybinds for the appropriate buffer (fixes telescope issues) ([b49c214](https://github.com/nvim-neorg/neorg/commit/b49c214f72ba33d5d76a63f7d70da43c840dc1e2)) * tangle files relative to norg file ([#1415](https://github.com/nvim-neorg/neorg/issues/1415)) ([3c3b977](https://github.com/nvim-neorg/neorg/commit/3c3b977dff916aecf5b2d63747896691c70639df)) ### Code Refactoring * make Neovim 0.10 a requirement for Neorg ([c916501](https://github.com/nvim-neorg/neorg/commit/c91650128130f05c79a2cf1e981a8d87b1f91113)) * migrate core.promo to new keybind system ([faad665](https://github.com/nvim-neorg/neorg/commit/faad665a8e9c32f9dceae613e7c4c2abdbda9585)) * move core.pivot to new keybind system ([0c1222b](https://github.com/nvim-neorg/neorg/commit/0c1222b4aa4faf21a76158fe8de1339700442e08)) * move esupports.hop to the new keybind schema ([cdfbe19](https://github.com/nvim-neorg/neorg/commit/cdfbe19125a5d71288ea5c28f7916f709ca57ddd)) * move qol.todo_items to new keybind API ([b71d8ba](https://github.com/nvim-neorg/neorg/commit/b71d8ba34c53a0a4d022cd72af90513029800b27)) * move rest of modules to new keybind system ([94b860b](https://github.com/nvim-neorg/neorg/commit/94b860b704bceb1180eb82443064e6530e001fae)) * remove `core.mode` ([49e8710](https://github.com/nvim-neorg/neorg/commit/49e8710b3c09b19d69fcce322769fcbbdc4e6f30)) * remove `ftdetect` file as Neorg is now natively recognized by Neovim ([#1083](https://github.com/nvim-neorg/neorg/issues/1083)) ([5c32056](https://github.com/nvim-neorg/neorg/commit/5c320566757d334ab255a287da960d961d7a9012)) * remove `traverse-link` and `traverse-heading` modes ([da3e062](https://github.com/nvim-neorg/neorg/commit/da3e0621e03ad33f62cdd2fa77ba02ffb1b52d2b)) * simplify code in preparation of lazy.nvim luarocks support ([12b7cf2](https://github.com/nvim-neorg/neorg/commit/12b7cf253e60f6ce8552e4498a1598c8b57acf66)) ## [8.9.0](https://github.com/nvim-neorg/neorg/compare/v8.8.1...v8.9.0) (2024-07-07) ### Features * **calendar-ui:** new mappings ([2a4081f](https://github.com/nvim-neorg/neorg/commit/2a4081fe89f8f264c672eff2ab88b79f91aa6898)) * **calendar-ui:** support count in keymappings ([6b4751c](https://github.com/nvim-neorg/neorg/commit/6b4751c2c486578c8a11dfd2f79dfd35cacaa5b8)) * **calendar:** even more keybinds + ; repeat ([a5c2624](https://github.com/nvim-neorg/neorg/commit/a5c2624bc41cb760c4689734b2c3c5a9f17e4c48)) ### Bug Fixes * **promo:** promote/demote range + keybinds ([a94d1e6](https://github.com/nvim-neorg/neorg/commit/a94d1e67f7df3a97c0f57894c107b70a54523a4d)) * **return:** return causing early exit when paired with auto_toc.exit_nvim ([959b8a2](https://github.com/nvim-neorg/neorg/commit/959b8a20114c63097261cd1b48f2dacfbe4ccd5f)) * **summary:** ordering and indentation of nested entries ([9279672](https://github.com/nvim-neorg/neorg/commit/9279672d2b97929fc52d1b182af754497dfd8d8a)) * **text-objects:** node selection inconsistencies ([99b3249](https://github.com/nvim-neorg/neorg/commit/99b32492b836b64a5ff4544d4c466496f0aec9bd)) * **toc:** various fixes ([#1500](https://github.com/nvim-neorg/neorg/issues/1500)) ([83637f1](https://github.com/nvim-neorg/neorg/commit/83637f11295aaaa6db5b0a28d6db8ac727592759)) ## [8.8.1](https://github.com/nvim-neorg/neorg/compare/v8.8.0...v8.8.1) (2024-06-26) ### Bug Fixes * remove `tree-sitter-norg` from the luarocks dependency list momentarily (delegate work to nvim-treesitter for the moment) ([4103d43](https://github.com/nvim-neorg/neorg/commit/4103d43898f0a612f1b702c5a6d2ef1e7fb76058)) ## [8.8.0](https://github.com/nvim-neorg/neorg/compare/v8.7.1...v8.8.0) (2024-06-25) ### Features * **latex-renderer:** add toggle renderer command ([a00628f](https://github.com/nvim-neorg/neorg/commit/a00628f54f895774dde09e4d7a3c99eb8340cbb6)) * **toc:** auto open/enter ToC ([988c2c1](https://github.com/nvim-neorg/neorg/commit/988c2c197c9f431d338519a3b81825cc5690b4e4)) * **toc:** close toc when neorg win is closed ([78e1155](https://github.com/nvim-neorg/neorg/commit/78e1155b8c3cc3f63e98817b7eea85f84ca7f6af)) * **toc:** configurable max width ([b0544ed](https://github.com/nvim-neorg/neorg/commit/b0544ed42c3aa28ceb8edf54a3a71c777de91e5d)) * **toc:** exit nvim when toc is the last window ([baf9934](https://github.com/nvim-neorg/neorg/commit/baf9934832866d28762b59c7e52c82854366d7dd)) ### Bug Fixes * broken true-zen integration ([#1477](https://github.com/nvim-neorg/neorg/issues/1477)) ([ace1cda](https://github.com/nvim-neorg/neorg/commit/ace1cdae9d0a95083bf9bf8cfb5e70dbb38d6351)) * don't update metadata unless buffer is modified ([#1469](https://github.com/nvim-neorg/neorg/issues/1469)) ([286d94b](https://github.com/nvim-neorg/neorg/commit/286d94bf6e30be4a5fc7ce89aa860538d39189a0)) * **hop:** describe which links are currently unsupported instead of "not found" ([9626def](https://github.com/nvim-neorg/neorg/commit/9626def65687a052b0e2c390e9ee9ee599344415)) * insert template at the end of a new journal entry ([#1468](https://github.com/nvim-neorg/neorg/issues/1468)) ([f33a491](https://github.com/nvim-neorg/neorg/commit/f33a4919b3aa2744209d8106886879e5522a38a4)) * **latex-renderer:** fixed renderer breaking when a snippet change made it stop compiling ([7fc91bb](https://github.com/nvim-neorg/neorg/commit/7fc91bbc7c316e0ad33132fb61325adc71d4f260)) * **toc:** don't try to open a toc from a toc ([6cdd6a1](https://github.com/nvim-neorg/neorg/commit/6cdd6a121c113d15c2aa55d79b6ec9915cc47284)) * **toc:** open new win from toc when neorg win was closed ([c2d58da](https://github.com/nvim-neorg/neorg/commit/c2d58da7c4800e9b8cb5b5ed1a6f38cde1a176d7)) * **todo-introspector:** check that the buffer is valid ([369ed28](https://github.com/nvim-neorg/neorg/commit/369ed28b0b0c3f221b46816ad53a509d73c7e7ed)) ## [8.7.1](https://github.com/nvim-neorg/neorg/compare/v8.7.0...v8.7.1) (2024-06-12) ### Bug Fixes * **latex-renderer:** fixed renderer failing silently ([4b22a4e](https://github.com/nvim-neorg/neorg/commit/4b22a4ea798efeeb756a6df52baa369715832a5a)) ## [8.7.0](https://github.com/nvim-neorg/neorg/compare/v8.6.0...v8.7.0) (2024-06-11) ### Features * anchor completion ([9917f99](https://github.com/nvim-neorg/neorg/commit/9917f993c505c3ab7d122e253a3af7cf13820fd0)) * **completion:** additional detached modifier extensions ([6b02bf1](https://github.com/nvim-neorg/neorg/commit/6b02bf156d4cf1ded9b2bb93cb4669a00e6a1e7f)) * link name completions ([8ec38e0](https://github.com/nvim-neorg/neorg/commit/8ec38e07ddffa84d0925faf425d4d52e5c1f91b7)) * **treesitter:** add get_ts_parser ([5221820](https://github.com/nvim-neorg/neorg/commit/5221820166a9cfac67451581ea79a9e2e7680215)) * **treesitter:** get_document_root accepts more sources ([4ebb7c7](https://github.com/nvim-neorg/neorg/commit/4ebb7c7bd62d12b77f0a8a0ec27e2e64ef204d65)) ### Bug Fixes * **concealer:** invalidate extmarks when range is deleted ([51be069](https://github.com/nvim-neorg/neorg/commit/51be06957fc6cc7140b310abd30be6682041962a)) * foreign link completions with item ([8537710](https://github.com/nvim-neorg/neorg/commit/85377108531d4083c0526623023a35aab2509410)) * supress swap file errors ([4420ddc](https://github.com/nvim-neorg/neorg/commit/4420ddc26ab80b42d4735ec78efea39c7cc7f547)) * thoroughly test for current Neovim version ([352570c](https://github.com/nvim-neorg/neorg/commit/352570cb174c73d50a82376f06b05badb1cda338)) * **todo-items:** error when switching from on-hold item with children ([f03435f](https://github.com/nvim-neorg/neorg/commit/f03435f03b5649598fb1478429d4e985dc5789bb)) ## [8.6.0](https://github.com/nvim-neorg/neorg/compare/v8.5.0...v8.6.0) (2024-05-31) ### Features * add `integrations.coq_nvim` as a new completion engine ([b8f9f83](https://github.com/nvim-neorg/neorg/commit/b8f9f834d999a6807ee9476857fb3af2c58f64a2)) * create `deps.json` for a universal list of Neorg dependencies ([a64c6af](https://github.com/nvim-neorg/neorg/commit/a64c6af4ac1ab4aa3a5de93d86111814125e3ed8)) * **latex:** async image rendering ([b1c96a0](https://github.com/nvim-neorg/neorg/commit/b1c96a049da8d534820f7452195fc9d961f3d025)) * **latex:** minimum length to render ([5a9d638](https://github.com/nvim-neorg/neorg/commit/5a9d6381581014c67219a823f149ce871f3af15d)) * modify deps.json to have proper version constraints ([5e9a3ef](https://github.com/nvim-neorg/neorg/commit/5e9a3ef798726fd2001d1596e6134b03f331a333)) * treesitter-based completions ([#1412](https://github.com/nvim-neorg/neorg/issues/1412)) ([79f6a49](https://github.com/nvim-neorg/neorg/commit/79f6a49b869a891bca9ce746f77781af46977e71)) ### Bug Fixes * clear extmarks on disable ([1be7808](https://github.com/nvim-neorg/neorg/commit/1be78080714b6f0cc1d77432629b91328880ce53)) * clear images at cursor after change ([9edd802](https://github.com/nvim-neorg/neorg/commit/9edd802c194ef180587c9c836ea84142927bc887)) * clear package.loaded cache when first installing neorg ([3d50b49](https://github.com/nvim-neorg/neorg/commit/3d50b49e1c1d37182c2ed94e718ecd5eed8cacd4)) * compute image dimensions before rendering ([94abd99](https://github.com/nvim-neorg/neorg/commit/94abd999cbb21d66080ee3970f41303c7093e1a6)) * conceal cursor updates ([2701e07](https://github.com/nvim-neorg/neorg/commit/2701e0770697ca10665277c0afd692567b24103d)) * **dirman:** swapfile error when opening index.norg ([#1451](https://github.com/nvim-neorg/neorg/issues/1451)) ([70d4b89](https://github.com/nvim-neorg/neorg/commit/70d4b899928d72ec9ab7961f69ae47fd24b8c3c4)) * handle switching buffers ([af4001e](https://github.com/nvim-neorg/neorg/commit/af4001ebd8678261e334591be4435f11e1aac294)) * image flashing on text change ([19737b0](https://github.com/nvim-neorg/neorg/commit/19737b02be7aee6c4395439fbd756901adb428a1)) * **latex-renderer:** handle broken latex ([#1438](https://github.com/nvim-neorg/neorg/issues/1438)) ([8140135](https://github.com/nvim-neorg/neorg/commit/81401353dc13ff87f4879b6e0b5f96ff2de14d9c)) * **latex:** predict image size for scale > 1 ([bde2402](https://github.com/nvim-neorg/neorg/commit/bde24023d2b1ae507034c0839144f36e96dc7dc2)) * limages losing track of their extmarks ([f05bce2](https://github.com/nvim-neorg/neorg/commit/f05bce247e53a53a19bfe47c57d6bc8023b5c43b)) * logic when inline = false ([fc8c054](https://github.com/nvim-neorg/neorg/commit/fc8c0542d8676155e8c47d0b735e816a366235b1)) * mark core.links as internal ([22e7151](https://github.com/nvim-neorg/neorg/commit/22e7151f02559cea0320d02b57e59d2fb8294956)) * render images on cursor line on enable ([dc51ff4](https://github.com/nvim-neorg/neorg/commit/dc51ff489a653c292c6bf84751a5d060e8018c6c)) * rendering order for multiple img in one line ([a50c034](https://github.com/nvim-neorg/neorg/commit/a50c03432ba9cd4c370ebedfa6d84db1f0e61b6d)) * **text-objects:** nil check node's parent ([72da6e0](https://github.com/nvim-neorg/neorg/commit/72da6e0773298356f5a83ce52c9efdd88d4147e5)) * use nio.scheduler to avoid double wrap ([2a9c3fa](https://github.com/nvim-neorg/neorg/commit/2a9c3fab1bb6beabc4160264835be7f3b9a579e7)) * various type errors in codebase ([c564e6c](https://github.com/nvim-neorg/neorg/commit/c564e6cd470e1582414b63720ef85f1d0abe1d64)) * virt text disapearing on conceallevel=0 ([acd4293](https://github.com/nvim-neorg/neorg/commit/acd4293ab269c42eb9f3294d182eb87e7a34d66b)) * work on one buffer at a time ([ba41187](https://github.com/nvim-neorg/neorg/commit/ba41187669002622b7f9778e4c49723f47faf69a)) ### Reverts * d8ba19a18c896cc146e7ecbd4d67bdbe03f2e0c4 ([c8e2947](https://github.com/nvim-neorg/neorg/commit/c8e2947bdb71838ce90614cb3d9a0eb530c45fd7)) ## [8.5.0](https://github.com/nvim-neorg/neorg/compare/v8.4.1...v8.5.0) (2024-05-17) ### Features * text-objects and working swap ([#1421](https://github.com/nvim-neorg/neorg/issues/1421)) ([49a3c64](https://github.com/nvim-neorg/neorg/commit/49a3c64c06dae55b5424b218dc7c6e6b643fd4f5)) ### Bug Fixes * **core.todo-introspector:** treesitter highlighter errors ([cb4f25b](https://github.com/nvim-neorg/neorg/commit/cb4f25bca799c49a81e471aeca1d6d95322c87a6)) ## [8.4.1](https://github.com/nvim-neorg/neorg/compare/v8.4.0...v8.4.1) (2024-04-13) ### Bug Fixes * **checkhealth:** add missing parameter to string.format ([#1386](https://github.com/nvim-neorg/neorg/issues/1386)) ([9656757](https://github.com/nvim-neorg/neorg/commit/9656757f7085e9e36d2469b8876f6b27f8e87c7b)) * lack of comma ([65c7550](https://github.com/nvim-neorg/neorg/commit/65c75508626ad02a3cb40a84c4c3df0cde7b0e5a)) * properly check for existence of a given module by trying to load it instead of passively scanning the loaded module list ([727ef2a](https://github.com/nvim-neorg/neorg/commit/727ef2ab4247b52733d1a84d87143257e783d4e9)) ## [8.4.0](https://github.com/nvim-neorg/neorg/compare/v8.3.0...v8.4.0) (2024-04-04) ### Features * add `integrations.otter` for LSP-like behaviours in code blocks ([#1329](https://github.com/nvim-neorg/neorg/issues/1329)) ([ccb7555](https://github.com/nvim-neorg/neorg/commit/ccb75557f8582e044c687452b8b249151f6e7098)) ### Bug Fixes * broken wiki ([68ea6b5](https://github.com/nvim-neorg/neorg/commit/68ea6b53b6cb58c54ac51256cdfd76eec540806c)) * don't load `core.todo-introspector` if not on nightly (oops) ([bc7830e](https://github.com/nvim-neorg/neorg/commit/bc7830ec3e538d381f5361ca80a9bc5f97fc8fa9)) ## [8.3.0](https://github.com/nvim-neorg/neorg/compare/v8.2.1...v8.3.0) (2024-04-04) ### Features * basic checkhealth for configuration checking ([645cd0c](https://github.com/nvim-neorg/neorg/commit/645cd0c257b2fccc655a32d0b04aa706c96fb1a8)) * check for the existence of `vhyrro/luarocks.nvim` ([a5aa931](https://github.com/nvim-neorg/neorg/commit/a5aa93108999de75c8d075a0ee4dcc6a715a9e1a)) * **dirman:** use pathlib for all dirman operations ([#1354](https://github.com/nvim-neorg/neorg/issues/1354)) ([1190dc7](https://github.com/nvim-neorg/neorg/commit/1190dc78b9785ad75301de9153ed8de83c179b66)) * implement the basics of the new todo introspector ([a7ad515](https://github.com/nvim-neorg/neorg/commit/a7ad51519f2e7a7274c069e35d4396a0a5f88ddc)) * **todo-introspector:** correctly enumerate amounts of done/undone items ([d284488](https://github.com/nvim-neorg/neorg/commit/d2844882ec0d18d59a7b8727b893a964fe76a754)) * **todo-introspector:** display all TODOs on file entry ([80b2d33](https://github.com/nvim-neorg/neorg/commit/80b2d33b1f402b0279009442caf6cfaa9cd308f4)) * **todo-introspector:** finalize display code ([949ae96](https://github.com/nvim-neorg/neorg/commit/949ae962558d43673130159cd6bce89e4bf4febc)) * **todo-introspector:** properly handle nodes broken into two ([76e6443](https://github.com/nvim-neorg/neorg/commit/76e6443c9823470897245951f37cef0cc5b966f0)) ### Bug Fixes * add `core.todo-introspector` to the list of default modules ([5202271](https://github.com/nvim-neorg/neorg/commit/52022710d47fd66c73280f7a8d7fe2013d899224)) * **checkhealth:** fix erroneous boolean check for lazy users ([d523688](https://github.com/nvim-neorg/neorg/commit/d523688ecc6ddf5fbac9b42e0b62515c7655bed3)) * **dirman:** norg link with `.` was misinterpreted as the extension ([96fe2c9](https://github.com/nvim-neorg/neorg/commit/96fe2c92d0e8fb2a2a853847909155aae4d5ca46)) * **dirman:** respect force option in dirman create_file ([0782ca4](https://github.com/nvim-neorg/neorg/commit/0782ca4a221cfd965f05752d7178b2692bb64ae0)) * **introspector:** various bugs related to extmark updates ([e8c9193](https://github.com/nvim-neorg/neorg/commit/e8c9193b10ea946e6f90e06daf0efeafb55fa970)) * issue a warning to users when dependencies cannot be found (instead of silently failing) ([04f4127](https://github.com/nvim-neorg/neorg/commit/04f4127a08a59d110c882464b11294dec0cf1258)) * **luarocks:** pin `pathlib` to `2.0` (as luarocks doesn't like it otherwise) ([5b93840](https://github.com/nvim-neorg/neorg/commit/5b93840d97c2ac0d1534321ca3659f4b8c1342e4)) * **pathlib:** bump pathlib version; fix `path:touch` ([bedbbe2](https://github.com/nvim-neorg/neorg/commit/bedbbe208e61491d1d8be0b6775793b246b444d0)) * **todo-introspector:** correctly handle deletion of lines ([a8b7ad0](https://github.com/nvim-neorg/neorg/commit/a8b7ad08f6e8cd36c83e371ab9a74cc1e5252a0b)) * **todo-introspector:** do not error when a line out of bounds is deleted ([62c7925](https://github.com/nvim-neorg/neorg/commit/62c7925e17ab25cc40c50cd266701a36aa854a50)) * **todo-introspector:** properly clear namespace for refreshed buffers ([5f67407](https://github.com/nvim-neorg/neorg/commit/5f6740795303a03b58c81bf6396ae03d487d5b7c)) ### Reverts * backout of commit 5b93840d97c2ac0d1534321ca3659f4b8c1342e4 ([e0690fa](https://github.com/nvim-neorg/neorg/commit/e0690fa65546bd91f9aa4883f1ce4cbea45e1898)) ## [8.2.1](https://github.com/nvim-neorg/neorg/compare/v8.2.0...v8.2.1) (2024-03-28) ### Bug Fixes * **core.neorgcmd.list:** don't accidentally open a separate file when displaying loaded modules ([eef1705](https://github.com/nvim-neorg/neorg/commit/eef1705ee78ae6e66917901da507a666743de877)) * don't allow TODOs with nested items of the same type to be given the pending status ([5942fdf](https://github.com/nvim-neorg/neorg/commit/5942fdf7eb42b6364ca0a754ac88bd0ca05ae660)) * load default modules even if an empty table is provided as input to setup() ([c1d36ad](https://github.com/nvim-neorg/neorg/commit/c1d36add07364e024dbf39276388741e54f7e955)) * **neorgcmd.modules.list:** add basic escape keybinds ([71e2f05](https://github.com/nvim-neorg/neorg/commit/71e2f05ee19c94705fadbc1f84e11990baf8ff1c)) * **typecheck:** use type definitions from nvim runtime instead ([#1358](https://github.com/nvim-neorg/neorg/issues/1358)) ([e7f393f](https://github.com/nvim-neorg/neorg/commit/e7f393f9e4a17c32289875e273f687863336894c)) ## [8.2.0](https://github.com/nvim-neorg/neorg/compare/v8.1.0...v8.2.0) (2024-03-25) ### Features * **metagen:** add author field to provide persistent custom author name ([#1331](https://github.com/nvim-neorg/neorg/issues/1331)) ([e576308](https://github.com/nvim-neorg/neorg/commit/e576308243b58838ed97309bec60bf180cde3c91)) ### Bug Fixes * **ci:** "could not find upvalue `lib`" error ([486a148](https://github.com/nvim-neorg/neorg/commit/486a148d1bf5b7fd14f52a771a0dacc1e6839174)) * **ci:** supply correct version to the lua setup CI ([c814ef6](https://github.com/nvim-neorg/neorg/commit/c814ef68295baffefed7bfb8a48f8835f73a55a6)) * **core/events:** fall back to the current window ID if it cannot be located ([22df349](https://github.com/nvim-neorg/neorg/commit/22df349df39d9401a95f7dc0e3dc13113f91a60a)) * **dirman:** properly escape directories and filenames ([#1232](https://github.com/nvim-neorg/neorg/issues/1232)) ([e1f5556](https://github.com/nvim-neorg/neorg/commit/e1f5556bfbe50cbae262dffc35f376f7469f68cf)) * do not add the line jump of a link to the jump list ([#1325](https://github.com/nvim-neorg/neorg/issues/1325)) ([918f2a3](https://github.com/nvim-neorg/neorg/commit/918f2a39f96e1447c00871eb611bed2018a047b5)) * **export.markdown:** export `authors` metadata field key as `author` ([#1319](https://github.com/nvim-neorg/neorg/issues/1319)) ([f30ce72](https://github.com/nvim-neorg/neorg/commit/f30ce728e1b99e23320114c3bddb18be2776baf7)) * **export.markdown:** fix incorrect reset of ordered list item count ([#1324](https://github.com/nvim-neorg/neorg/issues/1324)) ([ba58c1b](https://github.com/nvim-neorg/neorg/commit/ba58c1b29c9b013928025db345c6ff170e9693bf)) ## [8.1.0](https://github.com/nvim-neorg/neorg/compare/v8.0.1...v8.1.0) (2024-03-24) ### Features * **todo_items:** convert TODO item to "on hold" if all items are done but the rest are on hold ([#1339](https://github.com/nvim-neorg/neorg/issues/1339)) ([c32b238](https://github.com/nvim-neorg/neorg/commit/c32b238438a8f1130c89c13a2284961fe10e3e68)) ### Bug Fixes * remove old and hacky code related to nvim-treesitter's query cache invalidation ([e8d8d1e](https://github.com/nvim-neorg/neorg/commit/e8d8d1e6608e53e366109fc4f9d7ab364ea0fb5c)) ## [8.0.1](https://github.com/nvim-neorg/neorg/compare/v8.0.0...v8.0.1) (2024-03-24) ### Bug Fixes * broken wiki on github ([d4c10fe](https://github.com/nvim-neorg/neorg/commit/d4c10fe58519ce0d827cfc02f87832c75395045a)) * **ci:** try to fix the wiki generator with luarocks ([27ac595](https://github.com/nvim-neorg/neorg/commit/27ac595d90481bd8fa2d13290289d46287346903)) * **docgen:** invalid upvalues ([84ee928](https://github.com/nvim-neorg/neorg/commit/84ee928cd91db8705111c3d485e2a38ca5de61ec)) * **luarocks:** add proper dependencies ([81328d1](https://github.com/nvim-neorg/neorg/commit/81328d17ed9d5509e7dea8f1efc0fa568535e0e0)) ### Reverts * return back old logger code ([a8151f1](https://github.com/nvim-neorg/neorg/commit/a8151f1e21445739c9574d5eba9f4c635688cf98)) ## [8.0.0](https://github.com/nvim-neorg/neorg/compare/v7.0.0...v8.0.0) (2024-03-24) ### ⚠ BREAKING CHANGES * use decoupled lua-utils instead of the regular neorg utils * **lib:** deprecate `lib.map` * deprecate `core.upgrade` * **concealer:** simpler config for ordered list icon & multichar icon for unordered list ([#1179](https://github.com/nvim-neorg/neorg/issues/1179)) * **neorgcmd:** slowly move away from the deprecated `commands` directory * **highlights:** updated default groups to match names in treesitter ### Features * add basic build.lua ([efac9eb](https://github.com/nvim-neorg/neorg/commit/efac9eb8c16cfe5cd1a45705d2add4eca749e63f)) * add lua-utils.nvim to the list of required rocks ([b7b9eda](https://github.com/nvim-neorg/neorg/commit/b7b9edad6a852f33a2ce99051c748823dabd28cc)) * add new dependencies for norgopolis ([0e88310](https://github.com/nvim-neorg/neorg/commit/0e883108d8c782335615cf2108a703847a1295d9)) * add support for inline link targets ([132b73b](https://github.com/nvim-neorg/neorg/commit/132b73bfacd3014dc8afb56ddf7eed8c7acf6d6d)) * auto complete links ([#1295](https://github.com/nvim-neorg/neorg/issues/1295)) ([bd12dac](https://github.com/nvim-neorg/neorg/commit/bd12dacc9cf561cbffc8d6f8f4b76aa9d734665b)) * **concealer:** code block background `min_width` ([#1328](https://github.com/nvim-neorg/neorg/issues/1328)) ([efac835](https://github.com/nvim-neorg/neorg/commit/efac8350f4afe0b49f278129ef92ffb0a02d1c6f)) * **concealer:** simpler config for ordered list icon & multichar icon for unordered list ([#1179](https://github.com/nvim-neorg/neorg/issues/1179)) ([da74d14](https://github.com/nvim-neorg/neorg/commit/da74d14f217dc81bc364758bbecea3c5e934ba60)) * **concealer:** use empty foldmethod on nightly releases (for full folding passthrough) ([086891d](https://github.com/nvim-neorg/neorg/commit/086891d396ac9fccd91faf1520f563b6eb9eb942)) * **export.markdown:** option to export latex `embed` tags ([0abe7b7](https://github.com/nvim-neorg/neorg/commit/0abe7b737d35f2abd082bc6f694cf5a9fc166fb7)) * fix build.lua process (maybe once and for all?) ([eea6263](https://github.com/nvim-neorg/neorg/commit/eea6263ac4f3506d34d6e79839606e60b074757b)) * include plenary as a dependency ([6ea1eff](https://github.com/nvim-neorg/neorg/commit/6ea1eff15d3f1fa947255a94f99cadb298c8b66f)) * **keybinds:** add `opts` arg to `remap(_event)` ([27af839](https://github.com/nvim-neorg/neorg/commit/27af839eb6833f82765bc3066ab7e9b437233dd2)) * prepare neorg.core.lib for extraction ([c4eb7e9](https://github.com/nvim-neorg/neorg/commit/c4eb7e96ea1e2a0a4b6d47e6bda4f6816a908262)) * run sync-parsers as a build step ([9dd8331](https://github.com/nvim-neorg/neorg/commit/9dd8331bc1ad42117c7173cd5501b93570db85d5)) * **summary:** reimplement nested categories ([#1274](https://github.com/nvim-neorg/neorg/issues/1274)) ([6202285](https://github.com/nvim-neorg/neorg/commit/6202285214e70efe0d861c5a4969f8ee817bc985)) * undojoin timestamp updates ([#1272](https://github.com/nvim-neorg/neorg/issues/1272)) ([fe25e93](https://github.com/nvim-neorg/neorg/commit/fe25e93336b6a71b3cb3d7fd53ab6e4cb4a125c1)) * when absolutely no parameters are supplied, load Neorg with core.defaults ([b6fb57b](https://github.com/nvim-neorg/neorg/commit/b6fb57b723c02255a9d0c0f1a8fc957fe007d9c2)) ### Bug Fixes * **build.lua:** install dependencies instead of the actual plugin itself (prevent conflicts) ([da25527](https://github.com/nvim-neorg/neorg/commit/da2552769b572c012ff2f0ee9c11e3a26f061252)) * **build:** attempt to fix build script by deferring code execution ([fb45f83](https://github.com/nvim-neorg/neorg/commit/fb45f836da9dd43940c3fdd182e8255bbce9d9dc)) * bump version of `norgopolis-server` to 1.3.1 ([0d8a7ec](https://github.com/nvim-neorg/neorg/commit/0d8a7ecae258e15f40e88bc3b312d2b92192743f)) * **ci:** fix abs path to libs in luarc ([#1267](https://github.com/nvim-neorg/neorg/issues/1267)) ([0edde97](https://github.com/nvim-neorg/neorg/commit/0edde97b51a5247bd4db351a38d5f36131b642f7)) * **ci:** wrong version on typecheck ([fb23d2e](https://github.com/nvim-neorg/neorg/commit/fb23d2e78bf6ee601ed1de2a9ded23d6201f7506)) * **concealer:** footnote pattern should be matched against full string ([fc09cfc](https://github.com/nvim-neorg/neorg/commit/fc09cfc25e243a82653a758bc137395f4860b6f5)) * **config:** add support for bsd operating systems ([#1281](https://github.com/nvim-neorg/neorg/issues/1281)) ([2bdb89c](https://github.com/nvim-neorg/neorg/commit/2bdb89c388d5c9e1956e7aab949ffb003e9a8ea5)) * **config:** make the type system happy ([27482dc](https://github.com/nvim-neorg/neorg/commit/27482dcee4b14ed61a10ba51261919cb45351dad)) * **core.keybinds:** type errors with events ([dbe2841](https://github.com/nvim-neorg/neorg/commit/dbe28417222e044bcbec5bb016f0d604004bcbb3)) * **core.mode:** type errors with events ([fb2c561](https://github.com/nvim-neorg/neorg/commit/fb2c561f0080b621fd2853a3190d48f885a13b6d)) * **core.neorgcmd:** type errors with events ([1ab6236](https://github.com/nvim-neorg/neorg/commit/1ab6236a954cf2de6fe4b736a66ca5a17d85a6ff)) * **core.promo:** type errors with events ([0016fdd](https://github.com/nvim-neorg/neorg/commit/0016fdd8f2349dec1c1865f3412dbd08232b1bbd)) * **core.syntax:** remove deprecated functions, fix type errors in the code ([221bb2e](https://github.com/nvim-neorg/neorg/commit/221bb2eb10c8d7b7f62537393a9dce385d36b638)) * **core/modules:** reorder comments so that they are properly parsed by luals ([f20b40a](https://github.com/nvim-neorg/neorg/commit/f20b40a44a4e96ff9fa5ed252c3a678629adfda9)) * **docgen:** make the wiki work again ([d44dd38](https://github.com/nvim-neorg/neorg/commit/d44dd387d8f553791671f52f691be7580b98c6db)) * don't try to pull lua-utils when it's not applicable ([bcac799](https://github.com/nvim-neorg/neorg/commit/bcac79933f3930f04d9b1517106646a56efd8606)) * enable source of `nvim-cmp` only norg file type ([#1298](https://github.com/nvim-neorg/neorg/issues/1298)) ([1ab15f4](https://github.com/nvim-neorg/neorg/commit/1ab15f4b30627fd5e6dd175a23c7360c2c08b2bd)) * enforce contraint on norgopolis-server ([4b9f25c](https://github.com/nvim-neorg/neorg/commit/4b9f25ca9760e89702ccbe117d1ce17780b64641)) * error with import loop ([16b5479](https://github.com/nvim-neorg/neorg/commit/16b54794a545d8f80c0e9007952e374df2e417cd)) * **export.markdown:** fix error on unexported verbatim tags without parameters ([#1280](https://github.com/nvim-neorg/neorg/issues/1280)) ([e6d89d3](https://github.com/nvim-neorg/neorg/commit/e6d89d333aff65a771a98955fac9fc178345c01c)) * **export.markdown:** fix html `embed` tags not being exported ([5b2022c](https://github.com/nvim-neorg/neorg/commit/5b2022caaf689dc1c78b8959a2547249f8b05769)) * **export.markdown:** fix markdown `embed` tags not being exported ([f3d4230](https://github.com/nvim-neorg/neorg/commit/f3d4230d37da5d727d3ae13e1bada30e37b433ad)) * **export.markdown:** fix the first `tag` always being exported by default ([bda456d](https://github.com/nvim-neorg/neorg/commit/bda456d6685545893d446e841f2ee41633b6548a)) * **export.markdown:** use proper amount of parameters ([b0b5a43](https://github.com/nvim-neorg/neorg/commit/b0b5a4370228f27bd98516b9061bd6c87386c8f3)) * **highlights:** updated default groups to match names in treesitter ([56ad805](https://github.com/nvim-neorg/neorg/commit/56ad8056b6180dba60ddbd5bca2f29de12f3bd1d)) * **highlights:** updated unordered list, underline and strikethrough groups with standard names ([e7f524c](https://github.com/nvim-neorg/neorg/commit/e7f524c44f1a5d6fba6cced7e4eb3c22b9ff1473)) * incorrect code in upgrade module ([07967f1](https://github.com/nvim-neorg/neorg/commit/07967f1982b589974958689c7a055b33ea194691)) * **integrations.truezen:** use `setup()` instead of `load()` ([26cfe0e](https://github.com/nvim-neorg/neorg/commit/26cfe0e155c35695d2d4af7d938a9ffd160b8797)) * **integrations.truezen:** use `setup()` instead of `load()` ([3506236](https://github.com/nvim-neorg/neorg/commit/3506236e292de6d7989b6d6541ed5fcfa1e73bab)) * invalid vim.cmd syntax ([affdd6f](https://github.com/nvim-neorg/neorg/commit/affdd6fcbc2092fca293817d65e1664afbafe223)) * nobody figured it out so away it goes :) ([7b3e794](https://github.com/nvim-neorg/neorg/commit/7b3e794aa8722826418501608c8a3ffe4e19ea30)) * perform setup after the parsers have been installed ([f90c965](https://github.com/nvim-neorg/neorg/commit/f90c9654352f424690327271e3bd9a2c036489d0)) * properly install parsers ([59b6d61](https://github.com/nvim-neorg/neorg/commit/59b6d619213506e405a8ed13669dc82120653ac5)) * properly log TS error messages ([73db6b5](https://github.com/nvim-neorg/neorg/commit/73db6b51e9e28cce7ef17baf78a8416b563ca53a)) * properly require lua-utils ([b8a78c0](https://github.com/nvim-neorg/neorg/commit/b8a78c0c84dcfd3996480339c3d10c6e1ade8363)) * refactor library to not use lua utils ([5fcae0b](https://github.com/nvim-neorg/neorg/commit/5fcae0b080531ac1438faeefd47ae11e1633b463)) * refresh lua cache upon succesful installation of dependencies ([f1473cf](https://github.com/nvim-neorg/neorg/commit/f1473cf9ab1c1b610758e28fcb9e8a792a51ddf4)) * remove lua-utils from the loaded list to force a refresh ([af1e06c](https://github.com/nvim-neorg/neorg/commit/af1e06c801d6cb5682dde9a63b22053a8cf28665)) * rename Neorg index message to be more insightful ([6d686cd](https://github.com/nvim-neorg/neorg/commit/6d686cdc064489ed17b49b6f1463fc9b3e5ba698)) * **syntax:** ignore type annotation errors in syntax module ([6d94c2a](https://github.com/nvim-neorg/neorg/commit/6d94c2ac08f13208d84ce21b1e3eea13158b6491)) * TSInstallSync not found ([df6cc22](https://github.com/nvim-neorg/neorg/commit/df6cc22f36e347856bc14807b9db396e67b927d7)) * update module name to reflect breaking changes within `luarocks.nvim` ([1779e59](https://github.com/nvim-neorg/neorg/commit/1779e5962badca89505b60e9617b939489c661b0)) * use lua-utils ([b1ce837](https://github.com/nvim-neorg/neorg/commit/b1ce8374a88d638f42f0ce97b3b4b6b2b4e89023)) ### Code Refactoring * deprecate `core.upgrade` ([45f51ed](https://github.com/nvim-neorg/neorg/commit/45f51ed759d9cdd6c69b67e57ecbd054fd4cbaba)) * **lib:** deprecate `lib.map` ([8340274](https://github.com/nvim-neorg/neorg/commit/83402746b8b43190edb360329a023040bd388294)) * **neorgcmd:** slowly move away from the deprecated `commands` directory ([560d5a0](https://github.com/nvim-neorg/neorg/commit/560d5a04fb8143aaa5e64ba8eb100df97631fa36)) * use decoupled lua-utils instead of the regular neorg utils ([5f6bf7e](https://github.com/nvim-neorg/neorg/commit/5f6bf7e5444fe839d739bd376ec5cdb362f02dc6)) ## [7.0.0](https://github.com/nvim-neorg/neorg/compare/v6.2.0...v7.0.0) (2023-12-28) ### ⚠ BREAKING CHANGES * **selection_popup:** modernize code of selection popup ### ref * **selection_popup:** modernize code of selection popup ([310f3a4](https://github.com/nvim-neorg/neorg/commit/310f3a484d3d98b0d05650a38407dcaa7f090b96)) ### Features * allow upward paths in tangle ([265e6af](https://github.com/nvim-neorg/neorg/commit/265e6af8decbb30b0ee14aee373b1bfe9a78b858)) * **concealer:** add ability to disable spell checking in code blocks ([316403a](https://github.com/nvim-neorg/neorg/commit/316403ad1cbb665e7838f596384d44b1649f6c1b)) * **concealer:** add config for concealing numeric footnote title to superscript ([2a6fc9c](https://github.com/nvim-neorg/neorg/commit/2a6fc9c808f6d643bf7c2f911a767e4aac500560)) * **concealer:** add configuration for hrule start and end position ([3db316a](https://github.com/nvim-neorg/neorg/commit/3db316a33838eb0875eacd659af9d49bbd4aef39)) * **keyinds:** add keybind for entering link traversal mode ([#1177](https://github.com/nvim-neorg/neorg/issues/1177)) ([8cf5205](https://github.com/nvim-neorg/neorg/commit/8cf52058fb7e9c3057882430ade90be5bdfb3a94)) * prefix all keybind descriptions with "neorg" for discoverability ([15c24cd](https://github.com/nvim-neorg/neorg/commit/15c24cdb264807b09e9281e2d72b324145da1d57)) * **selection_popup:** allow keybinds to be processed from another buffer ([603b633](https://github.com/nvim-neorg/neorg/commit/603b633b8df231fe37a338856b1dea7cd955a969)) * **summary:** add strategy which uses workspace subfolders as category ([aa8e66d](https://github.com/nvim-neorg/neorg/commit/aa8e66dd40c07a4de58f9ed93f27ab4dac9a241c)) * **tangle:** add `report_on_empty` option in `core.tangle` ([#1250](https://github.com/nvim-neorg/neorg/issues/1250)) ([cc6d8b1](https://github.com/nvim-neorg/neorg/commit/cc6d8b150de7bf806f3a191867a7f143970b5112)) * **toc:** add config for enabling synchronized cursorline in toc window ([d3cbb45](https://github.com/nvim-neorg/neorg/commit/d3cbb45b66c865b1b92b5f8b2dbd5a5fff7f1a2f)) * **toc:** add toc item filter ([#1195](https://github.com/nvim-neorg/neorg/issues/1195)) ([5c42084](https://github.com/nvim-neorg/neorg/commit/5c420844227c75390cc9fdf6047bfc49466169d9)) * **toc:** auto adjust toc vsplit width upon creation ([81f6330](https://github.com/nvim-neorg/neorg/commit/81f6330af951e89f98e8468d23a648fc32acdd2f)) * **toc:** don't scroll content window when switching to toc ([c4fc7e6](https://github.com/nvim-neorg/neorg/commit/c4fc7e629e8ea7ecc9610107622f46e888764534)) * **toc:** enable folding in toc ([218e7eb](https://github.com/nvim-neorg/neorg/commit/218e7ebbce010846c5ed6da647264c556c6a7ad4)) * **toc:** faster toc generation ([0171df1](https://github.com/nvim-neorg/neorg/commit/0171df1d0f8a6db254020e8b02ac576188ffad23)) * **toc:** support one ToC per tabpage ([d8a456b](https://github.com/nvim-neorg/neorg/commit/d8a456b7fa1b9d860fc36750b6e9a200a8eff5f3)) * **toc:** support todo status ([4ac077b](https://github.com/nvim-neorg/neorg/commit/4ac077b1f19efe63fcec4e6c744bc6a68dfc7f6a)) * **toc:** sync cursor from ToC to content buffer ([47e7c86](https://github.com/nvim-neorg/neorg/commit/47e7c86877aaae4d85c1a2add166ad6c15b8add4)) * **toc:** sync toc cursor after creating toc, scroll content to center when previewing ([cfcb51e](https://github.com/nvim-neorg/neorg/commit/cfcb51ea9a403ee7223e49d4afb0142d6d5e1659)) ### Bug Fixes * "Keybind not found" display causing errors ([#1215](https://github.com/nvim-neorg/neorg/issues/1215)) ([a51abd5](https://github.com/nvim-neorg/neorg/commit/a51abd53d8afc7de81e35d0a4247c3aa6ccfc76a)) * `update-metadata` would fail to work with several parse trees in the document ([#1234](https://github.com/nvim-neorg/neorg/issues/1234)) ([5a44d3f](https://github.com/nvim-neorg/neorg/commit/5a44d3ffbd3b4fff762f8b2712ab1cfd16cff016)) * **action:** run lint action against pr head ([f367396](https://github.com/nvim-neorg/neorg/commit/f36739620410917a3119ee4299894c353a0d88af)) * **autocommands:** pass correct buffer id ([941119d](https://github.com/nvim-neorg/neorg/commit/941119d48a5e354cfbed24a4b314bb4eb401a75b)) * **concealer:** BufNewFile->FileType, get winid of bufid when rendering ([c0983ca](https://github.com/nvim-neorg/neorg/commit/c0983ca60f02e1a65e5990593726e57678e03c4a)) * **concealer:** do not render on range change if concealer is disabled ([9b0c31a](https://github.com/nvim-neorg/neorg/commit/9b0c31a5179f3881f9ff2350da22c9a5a11f32ab)) * **concealer:** ensure backwards compatibility for `vim.treesitter.foldexpr` ([5921cc4](https://github.com/nvim-neorg/neorg/commit/5921cc48cb3be616db0071fa058cfa4d6633c8a6)) * **concealer:** use vim.treesitter.foldexpr for stabler folding ([53cbffb](https://github.com/nvim-neorg/neorg/commit/53cbffb7ecfcb60f19c10c72c4162978e8021959)) * **config:** delete `neovim_version` as it is no longer in use ([00f9a62](https://github.com/nvim-neorg/neorg/commit/00f9a628683b7b3f738e1d1d1a79d517c26b6ff5)) * **config:** fix luajit version detection ([237abac](https://github.com/nvim-neorg/neorg/commit/237abac43a38e4aa770bb5819f30b3d38ae5f392)) * **export:** better handling of new lines in markdown metadata ([d56cc3c](https://github.com/nvim-neorg/neorg/commit/d56cc3c9a9cd10bfac5eac2514a9457a3e9e848d)) * **export:** fix metadata values being ignored when converting to markdown ([6f9b66c](https://github.com/nvim-neorg/neorg/commit/6f9b66cfa75241d4b8c0890a312872104a2d96a1)) * **export:** handle empty `object`/`array` nodes in markdown metadata ([3afbadb](https://github.com/nvim-neorg/neorg/commit/3afbadb3d116d6f8a5fb0aa3af1c06563c4a038e)) * **hop:** fix range check across lines ([1038016](https://github.com/nvim-neorg/neorg/commit/10380167975732444f21c882e522d15b0ec55b34)) * **journal:** value assigned to variable current_quarter is unused ([0e88151](https://github.com/nvim-neorg/neorg/commit/0e8815116b08bfbceb2b36a8c82d81005e2596e0)) * **latex:** Want image integration ([a80c025](https://github.com/nvim-neorg/neorg/commit/a80c025b231a6acd925d625d6d9ea302bc20bd49)) * **luacheck:** setting non-standard global variables in latex renderer module ([#1176](https://github.com/nvim-neorg/neorg/issues/1176)) ([3f4b279](https://github.com/nvim-neorg/neorg/commit/3f4b279d7505ac854fcd31d1aad24991542ea5d8)) * **modules:** Check the right config key in module.wants ([8b25435](https://github.com/nvim-neorg/neorg/commit/8b25435e8bc60f9e6f665b3a28870d64d20f2b59)) * **neorg.norg:** clarify horizontal line syntax ([#1230](https://github.com/nvim-neorg/neorg/issues/1230)) ([e35bf90](https://github.com/nvim-neorg/neorg/commit/e35bf907533281a6c641505eae3bb42100d7b5a0)) * record that module `upgrade` requires at least 1 arg ([#1207](https://github.com/nvim-neorg/neorg/issues/1207)) ([51f55f5](https://github.com/nvim-neorg/neorg/commit/51f55f5c6d54fa86fdaae805b55ca88aa9607c37)) * **summary:** set correct indentation for list items ([120fb52](https://github.com/nvim-neorg/neorg/commit/120fb52f5fe21c43fcc7285bac4a9bce8a54a6ec)) * **toc:** clear title after assigning prefix ([f446645](https://github.com/nvim-neorg/neorg/commit/f4466457396717d10d2d235d019e0a80e1770087)) * **toc:** fix all stylua errors ([ae38baf](https://github.com/nvim-neorg/neorg/commit/ae38baf90a319488b726ed25166fc00641b3e0ce)) * **toc:** get window id on the fly to avoid assertion errors ([1b0ab75](https://github.com/nvim-neorg/neorg/commit/1b0ab75e8e57b08bc981e0d72fe928b0fff34fe2)) * **toc:** handle buf close ([985364f](https://github.com/nvim-neorg/neorg/commit/985364f561518502cc002494db4d48ec92b00d80)) * **toc:** handle buf close ([2d65f6c](https://github.com/nvim-neorg/neorg/commit/2d65f6cf7a0f40b9a474e17bc347255514dbde0e)) * **toc:** listen cursormoved for all norg files ([19bff13](https://github.com/nvim-neorg/neorg/commit/19bff133659c16973e52546f54a13469bfecb1b6)) * **toc:** stop synching cursor when content window is hidden ([15ed981](https://github.com/nvim-neorg/neorg/commit/15ed981858658796b698f6fc204f1378eef4b01d)) * **typecheck:** fix type errors caused by autoformat ([3f531c3](https://github.com/nvim-neorg/neorg/commit/3f531c362d07d52c4956520e3798e9cfb5aeabdf)) ## [6.2.0](https://github.com/nvim-neorg/neorg/compare/v6.1.0...v6.2.0) (2023-11-18) ### Features * add `traverse-link` Neorg mode ([#1170](https://github.com/nvim-neorg/neorg/issues/1170)) ([ed25267](https://github.com/nvim-neorg/neorg/commit/ed25267eec3b08a3de8bdb4b55243f869ea4b8fd)) * add LaTex rendering for inline equations ([#1133](https://github.com/nvim-neorg/neorg/issues/1133)) ([b5393e8](https://github.com/nvim-neorg/neorg/commit/b5393e8bdcf704f660fa86cace89033c5fc95504)) * allow arguments for `:Neorg generate-workspace-summary` ([#1156](https://github.com/nvim-neorg/neorg/issues/1156)) ([46741ed](https://github.com/nvim-neorg/neorg/commit/46741ede577392f36cad1cb8c8e6029fabb729f6)) * allow nested workspace summaries ([#1144](https://github.com/nvim-neorg/neorg/issues/1144)) ([a923055](https://github.com/nvim-neorg/neorg/commit/a9230559fb6871f1f62996f8e862876169432f08)) * **hop:** feed wslview with decoded link ([c3b9653](https://github.com/nvim-neorg/neorg/commit/c3b965340f380740a12432536d2b23ee6c7564f9)) * **metagen:** customize timezone and its format ([b458149](https://github.com/nvim-neorg/neorg/commit/b4581496328d47ab7912148ec030dcb3ec1951c4)) * option to inject specific metadata instead of defaults ([#1128](https://github.com/nvim-neorg/neorg/issues/1128)) ([5509079](https://github.com/nvim-neorg/neorg/commit/55090798a2eed2dd00fc1b2774bc6bf309a3bd0b)) ### Bug Fixes * **dirman:** add `raw_path` option to work with arbitrary filetype ([#1143](https://github.com/nvim-neorg/neorg/issues/1143)) ([0c9f5de](https://github.com/nvim-neorg/neorg/commit/0c9f5dea0cfe8b7c3d38f26651d82624079774ed)) * **journal:** toc reset month & add link indent ([#1165](https://github.com/nvim-neorg/neorg/issues/1165)) ([16af444](https://github.com/nvim-neorg/neorg/commit/16af444ef804aa7f099c7a5ae03640dfc2b60303)) * remove LaTeX renderer and image.nvim integration from `core.defaults` ([5a88bcb](https://github.com/nvim-neorg/neorg/commit/5a88bcbf60590348e4196493c9c7642f23ba21d7)) * workspace summary ignore closed files and title field of metadata tag ([#1139](https://github.com/nvim-neorg/neorg/issues/1139)) ([d081937](https://github.com/nvim-neorg/neorg/commit/d081937a00e0f0c6966116428117e159a785abb5)) ## [6.1.0](https://github.com/nvim-neorg/neorg/compare/v6.0.0...v6.1.0) (2023-10-29) ### Features * support dotrepeat for `promo` and `todo_items` ([#1105](https://github.com/nvim-neorg/neorg/issues/1105)) ([2c43e6b](https://github.com/nvim-neorg/neorg/commit/2c43e6b3252af198973cbe91f8fa7a762ff61a77)) ### Bug Fixes * **calendar:** display weekdays based on `nvim_strwidth` ([5eadb3c](https://github.com/nvim-neorg/neorg/commit/5eadb3cce8ab490222d12dfbb5c86372c89a5773)) * **calendar:** use `nvim_strwidth` for month names as well ([a081397](https://github.com/nvim-neorg/neorg/commit/a0813979663d5e55c481bb557c250b551042d115)) * **dirman:** open index file in default workspace only if it exists ([d1bda3c](https://github.com/nvim-neorg/neorg/commit/d1bda3caf7d73ec93bed125d2d76ba32ce897789)) * don't autoload `core.neorgcmd` nor `core.keybinds` as dependencies of other modules ([#1051](https://github.com/nvim-neorg/neorg/issues/1051)) ([62ba931](https://github.com/nvim-neorg/neorg/commit/62ba93130eb795ccc2133841ce0e541f8bc51eb7)) * **meta:** fix treesitter deprecation warning ([#1104](https://github.com/nvim-neorg/neorg/issues/1104)) ([#1130](https://github.com/nvim-neorg/neorg/issues/1130)) ([5205f3f](https://github.com/nvim-neorg/neorg/commit/5205f3f1ed23545a3015021be11d35a012e3b02a)) * **utils:** don't dotrepeat insert mode actions ([#1111](https://github.com/nvim-neorg/neorg/issues/1111)) ([969b3f1](https://github.com/nvim-neorg/neorg/commit/969b3f106683c66ab685ecba2a67bf11cb806785)) ## [6.0.0](https://github.com/nvim-neorg/neorg/compare/v5.0.0...v6.0.0) (2023-09-23) ### ⚠ BREAKING CHANGES * adapt to new injection syntax for treesitter * **codebase:** make the `neorg` object local to a `core` module ([#1001](https://github.com/nvim-neorg/neorg/issues/1001)) ### Features * add blank lines between tangled blocks ([#958](https://github.com/nvim-neorg/neorg/issues/958)) ([1c41592](https://github.com/nvim-neorg/neorg/commit/1c41592ec975189c79987aa32228778c111eb67f)) * **concealer:** add option for opening all folds by default ([#1049](https://github.com/nvim-neorg/neorg/issues/1049)) ([6bfcaeb](https://github.com/nvim-neorg/neorg/commit/6bfcaeb8f36e0e4d2ae52dbde5e18b39d2351d5e)) * delimit tangle code blocks with file content ([#1014](https://github.com/nvim-neorg/neorg/issues/1014)) ([1809236](https://github.com/nvim-neorg/neorg/commit/18092365b21c73a0478b6bd6d9b3a66fd4b77a36)) * delimit tangled code blocks with headings ([#981](https://github.com/nvim-neorg/neorg/issues/981)) ([99bfcb1](https://github.com/nvim-neorg/neorg/commit/99bfcb11dc3fbc72c08259d5516738d3a1f7bd11)) * **document.meta:** indent items of incomplete lists/objects for nicer writing experience ([92f2e9d](https://github.com/nvim-neorg/neorg/commit/92f2e9d4a7bfdbb7ed0e9dcd9b8768db63188149)) * **esupports.hop:** add open mode for external link target ([#1072](https://github.com/nvim-neorg/neorg/issues/1072)) ([851a3a2](https://github.com/nvim-neorg/neorg/commit/851a3a2b3cea5335fca233273d3c8861a017da14)) * **esupports.hop:** support `os_open_link` for WSL ([#963](https://github.com/nvim-neorg/neorg/issues/963)) ([628ba9f](https://github.com/nvim-neorg/neorg/commit/628ba9f58e02db6b2818f68b62a1499c22eb9cd4)) * **esupports:** use `wslview` to open `wsl2` files ([#1038](https://github.com/nvim-neorg/neorg/issues/1038)) ([20502e5](https://github.com/nvim-neorg/neorg/commit/20502e50e9087248f6f8ed8d29fae9c849c1c77f)) * **itero:** allow fallback keys for when there is no object to iterate ([ba2899d](https://github.com/nvim-neorg/neorg/commit/ba2899d6580706cbf727720db2765aead9d342de)) * **keybinds:** allow `core.itero.next-iteration` to fall back to a specific key ([51ca15b](https://github.com/nvim-neorg/neorg/commit/51ca15b13e9a7b107bef54c9bed94b5863b9c5d5)) * **metagen:** allow falling back to the default template functions ([#1079](https://github.com/nvim-neorg/neorg/issues/1079)) ([8200ebc](https://github.com/nvim-neorg/neorg/commit/8200ebc5a5730a14efa2e47751a43539c8a16fb5)) * **metagen:** more precise timestamp with HH:MM:SS and timezone ([#1052](https://github.com/nvim-neorg/neorg/issues/1052)) ([a8f7a9e](https://github.com/nvim-neorg/neorg/commit/a8f7a9eeef5c22eac626e7533eeee0ac9def72ad)) ### Bug Fixes * `:h neorg` not working as intended ([0b3df86](https://github.com/nvim-neorg/neorg/commit/0b3df8633cc1cbb3ffd6f34d4e9073fd6f5083ab)) * **`:h neorg`:** make link point to correct line in specs ([#1092](https://github.com/nvim-neorg/neorg/issues/1092)) ([e20d032](https://github.com/nvim-neorg/neorg/commit/e20d032ea3c485fc499f4dbc4bf7ce6afd6767ba)) * `folke/todo-comments.nvim` comments highlighting (again) ([#1094](https://github.com/nvim-neorg/neorg/issues/1094)) ([d8e2c8e](https://github.com/nvim-neorg/neorg/commit/d8e2c8e309c05a7db4ca84fc1216be38cf6a010f)) * broken configuration merging in modules.lua ([#1062](https://github.com/nvim-neorg/neorg/issues/1062)) ([b4c7935](https://github.com/nvim-neorg/neorg/commit/b4c7935a0e692870f38ff34689fd900de40ea479)) * **calendar:** call `os.date` twice to generate correct weekday ([#1058](https://github.com/nvim-neorg/neorg/issues/1058)) ([61fb605](https://github.com/nvim-neorg/neorg/commit/61fb60508516b224ec78666187e70074397b37f8)) * **calendar:** give calendar enough space to render ([#950](https://github.com/nvim-neorg/neorg/issues/950)) ([6fece15](https://github.com/nvim-neorg/neorg/commit/6fece1546d051a5f2a2d932d5978beec1ef920ab)) * **concealer,indent:** "require'neorg'" missing in v:lua call ([#1010](https://github.com/nvim-neorg/neorg/issues/1010)) ([1d3b425](https://github.com/nvim-neorg/neorg/commit/1d3b4252862cadf80751e0e03463b27a1782ce94)) * **concealer:** avoid conflict between preset and custom icons ([9a0aab0](https://github.com/nvim-neorg/neorg/commit/9a0aab039b174625bfc4ff708ba32f3fc5713649)) * **concealer:** do not render missing node ([#1004](https://github.com/nvim-neorg/neorg/issues/1004)) ([08c7d19](https://github.com/nvim-neorg/neorg/commit/08c7d19125f5f8aa36911bfd3ea166b650e05e07)) * **concealer:** don't rerender at `conceallevel` change when disabled ([#1068](https://github.com/nvim-neorg/neorg/issues/1068)) ([63a7a10](https://github.com/nvim-neorg/neorg/commit/63a7a101387550a220186cab7e85df15635f3356)) * **concealer:** more precise anticonceal feature detection ([#1056](https://github.com/nvim-neorg/neorg/issues/1056)) ([b0117a4](https://github.com/nvim-neorg/neorg/commit/b0117a40675398cb6b7f0967a52e148d5ddb6f42)) * **concealer:** revert a wrong fix, make luacheck ignore empty if branch instead (supercedes [#1080](https://github.com/nvim-neorg/neorg/issues/1080)) ([0c82917](https://github.com/nvim-neorg/neorg/commit/0c82917b89a187662cf8c1f5fc3a17153866df9b)) * **concealer:** tolerate duplicate marks caused by undo during rendering ([#1015](https://github.com/nvim-neorg/neorg/issues/1015)) ([44bb353](https://github.com/nvim-neorg/neorg/commit/44bb3533465d30062b28a334115e37dbbe7e5118)) * **core:** assign custom field ([4b057ad](https://github.com/nvim-neorg/neorg/commit/4b057ad071f0e395fb1e983c9611913e9b46108f)) * **dirman:** correctly create nested directory ([#1061](https://github.com/nvim-neorg/neorg/issues/1061)) ([4f0888b](https://github.com/nvim-neorg/neorg/commit/4f0888bdf98f7b1eeb96365aca17aa08ba4a07ea)) * **docgen:** `neorg.core` not found ([bb29db9](https://github.com/nvim-neorg/neorg/commit/bb29db9320b353da8abdfaebcba74a0a1d6e1a20)) * **docgen:** inline `esupports.metagen` template function definitions ([#945](https://github.com/nvim-neorg/neorg/issues/945)) ([a993b35](https://github.com/nvim-neorg/neorg/commit/a993b357ab86e153ecd50e2d4b704b8dcffedc1f)) * don't use deprecated `query.get_node_text()` call ([#1067](https://github.com/nvim-neorg/neorg/issues/1067)) ([7248c34](https://github.com/nvim-neorg/neorg/commit/7248c347704d658daf0fa0a84706c120e92eb1a5)) * error in loading preventing wiki from generating ([2745ee1](https://github.com/nvim-neorg/neorg/commit/2745ee1371c1029171bb98f2d9fb258e688d2c20)) * fetched get_language_list from utils ([#1003](https://github.com/nvim-neorg/neorg/issues/1003)) ([3db1001](https://github.com/nvim-neorg/neorg/commit/3db10018e8893aee47f3b5eb9f4d7440f8db5136)) * **highlights:** always try to attach highlights when triggered ([#1025](https://github.com/nvim-neorg/neorg/issues/1025)) ([31b3bfd](https://github.com/nvim-neorg/neorg/commit/31b3bfddfc1a4e426b41879bdb1a039babc554e3)) * indents within `document.meta` would not work ([b14334e](https://github.com/nvim-neorg/neorg/commit/b14334e39dcf6d8a6edb18547b7c4580387dce63)) * issue a more friendly error message when user loads tempus pre-Neovim `0.10.0` ([#1035](https://github.com/nvim-neorg/neorg/issues/1035)) ([333a1fd](https://github.com/nvim-neorg/neorg/commit/333a1fd67aad3dee49305b0278bd59f8ae740f13)) * **journal:** expand entry path correctly (fixes [#780](https://github.com/nvim-neorg/neorg/issues/780)) ([#995](https://github.com/nvim-neorg/neorg/issues/995)) ([e76f0cb](https://github.com/nvim-neorg/neorg/commit/e76f0cb6b3ae5e990052343ebb73a5c8d8cac783)) * **journal:** Remove condition from 'toc' subcommand (fixes [#597](https://github.com/nvim-neorg/neorg/issues/597)) ([#996](https://github.com/nvim-neorg/neorg/issues/996)) ([99f33e0](https://github.com/nvim-neorg/neorg/commit/99f33e08fe074126b491e02854e5d00dab10f5ae)) * **looking-glass:** ensure both the target buffer and the source are loaded before pursuing any operations ([fba064d](https://github.com/nvim-neorg/neorg/commit/fba064db88eae3419d20ce35cf3961d02c355a8f)) * **maneoeuvre:** `lib` -> `utils` ([0949a4a](https://github.com/nvim-neorg/neorg/commit/0949a4a2816ef19cb19e0ef8d483d3410dd0895a)) * On close of TOC, only delete buffer if it exists ([#978](https://github.com/nvim-neorg/neorg/issues/978)) ([32bae17](https://github.com/nvim-neorg/neorg/commit/32bae172814611f82e90b696b72cac99ff8de0e9)) * **presenter:** ensure module.private is not overriden ([#1037](https://github.com/nvim-neorg/neorg/issues/1037)) ([c9dd9f7](https://github.com/nvim-neorg/neorg/commit/c9dd9f7d506717b00e99409e4088e5b739c36b39)) * replace `get_filetype` with `vim.filetype.match` ([#982](https://github.com/nvim-neorg/neorg/issues/982)) ([4e6dbb1](https://github.com/nvim-neorg/neorg/commit/4e6dbb184442bc33e20ce760f093c07b32ad4128)) * **summary:** escape ws_root special characters ([#1012](https://github.com/nvim-neorg/neorg/issues/1012)) ([32abc0d](https://github.com/nvim-neorg/neorg/commit/32abc0da29dd5bf4b42d340810b64754fd7a37b8)) * **tags:** make new tags work with updated neorg help document ([#994](https://github.com/nvim-neorg/neorg/issues/994)) ([3f946f8](https://github.com/nvim-neorg/neorg/commit/3f946f8814a59ac16baaf4bc1dd0f4aca3807736)) * **tangle:** accessing unused variable ([0f37ab8](https://github.com/nvim-neorg/neorg/commit/0f37ab86ea82838ddd9feeab94986d6d72d0d85a)) * **toc:** preserve heading hierarchy ([#1053](https://github.com/nvim-neorg/neorg/issues/1053)) ([1c1060f](https://github.com/nvim-neorg/neorg/commit/1c1060f0d187cd0939b05c1310bb58911e84bc22)) * **ui:** remove possible ui noise caused by user's opts ([68eae35](https://github.com/nvim-neorg/neorg/commit/68eae352bf4b936e667b5eb4d454d4d280d2286d)) * Update `get_username` call ([#1005](https://github.com/nvim-neorg/neorg/issues/1005)) ([93bf092](https://github.com/nvim-neorg/neorg/commit/93bf092a817df07f75cee578c74b4eabab3b7c87)) ### Code Refactoring * adapt to new injection syntax for treesitter ([064f8f6](https://github.com/nvim-neorg/neorg/commit/064f8f65dd32f4fe728e76acfa3e4e153b121147)) * **codebase:** make the `neorg` object local to a `core` module ([#1001](https://github.com/nvim-neorg/neorg/issues/1001)) ([5706f1e](https://github.com/nvim-neorg/neorg/commit/5706f1efdcf55f273de8f52deeb35375a303be72)) ## [5.0.0](https://github.com/nvim-neorg/neorg/compare/v4.6.0...v5.0.0) (2023-06-07) ### ⚠ BREAKING CHANGES * **core.ui:** don't use old Neovim APIs, fix errors when using `nn` * **core.highlights:** remove `todo_items_match_color` option * **highlights:** simplify highlights for performance reasons * **summary:** fix norg links, use first heading as title if found ([#928](https://github.com/nvim-neorg/neorg/issues/928)) * **core:** remove `real`/imaginary components of modules, improve startup time, remove `imports` from `module.setup` * remove the `core.news` module * **concealer:** rewrite for performance and stability ([#834](https://github.com/nvim-neorg/neorg/issues/834)) * since 5.0 do not longer warn about deprecated `core.norg.*` modules * move to new/improved metadata parser, change highlight queries ### Features * add extra nesting level, make icons specific to non-anticonceal usage ([84ea792](https://github.com/nvim-neorg/neorg/commit/84ea792d97977b98caab8e63538d3286f58b2b1b)) * add highlights to `&variable&`s ([#710](https://github.com/nvim-neorg/neorg/issues/710)) ([97080f7](https://github.com/nvim-neorg/neorg/commit/97080f798e0872a52510e33cf7f9064af5501da3)) * add neorg to luarocks ([4fceaa6](https://github.com/nvim-neorg/neorg/commit/4fceaa67656a0ebf17daeac133db2387df44552a)) * conceal the `{* }` parts of links ([729e7ac](https://github.com/nvim-neorg/neorg/commit/729e7ac46b5feac7f97826f755695f0e2c4799f9)) * **concealer:** add more icon generators ([49b9788](https://github.com/nvim-neorg/neorg/commit/49b9788a4988235d4357f8ae87d3ce82ee39302e)) * **concealer:** add numeric anticonceal if supported ([55feccf](https://github.com/nvim-neorg/neorg/commit/55feccf37df2b1143ea85151b9430149c617aa99)) * **concealer:** rewrite for performance and stability ([#834](https://github.com/nvim-neorg/neorg/issues/834)) ([151c033](https://github.com/nvim-neorg/neorg/commit/151c0337684a30ab8a9b31683b7a2fa28b0a15b0)) * **esupports.hop:** link jump to line + fixes + refactoring ([#903](https://github.com/nvim-neorg/neorg/issues/903)) ([49610cd](https://github.com/nvim-neorg/neorg/commit/49610cdee13050fc872cc006a690a911dda68413)) * **indent:** add `dedent_excess` configuration option ([#624](https://github.com/nvim-neorg/neorg/issues/624)) ([66d5a22](https://github.com/nvim-neorg/neorg/commit/66d5a2251b0871aa037135644b6fca2a856de5b4)) * **itero:** don't start newline on empty line ([#911](https://github.com/nvim-neorg/neorg/issues/911)) ([4c76b74](https://github.com/nvim-neorg/neorg/commit/4c76b741a0003417ed38bf0f43727810c27fb042)) * **keybinds.lua:** add `desc` fields to task keybinds ([#926](https://github.com/nvim-neorg/neorg/issues/926)) ([978fdc1](https://github.com/nvim-neorg/neorg/commit/978fdc1dede2325374dc5a32db10a4b6dad87bf0)) * **keybinds.lua:** add descriptions to all keybinds ([bb50538](https://github.com/nvim-neorg/neorg/commit/bb505384372b87ae6193c9ceeb02312d50f0df3c)) * move to new/improved metadata parser, change highlight queries ([962e45a](https://github.com/nvim-neorg/neorg/commit/962e45a29f1d61f685a5bacb9a2b00eb0a11d9c5)) * **promo:** promote/demote prefix without following text ([#912](https://github.com/nvim-neorg/neorg/issues/912)) ([544bb06](https://github.com/nvim-neorg/neorg/commit/544bb06c28956c4e21b6d6d32b1b3ea7415be7cd)) ### Bug Fixes * **completion:** selected completion engine not being engaged ([474af82](https://github.com/nvim-neorg/neorg/commit/474af829b0f3e25e09e68d2842ffcb6ca24d359b)) * **concealer:** disable assertion for prefixes until parser changes ([#932](https://github.com/nvim-neorg/neorg/issues/932)) ([92aa737](https://github.com/nvim-neorg/neorg/commit/92aa7373ccdfc5c9d1616027173237ee9cc4098e)) * **concealer:** do not listen vimleavepre ([#920](https://github.com/nvim-neorg/neorg/issues/920)) ([865224a](https://github.com/nvim-neorg/neorg/commit/865224a59982a148e9b11647d23e2de61272c42c)) * **concealer:** fix concealing in anchors, don't error on broken config ([#923](https://github.com/nvim-neorg/neorg/issues/923)) ([f448b58](https://github.com/nvim-neorg/neorg/commit/f448b581c6a6cf2747b33ff6bfece6c21c72b03f)) * **concealer:** minor fixes, plus wiki error fix ([#916](https://github.com/nvim-neorg/neorg/issues/916)) ([5629898](https://github.com/nvim-neorg/neorg/commit/5629898cf24bf25a39723e4113ce87a08f0d9dc1)) * **concealer:** record cursor upon init to fix first line conceal ([#924](https://github.com/nvim-neorg/neorg/issues/924)) ([44ee0cb](https://github.com/nvim-neorg/neorg/commit/44ee0cb8db3d655d45d5ca5cedc2b0745b232659)) * **core.highlights:** fix disappearing highlights when opening up norg files ([9db5645](https://github.com/nvim-neorg/neorg/commit/9db56453e2f7f6bc7e81baa338e09a2565ccaff1)) * **core.highlights:** wrongly placed bracket ([1886d36](https://github.com/nvim-neorg/neorg/commit/1886d363e9f397251060a4d6681fa975ef9d3b64)) * **core.summary:** bugs + flexibility around incomplete metadata ([#927](https://github.com/nvim-neorg/neorg/issues/927)) ([30343db](https://github.com/nvim-neorg/neorg/commit/30343dbdcdb511ecb6f484c46a9ae6f20a66ff7d)) * **docgen:** don't fail on mixed-type tables (lists and dictionaries at the same time) ([1afcaf8](https://github.com/nvim-neorg/neorg/commit/1afcaf804bae0048bfca1c0d49b69c968f2c187b)) * **docgen:** fix incorrect markdown indentation in wiki ([2bf6e63](https://github.com/nvim-neorg/neorg/commit/2bf6e63c299903d6e83fe14a521987dd0745efb0)) * **docgen:** propagate docgen error exit code ([#917](https://github.com/nvim-neorg/neorg/issues/917)) ([0e97976](https://github.com/nvim-neorg/neorg/commit/0e97976417d3e387d9be2f4fb42cd66c72254b6b)) * **highlights:** assert on treesitter being enabled ([#914](https://github.com/nvim-neorg/neorg/issues/914)) ([330f04e](https://github.com/nvim-neorg/neorg/commit/330f04ef693fb379c5ff199a05813e270718c850)) * **highlights:** attempt to reenable highlighting when none is found ([d1fb8c9](https://github.com/nvim-neorg/neorg/commit/d1fb8c94c57161e675402ec06ed80dc9223df655)) * **presenter:** errors on startup ([ea5fe1b](https://github.com/nvim-neorg/neorg/commit/ea5fe1b51d0a5b9f33a2fdd81906c5661b9198d6)) * **summary:** fix norg links, use first heading as title if found ([#928](https://github.com/nvim-neorg/neorg/issues/928)) ([6f893a2](https://github.com/nvim-neorg/neorg/commit/6f893a205a7543f2b7390b31176cf6e4ee2442c0)) * **todo_items:** don't look at child if parent is todo ([#909](https://github.com/nvim-neorg/neorg/issues/909)) ([8e3bcb2](https://github.com/nvim-neorg/neorg/commit/8e3bcb295a834dd57ba1d41ef2903f3dcc53a70e)) ### Performance Improvements * **core.highlights:** remove `todo_items_match_color` option ([7b5d550](https://github.com/nvim-neorg/neorg/commit/7b5d550843a3a2576aa95a90972c2ffc0e5c682f)) * **core.neorgcmd:** unnecessary `vim.tbl_deep_extend` ([71d291f](https://github.com/nvim-neorg/neorg/commit/71d291f97dc7e7fab4ca5740181e25f6d50a6e2d)) * **core.promo:** don't check `v.count`, use `v.count1` instead ([ca98238](https://github.com/nvim-neorg/neorg/commit/ca982387110ce2b796e585a10cd6f6922cec6c69)) * **events:** don't deepcopy a table on each new event ([12198ef](https://github.com/nvim-neorg/neorg/commit/12198efd76ec057be207e567dbeed3c8022d6eb6)) * **hop:** load plenary only when required, remove startup hiccup ([3caca5a](https://github.com/nvim-neorg/neorg/commit/3caca5ac209aa8098a355837b5c4696d16804e19)) ### Code Refactoring * **core.ui:** don't use old Neovim APIs, fix errors when using `<LocalLeader>nn` ([bbb25ff](https://github.com/nvim-neorg/neorg/commit/bbb25ffa380a2c159b0d301df9b81a8fcf3ab67a)) * **core:** remove `real`/imaginary components of modules, improve startup time, remove `imports` from `module.setup` ([593e9b2](https://github.com/nvim-neorg/neorg/commit/593e9b2a0826dfb8068a02277f4a45db00573e9a)) * **highlights:** simplify highlights for performance reasons ([f1ecd61](https://github.com/nvim-neorg/neorg/commit/f1ecd613d9c2911c7f7d5abd7f6f471614d05518)) * remove the `core.news` module ([1b9f8da](https://github.com/nvim-neorg/neorg/commit/1b9f8da57fb3e0bab9d1594fce87808ead8d650d)) * since 5.0 do not longer warn about deprecated `core.norg.*` modules ([19e0e8a](https://github.com/nvim-neorg/neorg/commit/19e0e8a3e983bf0a87c5c791863d4a480f0ff54c)) ## [4.6.0](https://github.com/nvim-neorg/neorg/compare/v4.5.0...v4.6.0) (2023-05-25) ### Features * **todo-items:** add missing "need input" icon and action ([#896](https://github.com/nvim-neorg/neorg/issues/896)) ([4cb0fa9](https://github.com/nvim-neorg/neorg/commit/4cb0fa9e56cf16672c258d1d97545d0526b506b5)) ### Bug Fixes * **esupports:** use structured api to avoid injection ([#899](https://github.com/nvim-neorg/neorg/issues/899)) ([e50b8ae](https://github.com/nvim-neorg/neorg/commit/e50b8aecb61dae1dd726fe00f40d3a554ba1b694)) * **tempus:** supply unprovided parameters from the current date when converting to `osdate` (supercedes [#897](https://github.com/nvim-neorg/neorg/issues/897)) ([f367451](https://github.com/nvim-neorg/neorg/commit/f36745161d82067e0f26865d93858fd3a15d8ad4)) ## [4.5.0](https://github.com/nvim-neorg/neorg/compare/v4.4.1...v4.5.0) (2023-05-24) ### Features * add colouring to TODO items ([238152a](https://github.com/nvim-neorg/neorg/commit/238152ab40ec1fb293fae75744942146876ed08f)) ### Bug Fixes * **metagen:** update generation to use user config for `updated` tag ([#882](https://github.com/nvim-neorg/neorg/issues/882)) ([6ed0f3a](https://github.com/nvim-neorg/neorg/commit/6ed0f3aa088e7b3141f01e3a82f3ec6517c34485)), closes [#865](https://github.com/nvim-neorg/neorg/issues/865) * TSInstall issues on macOS, hopefully once and for good ([#891](https://github.com/nvim-neorg/neorg/issues/891)) ([4988a6f](https://github.com/nvim-neorg/neorg/commit/4988a6f9166b6ac7b9ba5115e61dc3a2b13e820c)) ## [4.4.1](https://github.com/nvim-neorg/neorg/compare/v4.4.0...v4.4.1) (2023-05-17) ### Bug Fixes * **tempus:** paste correct weekday from calendar ([ba54231](https://github.com/nvim-neorg/neorg/commit/ba54231e14a31c0571ff7baa4828de121a5e3072)) * **tempus:** properly handle conversions w.r.t Sun-Sat/Mon-Sun ([e39fa1b](https://github.com/nvim-neorg/neorg/commit/e39fa1b1626fc6f4bb9f4695b15d7065561c2567)) ## [4.4.0](https://github.com/nvim-neorg/neorg/compare/v4.3.0...v4.4.0) (2023-05-16) ### Features * **journal:** allow `custom` to take in no arguments, in which case ([ea0497a](https://github.com/nvim-neorg/neorg/commit/ea0497aea783507ce640e909b6764be4fcd5a388)) ### Bug Fixes * **promo:** don't add whitespace to empty lines ([#852](https://github.com/nvim-neorg/neorg/issues/852)) ([a7291f4](https://github.com/nvim-neorg/neorg/commit/a7291f4662664d0c3be3016adff6767dc52f907d)) * **tempus:** don't use the `re` module if it doesn't exist ([#872](https://github.com/nvim-neorg/neorg/issues/872)) ([3c99638](https://github.com/nvim-neorg/neorg/commit/3c99638db0ce4293e221216bdda03a55da6ad82b)) ## [4.3.0](https://github.com/nvim-neorg/neorg/compare/v4.2.0...v4.3.0) (2023-05-15) ### Features * **calendar:** add `t` command for "today" ([e53a509](https://github.com/nvim-neorg/neorg/commit/e53a5099b5725162c8f0a626823cac4819a9427d)) * **hop:** allow users to jump to timestamps ([22b12fb](https://github.com/nvim-neorg/neorg/commit/22b12fb2301582fd9552ab10ac0c934cda4d0a14)) ### Bug Fixes * **hop:** assume <current-day> when some parameters to dates are not supplied ([65bf064](https://github.com/nvim-neorg/neorg/commit/65bf06493ecb411b1589ad345771ae29aa17cd33)) * **tempus:** days like `4th`/`2nd` would not get parsed properly ([7368a8a](https://github.com/nvim-neorg/neorg/commit/7368a8ae10a0bab32729bd00dcac6f24cb55a8ef)) ## [4.2.0](https://github.com/nvim-neorg/neorg/compare/v4.1.1...v4.2.0) (2023-05-15) ### Features * **tempus:** add `,id` (insert date) keybinding ([34f13ba](https://github.com/nvim-neorg/neorg/commit/34f13ba253c160e72ef7817a950508430ed050d1)) * **tempus:** add insert mode `<M-d>` keybind to insert a date ([b420f69](https://github.com/nvim-neorg/neorg/commit/b420f69602b23fa8fc2f7f6526f49838f9521b10)) * **tempus:** allow dates to be converted to norg-compatible dates with `tostring()` ([3ec5f96](https://github.com/nvim-neorg/neorg/commit/3ec5f96dfd673c2c2a34b09748518accf61ec677)) ### Bug Fixes * don't allow tempus to load unless the Neovim ver is at least 0.10.0 ([c4429fa](https://github.com/nvim-neorg/neorg/commit/c4429fa1e1eb0c3c5652495b00aa4e1c56068914)) * **tempus:** do not assume `osdate` has all fields set ([c37a104](https://github.com/nvim-neorg/neorg/commit/c37a104c992326f8924de783d667f7c4c34f92b7)) ## [4.1.1](https://github.com/nvim-neorg/neorg/compare/v4.1.0...v4.1.1) (2023-05-15) ### Bug Fixes * remove calendar as a dependency of `core.ui`, fix errors for people not on nightly ([cd26a22](https://github.com/nvim-neorg/neorg/commit/cd26a220e999cc9103a2502299d16ae8e6fab4d9)) ## [4.1.0](https://github.com/nvim-neorg/neorg/compare/v4.0.1...v4.1.0) (2023-05-14) ### Features * add `core.tempus` module for date management ([b73ec2f](https://github.com/nvim-neorg/neorg/commit/b73ec2f5e1b11864ca0628a842a53a617d5851ce)) * add left-right cursor movement ([ea588bb](https://github.com/nvim-neorg/neorg/commit/ea588bbc2cabe37f90652a8cb49bf8b286498d2a)) * add skeleton for the calendar UI element ([3c99106](https://github.com/nvim-neorg/neorg/commit/3c99106d64792533a3cf10ac6ef20a089e94c1ff)) * **calendar:** add `?` help page for custom input ([211b0ba](https://github.com/nvim-neorg/neorg/commit/211b0ba61b5cf8f4520b5e03f5235f6de87e4417)) * **calendar:** add `$` and `0`/`_` navigation keybinds ([0061928](https://github.com/nvim-neorg/neorg/commit/006192808d436c27f8ceca0fffcc4a238ec402a7)) * **calendar:** add `m`/`M`, `L`/`H` and `y`/`Y` keybinds for the monthly view ([9bf562d](https://github.com/nvim-neorg/neorg/commit/9bf562d4633abac71b749ad7380cfe010a4c3bd7)) * **calendar:** add basic help popup when `?` is invoked ([779d089](https://github.com/nvim-neorg/neorg/commit/779d089e17139acfdd2a4988c34eea892f29a475)) * **calendar:** allow many simultaneous calendars ([f816fe7](https://github.com/nvim-neorg/neorg/commit/f816fe77ef2abecff9e98d8d35ff48a453317cf0)) * **calendar:** generalize functions even further, allow for offsets ([d857c34](https://github.com/nvim-neorg/neorg/commit/d857c34fe7a4645501551f2b66dd7915b9575b4f)) * **calendar:** implement basic `i` functionality ([6713f40](https://github.com/nvim-neorg/neorg/commit/6713f40d5d1f9e7a0e8b80ffdc82d4fff79c16c0)) * **calendar:** render as many months as is possible on screen ([fa23767](https://github.com/nvim-neorg/neorg/commit/fa237674cf75bf2bbc62a438b1606b65cc277ebd)) * **core.ui.calendar:** add day of the month rendering ([8bc3364](https://github.com/nvim-neorg/neorg/commit/8bc3364f306d5df528193a8ca68fa8b4a45701ef)) * **core.ui.calendar:** add static calendar ui ([adbb415](https://github.com/nvim-neorg/neorg/commit/adbb4151677bf22c809f9b6dfd35de5e07da6c7a)) * **core.ui.calendar:** highlight the current day differently ([eada386](https://github.com/nvim-neorg/neorg/commit/eada386cc79c122b648580de50b1f825b74a9627)) * **core.ui.calendar:** implement more of the barebones UI ([364f44a](https://github.com/nvim-neorg/neorg/commit/364f44a7d1179d5aa98d1f4ff6b4b6b1b6078bd3)) * **core.ui.calendar:** make the calendar display full month names ([c6cc059](https://github.com/nvim-neorg/neorg/commit/c6cc059992c812712c9a2bb4075b2d9b31f84f5c)) * **core.ui:** let `create_split` take in a `height` variable ([7dbbe9d](https://github.com/nvim-neorg/neorg/commit/7dbbe9d236596d8990827e717ea892cd98e79b23)) * correctly handle year boundaries ([58b55e1](https://github.com/nvim-neorg/neorg/commit/58b55e16366ecd431bece7ba4d42d512b21b972e)) * implement `render_month` function ([343fb8d](https://github.com/nvim-neorg/neorg/commit/343fb8d02422fe2f2a3c791f2bdba0be95c3c96b)) * place cursor over current day when creating calendar ([3ce268b](https://github.com/nvim-neorg/neorg/commit/3ce268b703d321561b86e546c7633326b39fa494)) * **tempus:** add `to_lua_date` function ([ef62e53](https://github.com/nvim-neorg/neorg/commit/ef62e5308c684468a822684382d14de8f8f63193)) ### Bug Fixes * **calendar:** allow the view to be written to on rerender ([8e247d4](https://github.com/nvim-neorg/neorg/commit/8e247d414bcb0d1123b2b12c7ff29bdf36c50cbd)) * **calendar:** fix incorrect movement with `H` across boundaries of months with different lengths ([48face2](https://github.com/nvim-neorg/neorg/commit/48face25855d7844302b13a125363c30b8a6fe9a)) * **calendar:** fix rest of highlight groups ([ead4c4c](https://github.com/nvim-neorg/neorg/commit/ead4c4c53769839b5063fab71ebb92d155d53676)) * **calendar:** if another calendar is open then close it instead of erroring ([9751e7d](https://github.com/nvim-neorg/neorg/commit/9751e7d62af0b7e49ff788058154b966be205e2e)) * **calendar:** make distance between each month uniform and support modifying the distance between each month ([746354d](https://github.com/nvim-neorg/neorg/commit/746354dea70e9657f61531375329e407e7f5a203)) * **calendar:** make month rendering work again ([164028f](https://github.com/nvim-neorg/neorg/commit/164028fd621e3c5b56603d88d6d5e2ba5db51d42)) * **calendar:** overlapping month names in the calendar view ([709cf78](https://github.com/nvim-neorg/neorg/commit/709cf78410b6ea631192ad004d3f2b83761f9953)) * **calendar:** prevent the buffer from being modifiable after it has been filled ([351e103](https://github.com/nvim-neorg/neorg/commit/351e10326e0e2bb6166e165ddb6598e917e6d25c)) * **calendar:** properly display "today's day" in the calendar view ([74ee71a](https://github.com/nvim-neorg/neorg/commit/74ee71a446662f92afa3cbd49f6c980bdf25ae92)) * **calendar:** reversed namespace names ([77b214c](https://github.com/nvim-neorg/neorg/commit/77b214cef220580cdcf527265a15ef980e7bcaf3)) * **core.ui.calendar:** logic error when parsing virt_text length for `set_logical_extmark` ([d5b29ee](https://github.com/nvim-neorg/neorg/commit/d5b29eea8e09d7bd0add778c6818539719914301)) * **core.ui.calendar:** wrong extmark being queried in month render routine ([46624b9](https://github.com/nvim-neorg/neorg/commit/46624b9a02e0d0e928026a0fd4852c4dd3ca7e0d)) ## [4.0.1](https://github.com/nvim-neorg/neorg/compare/v4.0.0...v4.0.1) (2023-05-11) ### Bug Fixes * **highlights.scm:** free form open/close chars would not be concealed ([5de014e](https://github.com/nvim-neorg/neorg/commit/5de014e7cc3dc6eed0a62854fe8ba58f664d97ea)) * **qol.toc:** display headings with TODO statuses unless the status is "cancelled" ([2e44346](https://github.com/nvim-neorg/neorg/commit/2e44346813310de9afc411e2348cf2be8540f70c)) * stop syntax processing if a buffer is already closed ([#859](https://github.com/nvim-neorg/neorg/issues/859)) ([cc2834a](https://github.com/nvim-neorg/neorg/commit/cc2834ae2beb2d5baa75d15848a94dae022faa2c)) ## [4.0.0](https://github.com/nvim-neorg/neorg/compare/v3.2.2...v4.0.0) (2023-05-05) ### ⚠ BREAKING CHANGES * move all `gt*` keybinds to `t*` * remove `core.news` ### Features * add basic cheatsheet (viewable via `:h neorg-cheatsheet`) ([d3e37a6](https://github.com/nvim-neorg/neorg/commit/d3e37a681743181a34dcfa7adb6ec61fb5aeb63c)) * **keybinds:** warn when a deprecated keybind is used (will be removed with `5.0`) ([e20d3c3](https://github.com/nvim-neorg/neorg/commit/e20d3c324b091cac29ccd7ec8431d24aa9b792c8)) ### Bug Fixes * **concealer:** buggy debounce logic causing visual artifacts (especially on the first line of a buffer) ([45388fc](https://github.com/nvim-neorg/neorg/commit/45388fc0478e8d1273bd80789e7e1af1df76458f)) * **concealer:** stop concealer if buffer is not loaded ([#836](https://github.com/nvim-neorg/neorg/issues/836)) ([6aa9fd3](https://github.com/nvim-neorg/neorg/commit/6aa9fd303c807ed1ca3fb15cdeab1e322d02fd31)) * **dirman.expand_path:** search for both `$/` and `$\` in links to support windows paths ([#830](https://github.com/nvim-neorg/neorg/issues/830)) ([160d40f](https://github.com/nvim-neorg/neorg/commit/160d40f5261be5149842942adbf260d6e359d9ec)) * **esupports.hop:** anchors to files woul dresult in a "link not found" ([#688](https://github.com/nvim-neorg/neorg/issues/688)) ([3009adf](https://github.com/nvim-neorg/neorg/commit/3009adf2cf48aedcbb309d0765e0fbbb64a0fdf4)) * **keybinds.lua:** remove dead `toc` keybinds ([06666f2](https://github.com/nvim-neorg/neorg/commit/06666f298e146d758d691366ca3465a3bd1e3f7f)) ### Code Refactoring * move all `gt*` keybinds to `<LocalLeader>t*` ([f67110d](https://github.com/nvim-neorg/neorg/commit/f67110d11d37fde09756eb2de8a1814d04a4a03b)) * remove `core.news` ([4086d9f](https://github.com/nvim-neorg/neorg/commit/4086d9f17d823cfe5a13e7b12b30e13b5d3b796d)) ## [3.2.2](https://github.com/nvim-neorg/neorg/compare/v3.2.1...v3.2.2) (2023-04-27) ### Bug Fixes * **core.ui:** clear the `winbar` option in Neorg popups to prevent "not enough room" errors ([fcebf9f](https://github.com/nvim-neorg/neorg/commit/fcebf9f6caf0667f99b1481e2c0a49f0eeb68fe9)) * **esupports.hop:** broken definitions and footnotes ([#733](https://github.com/nvim-neorg/neorg/issues/733)) ([94cf7d2](https://github.com/nvim-neorg/neorg/commit/94cf7d2889b386ce1313e80c8c04adf18872c028)) ## [3.2.1](https://github.com/nvim-neorg/neorg/compare/v3.2.0...v3.2.1) (2023-04-27) ### Bug Fixes * **export:** `gsub` export links that contain `#`, `?`. closes [#807](https://github.com/nvim-neorg/neorg/issues/807) ([#816](https://github.com/nvim-neorg/neorg/issues/816)) ([7f3a3b8](https://github.com/nvim-neorg/neorg/commit/7f3a3b850c8d4b73e7f85971aae2a96162bcb150)) * **export:** markdown export for horizontal_line ([#820](https://github.com/nvim-neorg/neorg/issues/820)) ([2178447](https://github.com/nvim-neorg/neorg/commit/217844796e00a1cea7c051435f9c49bee25e7caa)) ## [3.2.0](https://github.com/nvim-neorg/neorg/compare/v3.1.0...v3.2.0) (2023-04-22) ### Features * add `core.pivot` for toggling list types ([cbf383f](https://github.com/nvim-neorg/neorg/commit/cbf383ff4eca0e23a24d4244af20bed415ed400c)) * **keybinds:** add default keybinds for `core.pivot` ([2f49628](https://github.com/nvim-neorg/neorg/commit/2f496283504dcfb30d9ee60101a8290e743c1753)) * **pivot:** add `core.pivot.invert-list-type` keybind ([2d0446a](https://github.com/nvim-neorg/neorg/commit/2d0446a2d8e3789bbd17bbb3cb97e73befccb327)) ### Bug Fixes * **core.summary:** wrong module name in header, wrong internal command names ([a046900](https://github.com/nvim-neorg/neorg/commit/a0469001430a68f521d3292f8a8252655cfda941)) * **docgen:** installation documentation link for wiki ([ba8b31d](https://github.com/nvim-neorg/neorg/commit/ba8b31dc2491f80b9f65fadbafdfd94d6ef26988)), closes [#548](https://github.com/nvim-neorg/neorg/issues/548) * **summary:** broken wiki entry ([69fbabf](https://github.com/nvim-neorg/neorg/commit/69fbabfb5764cd164453a764174cf5cfa813ae60)) ## [3.1.0](https://github.com/nvim-neorg/neorg/compare/v3.0.0...v3.1.0) (2023-04-19) ### Features * warn access to `core.norg` modules instead of breaking ([ed761a5](https://github.com/nvim-neorg/neorg/commit/ed761a5c5a9100861034b31978049401444fd6fb)) ## [3.0.0](https://github.com/nvim-neorg/neorg/compare/v2.0.1...v3.0.0) (2023-04-19) ### ⚠ BREAKING CHANGES * move all `core.norg.*` modules into `core.*` * **Makefile:** remove `install_pre_commit` target * move `core.norg.dirman.summary` -> `core.summary` * **summary:** refactor of the `core.norg.dirman.summary` module * **docgen:** wipe whole wiki on every reparse ### Features * add `dirman.summary` module ([#750](https://github.com/nvim-neorg/neorg/issues/750)) ([93c40f2](https://github.com/nvim-neorg/neorg/commit/93c40f2e38a0770e9ce95787c8363320344a87c3)) * add `Home.md` generation capability ([6bdf557](https://github.com/nvim-neorg/neorg/commit/6bdf557ece33850f9733dddc343369d743a51564)) * **ci:** add `version_in_code.yml` workflow ([5746245](https://github.com/nvim-neorg/neorg/commit/5746245756bac83fcf02338c93bc87f6089e2bf3)) * cleanup, add document comments to all modules, add more error checks ([81284c1](https://github.com/nvim-neorg/neorg/commit/81284c1e2f6e441f6532678b76ff5378396dda2c)) * **config.lua:** add `norg_version`, bump `version` to `3.0.0` ([8d76723](https://github.com/nvim-neorg/neorg/commit/8d767232a571513a3ab8c5c14ddc6f26d09aa98a)) * **core.integrations.treesitter:** Return all same attributes of a tag ([bedf13d](https://github.com/nvim-neorg/neorg/commit/bedf13dbcef63099a52dd4f160d90c46fc1de440)) * **dirman:** add new `use_popup` option for `dirman` ([#743](https://github.com/nvim-neorg/neorg/issues/743)) ([6350254](https://github.com/nvim-neorg/neorg/commit/63502544afde1c15d79ce097ad1928314cb8c7cd)) * **docgen:** add `module` page generator ([17496a8](https://github.com/nvim-neorg/neorg/commit/17496a8e975f1bd9d896d7cc78a6e61d1a131245)) * **docgen:** add basic rendering skeleton logic ([215719e](https://github.com/nvim-neorg/neorg/commit/215719ece560400592c2fef2ed75ab57430baf9b)) * **docgen:** add comment integrity checking logic ([799886f](https://github.com/nvim-neorg/neorg/commit/799886f7ba5a072a453d5a90708686109ce4fa21)) * **docgen:** allow strings as table keys ([4adf04e](https://github.com/nvim-neorg/neorg/commit/4adf04e05d98b6bcb6a3aac4cab60d64fd0d86f9)) * **docgen:** auto-open <details> tags that contain tables or lists ([1f2e0dc](https://github.com/nvim-neorg/neorg/commit/1f2e0dc23f6944bad660553ad4550847cf68e096)) * **docgen:** differentiate between lists and tables ([c0062e5](https://github.com/nvim-neorg/neorg/commit/c0062e5a75226f063b59eb5ee8250cb4da6ea202)) * **docgen:** differentiate empty and nonempty tables/lists ([0ab1a8d](https://github.com/nvim-neorg/neorg/commit/0ab1a8d469667d6b763e299c390a3e9bc7ea1a13)) * **docgen:** implement `Required By` field ([7033c4b](https://github.com/nvim-neorg/neorg/commit/7033c4bd2dc4633d0874b2da05b0ae67928b1117)) * **docgen:** implement `Required By` section ([15bf71b](https://github.com/nvim-neorg/neorg/commit/15bf71b15e07917e0f3a55de44e79fcdfbfc557d)) * **docgen:** implement configuration_options parsing logic ([b34658a](https://github.com/nvim-neorg/neorg/commit/b34658a21602fdd286889c2e57bf2d68d63d4472)) * **docgen:** implement function rendering, fix incorrect interpretation of function calls ([a023488](https://github.com/nvim-neorg/neorg/commit/a023488944473dfcd611308d5e21b7a9b2d7690d)) * **docgen:** implement table rendering ([9074328](https://github.com/nvim-neorg/neorg/commit/907432885e40a16f064d4406cd45efeb895f0962)) * **docgen:** indent nested table keys ([9cf679a](https://github.com/nvim-neorg/neorg/commit/9cf679a24f3bb9db145ae4dfbeb43878a49839e3)) * **docgen:** massive structure changes, implement proper table rendering ([42b8728](https://github.com/nvim-neorg/neorg/commit/42b8728f291072b9d8a11bdf9c7e205ef15fb94d)) * **docgen:** parse config tables ([93c41e1](https://github.com/nvim-neorg/neorg/commit/93c41e1f0aa290d0ad2e2590753312c71a782395)) * **docgen:** perform `[@module](https://github.com/module)` lookups, pasre complex data structures like tables ([19f2381](https://github.com/nvim-neorg/neorg/commit/19f23811ba0366fe3ec9423d26aec33d9d34fcc2)) * **docgen:** properly implement recursive table scanning ([33e06b8](https://github.com/nvim-neorg/neorg/commit/33e06b8d0fc2e7a9b386fc95e6bce4dfb714e56f)) * **docgen:** sort entries when rendering ([b420e70](https://github.com/nvim-neorg/neorg/commit/b420e70532766475bce0bf1d34129e711a31e21a)) * **docgen:** start generating true module pages ([5115d5c](https://github.com/nvim-neorg/neorg/commit/5115d5cd4bd1fefb11f82cb3e3da18b74d4e9b9e)) * **helpers/lib:** add `read_files` and `title` functions ([d59f41b](https://github.com/nvim-neorg/neorg/commit/d59f41b78755b102a41d172d4e3f64d59cb86b8b)) * **helpers:** add `ensure_nested` function ([2c4e8d0](https://github.com/nvim-neorg/neorg/commit/2c4e8d02feb7f1e6878307e7813a9f13ec000a73)) * **helpers:** Add wrapper to vim.notify ([#778](https://github.com/nvim-neorg/neorg/issues/778)) ([c278f6f](https://github.com/nvim-neorg/neorg/commit/c278f6f895c6b2f9ef4fc217ed867675108d804e)) * implement _Sidebar generation ([733b74c](https://github.com/nvim-neorg/neorg/commit/733b74c92481bc955f1f46594e50fb4931ab3cf5)) * implement necessary APIs for complex data structure parsing ([b78f01c](https://github.com/nvim-neorg/neorg/commit/b78f01cd951b8ecfe7e842d31f609e6e4d5ac9db)) * implement new docgen featuring top-comment validation ([b77fbd5](https://github.com/nvim-neorg/neorg/commit/b77fbd52f96db049687901ac1f0a8aea7ab4bdfa)) * **indent:** adapt indentation of nestable detached modifiers when a detached modifier extension is found ([56e59da](https://github.com/nvim-neorg/neorg/commit/56e59daff56ba7f4d76b11ff3fc6dd70c4b54524)) * **makefile:** add `local-documentation` option ([ed20f79](https://github.com/nvim-neorg/neorg/commit/ed20f796f5bb337d230f5d33a3f6ba420a8d30a4)) * **qol.todo_items:** add new `create_todo_items` option ([d810aa4](https://github.com/nvim-neorg/neorg/commit/d810aa43c96301db35af351306eab54e35071d57)) * **qol.todo_items:** add new `create_todo_parents` option (false by default) ([6b6ef04](https://github.com/nvim-neorg/neorg/commit/6b6ef04e5fb0a5b1c3ff59699a9371afd659d9ff)) * **qol.todo_items:** when only done and uncertain items are present in ([1d6b0b0](https://github.com/nvim-neorg/neorg/commit/1d6b0b056b097e9f4bacf8877c49fdacbc445b2c)) * strip leading `--` from comments ([ecea630](https://github.com/nvim-neorg/neorg/commit/ecea6305a82007b6c8c509fd594f8b52c3331021)) * **summary:** implement `metadata` strategy and reimplement summary generation code ([f948288](https://github.com/nvim-neorg/neorg/commit/f9482881315d49d0a35206c7936a7f48c20dfcbf)) * **toc:** add `close_after_use` configuration option ([#785](https://github.com/nvim-neorg/neorg/issues/785)) ([e5d7fbb](https://github.com/nvim-neorg/neorg/commit/e5d7fbb0291e658f78545c29318c9162cf505d15)) ### Bug Fixes * `:Neorg journal today` would fail on alternative path separators ([#749](https://github.com/nvim-neorg/neorg/issues/749)) ([e7a5054](https://github.com/nvim-neorg/neorg/commit/e7a50542ad9921a8c7d652eeca6a9006cc024b79)) * **base.lua:** don't assign the `extension` flag to parent modules, only to the imports themselves ([fa5f561](https://github.com/nvim-neorg/neorg/commit/fa5f56163510eb00a0d75bec81d40d901c175d3b)) * **clipboard.code-blocks:** don't cut off characters from non-visual-line selection ([744ae49](https://github.com/nvim-neorg/neorg/commit/744ae49fe5fab9e54de96282778a202f85a2f37b)) * **code.looking-glass:** Use last attribute as start row of looking-glass (fix [#777](https://github.com/nvim-neorg/neorg/issues/777)) ([beef6fd](https://github.com/nvim-neorg/neorg/commit/beef6fd9420d6a798ddd796779b96f006b14ca12)) * **commands.return:** don't override the workspace to `default` after running `:Neorg return` ([169c7be](https://github.com/nvim-neorg/neorg/commit/169c7bee8a5f101c63c3a473a577dce079f7ddec)) * **concealer:** whenever running any scheduled command ensure that the buffer exists first ([b926416](https://github.com/nvim-neorg/neorg/commit/b9264161d0ef10ee61ace6ebeb0a55ca461b638a)) * **core.clipboard.code-blocks:** module would not work past version `1.0.0` ([ac88283](https://github.com/nvim-neorg/neorg/commit/ac8828369cb2a4b2e1e17e6b495645585ed2a37b)) * **core.clipboard.code-blocks:** visual selection would cut off one character too little ([87ed4bf](https://github.com/nvim-neorg/neorg/commit/87ed4bfde4a00a4cf4279298de02280bf11c7a74)) * **core.export.markdown:** Update markdown exporter for new todo syntax (fix [#757](https://github.com/nvim-neorg/neorg/issues/757)) ([336416f](https://github.com/nvim-neorg/neorg/commit/336416f6c41777a4025cc80b8a085e21e758931f)) * **core.itero:** preserve indentation on continued items ([92c31c4](https://github.com/nvim-neorg/neorg/commit/92c31c491caedd7d1a82b42d4ba6c2227c05d930)) * **core.norg.esupports.hop:** Make hop on anchors work again ([#756](https://github.com/nvim-neorg/neorg/issues/756)) ([d38a229](https://github.com/nvim-neorg/neorg/commit/d38a22940aaa55351cd4dc106540fa302fad4f0d)) * **core.norg.journal:** fixes [#736](https://github.com/nvim-neorg/neorg/issues/736) , now generates TOC correctly ([19c5558](https://github.com/nvim-neorg/neorg/commit/19c555836bc31f482e0ea42f08d150110754644f)) * **core.promo:** don't error when the concealer is not loaded ([#767](https://github.com/nvim-neorg/neorg/issues/767)) ([3e09f69](https://github.com/nvim-neorg/neorg/commit/3e09f698b8a4151f2b4f77ee917e4b54388bc97a)) * **dirman:** automatically create the index file if it exists when running `:Neorg index` ([7ce2db5](https://github.com/nvim-neorg/neorg/commit/7ce2db5d2eeec37b4a4c4bc43009c4741c3755da)) * **dirman:** corrected win width and height calculation ([9766bef](https://github.com/nvim-neorg/neorg/commit/9766bef893ec993af9408ea0d44a8f13adbd1e80)) * **dirman:** don't create `index.norg` files in the default workspace when running `:Neorg index` ([c60747f](https://github.com/nvim-neorg/neorg/commit/c60747fcc567d7eb50b16c2007bcfd3a81a934d1)) * **docgen:** `<h6>` tags not being rendered properly ([d0a0da0](https://github.com/nvim-neorg/neorg/commit/d0a0da017135b48c5f4a325bfaedbcdf1ca79fe3)) * **docgen:** could not find module `neorg` ([b68a945](https://github.com/nvim-neorg/neorg/commit/b68a945d6b1a8c2f8c57e0c366f224a162f391e3)) * **docgen:** display listed modules in alphabetical order ([264b451](https://github.com/nvim-neorg/neorg/commit/264b451d74d3e4bc3f856d087070b9dac46f8e90)) * **docgen:** don't double-render numeric values ([35df191](https://github.com/nvim-neorg/neorg/commit/35df1918de321617972f10edd88d9c32cc8102a2)) * **docgen:** don't render description tags if no description is present ([64dc28d](https://github.com/nvim-neorg/neorg/commit/64dc28deea9a7f9c52c3ba5343f1ce3c754a566e)) * **docgen:** don't unnecessarily copy parsers ([46e7936](https://github.com/nvim-neorg/neorg/commit/46e79366775136592540fbe5f0532c001012daa5)) * **docgen:** incorrect wiki paths ([2dbead6](https://github.com/nvim-neorg/neorg/commit/2dbead687053610147161ecda1a908070720731f)) * **docgen:** internal modules that were part of `core.defaults` would not be displayed in the developer modules section ([c3099eb](https://github.com/nvim-neorg/neorg/commit/c3099ebd595d3ec491613c297f4199e316f85853)) * **docgen:** list items with no summary would break rendering ([b69ea57](https://github.com/nvim-neorg/neorg/commit/b69ea57029a2c62e72ea86c93d295102059c58ab)) * **docgen:** lists within lists would never be rendered ([06894bb](https://github.com/nvim-neorg/neorg/commit/06894bb090ad6deb52ba7b19f7e13bbb41385a63)) * **docgen:** make the spacing nicer to look at ([426ca24](https://github.com/nvim-neorg/neorg/commit/426ca246e310a212cef5f6bba367d7ebc84bf70b)) * **docgen:** remove debug log ([8ffcaed](https://github.com/nvim-neorg/neorg/commit/8ffcaed1095743b8474a16f25855b809cf4fe65d)) * **docgen:** this should work now i think (after 20 tries) ([72d3d49](https://github.com/nvim-neorg/neorg/commit/72d3d4981d85fd1114d3185e40cc135a7892a4a4)) * **docgen:** use minimal_init.vim instead of custom_init.vim ([a7cb7ab](https://github.com/nvim-neorg/neorg/commit/a7cb7ab443c24fbd1d9f696abddd91c16f05d842)) * **docgen:** wrong `require` order in `docgen.lua` ([7494b51](https://github.com/nvim-neorg/neorg/commit/7494b51a61cc31371514cb7b2e1ccdf4cef164e2)) * finalize `version_in_code.yml` CI (it works yay) ([db9ed0b](https://github.com/nvim-neorg/neorg/commit/db9ed0b98ba30e2b783ea9da0fddda4cf6b2a47e)) * **metagen:** use `norg_version` ([a5c2553](https://github.com/nvim-neorg/neorg/commit/a5c25531de2790133310ad874fcbbb976d082c78)) * neovim 0.9 vim.treesitter.parse_query deprecation ([#784](https://github.com/nvim-neorg/neorg/issues/784)) ([f4a9759](https://github.com/nvim-neorg/neorg/commit/f4a9759e53fadaece9d93118a0471ddffd05d394)) * **qol.todo_item:** `<C-space>` would not create a new TODO item with ([fc45beb](https://github.com/nvim-neorg/neorg/commit/fc45bebde0fc9811ca4e770e2ba29c791035c885)) * **qol.todo_items:** `<C-space>` would not respect the `create_todo_items` option ([e764b92](https://github.com/nvim-neorg/neorg/commit/e764b92065ddd6bc206aaefd92837be8d0bd8419)) * **qol.todo_items:** TODO attributes would be erroneously assigned multiple times ([1303097](https://github.com/nvim-neorg/neorg/commit/13030974acee5a49dd02c51bedeac104b4f33cb7)) * **summary:** appropriately indent nested entries ([b725a58](https://github.com/nvim-neorg/neorg/commit/b725a58f25525efc1c39a13a09e6cec0c1c0ba4d)) * **version_in_code.yml:** perform checkout in the current directory ([3d7ad5a](https://github.com/nvim-neorg/neorg/commit/3d7ad5aa4b1277ea0f3ebb93ea79179eda5f6e27)) * **version_in_code.yml:** use `fetch-depth` of `0` ([2e8fa52](https://github.com/nvim-neorg/neorg/commit/2e8fa524d2cc73002378875970048d70ae70cc0b)) ### Performance Improvements * **concealer:** don't rerender the whole file on every single BufEnter ([7419cbb](https://github.com/nvim-neorg/neorg/commit/7419cbb7262200dd94df9d398ee1e7f5b9503a50)) ### Miscellaneous Chores * **docgen:** wipe whole wiki on every reparse ([09cb3e6](https://github.com/nvim-neorg/neorg/commit/09cb3e62022ff0f93965800a728ed698db540240)) ### ref * **Makefile:** remove `install_pre_commit` target ([9a497f5](https://github.com/nvim-neorg/neorg/commit/9a497f5e8195e5b974d520f937a946cb8819f320)) * move `core.norg.dirman.summary` -> `core.summary` ([254b6a6](https://github.com/nvim-neorg/neorg/commit/254b6a60b6c9f845400d2bcb0728ee7f38823781)) * **summary:** refactor of the `core.norg.dirman.summary` module ([a2fe3ee](https://github.com/nvim-neorg/neorg/commit/a2fe3eea24c628fa15b7f854cef6c7acaf9ec3f9)) ### Code Refactoring * move all `core.norg.*` modules into `core.*` ([a5824ed](https://github.com/nvim-neorg/neorg/commit/a5824edf6893b8602e560ed7675c0d4174e263e4)) ## [2.0.1](https://github.com/nvim-neorg/neorg/compare/v2.0.0...v2.0.1) (2023-02-02) ### Bug Fixes * completion for TODO items ([#711](https://github.com/nvim-neorg/neorg/issues/711)) ([9184027](https://github.com/nvim-neorg/neorg/commit/91840274112f1286ff5f4063ac6f515683b6dc67)) * **core.norg.journal:** add proper error handling for `vim.loop.fs_scandir` ([4a9a5fe](https://github.com/nvim-neorg/neorg/commit/4a9a5fe13cd454692fc4db0b27783cd005e6be56)) * **treesitter:** don't constantly log errors about erroneous document syntax trees ([9f8b0a1](https://github.com/nvim-neorg/neorg/commit/9f8b0a1759d883fae901579ea83b3ffbfc81a53b)) ## [2.0.0](https://github.com/nvim-neorg/neorg/compare/v1.1.1...v2.0.0) (2023-01-06) ### ⚠ BREAKING CHANGES * **core.norg.qol.toc:** rewrite the table of contents implementation ### Features * **core.export:** add `NeorgExportComplete` user autocommand ([8b10e61](https://github.com/nvim-neorg/neorg/commit/8b10e61d2f2c5e626849f9a6f8cb4399c28a1a47)) * **core.norg.qol.toc:** add multiple buffer handling logic ([467e311](https://github.com/nvim-neorg/neorg/commit/467e3113c32b8b9f1950a9425aa7b74c13cd88b8)) * **core.norg.qol.toc:** implement `qflist` generation option ([77c5149](https://github.com/nvim-neorg/neorg/commit/77c514970a9d4648b05b2334a060263666f588e2)) * **treesitter:** add `execute_query` function ([310ebaa](https://github.com/nvim-neorg/neorg/commit/310ebaaef538dfd41d02a2903663be05fd38834b)) ### Bug Fixes * **core.ui:** do not modify the user's `scrolloffset` ([bd2e58c](https://github.com/nvim-neorg/neorg/commit/bd2e58cf6f9d42527aa2b692fb187eafa82bd91e)) ### Performance Improvements * further optimize `toc` infirm tag grabber ([5e8d059](https://github.com/nvim-neorg/neorg/commit/5e8d05968e04f7945576d50a6b1576cc722f96fc)) * optimize the `toc` infirm tag grabber code ([a41bd4a](https://github.com/nvim-neorg/neorg/commit/a41bd4a92afefb7e2630b821b59f7707a054baac)) ### ref * **core.norg.qol.toc:** rewrite the table of contents implementation ([c0104fb](https://github.com/nvim-neorg/neorg/commit/c0104fb9faed3b3213e4e275a55a522a299a2d0e)) ## [1.1.1](https://github.com/nvim-neorg/neorg/compare/v1.1.0...v1.1.1) (2023-01-05) ### Bug Fixes * **core.export:** incorrect exporting of code blocks with no parameters ([#701](https://github.com/nvim-neorg/neorg/issues/701)) ([0922815](https://github.com/nvim-neorg/neorg/commit/0922815837a374bd0b2a3cf0477b54e6668e133d)) ## [1.1.0](https://github.com/nvim-neorg/neorg/compare/v1.0.1...v1.1.0) (2023-01-05) ### Features * keep checkboxes with `core.itero` ([#663](https://github.com/nvim-neorg/neorg/issues/663)) ([00532bd](https://github.com/nvim-neorg/neorg/commit/00532bd997d2aef0384ed8f11500d33d229a7e53)) ### Bug Fixes * **core.export.markdown:** incorrectly exported code blocks ([dd2750c](https://github.com/nvim-neorg/neorg/commit/dd2750c0e4d847b67a6ead79ff5043e671cac8bd)) * **folds:** correctly fold document metadata ([adc000a](https://github.com/nvim-neorg/neorg/commit/adc000aadd41e68e4de8a2d1bb90b2e910ffef1b)) ## [1.0.1](https://github.com/nvim-neorg/neorg/compare/1.0.0...v1.0.1) (2022-12-23) ### Bug Fixes * **core.looking-glass:** buffer being closed for no reason after leaving buffer ([828a37f](https://github.com/nvim-neorg/neorg/commit/828a37fe1f008dbfd70cd7fc0f7ba9d0bc75da2a)) * do not run tests for nightly/neorg-main, as GTD is no longer existent ([37f1f9a](https://github.com/nvim-neorg/neorg/commit/37f1f9a44ba65603b5992fc36761c61d921fab78)) ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: Makefile ================================================ documentation: nvim --headless -u docgen/minimal_init.vim -c "cd ./docgen" -c "source init.lua" -c 'qa' local-documentation: nvim --headless -c "cd ./docgen" -c "source init.lua" -c 'qa' format: stylua -v --verify . check: nix flake check shell: nix develop integration-test: nix run ".#integration-test" test: LUA_PATH="$(shell luarocks path --lr-path --lua-version 5.1 --local)" \ LUA_CPATH="$(shell luarocks path --lr-cpath --lua-version 5.1 --local)" \ luarocks test --local --lua-version 5.1 ================================================ FILE: README.md ================================================

**:warning: Neorg `9.0.0` has introduced some breaking changes! Please see [this blog post](https://vhyrro.github.io/posts/neorg-9-0-0/) on what changed.** ## Summary
Neorg (_Neo_ - new, _org_ - organization) is a Neovim plugin designed to reimagine organization as you know it. Grab some coffee, start writing some notes, let your editor handle the rest. ### What is Neorg? Neorg is an all-encompassing tool based around structured note taking, project and task management, time tracking, slideshows, writing typeset documents and much more. The premise is that all of these features are built on top of a single base file format (`.norg`), which the user only has to learn once to gain access to all of Neorg's functionality. Not only does this yield a low barrier for entry for new users it also ensures that all features are integrated with each other and speak the same underlying language. The file format is built to be expressive and easy to parse, which also makes `.norg` files easily usable anywhere outside of Neorg itself. A good way of thinking about Neorg is as a plaintext environment which can be adapted to a variety of use cases. If a problem can be represented using raw text, it can be solved using Neorg. ###### :exclamation: **IMPORTANT**: Neorg is young software. We consider it stable however be prepared for occasional breaking workflow changes. Make sure to pin the version of Neorg you'd like to use and only update when you are ready. ## 🌟 Tutorial A video tutorial may be found on Youtube: ## 📦 Installation Neorg's setup process is slightly more complex than average, so we encourage you to be patient :) **Neorg requires Neovim 0.10 or above to function. After you're done with the installation process, run `:checkhealth neorg` to see if everything's correct!** ### `neorg-kickstart` Not bothered to set up Neovim on your own? Check out our [kickstart config](https://github.com/nvim-neorg/neorg/wiki/Kickstart) which will get you up and running with Neorg without any prior Neovim configuration knowledge. ### `rocks.nvim` One way of installing Neorg is via [rocks.nvim](https://github.com/nvim-neorocks/rocks.nvim).
Installation snippet. - Run `:Rocks install rocks-config.nvim` (if you don't have it already!). - Run `:Rocks install neorg`. - From the root of your configuration (`~/.config/nvim/` on unix-like systems), create a `lua/plugins/neorg.lua` file and place the following content inside: ```lua require("neorg").setup() ``` For the time being you also need `nvim-treesitter` installed. - `:Rocks install nvim-treesitter-legacy-api` - Just like the `neorg.lua` file, create a `lua/plugins/treesitter.lua` file and place the following content inside: ```lua require("nvim-treesitter.configs").setup({ highlight = { enable = true, }, }) ``` The last three steps will eventually not be required to run Neorg.
### `lazy.nvim` To install Neorg via lazy, first ensure that you have `luarocks` installed on your system. On Linux/Mac, this involves installing using your system's package manager. On Windows, consider the [Lua for Windows](https://github.com/rjpcomputing/luaforwindows) all-in-one package.
Click for installation snippet. ```lua { "nvim-neorg/neorg", lazy = false, -- Disable lazy loading as some `lazy.nvim` distributions set `lazy = true` by default version = "*", -- Pin Neorg to the latest stable release config = true, } ``` While lazy [supports lazy-loading upon specific commands and filetypes](https://lazy.folke.io/spec#spec-lazy-loading), it can cause neorg to load incorrectly, leading to a 'broken' plugin. Lazy load at your own risk, and disable lazy loading as your first debugging step.
### `packer.nvim` Neorg can be installed purely via luarocks on packer, pulling in all required dependencies in the process. It is not recommended to use packer as it is now unmaintained.
Click for installation snippet. ```lua use { "nvim-neorg/neorg", rocks = { "lua-utils.nvim", "nvim-nio", "nui.nvim", "plenary.nvim", "pathlib.nvim", "nvim-treesitter-legacy-api" }, tag = "*", -- Pin Neorg to the latest stable release config = function() require("neorg").setup() end, } ```
### Other Plugin Managers Because of the complexities of `luarocks`, we are choosing not to support other plugin managers for the time being. It is actively on our TODO list, however! ## 📚 Further Learning After you have installed Neorg, we recommend you head over to either the Youtube tutorial series or to the [wiki](https://github.com/nvim-neorg/neorg/wiki)! ## Credits Massive shoutouts go to all the contributors actively working on the project together to form a fantastic integrated workflow: - [mrossinek](https://github.com/mrossinek) - for basically being my second brain when it comes to developing new features and adding new syntax elements - [danymat](https://github.com/danymat) - for creating the excellent foundations for the up and coming GTD system And an extra thank you to: - [Binx](https://github.com/dvchoudh) - for making that gorgeous logo for free! - [bandithedoge](https://github.com/bandithedoge) - for converting the PNG version of the logo into SVG form ## Support Love what I do? Want to see more get done faster? Want to support future projects? Any sort of support is always heartwarming and fuels the urge to keep going :heart:. You can show support here: - [Buy me a coffee!](https://buymeacoffee.com/vhyrro) - [Support me via Github Sponsors](https://github.com/sponsors/vhyrro) - [Support me on Patreon](https://patreon.com/vhyrro) Immense thank you to all of the sponsors of my work!
vsedov   molleweide   danymat   jgregoire   bottd   kvodenicharov   
================================================ FILE: ROADMAP.md ================================================
# Neorg Roadmap
Given the rapid growth and adoption of Neorg it's critical that a development plan is established to not only help the development team keep track of progress but to also facilitate and encourage contributions from the flourishing community. This file is written and maintained in Markdown for easy viewing on GitHub. It will be switched to a `.norg` file when possible. # Neorg ## Miscellaneous - [ ] Make `core.clipboard.code-blocks` work with a visual selection. - [x] Reimplement the `core.maneouvre` module, which has been deprecated since `1.0`. - [ ] The `a` and `b` commands in the hop module are not implemented. - [ ] Readd colouring to TODO items. ## Workflow - [x] [The Calendar UI](#calendar) - [ ] The GTD Methodology - [ ] Zettelkasten - [ ] The `sc-im`esque Table Editor To achieve the above, a set of cross-platform tools must be initially developed. For the motivation behind this, see the [external tools](#external-tooling) section. ## Documentation - [ ] Develop and ship a Neorg landing page with documentation, presumably with docasaurus. - [x] Provide a written tutorial on how to get started with Neorg. - [ ] Provided a tutorial on how to develop for Neorg (modules, events, etc.). - [ ] Provide a dropdown in the wiki which will display a module's entire configuration as a lua snippet. ### Calendar The calendar is a planned sophisticated and flexible tool for selecting a date. While it sounds rather trivial, the calendar is the main user interaction for any date related operation. Because of this, there are many components to a calendar. At its core the calendar is simply supposed to allow the user to select a date. This is such a vast concept however that the calendar should be able to handle a variety of situations/contexts. #### Context Switching Depending on the preferred context, the calendar should be able to work in four major views - `DAILY`, `WEEKLY`, `MONTHLY` and `YEARLY`. Each mode has a different layout fit for the task at hand, and displays varying amounts of information based on said view. - The daily view displays only the current day, but allows you to quickly select an hour in that day. Moving left or right moves one hour forward or backward, respectively. The view is vertical, meaning an hour takes up a horizontal bar and they are stacked top to bottom starting from midnight. - The weekly view displays all 7 days of the week. Moving forwards or backwards moves one day forward or backward, respectively. This view is also vertical, where each day takes up a single bar of the view. - The monthly view shows all days of the month in a horizontally rendered fashion. It is the default mode if none is specified. - The yearly view is meant to summarize more than it is supposed to serve any function. The details of this view have not yet been fully drawn out. As with all things Neorg, other modules should be free to create their own views via an API. Apart from just creating views, any module should be free to add "custom data" to the view, which the view should be able to handle appropriately through a set of default API functions. An example of this may be the GTD module, which could display all the tasks for a day in the daily view, or it could highlight a day as red in the monthly view if there are urgent tasks that have not been yet completed. #### Keybinds The user experience comes first - the keybinds should be as close as possible to vim, with slight deviations if the keybinds hinder the "mnemonic" keybind model, where a sequence of words (e.g. `delete around word`) can be converted to a set of keybinds (`daw`). # Cross-Compatibility Walled gardens? No thanks. Conversion from (and to) `.norg` and Neorg workflows should be hassle-free. - [ ] [Pandoc](https://pandoc.org/) integration. A pandoc parser (or lua filter) would be super useful to convert to and from the `.norg` file format. Work has begun (but is currently stalled) in the following [PR](https://github.com/nvim-neorg/neorg/pull/684). There is also an incomplete pandoc parser written in Haskell [here](https://github.com/Simre1/neorg-haskell-parser), so if you have some Haskell skills then this is your time to help out! :) - [ ] Inbuilt upgraders from `.org` to `.norg`, and vice versa. Org and markdown are the most common formats to convert to `.norg`, and whereas markdown can be handled just fine by pandoc, org may need some extra fine tuning to work effectively. We'd like for Neorg to be able to "understand" workflows within emacs's `org-mode`, such as the inbuilt agenda view and others, and convert them into the Neorg-appropriate counterpart. # External Tooling External tooling is a great way to help Neorg see adoption in spaces other than Neovim. While the best features will remain within the Neorg plugin for Neovim, writing tools for `.norg` and for Neorg should not be difficult nor discouraging. As a result, we'll create and maintain a set of embeddable-anywhere tools. These include: - [ ] Parsers in several languages - the first step of supporting Neorg is supporting its underlying file format, `.norg`. Because Norg is a well-standardized format, many parsers can be created for it without forming annoying "dialects". Currently, the most well supported parser is the `tree-sitter` parser, which is embeddable essentially anywhere thanks to its library being written in C. Apart from [`tree-sitter`](https://github.com/nvim-neorg/tree-sitter-norg), work-in-progress parsers can be found for many languages like [`rust`](https://github.com/max397574/rust-norg), [`zig`](https://github.com/vhyrro/zig-norg), [`haskell`](https://github.com/Simre1/neorg-haskell-parser) and [`julia`](https://github.com/klafyvel/Norg.jl)! - [x] Directory Manager and File Aggregator - workspaces are a pretty fundamental concept within Neorg, and writing a tool that can efficiently manage and manipulate enormous collections of notes will be critical for any Neorg-like tool. - [x] Multithreaded parsing library - note collections can get big, like really big. Parsing all of these on a single thread could take even minutes. Having a good multithreaded parsing library will help a lot. - [x] Norgopolis and its related modules - norgopolis servers as a router for all server side logic, including the database, for the multithreaded Treesitter parser, as well as for managing active workspaces. Many clients may connect to this server, establishing a single source of truth for any `n` amount of clients. - [x] SurrealDB - Neorg's preferred backend for execution is SurrealDB, a modern multi-model database. It was specifically chosen because, apart from just being able to store data in a relational format (like sqlite), it also has the ability of creating and operating on nodes like a graph database. This allows for lighting fast lookups of e.g. links, tasks and/or inline metadata in the file. - [ ] GTD - this library would form the backend for the "Getting Things Done" methodology. It would do all the heavy lifting, including managing notes, contexts and a bit more. Afterwards all it takes is to write a frontend (the UI) in the application of your choice to fully support Neorg's GTD capabilities. - [ ] Zettelkasten - similarly to GTD, the backend for zettelkasten will be implemented as a library. This library would primarily handle backlinks, which are painfully slow to track natively in Lua, which runs on a single thread. A logistics question - how do these libraries play with Neorg in Neovim itself? All of the libaries we mentioned will be written in Rust, and via `cbindgen` we will generate `.o`/`.so` library files that may be used anywhere, including Neovim. Lua has great support for importing shared library objects, so all that it takes is a Github action that compiles e.g. the `GTD` backend library, pulling in all dependencies, packing it into a small `.so`, and then shipping it directly with Neorg! # Mobile Application A mobile tool for Neorg would greatly increase it's adoption range thanks to portability. Such a mobile application could utilize all of the [external tools](#external-tooling) written by us to ensure that it's consistent in behaviour with all of the other Neorg tools in the ecosystem. Features of a mobile application would include: - [ ] A synchronisation mechanism between the mobile device and any other end devices. - [ ] A fast note capturing mechanism - if you have a random idea while e.g. going on a walk, writing it down should only be two clicks away. This note capturing mechanism should ideally be powered by the [GTD](#external-tooling) library. - [ ] A pleasant UX - phone screens are fairly small, so UI fluidness and the way notes/tasks are displayed should be as effective as possible. # Artificial Intelligence Do not worry, we're not a new startup overhyping AI thinking it will change the world. Also don't fret, we will not use AI like most companies as a tactic to collect and send information to servers for data analysis. We strongly believe that for a specific and specialized subset of tasks artificial intelligence (or, more specifically, natural language processing tools) are a novel way to aid in note taking. All models shipped by Neorg would not be in the main Neorg package, but as addon plugins. These addon plugins would ship with the model placed locally on your machine, and no contact with an external server would be required to use these models. There are currently only three use cases that we would like to use natural language processing for, and these are: - [ ] Speech to text recognition for the [mobile application](#mobile-application). - [ ] Automatic detection of contexts for a given GTD task. This takes away the manual process of having to assign contexts for each of your tasks. - [ ] Zettelkasten sorters - because an NLP model has a good understanding of loosely similar information and topics, it could be much more effective at categorizing, reordering and linking zettels than a traditional programmatic algorithm. All of these plans are for the very far future, long after [GTD and Zettelkasten](#workflow) are complete. However, if you have experience in training such models or would like to help, then do not hesitate! ================================================ FILE: doc/cheatsheet.norg ================================================ * Your Quick Neorg Guide This is a cheatsheet that quickly lets you get a grip on the Norg syntax. It is by no means exhaustive, but quickly gets you up to speed. *Press `gO` for a Table of Contents*. * Structure ** Headings You've already seen them in action in this guide. @code norg * Heading 1 ** Heading 2 *** Heading 3 **** And so on... @end Norg supports infinite heading levels, but you'll find that Neorg caps out at six. You can still go higher, but the parser will treat it as if it were a level six heading. ** Reverse Headings Need to go back a level? `---` takes you back a single level, while `===` closes all headings. @code norg * Heading 1 ** Heading 2 Part of heading 2. --- Part of heading 1. *** Heading 3 **** Heading 4 Part of heading 4. === Not part of any heading. @end ** Lists *** Unordered Lists @code norg - List item. -- Nested list item -- Also nested. - Next list item. @end *** Ordered Lists @code norg ~ List item. ~~ Nested list item. ~~ Also nested. ~ Next list item. @end ** Code Blocks Run `:Neorg toggle-concealer` to see the raw syntax. @code This is literally an empty code block. @end @code lua print("If you have the lua treesitter parser installed, this will be highlighted as lua.") print("If not, run `:TSInstall lua`.") @end * Inline Markup ** Styling To see the syntax raw, run `:setlocal conceallevel=0`. Set it back to `2` later for a more concise reading experience :) @code norg This is *bold*. This is /italic/. This is _underline_. This is -strikethrough-. This is a spoiler: !org-mode is a bozo!, to see the content you either enter insert mode on this line (good luck) or toggle the concealer off. This is a `verbatim object, you can't put *markup* in here!`. %This is a comment, and you never see it in your exported output.% This is _underline, /italic/ and *-strikethrough bold-*_. Mix and match how you see fit, just make sure you're not trying to put markup inside of a verbatim object! @end For the mathematical nerds: @code norg This is ^superscript^ (it won't render properly in Neovim). This is ,subscript, (same as above). This is inline maths: $f(x) = supports \\LaTeX$, but beware! You have to escape backslashes like in the example. Because of this, you should always use the above syntax for very simple things, and the "rigid" syntax for more complicated expressions: $|\int_0^t d(\ln{S}) = ...|$ (notice the `\|` and the use of single backslashes). @end *** Styling with a Pipe As shown in the maths example above, you can use the pipe `\|` to allow arbitrary whitespace within the markup. You also don't have to escape backslashes! @code norg / this is not allowed / /| but this is fine!! |/ @end ** Links *NOTE: attempting to hit enter on the links will not work.* *** Links to URLs @code norg {https://github.com/nvim-neorg/neorg} {file:///home/vhyrro/top-secret.txt} - as long as your OS supports your URI (`file://`) it'll work in Neorg @end *** Links to Objects To link to an object, you use its prefix and its name. The name part is *only word and punctuation sensitive*. Case and whitespace are ignored. @code norg {*** styling, but extended} - valid {*** styling,butextended} - valid {*** styling but extended} - invalid, no `,` punctuation @end *** Links to non-norg Files @code norg {/ myfile.txt} {/ myfile.txt:43} - line 43 of `myfile.txt` @end *** Links with Descriptions @code norg {* some heading}[my custom description] @end *** Links to Anything Don't care about what object type you're linking to? Just use the `#` character. @code norg {# anything with this title} @end *** Links across Files Take your link but prefix it with `:filename:` (without the `.norg` extension). @code norg {:dir/file:* Heading in that file} {:$/index.norg:* Heading in that file} - `$` signifies the path of the current workspace {:$notes/index.norg:* Heading in that file} - you can also link to other workspaces @end ** Anchors It's like links, but the syntax is reversed. Anchors can save you from writing the same thing countless times. @code norg This is a link to [my site]{https://very.long.link.to.my.boring.site}. `<- Works just like a normal link with a description` Be sure to reference [my site] if you're interested! `<- Takes you to the correct site` @end As long as the name inbetween the square brackets is the same you'll be linked to the correct place. * Tasks You can assign a task status to anything by adding a `|( )|`. @code norg - ( ) Here's a classical TODO item. * (x) Here's a heading with a done TODO state. Curious. @end All supported states: @code norg ( ) Undone -> not done yet (x) Done -> done with that (?) Needs further input (!) Urgent -> high priority task (+) Recurring task with children (-) Pending -> currently in progress (=) Task put on hold (_) Task cancelled (put down) @end === #comment vim:tw=100:ft=norg:norl:conceallevel=2:concealcursor=nv: ================================================ FILE: doc/neorg.norg ================================================ @document.meta title: Neorg help-page description: The Neorg vim help-page author: The Neorg Community categories: docs created: 2021-09-05 version: 0.1 @end .toc * The `.norg` File Format If you find the {https://github.com/nvim-neorg/norg-specs/blob/main/1.0-specification.norg}[spec] too long and want to jump-start your Neorg skills, you've come to the right place! NOTE: If you have `core.concealer` enabled, then be sure to run `:Neorg toggle-concealer` any time you want to see the raw markup. This document automatically disables the hiding of characters for clarity. If you would like to reenable this, run `:set conceallevel=2`. ** Basic Markup Here is how you can do very basic markup. First you see it raw, then rendered: - *bold* - /italic/ - _underline_ - -strikethrough- - !spoiler! - `inline code` - ^superscript^ (when nested into `subscript`, will highlight as an error) - ,subscript, (when nested into `superscript`, will highlight as an error) - $f(x) = y$ (see also {# Math}) - &variable& (see also {# Variables}) - %inline comment% This also immediately shows you how to escape a special character using the backslash, \\. Adding the [pipe]{:cheatsheet:*** Styling with a Pipe} modifier inside any markup characters allows you to include white space before and after the markup characters, as well as ignore escape characters inside. Example: *| look ma, white space! |* ** Nesting Neorg generally does *NOT* care about indentation! 🎉 Thus, nesting is done via repeating modifiers like you are used to from Markdown headings. Note, that this allows you to start at an arbitrary nesting level if you so desire! *** Unordered lists - Unordered list level 1 -- Unordered list level 2 --- Unordered list level 3 ---- Unordered list level 4 ----- Unordered list level 5 ------ Unordered list level 6 *** Ordered lists ~ Ordered list level 1 ~~ Ordered list level 2 ~~~ Ordered list level 3 ~~~~ Ordered list level 4 ~~~~~ Ordered list level 5 ~~~~~~ Ordered list level 6 *** Tasks - ( ) Undone -> not done yet - (x) Done -> done with that - (?) Needs further input - (!) Urgent -> high priority task - (+) Recurring task with children - (-) Pending -> currently in progress - (=) Task put on hold - (_) Task cancelled (put down) The task modifier can be placed on any detached modifier, including headings, definitions, footnotes etc. to track their state. *** Quotes > 1. level quote >> 2. level quote >>> 3. level quote >>>> 4. level quote >>>>> 5. level quote >>>>>> 6. level quote *** Headings You already saw headings up to the third out of six levels. I assume by now you know how they work. But now... **** ... prepare to have your mind blown! Because here is something very special and unique to Neorg: ***** Indentation reversion As you would expect, this paragraph belongs to the fifth level heading. ****** Final heading level And this paragraph belongs to the sixth level. But by using the following modifier: --- We can move this text to the fifth level again! 🤯 --- So using 3 or more `-` signs not followed by anything, you move *one* level backwards in the indentation (*NOTE:* this should actually be `2` or more signs as per the latest spec revision, but the parser is in the process of being updated). Doing the same but with `=` characters instead all heading levels will be closed and you will be dropped back to the root level of the document! ** Horizontal Lines You can also place horizontal lines using three or more underscores like so: ___ This will never affect the indentation level of the following text, but it will immediately terminate the paragraph which is why this is a new paragraph despite the absence of two (or more) consecutive new lines. ** Links For more info on links check the {https://github.com/nvim-neorg/norg-specs/blob/main/1.0-specification.norg#L1340}[spec]. *** Link Targets The following things can be used as link /targets/: - `* Heading1` (+ nesting levels) - `^ Footnote` - `$ Definition` - `# magic` (any of the above) - `:path:# magic` (target in another norg file at a given path) - `:path:` (another norg file at a given path without specific target) - `/ path` (a non-norg file at a given path) - `https://github.com` (a URL) - `file:///some/path` (any file, opened via `(xdg-)open`) Any of the paths used in `:path:` or `/ path` can be formatted in either of the following ways: - `:path/to/norg/file:` relative to the file which contains this link - `:/path/from/root:` absolute w.r.t. the entire filesystem - `:~/path/from/user/home:` relative to the user's home directory (e.g. `/home/user` on Linux machines) - `:../path/to/norg/file:` these paths also understand `../` - `:$/path/from/current/workspace:` relative to current workspace root - `:$gtd/path/in/gtd/workspace:` relative to the root of the workspace called `gtd`. ** There are multiple ways of using links: *** Pure link location An inline link to {* my heading}. The text used for this link can be inferred from the /target/. If it is not a norg-target, this falls back to the URL or filename, etc. *** Custom link text An inline link {* my heading}[with custom text]. This links to the same marker but uses a custom link text. *** Anchors A link to [our website]. Be sure to check out [our website]{https://github.com/nvim-neorg/neorg}! The standalone link /text/ is called an *anchor declaration*. It requires an *anchor definition* (last line in the code block) which defines where an anchored link points to. This is very useful when you find yourself refering to the same target often. *** Examples: {* Heading 1} {** Heading 2} {*** Heading 3} {**** Heading 4} {***** Heading 5} {****** Heading 6} {******* Heading level above 6} {# Generic} {$ Definition} {^ Footnote} {:norg_file:} {:norg_file:* Heading 1} {:norg_file:** Heading 2} {:norg_file:*** Heading 3} {:norg_file:**** Heading 4} {:norg_file:***** Heading 5} {:norg_file:****** Heading 6} {:norg_file:******* Heading level above 6} {:norg_file:# Generic} {:norg_file:* Marker} {:norg_file:$ Definition} {:norg_file:^ Footnote} {https://github.com/} {file:///dev/null} {/ external_file.txt} Note that the following links are malformed: {:norg_file:/ external_file.txt} {:norg_file:https://github.com/} ** Definitions There are two kinds of definitions: *** Single-paragraph definitions $ Term The definition of the object in a single paragraph. This is not considered part of the definition. *** Multi-paragraph definitions $$ Term Here, I can place any number of paragraphs or other format objects. Even a code example: @code lua print("Hello world!") @end $$ This is no longer part of the definition because the `$$` on the previous line marked its end. ** Footnotes There are also two kinds of footnotes: *** Single-paragraph footnotes ^ This is the title of my footnote. I can use this as a link target. This is the actual footnote content. This is no longer part of the footnote. *** Multi-paragraph footnotes ^^ This is a multi-paragraph footnote. Here go the actual contents... ... which I can even continue down here. ^^ Now, the footnote has ended. ** Data Tags Neorg supports a number of tags. The general format is: @data possible parameters contents @end *** Carryover Tags Carryover tags are a variant of tags that apply some data to the next object. The general syntax looks like this: #tagname parameter1 parameter2 The object to apply the tag to. Apart from just `#` you may also use the `+` character. It's best to use `#` most of the time, as it applies recursively to the element you're tagging. Use `+` only if you don't want your tag to apply recursively to the children of an object. **** Name #name awesome quotes > This is a quote. > We can talk about anything we like This quote now has a /name/! You can link to it with the magic char: {# awesome quotes}! *** Code Blocks @code console.log("But I want syntax highlighting...") @end @code javascript console.log("Thank you!") @end *** Media *NOTE: Media is currently supported only via {https://github.com/3rd/image.nvim}[image.nvim].* You can embed images directly in base64 format like so: @image png svg jpeg jfif exif @end Obviously you need to pick one of the available formats. You can embed external image or video files like so: .image https://raw.githubusercontent.com/nvim-neorg/neorg/main/res/neorg.svg *** Math There are two ways of typesetting mathematics: ~ Inline mathematics using the `$` attached modifier like so: $f(x) = y$. To access LaTeX-typeset math within inline mathematics, use the [pipe] modifier: $|\theta = \frac{\pi}{3}|$ ~ Multi-line mathematics using the `math` ranged tag which supports any LaTeX-typeset math. @math f(x) = y @end ** Advanced markup There are some more advanced markup features: *** The Link modifier If you want to mark-up text which is not surrounded by punctuation or whitespace, you need to use the *link* modifier, `:`, like so: W:*h*:y w:/oul/:d a:_nyon_:e w:-an-:t to do t:`hi`:s? *** Nested markup You can nest multiple markup groups to combine their effect. Some examples: - *Text can be bold _and underlined_!* - You can create ,/italic subscripts/,. - If you want to shout on the internet, use */_DOUBLY EMPHASIZED AND UNDERLINED CAPS_/* Note: You can:*not* combine sub- and superscripts like so: ^,This should be super- and subscript.,^ - gets highlighted as an error. *** Variables *NOTE: Variables are not yet implemented in Neorg* You can define variables which you can access later in your document like so: @code norg =variable-name value =end @end You can refer to this variable later in your document using the `&` attached modifier like so: @code norg Insert my &variable&. @end #comment vim:tw=100:ft=norg:norl:conceallevel=0:concealcursor=: ================================================ FILE: doc/tags ================================================ neorg neorg.norg /* The `.norg` File Format neorg-advanced-markup neorg.norg /** Advanced Markup neorg-anchors neorg.norg /*** Anchors neorg-basic-markup neorg.norg /** Basic Markup neorg-breaking-changes breaking-changes.norg /*NOTE: neorg-carryover-tags neorg.norg /*** Carryover neorg-code-blocks neorg.norg /*** Code Blocks neorg-data-tags neorg.norg /** Data Tags neorg-definitions neorg.norg /** Definitions neorg-footnotes neorg.norg /** Footnotes neorg-headings neorg.norg /*** Headings neorg-indentation-reversal neorg.norg /***** Indentation reversion neorg-link-descriptions neorg.norg /*** Custom link text neorg-link-examples neorg.norg /*** Examples neorg-link-locations neorg.norg /*** Pure link location neorg-link-modifier neorg.norg /*** The Link modifier neorg-link-targets neorg.norg /*** Link Targets neorg-links neorg.norg /** Links neorg-math neorg.norg /*** Math neorg-media neorg.norg /*** Media neorg-name-tag neorg.norg /*** Name neorg-nested-markup neorg.norg /*** Nested markup neorg-ordered-lists neorg.norg /*** Ordered lists neorg-quotes neorg.norg /*** Quotes neorg-tasks neorg.norg /*** Tasks neorg-unordered-lists neorg.norg /*** Unordered lists neorg-variables neorg.norg /*** Variables neorg-cheatsheet cheatsheet.norg /* Your Quick Neorg Guide neorg-cheatsheet-headings cheatsheet.norg /** Headings neorg-cheatsheet-reverse-headings cheatsheet.norg /** Reverse Headings neorg-cheatsheet-lists cheatsheet.norg /** Lists neorg-cheatsheet-code-blocks cheatsheet.norg /** Code Blocks neorg-cheatsheet-styling cheatsheet.norg /** Styling neorg-cheatsheet-links-urls cheatsheet.norg /*** Links to URLs neorg-cheatsheet-links-objects cheatsheet.norg /*** Links to Objects neorg-cheatsheet-links-non-norg cheatsheet.norg /*** Links to non-norg Files neorg-cheatsheet-links-descriptions cheatsheet.norg /*** Links with Descriptions neorg-cheatsheet-links-anything cheatsheet.norg /*** Links to Anything neorg-cheatsheet-links-files cheatsheet.norg /*** Links across Files neorg-cheatsheet-anchors cheatsheet.norg /** Anchors neorg-cheatsheet-tasks cheatsheet.norg /* Tasks ================================================ FILE: docgen/docgen.lua ================================================ local neorg = require("neorg.core") local docgen = {} -- Create the directory if it does not exist docgen.output_dir = "../wiki" docgen.static_dir = "../res/wiki/static" pcall(vim.fn.mkdir, docgen.output_dir) -- Copy static wiki resources into the wiki vim.loop.fs_scandir(docgen.static_dir, function(err, handle) assert(handle, err) -- will not kill docgen on fail, because it is within async callback local name, type = vim.loop.fs_scandir_next(handle) while name do if type == "file" then assert(vim.loop.fs_copyfile(docgen.static_dir .. "/" .. name, docgen.output_dir .. "/" .. name)) end name, type = vim.loop.fs_scandir_next(handle) end end) require("neorg").setup({ load = { ["core.defaults"] = {}, ["core.integrations.treesitter"] = { config = { configure_parsers = false, }, }, }, }) local lib, modules, utils, log = neorg.lib, neorg.modules, neorg.utils, neorg.log -- Start neorg neorg.org_file_entered(false) -- Extract treesitter utility functions provided by Neorg ---@type core.integrations.treesitter local ts = modules.get_module("core.integrations.treesitter") assert(ts, "treesitter not available") --- Aggregates all the available modules. ---@return table #A list of paths to every module's `module.lua` file docgen.aggregate_module_files = function() return vim.fs.find("module.lua", { path = "..", type = "file", limit = math.huge, }) end --- Opens a file from a given path in a new buffer ---@param path string #The path of the file to open ---@return number #The buffer ID of the opened file docgen.open_file = function(path) local uri = vim.uri_from_fname(path) local buf = vim.uri_to_bufnr(uri) vim.fn.bufload(buf) return buf end --- Get the first comment (at line 0) from a module and get it's content --- @param buf number #The buffer number to read from --- @return table? #A table of lines docgen.get_module_top_comment = function(buf) local node = ts.get_first_node_recursive("comment", { buf = buf, ft = "lua" }) if not node then return end -- Verify if it's the first line local start_row = node:range() if start_row ~= 0 then return end local comment = vim.split(ts.get_node_text(node, buf), "\n") -- Stops execution if it's not a multiline comment if comment[1] ~= [[--[[]] or comment[#comment] ~= "--]]" then return end -- Removes first and last braces table.remove(comment, 1) table.remove(comment, #comment) return comment end ---@alias TopComment { file: string, title: string, summary: string, description: string, embed: string, markdown: string[], internal: boolean } --- Parses the top comment ---@param comment string[] #The comment ---@return TopComment #The parsed comment docgen.parse_top_comment = function(comment) ---@type TopComment local result = { -- file = "", -- title = "", -- summary = "", markdown = {}, } local can_have_options = true for _, line in ipairs(comment) do if line:match("^%s*%-%-%-%s*$") then can_have_options = false else local option_name, value = line:match("^%s*(%w+):%s*(.+)") if vim.tbl_contains({ "true", "false" }, value) then value = (value == "true") end if option_name and can_have_options then result[option_name:lower()] = value else table.insert(result.markdown, line) end end end return result end --- Ensures that the top comment of each Neorg module follows a certain set of rules. ---@param top_comment TopComment #The comment to check for errors ---@return string|TopComment #An error string or the comment itself docgen.check_top_comment_integrity = function(top_comment) local tc = vim.tbl_deep_extend("keep", top_comment, { title = "", summary = "", markdown = {}, }) if not tc.file then return "no `File:` field provided." elseif tc.summary:sub(1, 1):upper() ~= tc.summary:sub(1, 1) then return "summary does not begin with a capital letter." elseif tc.summary:sub(tc.summary:len()) ~= "." then return "summary does not end with a full stop." elseif tc.title:find("neorg") then return "`neorg` written with lowercase letter. Use uppercase instead." -- elseif vim.tbl_isempty(tc.markdown) then -- return "no overview provided." end return top_comment end --- Retrieves the TS node corresponding to the `module.config.public` treesitter node ---@param buffer number #Buffer ID ---@param root TSNode #The root node ---@return TSNode? #The `module.config.public` node docgen.get_module_config_node = function(buffer, root) local query = utils.ts_parse_query( "lua", [[ (assignment_statement (variable_list) @_name (#eq? @_name "module.config.public")) @declaration ]] ) local _, declaration_node = query:iter_captures(root, buffer)() return declaration_node and declaration_node:named_child(1):named_child(0) or nil end ---@alias ConfigOptionData { node: TSNode, name: string, value: userdata, parents: string[] } --- Recursively maps over each item in the `module.config.public` table, -- invoking a callback on each run. Also descends down recursive tables. ---@param buffer number #Buffer ID ---@param start_node table #The node to start parsing ---@param callback fun(ConfigOptionData, table) #Invoked on each node with the corresponding data ---@param parents string[]? #Used internally to track nesting levels docgen.map_config = function(buffer, start_node, callback, parents) parents = parents or {} ---@type string[] local comments = {} local index = 1 for node in start_node:iter_children() do if node:type() == "comment" then table.insert(comments, ts.get_node_text(node, buffer)) elseif node:type() == "field" then local name_node = node:field("name")[1] -- If a node does not have an associated name, then -- it's part of a list if name_node and name_node:type() == "string" then name_node = name_node:field("content")[1] end local name = name_node and ts.get_node_text(name_node, buffer) or nil local value = node:field("value")[1] -- If the right hand side of the expression is a table -- then go down it recursively if value:type() == "table_constructor" then callback({ node = node, name = name, value = value, parents = parents, }, comments) -- The deepcopy is necessary or else -- the parents table would be overwritten in-place local copy = vim.deepcopy(parents) table.insert(copy, name or index) docgen.map_config(buffer, value, callback, copy) else callback({ node = node, name = name, value = value, parents = parents, }, comments) end comments = {} index = index + 1 else comments = {} end end end --- Goes through a table and evaluates all functions in that table, merging the -- return values back into the original table. ---@param tbl table #Input table ---@return table #The new table docgen.evaluate_functions = function(tbl) local new = {} lib.map(tbl, function(_, value) if type(value) == "function" then vim.list_extend(new, value()) else table.insert(new, value) end end) return new end ---@alias Module { top_comment_data: TopComment, buffer: number, parsed: table } ---@alias Modules { [string]: Module } --- Returns a function which itself returns a table of links to modules -- in a markdown-like unordered list. ---@param mods Modules #A table of modules to enumerate ---@param predicate fun(Module):boolean #A predicate that determines whether or not to render this object. --- If the predicate returns false, then the object is dismissed. ---@return fun():string[] #An array of markdown strings with the enumerated modules local function list_modules_with_predicate(mods, predicate) local sorted = lib.unroll(mods) table.sort(sorted, function(x, y) return x[1] < y[1] end) return function() local res = {} for _, kv_pair in ipairs(sorted) do local mod = kv_pair[1] local data = kv_pair[2] if predicate and predicate(data) then local insert if data.top_comment_data.file then insert = "- [`" .. data.parsed.name .. "`](https://github.com/nvim-neorg/neorg/wiki/" .. data.top_comment_data.file .. ")" else insert = "- `" .. mod .. "`" end if data.top_comment_data.summary then insert = insert .. " - " .. data.top_comment_data.summary else insert = insert .. " - undocumented module" end table.insert(res, insert) end end return res end end docgen.generators = { --- Generates the Home.md file ---@param mods Modules #A table of modules homepage = function(mods) local core_defaults = mods["core.defaults"] assert(core_defaults, "core.defaults module not loaded!") local structure = { '
', "", "# Welcome to the Neorg wiki!", "Want to know how to properly use Neorg? Your answers are contained here.", "", "
", "", "# Kickstart", "", "If you would like a Neovim setup that has Neorg configured out of the box look no further than the [kickstart guide](https://github.com/nvim-neorg/neorg/wiki/Kickstart)!", "", "# Using Neorg", "", "Neorg depends on a number of other technologies, all of which have to be correctly configured to keep Neorg running smoothly.", "For some help on understanding how your terminal, Neovim, colourschemes, tree-sitter and more come together to produce your Neorg experience (or Neorg problems), see [this document on understanding Neorg dependencies](Dependencies).", "", "At first configuring Neorg might be rather scary. I have to define what modules I want to use in the `require('neorg').setup()` function?", "I don't even know what the default available values are!", "Don't worry, there are guides you are free to check out. The [tutorial](https://github.com/nvim-neorg/neorg/wiki/Tutorial) guides you through what Neorg is and its basics.", "Afterwards, feel free to check out the [configuration guide](https://github.com/nvim-neorg/neorg/wiki/Setup-Guide) as well as the [cookbook](https://github.com/nvim-neorg/neorg/wiki/Cookbook).", "", "# Broken Installation", "", "Having issues when installing Neorg, specifically past the `8.0.0` version? Check out the [following page](https://github.com/pysan3/Norg-Tutorial/blob/main/MIGRATION-v8.md) where you can troubleshoot your issue from start to finish.", "", "# Contributing to Neorg", "", "Neorg is a very big and powerful tool behind the scenes - way bigger than it may initially seem.", "Modules are its core foundation, and building modules is like building lego bricks to form a massive structure.", "There's an in-the-works tutorial dedicated to making modules [right here](https://github.com/andreadev-it/neorg-module-tutorials/blob/main/introduction.md)!", "", "# Module naming convention", "", "Neorg provides default modules, and users can extend Neorg by creating community modules.", "We agreed on a module naming convention, and it should be used as is.", "This convention should help users know at a glance what function the module serves in the grand scheme of things.", "- Core modules: `core.*`", "- Integrations with 3rd party software that are embedded in neorg: `core.integrations.*`", "- External modules: `external.*`", "- Integrations with 3rd party software that aren't embedded in neorg: `external.integrations.*`", "", "# Default Modules", "", function() local link = "[`core.defaults`](https://github.com/nvim-neorg/neorg/wiki/" .. core_defaults.top_comment_data.file .. ")" return { "Neorg comes with some default modules that will be automatically loaded if you require the " .. link .. " module:", } end, "", list_modules_with_predicate(mods, function(data) return vim.tbl_contains(core_defaults.parsed.config.public.enable, data.parsed.name) and not data.top_comment_data.internal end), "", "# Other Modules", "", "Some modules are not included by default as they require some manual configuration or are merely extra bells and whistles", "and are not critical to editing `.norg` files. Below is a list of all modules that are not required by default:", "", list_modules_with_predicate(mods, function(data) return not data.parsed.extension and not vim.tbl_contains(core_defaults.parsed.config.public.enable, data.parsed.name) and not data.top_comment_data.internal end), "", "# Developer modules", "", "These are modules that are only meant for developers. They are generally required in other modules:", "", list_modules_with_predicate(mods, function(data) return not data.parsed.extension and data.top_comment_data.internal end), } return docgen.evaluate_functions(structure) end, --- Generates the _Sidebar.md file ---@param mods Modules #A table of modules sidebar = function(mods) local structure = { "
", "", "# :star2: Neorg", "
", "", "", "- [Setup Guide](https://github.com/nvim-neorg/neorg/wiki/Setup-Guide)", "- [Tutorial](https://github.com/nvim-neorg/neorg/wiki/Tutorial)", "- [Default Keybinds](https://github.com/nvim-neorg/neorg/wiki/Default-Keybinds)", "", "
", "

Inbuilt modules:

", "", function() local res = {} local names = {} for n, data in pairs(mods) do if data.parsed.extension ~= true then table.insert(names, n) end end table.sort(names) for _, name in ipairs(names) do local data = mods[name] if not data.parsed.internal then local insert = "" if data.top_comment_data.file then insert = insert .. "- [`" .. data.parsed.name .. "`](https://github.com/nvim-neorg/neorg/wiki/" .. data.top_comment_data.file .. ")" else insert = insert .. "- `" .. name .. "`" end table.insert(res, insert) end end return res end, "", "
", } return docgen.evaluate_functions(structure) end, --- Generates the page for any Neorg module ---@param mods Modules #The list of currently loaded modules ---@param module Module #The module we want to generate the page for ---@param configuration string[] #An array of markdown strings detailing the configuration options for the module ---@return string[] #A table of markdown strings representing the page module = function(mods, module, configuration) local structure = { '
', "", "# `" .. module.parsed.name .. "`", "", "### " .. (module.top_comment_data.title or ""), "", module.top_comment_data.description or "", "", module.top_comment_data.embed and ("![module-showcase](" .. module.top_comment_data.embed .. ")") or "", "", "
", "", function() if module.top_comment_data.markdown and not vim.tbl_isempty(module.top_comment_data.markdown) then return vim.list_extend({ "# Overview", "", }, module.top_comment_data.markdown) end return {} end, "", "# Configuration", "", function() if vim.tbl_isempty(configuration) then return { "This module provides no configuration options!", } else return configuration end end, "", function() local required_modules = module.parsed.setup().requires or {} if vim.tbl_isempty(required_modules) then return {} end local module_list = {} for _, module_name in ipairs(required_modules) do module_list[module_name] = mods[module_name] end return docgen.evaluate_functions({ "# Dependencies", "", list_modules_with_predicate(module_list, function() return true end), }) end, "", function() local required_by = {} for mod, data in pairs(mods) do local required_modules = data.parsed.setup().requires or {} if vim.tbl_contains(required_modules, module.parsed.name) then required_by[mod] = data end end if vim.tbl_isempty(required_by) then return {} end return docgen.evaluate_functions({ "# Required By", "", list_modules_with_predicate(required_by, function() return true end), }) end, } return docgen.evaluate_functions(structure) end, keybinds = function(mods, buffer) local keybind_data = docgen.parse_keybind_data(buffer) local layout = { '
', "", "# :keyboard: Neorg Keybinds :keyboard:", "A comprehensive list of all keys available in Neorg.", "", "
", "", "### Further Reading", "", docgen.lookup_modules( mods, "To find out how to rebind the available keys consult the [`core.keybinds`](@core.keybinds) wiki entry." ), "", } for preset_name, preset_data in vim.spairs(keybind_data) do for neorg_mode_name, neorg_mode_data in vim.spairs(preset_data) do if neorg_mode_name == "all" then table.insert(layout, string.format("## Preset `%s` // All Files", preset_name)) table.insert(layout, "") elseif neorg_mode_name == "norg" then table.insert(layout, string.format("## Preset `%s` // Norg Only", preset_name)) table.insert(layout, "") end for mode_name, mode_data in vim.spairs(neorg_mode_data) do mode_name = lib.match(mode_name)({ n = "Normal Mode", i = "Insert Mode", v = "Visual Mode", }) table.insert(layout, "### " .. mode_name) table.insert(layout, "") for key, data in vim.spairs(mode_data) do if not vim.tbl_isempty(data.comments) then local comments = vim.iter(data.comments) :map(function(comment) return (comment:gsub("^%s*%-%-%s*", "")) end) :totable() local mnemonic = docgen.extract_mnemonic(comments) local summary = comments[1]:sub(1, 1):lower() .. comments[1]:sub(2) local description = vim.list_slice(comments, 2) local err = docgen.check_comment_integrity(summary) if err then log.error("Invalid keybind description:", err) end table.insert(layout, "
") table.insert(layout, "") table.insert(layout, "") table.insert(layout, string.format("#### `%s` - %s", key, summary)) table.insert(layout, "") table.insert(layout, "") table.insert(layout, "") vim.list_extend(layout, description) table.insert(layout, string.format("- Maps to: `%s`", data.rhs)) if mnemonic then table.insert( layout, string.format("- Mnemonic: %s", docgen.format_mnemonic(mnemonic)) ) end table.insert(layout, "") table.insert(layout, "
") table.insert(layout, "") end end end end end return layout end, } --- Check the integrity of the description comments found in configuration blocks ---@param comment string #The comment to check the integrity of ---@return nil|string #`nil` for success, `string` if there was an error docgen.check_comment_integrity = function(comment) if comment:match("^%s*%-%-+%s*") then return "found leading `--` comment text." elseif comment:sub(1, 1):upper() ~= comment:sub(1, 1) then return "comment does not begin with a capital letter." elseif comment:find(" neorg ") then return "`neorg` written with lowercase letter. Use uppercase instead." end end --- Replaces all instances of a module reference (e.g. `@core.concealer`) with a link in the wiki ---@param mods Modules #The list of loaded modules ---@param str string #The string to perform the lookup in ---@return string #The original `str` parameter with all `@` references replaced with links docgen.lookup_modules = function(mods, str) return ( str:gsub("@([%-%.%w]+)", function(target_module_name) if not mods[target_module_name] then return table.concat({ "@", target_module_name }) else return table.concat({ "https://github.com/nvim-neorg/neorg/wiki/", mods[target_module_name].top_comment_data.file, }) end end) ) end --- Renders a treesitter node to a lua object ---@param node userdata #The node to render ---@param chunkname string? #The custom name to give to the chunk ---@return any #The converted object docgen.to_lua_object = function(module, buffer, node, chunkname) local loaded = loadstring(table.concat({ "return ", ts.get_node_text(node, buffer) }), chunkname) if loaded then return setfenv(loaded, vim.tbl_extend("force", getfenv(0), { module = module }))() end end ---@alias ConfigOptionArray { [string|number]: ConfigOptionArray, self: ConfigOption } ---@alias ConfigOption { buffer: number, data: ConfigOptionData, comments: string[], object: any } --- Converts a ConfigOptionData struct to a html node in the resulting HTML document ---@param configuration_option ConfigOptionArray #The config option to render ---@param open boolean? #Whether to auto-open the generated `
` tag. Defaults to false. ---@return string[] #A list of markdown tables corresponding to the rendered element. docgen.render = function(configuration_option, open) open = open or false local self = configuration_option.self local type_of_object = (function() local t = type(self.object) if t == "table" then return (vim.tbl_isempty(self.object) and "empty " or "") .. (vim.islist(self.object) and "list" or "table") else return t end end)() local basis = { "* " or ">"), "", ((self.data.name or ""):match("^%s*$") and "" or table.concat({ "
", self.data.name, "
", })) .. " (" .. type_of_object .. ")
", "", } if not vim.tbl_isempty(self.comments) then vim.list_extend(basis, { "
", "", }) vim.list_extend(basis, self.comments) vim.list_extend(basis, { "", "
", "", }) else vim.list_extend(basis, { "
", "", }) end vim.list_extend(basis, docgen.htmlify(configuration_option)) vim.list_extend(basis, { "", "
", }) for i, str in ipairs(basis) do basis[i] = string.rep(" ", 2 - (i == 1 and 2 or 0)) .. str end return basis end --- Converts an object directly into HTML, with no extra fluff. ---@param configuration_option ConfigOptionArray ---@return string[] #An array of markdown strings with the rendered HTML inside docgen.htmlify = function(configuration_option) local self = configuration_option.self local result = {} local code_block = true lib.match(self.data.value:type())({ string = function() table.insert(result, table.concat({ '"', self.object, '"' })) end, table_constructor = function() table.insert(result, "") local unrolled = lib.unroll(self.object) table.sort(unrolled, function(x, y) return tostring(x[1]) < tostring(y[1]) end) for _, data in ipairs(unrolled) do ---@type number|string local name_or_index = data[1] local subitem = configuration_option[name_or_index] if subitem then vim.list_extend(result, docgen.render(subitem)) end end table.insert(result, "") code_block = false end, function_definition = function() local text = ts.get_node_text(self.data.value, self.buffer):match("^function%s*(%b())") if not text then log.error(string.format("Unable to parse function, perhaps some wrong formatting?")) table.insert(result, "") return end table.insert(result, "function" .. text) end, _ = function() table.insert(result, ts.get_node_text(self.data.value, self.buffer)) end, }) if code_block then table.insert(result, 1, "```lua") table.insert(result, "```") end return result end --- Parses keybind data and returns it in a readable format. ---@param buffer number The buffer ID to extract information from. ---@return table docgen.parse_keybind_data = function(buffer) local query = utils.ts_parse_query( "lua", [[ (field name: (identifier) @_ident (#eq? @_ident "presets")) @presets ]] ) local root = assert(vim.treesitter.get_parser(buffer, "lua"):parse()[1]:root(), "unable to parse keybinds!") local _, presets = query:iter_captures(root, buffer)() assert(presets, "could not find presets") local available_keys = neorg.modules.loaded_modules["core.keybinds"].private.presets local output = vim.defaulttable() for preset in presets:named_child(1):iter_children() do if preset:type() == "field" then local preset_name, preset_data = vim.treesitter.get_node_text(assert(preset:named_child(0)), buffer), preset:named_child(1) for neorg_mode in assert(preset_data):iter_children() do if neorg_mode:type() == "field" then local neorg_mode_name, neorg_mode_data = vim.treesitter.get_node_text(assert(neorg_mode:named_child(0)), buffer), neorg_mode:named_child(1) for neovim_mode in assert(neorg_mode_data):iter_children() do if neovim_mode:type() == "field" then local mode_name, mode_data = vim.treesitter.get_node_text(assert(neovim_mode:named_child(0)), buffer), neovim_mode:named_child(1) local comments = {} local i, keybind_data for comment_or_data in assert(mode_data):iter_children() do if comment_or_data:type() == "comment" then table.insert( comments, vim.trim(vim.treesitter.get_node_text(comment_or_data, buffer)) ) elseif comment_or_data:type() == "field" then i, keybind_data = next(available_keys[preset_name][neorg_mode_name][mode_name], i) output[preset_name][neorg_mode_name][mode_name][keybind_data[1]] = { comments = comments, rhs = keybind_data[2], } comments = {} end end end end end end end end return output end docgen.format_mnemonic = function(str) return str:gsub("([A-Z])", "%1"):lower() end docgen.extract_mnemonic = function(comments) for i, comment in ipairs(comments) do local mnemonic = comment:match("^%s*%^(.+)") if mnemonic then table.remove(comments, i) return mnemonic end end end return docgen ================================================ FILE: docgen/fileio.lua ================================================ ---@author vhyrro ---@license GPLv3 local io = {} io.write_to_wiki = function(filename, content) vim.fn.writefile(content, "../wiki/" .. filename .. ".md") end return io ================================================ FILE: docgen/init.lua ================================================ local docgen = require("docgen") local fileio = require("fileio") local neorg = require("neorg.core") local lib, modules = neorg.lib, neorg.modules --- CONFIGURABLE DOCGEN BEHAVIOUR --- Tweak as you see fit. local config = { --- When true, will auto-unfold the top-level
--- tags generated when rendering module configuration options. auto_open_first_level_tags = true, } ---@type Modules local doc_modules = { --[[ [name] = { top_comment_data... buffer = id, parsed = `ret value from sourcing the file`, } --]] } --- Fully renders a large set of configuration options ---@param configuration_options ConfigOptionArray[] An array of ConfigOptionArrays ---@return string[] #An array of markdown strings corresponding to all of the rendered configuration options local function concat_configuration_options(configuration_options) local result = {} local unrolled = lib.unroll(configuration_options) table.sort(unrolled, function(x, y) return x[1] < y[1] end) for _, values in pairs(unrolled) do vim.list_extend(result, docgen.render(values[2], config.auto_open_first_level_tags)) table.insert(result, "") end return result end for _, file in ipairs(docgen.aggregate_module_files()) do local fullpath = vim.fn.fnamemodify(file, ":p") local buffer = docgen.open_file(fullpath) local top_comment = docgen.get_module_top_comment(buffer) if not top_comment then vim.notify("no top comment found for module " .. file) goto continue end local top_comment_data = docgen.check_top_comment_integrity(docgen.parse_top_comment(top_comment)) if type(top_comment_data) == "string" then vim.notify("Error when parsing module '" .. file .. "': " .. top_comment_data) goto continue end -- Source the module file to retrieve some basic information like its name local ok, parsed_module = pcall(dofile, fullpath) if not ok then vim.notify("Error when sourcing module '" .. file .. "': " .. parsed_module) goto continue end -- Make Neorg load the module, which also evaluates dependencies local _ok, err = pcall(modules.load_module, parsed_module.name) if not _ok then vim.notify("Error when loading module '" .. file .. "': " .. err) goto continue end -- Retrieve the module from the `loaded_modules` table. parsed_module = modules.loaded_modules[parsed_module.name] if parsed_module then doc_modules[parsed_module.name] = { top_comment_data = top_comment_data, buffer = buffer, parsed = parsed_module, } end ::continue:: end -- Non-module pages have their own dedicated generators fileio.write_to_wiki("Home", docgen.generators.homepage(doc_modules)) fileio.write_to_wiki( "Default-Keybinds", docgen.generators.keybinds( doc_modules, docgen.open_file(vim.fn.fnamemodify("../lua/neorg/modules/core/keybinds/module.lua", ":p")) ) ) fileio.write_to_wiki("_Sidebar", docgen.generators.sidebar(doc_modules)) -- Loop through all modules and generate their respective wiki files for module_name, module in pairs(doc_modules) do local buffer = module.buffer -- Query the root node and try to find a `module.config.public` table local root = vim.treesitter.get_parser(buffer, "lua"):parse()[1]:root() local config_node = docgen.get_module_config_node(buffer, root) -- A collection of data about all the configuration options for the current module ---@type ConfigOptionArray[] local configuration_options = {} if config_node then docgen.map_config(buffer, config_node, function(data, comments) for i, comment in ipairs(comments) do comments[i] = docgen.lookup_modules(doc_modules, comment:gsub("^%s*%-%-+%s*", "")) end do local error = docgen.check_comment_integrity(table.concat(comments, "\n")) if type(error) == "string" then -- Get the exact location of the error with data.node and the file it was contained in local start_row, start_col = data.node:start() vim.notify( ("Error when parsing annotation in module '%s' on line (%d, %d): %s"):format( module_name, start_row, start_col, error ) ) return end end if not data.value then return end local object = docgen.to_lua_object(module.parsed, buffer, data.value, module_name) do lib.ensure_nested(configuration_options, unpack(data.parents)) local ref = vim.tbl_get(configuration_options, unpack(data.parents)) or configuration_options if data.name then ref[data.name] = { self = { buffer = buffer, data = data, comments = comments, object = object, }, } else table.insert(ref, { self = { buffer = buffer, data = data, comments = comments, object = object, }, }) end end end) end -- Perform module lookups in the module's top comment markdown data. -- This cannot be done earlier because then there would be no guarantee -- that all the modules have been properly indexed and parsed. for i, line in ipairs(module.top_comment_data.markdown) do module.top_comment_data.markdown[i] = docgen.lookup_modules(doc_modules, line) end fileio.write_to_wiki( module.top_comment_data.file, docgen.generators.module(doc_modules, module, concat_configuration_options(configuration_options)) ) end ================================================ FILE: docgen/minimal_init.vim ================================================ " Copied from: https://github.com/ThePrimeagen/refactoring.nvim/blob/master/scripts/minimal.vim " Current neorg code set rtp+=. " For test suites set rtp+=./plenary.nvim set rtp+=./nvim-treesitter set noswapfile runtime! plugin/plenary.vim runtime! plugin/nvim-treesitter.vim lua << EOF require("nvim-treesitter").setup({}) local ok, module = pcall(require,'nvim-treesitter.configs') if ok then module.setup({}) end package.path = "../lua/?.lua;" .. "../lua/?/init.lua;" .. package.path package.path = "../plenary.nvim/lua/?.lua;" .. package.path package.path = "../nvim-treesitter/lua/?.lua;" .. package.path package.path = "/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua;" .. package.path package.path = "/usr/share/lua/5.1/?.so;" .. package.path vim.cmd.TSInstallSync({ bang = true, args = { "lua", "norg" }, }) EOF ================================================ FILE: docs/CONTRIBUTING.md ================================================ # Contributions welcome! Whilst any help is very special, please be considerate of certain rules in order to make the codebase uniform/coherent. --- ### Table of Contents - When you need help: - [What to do when you encounter a bug](#reporting-a-bug-or-issue) - When contributing: - [Formatting rules](#formatting-rules) - [Coding style](#coding-style) - [Adding extra functionality](#adding-functionality) - [Helping with documentation](#helping-with-documentation) --- ### Reporting a bug or issue Whenever something happens the way it's not supposed to, [file an issue!](https://github.com/nvim-neorg/neorg/issues/new/choose), but before you do, try the following: - If you haven't already, always try updating the plugin first. Issues may be fixed before you can even file one, so always make sure you are up to date beforehand. - Make sure you are running the latest neovim version. Whenever I test neorg, I test it on the latest compiled neovim from source which I recompile everyday. While you don't have to be *that* extreme with your updates, make sure you're at least running the latest neovim nightly. If you're certain it's a fault of the plugin, not your configuration, in the issue please provide the following: - The neovim version you're running (`nvim --version`) - The neorg log file (you'll find it at `stdpath('data') .. '/neorg.log'`). This file will contain the necessary info for me to effectively debug. You can run `:echo stdpath('data')` if you're unsure where that path resides. - The branch of Neorg you are using (unstable/main/some other experimental branch) - The list of modules you have loaded (you can run `:Neorg module list` to see a comprehensive list) - Other plugins you are using which you think could potentially be conflicting with neorg. - Steps to reproduce the bug, if any - sometimes bugs get triggered only on certain configurations, which can be a pain. If you're aware that the bug requires a specific config, be sure to include that information as well! --- ### Formatting rules Formatting is done in the project via `stylua`. You should install it with lua 5.2 support, as that version allows for the formatting of `goto` blocks. You can install it via cargo: `cargo install stylua --features lua52`. You can then run `make format` in the project root to automatically format all your lua code. Good stuff. ### Coding style - I use snake_case for everything, and you should too :P - **Please** comment all your code! Annotations for functions are generated by [neogen](https://github.com/danymat/neogen). ## Adding functionality Whenever you are planning on extending neorg, try your best to add *all* extra functionality through modules and modules only. Make changes to the Neorg core only if absolutely necessary. When adding stuff, use lua only. `vim.cmd` in extreme cases :) ## Helping with documentation The documentations (wiki pages) are generated based on the comments in the code. All the logic for generating the wiki is in [`docgen/`](../docgen/). To generate documentations locally, run `make local-documentation` (see [`Makefile`](../Makefile) for more details). If there's no error, you should then see a `wiki/` folder generated. If you feel like improving the documentation, please make changes to the corresponding comments in the correct file. For example, to make changes to the `core.qol.todo_items` documentation, please make changes to the comments in [`./lua/neorg/modules/core/qol/todo_items/module.lua`](../lua/neorg/modules/core/qol/todo_items/module.lua) > Tips: `grep` for the string you want to find. ================================================ FILE: docs/README.md ================================================
# Neorg Documentation The place where you'll find all the goodies regarding Neorg docs.
--- Table of contents: - [A bug has occurred, help!](#bug-reports) - [I wanna know how to contribute! What do I need to know?](#contribution) - [Alright, I've read the contribution file, what now?](#creating-modules) - [What's the plan for the future?](#the-future) --- # Bug Reports Oh no, did something break? See [here](/docs/CONTRIBUTING.md#reporting-a-bug-or-issue) to find out how to report it and the steps to take # Contribution All the things you will need to know are described [right here](/docs/CONTRIBUTING.md#formatting-rules). So go ahead and make something already! :P # Creating Modules Now's the fun part, you gotta learn how the module system works and how to program for it! A mini walkthrough is available in the [Creating Modules](https://github.com/vhyrro/neorg/wiki/Creating-Modules) document in the wiki. # The Future Want to know what's brewing up behind the scenes? Take a look at the [written roadmap](/doc/roadmap.norg) (or do `:h neorg-roadmap` in neovim) to see where we're headed! ================================================ FILE: flake.nix ================================================ # General TODOS: # - Add comments explaining the more terse parts of the flake. { description = "Flake for Neorg development and testing"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; neorocks.url = "github:nvim-neorocks/neorocks"; neorocks.inputs.nixpkgs.follows = "nixpkgs"; gen-luarc.url = "github:mrcjkb/nix-gen-luarc-json"; gen-luarc.inputs.nixpkgs.follows = "nixpkgs"; git-hooks.url = "github:cachix/git-hooks.nix"; git-hooks.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = inputs @ { self, nixpkgs, flake-parts, neorocks, gen-luarc, git-hooks, ... }: flake-parts.lib.mkFlake {inherit inputs;} { systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ]; _module.args = {inherit gen-luarc neorocks git-hooks;}; imports = [ ./nix/overlays ./nix/checks ]; perSystem = {pkgs, ...}: { formatter = pkgs.alejandra; imports = [ ./nix/packages ./nix/shells ]; }; }; } ================================================ FILE: lua/neorg/core/callbacks.lua ================================================ --- @brief [[ --- Defines user callbacks - ways for the user to directly interact with Neorg and respond on certain events. --- @brief ]] --- @module "neorg.core.modules" --- @class neorg.callbacks local callbacks = { ---@type table callback_list = {}, } --- Triggers a new callback to execute whenever an event of the requested type is executed. --- @param event_name string The full path to the event we want to listen on. --- @param callback fun(event: neorg.event, content: table|any) The function to call whenever our event gets triggered. --- @param content_filter? fun(event: neorg.event): boolean # A filtering function to test if a certain event meets our expectations. function callbacks.on_event(event_name, callback, content_filter) -- If the table doesn't exist then create it callbacks.callback_list[event_name] = callbacks.callback_list[event_name] or {} -- Insert the callback and content filter table.insert(callbacks.callback_list[event_name], { callback, content_filter }) end --- Used internally by Neorg to call all callbacks with an event. --- @param event neorg.event An event as returned by `modules.create_event()` --- @see modules.create_event function callbacks.handle_callbacks(event) -- Query the list of registered callbacks local callback_entry = callbacks.callback_list[event.type] -- If the callbacks exist then if callback_entry then -- Loop through every callback for _, callback in ipairs(callback_entry) do -- If the filter event has not been defined or if the filter returned true then if not callback[2] or callback[2](event) then -- Execute the callback callback[1](event, event.content) end end end end return callbacks ================================================ FILE: lua/neorg/core/config.lua ================================================ --- @brief [[ --- Defines the configuration table for use throughout Neorg. --- @brief ]] -- TODO(vhyrro): Make `norg_version` and `version` a `Version` class. --- @alias OperatingSystem --- | "windows" --- | "wsl" --- | "wsl2" --- | "mac" --- | "linux" --- | "bsd" --- @alias neorg.configuration.module { config?: table } --- @class (exact) neorg.configuration.user --- @field hook? fun(manual: boolean, arguments?: string) A user-defined function that is invoked whenever Neorg starts up. May be used to e.g. set custom keybindings. --- @field lazy_loading? boolean Whether to defer loading the Neorg core until after the user has entered a `.norg` file. --- @field load table A list of modules to load, alongside their configurations. --- @field logger? neorg.log.configuration A configuration table for the logger. --- @class (exact) neorg.configuration --- @field arguments table A list of arguments provided to the `:NeorgStart` function in the form of `key=value` pairs. Only applicable when `user_config.lazy_loading` is `true`. --- @field manual boolean? Used if Neorg was manually loaded via `:NeorgStart`. Only applicable when `user_config.lazy_loading` is `true`. --- @field modules table Acts as a copy of the user's configuration that may be modified at runtime. --- @field norg_version string The version of the file format to be used throughout Neorg. Used internally. --- @field os_info OperatingSystem The operating system that Neorg is currently running under. --- @field pathsep "\\"|"/" The operating system that Neorg is currently running under. --- @field started boolean Set to `true` when Neorg is fully initialized. --- @field user_config neorg.configuration.user Stores the configuration provided by the user. --- @field version string The version of Neorg that is currently active. Automatically updated by CI on every release. --- Gets the current operating system. --- @return OperatingSystem local function get_os_info() local os = vim.loop.os_uname().sysname:lower() if os:find("windows_nt") or os:find("mingw32_nt") then return "windows" elseif os == "darwin" then return "mac" elseif os == "linux" then local f = io.open("/proc/version", "r") if f ~= nil then local version = f:read("*all") f:close() if version:find("WSL2") then return "wsl2" elseif version:find("microsoft") then return "wsl" end end return "linux" elseif os:find("bsd") then return "bsd" end error("[neorg]: Unable to determine the currently active operating system!") end local os_info = get_os_info() --- Stores the configuration for the entirety of Neorg. --- This includes not only the user configuration (passed to `setup()`), but also internal --- variables that describe something specific about the user's hardware. --- @see neorg.setup --- --- @type neorg.configuration local config = { user_config = { lazy_loading = false, load = { --[[ ["name"] = { config = { ... } } --]] }, }, modules = {}, manual = nil, arguments = {}, norg_version = "1.1.1", version = "9.4.0", os_info = os_info, pathsep = os_info == "windows" and "\\" or "/", hook = nil, started = false, } return config ================================================ FILE: lua/neorg/core/init.lua ================================================ local neorg = { callbacks = require("neorg.core.callbacks"), config = require("neorg.core.config"), log = require("neorg.core.log"), modules = require("neorg.core.modules"), utils = require("neorg.core.utils"), lib = require("lua-utils"), } return neorg ================================================ FILE: lua/neorg/core/log.lua ================================================ -- log.lua -- -- Inspired by rxi/log.lua -- Modified by tjdevries and can be found at github.com/tjdevries/vlog.nvim -- Modified again by Vhyrro for use with neorg :) -- -- This library is free software; you can redistribute it and/or modify it -- under the terms of the MIT license. See LICENSE for details. --- @alias LogLevel --- | "trace" --- | "debug" --- | "info" --- | "warn" --- | "error" --- | "fatal" --- @class (exact) neorg.log.configuration --- @field plugin string Name of the plugin. Prepended to log messages. --- @field use_console boolean Whether to print the output to Neovim while running. --- @field highlights boolean Whether highlighting should be used in console (using `:echohl`). --- @field use_file boolean Whether to write output to a file. --- @field level LogLevel Any messages above this level will be logged. --- @field modes ({ name: LogLevel, hl: string, level: number })[] Level configuration. --- @field float_precision number Can limit the number of decimals displayed for floats. --- User configuration section --- @type neorg.log.configuration local default_config = { plugin = "neorg", use_console = true, highlights = true, use_file = true, level = "warn", modes = { { name = "trace", hl = "Comment", level = vim.log.levels.TRACE }, { name = "debug", hl = "Comment", level = vim.log.levels.DEBUG }, { name = "info", hl = "None", level = vim.log.levels.INFO }, { name = "warn", hl = "WarningMsg", level = vim.log.levels.WARN }, { name = "error", hl = "ErrorMsg", level = vim.log.levels.ERROR }, { name = "fatal", hl = "ErrorMsg", level = 5 }, }, float_precision = 0.01, } -- {{{ NO NEED TO CHANGE local log = {} log.get_default_config = function() return default_config end local unpack = unpack or table.unpack --- @param config neorg.log.configuration --- @param standalone boolean log.new = function(config, standalone) config = vim.tbl_deep_extend("force", default_config, config) config.plugin = "neorg" -- Force the plugin name to be neorg local outfile = string.format("%s/%s.log", vim.api.nvim_call_function("stdpath", { "data" }), config.plugin) local obj = standalone ~= nil and log or {} local levels = {} for _, v in ipairs(config.modes) do levels[v.name] = v.level end local round = function(x, increment) increment = increment or 1 x = x / increment return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment end local make_string = function(...) local t = {} for i = 1, select("#", ...) do local x = select(i, ...) if type(x) == "number" and config.float_precision then x = tostring(round(x, config.float_precision)) elseif type(x) == "table" then x = vim.inspect(x) else x = tostring(x) end t[#t + 1] = x end return table.concat(t, " ") end local log_at_level = function(level_config, message_maker, ...) -- Return early if we"re below the config.level if levels[level_config.name] < levels[config.level] then return end local nameupper = level_config.name:upper() local msg = message_maker(...) local info = debug.getinfo(2, "Sl") local lineinfo = info.short_src .. ":" .. info.currentline -- Output to console if config.use_console then local v = string.format("(%s)\n%s\n%s", os.date("%H:%M:%S"), lineinfo, msg) if config.highlights and level_config.hl then (vim.schedule_wrap(function() vim.cmd(string.format("echohl %s", level_config.hl)) end))() end (vim.schedule_wrap(function() vim.notify(string.format("[%s] %s", config.plugin, vim.fn.escape(v, '"')), level_config.level) -- vim.cmd(string.format([[echom "[%s] %s"]], config.plugin, vim.fn.escape(v, '"'))) end))() if config.highlights and level_config.hl then (vim.schedule_wrap(function() vim.cmd("echohl NONE") end))() end end -- Output to log file if config.use_file then local fp = assert(io.open(outfile, "a")) local str = string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg) fp:write(str) fp:close() end end for _, x in ipairs(config.modes) do obj[x.name] = function(...) return log_at_level(x, make_string, ...) end obj[("fmt_%s"):format(x.name)] = function() return log_at_level(x, function(...) local passed = { ... } local fmt = table.remove(passed, 1) local inspected = {} for _, v in ipairs(passed) do table.insert(inspected, vim.inspect(v)) end return string.format(fmt, unpack(inspected)) end) end end end -- }}} return log ================================================ FILE: lua/neorg/core/modules.lua ================================================ --- @brief [[ --- Base file for modules. --- This file contains the base implementation for "modules", building blocks of the Neorg environment. --- @brief ]] -- TODO: What goes below this line until the next notice used to belong to modules.base -- We need to find a way to make these constructors easier to maintain and more efficient local callbacks = require("neorg.core.callbacks") local config = require("neorg.core.config") local log = require("neorg.core.log") local utils = require("neorg.core.utils") --- @alias neorg.module.public { version: string, [any]: any } --- @class (exact) neorg.module.resolver --- @field ["core.autocommands"] core.autocommands --- @field ["core.clipboard"] core.clipboard --- @field ["core.completion"] core.completion --- @field ["core.concealer"] core.concealer --- @field ["core.dirman"] core.dirman --- @field ["core.esupports.hop"] core.esupports.hop --- @field ["core.esupports.indent"] core.esupports.indent --- @field ["core.esupports.metagen"] core.esupports.metagen --- @field ["core.export"] core.export --- @field ["core.export.markdown"] core.export.markdown --- @field ["core.fs"] core.fs --- @field ["core.highlights"] core.highlights --- @field ["core.integrations.treesitter"] core.integrations.treesitter --- @field ["core.itero"] core.itero --- @field ["core.journal"] core.journal --- @field ["core.keybinds"] core.keybinds --- @field ["core.latex.renderer"] core.latex.renderer --- @field ["core.links"] core.links --- @field ["core.looking-glass"] core.looking-glass --- @field ["core.neorgcmd"] core.neorgcmd --- @field ["core.pivot"] core.pivot --- @field ["core.presenter"] core.presenter --- @field ["core.promo"] core.promo --- @field ["core.qol.toc"] core.qol.toc --- @field ["core.qol.todo_items"] core.qol.todo_items --- @field ["core.queries.native"] core.queries.native --- @field ["core.scanner"] core.scanner --- @field ["core.storage"] core.storage --- @field ["core.summary"] core.summary --- @field ["core.syntax"] core.syntax --- @field ["core.tangle"] core.tangle --- @field ["core.tempus"] core.tempus --- @field ["core.text-objects"] core.text-objects --- @field ["core.todo-introspector"] core.todo-introspector --- @field ["core.ui"] core.ui --- @field ["core.ui.calendar"] core.ui.calendar --- @field ["core.ui.calendar.views.monthly"] core.ui.calendar.views.monthly --- @field ["core.ui.selection_popup"] core.ui.selection_popup --- @field ["core.ui.text_popup"] core.ui.text_popup --- Defines both a public and private configuration for a Neorg module. --- Public configurations may be tweaked by the user from the `neorg.setup()` function, --- whereas private configurations are for internal use only. --- @class (exact) neorg.module.configuration --- @field custom? table Internal table that tracks the differences (changes) between the default `public` table and the new (altered) `public` table. It contains only the tables that the user has altered in their own configuration. --- @field public private? table Internal configuration variables that may be tweaked by the developer. --- @field public public? table Configuration variables that may be tweaked by the user. --- @class (exact) neorg.module.events --- @field defined? { [string]: neorg.event } Lists all events defined by this module. --- @field subscribed? { [string]: { [string]: boolean } } Lists the events that the module is subscribed to. --- @alias neorg.module.setup { success: boolean, requires?: string[], replaces?: string, replace_merge?: boolean, wants?: string[] } --- Defines a module. --- A module is an object that contains a set of hooks which are invoked by Neorg whenever something in the --- environment occurs. This can be an event, a simple act of the module being loaded or anything else. --- @class (exact) neorg.module --- @field config? neorg.module.configuration The configuration for the module. --- @field events? neorg.module.events Describes all information related to events for this module. --- @field examples? table Contains examples of how to use the modules that users or developers may sift through. --- @field imported? table Imported submodules of the given module. Contrary to `required`, which only exposes the public API of a module, imported modules can be accessed in their entirety. --- @field load? fun() Function that is invoked once the module is considered "stable", i.e. after all dependencies are loaded. Perform your main loading routine here. --- @field name string The name of the module. --- @field neorg_post_load? fun() Function that is invoked after all modules are loaded. Useful if you want the Neorg environment to be fully set up before performing some task. --- @field path string The full path to the module (a more verbose version of `name`). May be used in lua's `require()` statements. --- @field public private? table A convenience table to place all of your private variables that you don't want to expose. --- @field public public? neorg.module.public Every module can expose any set of information it sees fit through this field. All functions and variables declared in this table will be visiable to any other module loaded. --- @field required? neorg.module.resolver Contains the public tables of all modules that were required via the `requires` array provided in the `setup()` function of this module. --- @field setup? fun(): neorg.module.setup? Function that is invoked before any other loading occurs. Should perform preliminary startup tasks. --- @field replaced? boolean If `true`, this means the module is a replacement for a core module. This flag is set automatically whenever `setup().replaces` is set to a value. --- @field on_event fun(event: neorg.event) A callback that is invoked any time an event the module has subscribed to has fired. ---@class neorg.modules local modules = {} --- Returns a new Neorg module, exposing all the necessary function and variables. --- @param name string The name of the new module. Make sure this is unique. The recommended naming convention is `category.module_name` or `category.subcategory.module_name`. --- @param imports? string[] A list of imports to attach to the module. Import data is requestable via `module.required`. Use paths relative to the current module. --- @return neorg.module function modules.create(name, imports) ---@type neorg.module local new_module = { setup = function() return { success = true, requires = {}, replaces = nil, replace_merge = false } end, load = function() end, on_event = function() end, neorg_post_load = function() end, name = "core.default", path = "modules.core.default.module", private = {}, public = { version = config.norg_version, }, config = { private = { --[[ config_option = false, ["option_group"] = { sub_option = true } --]] }, public = { --[[ config_option = false, ["option_group"] = { sub_option = true } --]] }, custom = {}, }, events = { subscribed = { -- The events that the module is subscribed to --[[ ["core.test"] = { -- The name of the module that has events bound to it ["test_event"] = true, -- Subscribes to event core.test.events.test_event ["other_event"] = true -- Subscribes to event core.test.events.other_event } --]] }, defined = { -- The events that the module itself has defined --[[ ["my_event"] = { event_data } -- Creates an event of type category.module.events.my_event --]] }, }, required = { --[[ ["core.test"] = { -- Their public API here... }, ["core.some_other_plugin"] = { -- Their public API here... } --]] }, examples = { --[[ a_cool_test = function() print("Some code!") end --]] }, imported = { --[[ ["my.module.submodule"] = { ... }, --]] }, tests = function() end, } if imports then for _, import in ipairs(imports) do local fullpath = table.concat({ name, import }, ".") if not modules.load_module(fullpath) then log.error("Unable to load import '" .. fullpath .. "'! An error occured (see traceback below):") assert(false) -- Halt execution, no recovering from this error... end new_module.imported[fullpath] = modules.loaded_modules[fullpath] end end if name then new_module.name = name new_module.path = "modules." .. name end return new_module end --- Constructs a metamodule from a list of submodules. Metamodules are modules that can autoload batches of modules at once. --- @param name string The name of the new metamodule. Make sure this is unique. The recommended naming convention is `category.module_name` or `category.subcategory.module_name`. --- @param ... string A list of module names to load. --- @return neorg.module function modules.create_meta(name, ...) local module = modules.create(name) module.config.public.enable = { ... } module.setup = function() return { success = true } end module.load = function() module.config.public.enable = (function() -- If we haven't define any modules to disable then just return all enabled modules if not module.config.public.disable then return module.config.public.enable end local ret = {} -- For every enabled module for _, mod in ipairs(module.config.public.enable) do -- If that module does not exist in the disable table (ie. it is enabled) then add it to the `ret` table if not vim.tbl_contains(module.config.public.disable, mod) then table.insert(ret, mod) end end -- Return the table containing all the modules we would like to enable return ret end)() -- Go through every module that we have defined in the metamodule and load it! for _, mod in ipairs(module.config.public.enable) do modules.load_module(mod) end end return module end -- TODO: What goes below this line until the next notice used to belong to modules -- We need to find a way to make these functions easier to maintain --- Tracks the amount of currently loaded modules. modules.loaded_module_count = 0 --- The table of currently loaded modules --- @type { [string]: neorg.module } modules.loaded_modules = {} --- Loads and enables a module --- Loads a specified module. If the module subscribes to any events then they will be activated too. --- @param module neorg.module The actual module to load. --- @return boolean # Whether the module successfully loaded. function modules.load_module_from_table(module) log.info("Loading module with name", module.name) -- If our module is already loaded don't try loading it again if modules.loaded_modules[module.name] then log.trace("Module", module.name, "already loaded. Omitting...") return true end -- Invoke the setup function. This function returns whether or not the loading of the module was successful and some metadata. ---@type neorg.module.setup local loaded_module = module.setup and module.setup() or { success = true, replaces = {}, replace_merge = false, requires = {}, wants = {}, } -- We do not expect module.setup() to ever return nil, that's why this check is in place if not loaded_module then log.error( "Module", module.name, "does not handle module loading correctly; module.setup() returned nil. Omitting..." ) return false end -- A part of the table returned by module.setup() tells us whether or not the module initialization was successful if loaded_module.success == false then log.trace("Module", module.name, "did not load properly.") return false end --[[ -- This small snippet of code creates a copy of an already loaded module with the same name. -- If the module wants to replace an already loaded module then we need to create a deepcopy of that old module -- in order to stop it from getting overwritten. --]] ---@type neorg.module local module_to_replace -- If the return value of module.setup() tells us to hotswap with another module then cache the module we want to replace with if loaded_module.replaces and loaded_module.replaces ~= "" then module_to_replace = vim.deepcopy(modules.loaded_modules[loaded_module.replaces]) end -- Add the module into the list of loaded modules -- The reason we do this here is so other modules don't recursively require each other in the dependency loading loop below modules.loaded_modules[module.name] = module -- If the module "wants" any other modules then verify they are loaded if loaded_module.wants and not vim.tbl_isempty(loaded_module.wants) then log.info("Module", module.name, "wants certain modules. Ensuring they are loaded...") -- Loop through each dependency and ensure it's loaded for _, required_module in ipairs(loaded_module.wants) do log.trace("Verifying", required_module) -- This would've always returned false had we not added the current module to the loaded module list earlier above if not modules.is_module_loaded(required_module) then if config.user_config.load[required_module] then log.trace( "Wanted module", required_module, "isn't loaded but can be as it's defined in the user's config. Loading..." ) if not modules.load_module(required_module) then log.error( "Unable to load wanted module for", module.name, "- the module didn't load successfully" ) -- Make sure to clean up after ourselves if the module failed to load modules.loaded_modules[module.name] = nil return false end else log.error( ("Unable to load module %s, wanted dependency %s was not satisfied. Be sure to load the module and its appropriate config too!"):format( module.name, required_module ) ) -- Make sure to clean up after ourselves if the module failed to load modules.loaded_modules[module.name] = nil return false end end -- Create a reference to the dependency's public table module.required[required_module] = modules.loaded_modules[required_module].public end end -- If any dependencies have been defined, handle them if loaded_module.requires and vim.tbl_count(loaded_module.requires) > 0 then log.info("Module", module.name, "has dependencies. Loading dependencies first...") -- Loop through each dependency and load it one by one for _, required_module in pairs(loaded_module.requires) do log.trace("Loading submodule", required_module) -- This would've always returned false had we not added the current module to the loaded module list earlier above if not modules.is_module_loaded(required_module) then if not modules.load_module(required_module) then log.error( ("Unable to load module %s, required dependency %s did not load successfully"):format( module.name, required_module ) ) -- Make sure to clean up after ourselves if the module failed to load modules.loaded_modules[module.name] = nil return false end else log.trace("Module", required_module, "already loaded, skipping...") end -- Create a reference to the dependency's public table module.required[required_module] = modules.loaded_modules[required_module].public end end -- After loading all our dependencies, see if we need to hotswap another module with ourselves if module_to_replace then -- Make sure the names of both modules match module.name = module_to_replace.name -- Whenever a module gets hotswapped, a special flag is set inside the module in order to signalize that it has been hotswapped before -- If this flag has already been set before, then throw an error - there is no way for us to know which hotswapped module should take priority. if module_to_replace.replaced then log.error( ("Unable to replace module %s - module replacement clashing detected. This error triggers when a module tries to be replaced more than two times - neorg doesn't know which replacement to prioritize."):format( module_to_replace.name ) ) -- Make sure to clean up after ourselves if the module failed to load modules.loaded_modules[module.name] = nil return false end -- If the replace_merge flag is set to true in the setup() return value then recursively merge the data from the -- previous module into our new one. This allows for practically seamless hotswapping, as it allows you to retain the data -- of the previous module. if loaded_module.replace_merge then module = vim.tbl_deep_extend("force", module, { private = module_to_replace.private, config = module_to_replace.config, public = module_to_replace.public, events = module_to_replace.events, }) end -- Set the special module.replaced flag to let everyone know we've been hotswapped before module.replaced = true end log.info("Successfully loaded module", module.name) -- Keep track of the number of loaded modules modules.loaded_module_count = modules.loaded_module_count + 1 -- NOTE(vhyrro): Left here for debugging. -- Maybe make controllable with a switch in the future. -- local start = vim.loop.hrtime() -- Call the load function if module.load then module.load() end -- local msg = ("%fms"):format((vim.loop.hrtime() - start) / 1e6) -- vim.notify(msg .. " " .. module.name) modules.broadcast_event({ type = "core.module_loaded", split_type = { "core", "module_loaded" }, filename = "", filehead = "", cursor_position = { 0, 0 }, referrer = "core", line_content = "", content = module, broadcast = true, buffer = vim.api.nvim_get_current_buf(), window = vim.api.nvim_get_current_win(), mode = vim.fn.mode(), }) return true end --- Unlike `load_module_from_table()`, which loads a module from memory, `load_module()` tries to find the corresponding module file on disk and loads it into memory. --- If the module cannot not be found, attempt to load it off of github (unimplemented). This function also applies user-defined config and keymaps to the modules themselves. --- This is the recommended way of loading modules - `load_module_from_table()` should only really be used by neorg itself. --- @param module_name string A path to a module on disk. A path seperator in neorg is '.', not '/'. --- @param cfg table? A config that reflects the structure of `neorg.config.user_config.load["module.name"].config`. --- @return boolean # Whether the module was successfully loaded. function modules.load_module(module_name, cfg) -- Don't bother loading the module from disk if it's already loaded if modules.is_module_loaded(module_name) then return true end -- Attempt to require the module, does not throw an error if the module doesn't exist local module = require("neorg.modules." .. module_name .. ".module") -- If the module is nil for some reason return false if not module then log.error( "Unable to load module", module_name, "- loaded file returned nil. Be sure to return the table created by modules.create() at the end of your module.lua file!" ) return false end -- If the value of `module` is strictly true then it means the required file returned nothing -- We obviously can't do anything meaningful with that! if module == true then log.error( "An error has occurred when loading", module_name, "- loaded file didn't return anything meaningful. Be sure to return the table created by modules.create() at the end of your module.lua file!" ) return false end -- Load the user-defined config if cfg and not vim.tbl_isempty(cfg) then module.config.custom = cfg module.config.public = vim.tbl_deep_extend("force", module.config.public, cfg) else module.config.custom = config.modules[module_name] module.config.public = vim.tbl_deep_extend("force", module.config.public, module.config.custom or {}) end -- Pass execution onto load_module_from_table() and let it handle the rest return modules.load_module_from_table(module) end --- Has the same principle of operation as load_module_from_table(), except it then sets up the parent module's "required" table, allowing the parent to access the child as if it were a dependency. --- @param module neorg.module A valid table as returned by modules.create() --- @param parent_module string|neorg.module If a string, then the parent is searched for in the loaded modules. If a table, then the module is treated as a valid module as returned by modules.create() function modules.load_module_as_dependency_from_table(module, parent_module) if modules.load_module_from_table(module) then if type(parent_module) == "string" then modules.loaded_modules[parent_module].required[module.name] = module.public elseif type(parent_module) == "table" then parent_module.required[module.name] = module.public end end end --- Normally loads a module, but then sets up the parent module's "required" table, allowing the parent module to access the child as if it were a dependency. --- @param module_name string A path to a module on disk. A path seperator in neorg is '.', not '/' --- @param parent_module string The name of the parent module. This is the module which the dependency will be attached to. --- @param cfg? table A config that reflects the structure of neorg.config.user_config.load["module.name"].config function modules.load_module_as_dependency(module_name, parent_module, cfg) if modules.load_module(module_name, cfg) and modules.is_module_loaded(parent_module) then modules.loaded_modules[parent_module].required[module_name] = modules.get_module_config(module_name) end end --- Retrieves the public API exposed by the module. --- @generic T --- @param module_name `T` The name of the module to retrieve. --- @return T? function modules.get_module(module_name) if not modules.is_module_loaded(module_name) then log.trace("Attempt to get module with name", module_name, "failed - module is not loaded.") return end return modules.loaded_modules[module_name].public end --- Returns the module.config.public table if the module is loaded --- @param module_name string The name of the module to retrieve (module must be loaded) --- @return table? function modules.get_module_config(module_name) if not modules.is_module_loaded(module_name) then log.trace("Attempt to get module config with name", module_name, "failed - module is not loaded.") return end return modules.loaded_modules[module_name].config.public end --- Returns true if module with name module_name is loaded, false otherwise --- @param module_name string The name of an arbitrary module --- @return boolean function modules.is_module_loaded(module_name) return modules.loaded_modules[module_name] ~= nil end --- Reads the module's public table and looks for a version variable, then converts it from a string into a table, like so: `{ major = , minor = , patch = }`. --- @param module_name string The name of a valid, loaded module. --- @return table? parsed_version function modules.get_module_version(module_name) -- If the module isn't loaded then don't bother retrieving its version if not modules.is_module_loaded(module_name) then log.trace("Attempt to get module version with name", module_name, "failed - module is not loaded.") return end -- Grab the version of the module local version = modules.get_module(module_name).version -- If it can't be found then error out if not version then log.trace("Attempt to get module version with name", module_name, "failed - version variable not present.") return end return utils.parse_version_string(version) end --- Executes `callback` once `module` is a valid and loaded module, else the callback gets instantly executed. --- @param module_name string The name of the module to listen for. --- @param callback fun(module_public_table: neorg.module.public) The callback to execute. function modules.await(module_name, callback) if modules.is_module_loaded(module_name) then callback(assert(modules.get_module(module_name))) return end callbacks.on_event("core.module_loaded", function(_, module) callback(module.public) end, function(event) return event.content.name == module_name end) end --- @alias Mode --- | "n" --- | "no" --- | "nov" --- | "noV" --- | "noCTRL-V" --- | "CTRL-V" --- | "niI" --- | "niR" --- | "niV" --- | "nt" --- | "Terminal" --- | "ntT" --- | "v" --- | "vs" --- | "V" --- | "Vs" --- | "CTRL-V" --- | "CTRL-Vs" --- | "s" --- | "S" --- | "CTRL-S" --- | "i" --- | "ic" --- | "ix" --- | "R" --- | "Rc" --- | "Rx" --- | "Rv" --- | "Rvc" --- | "Rvx" --- | "c" --- | "cr" --- | "cv" --- | "cvr" --- | "r" --- | "rm" --- | "r?" --- | "!" --- | "t" --- @class (exact) neorg.event --- @field type string The type of the event. Exists in the format of `category.name`. --- @field split_type string[] The event type, just split on every `.` character, e.g. `{ "category", "name" }`. --- @field content? table|any The content of the event. The data found here is specific to each individual event. Can be thought of as the payload. --- @field referrer string The name of the module that triggered the event. --- @field broadcast boolean Whether the event was broadcast to all modules. `true` is so, `false` if the event was specifically sent to a single recipient. --- @field cursor_position { [1]: number, [2]: number } The position of the cursor at the moment of broadcasting the event. --- @field filename string The name of the file that the user was in at the moment of broadcasting the event. --- @field filehead string The directory the user was in at the moment of broadcasting the event. --- @field line_content string The content of the line the user was editing at the moment of broadcasting the event. --- @field buffer number The buffer ID of the buffer the user was in at the moment of broadcasting the event. --- @field window number The window ID of the window the user was in at the moment of broadcasting the event. --- @field mode Mode The mode Neovim was in at the moment of broadcasting the event. -- TODO: What goes below this line until the next notice used to belong to modules -- We need to find a way to make these functions easier to maintain --[[ -- NEORG EVENT FILE -- This file is responsible for dealing with event handling and broadcasting. -- All modules that subscribe to an event will receive it once it is triggered. --]] --- The working of this function is best illustrated with an example: -- If type == 'core.some_plugin.events.my_event', this function will return { 'core.some_plugin', 'my_event' } --- @param type string The full path of a module event --- @return string[]? function modules.split_event_type(type) local start_str, end_str = type:find("%.events%.") local split_event_type = { type:sub(0, start_str - 1), type:sub(end_str + 1) } if #split_event_type ~= 2 then log.warn("Invalid type name:", type) return end return split_event_type end --- Returns an event template defined in `module.events.defined`. --- @param module neorg.module A reference to the module invoking the function --- @param type string A full path to a valid event type (e.g. `core.module.events.some_event`) --- @return neorg.event? function modules.get_event_template(module, type) -- You can't get the event template of a type if the type isn't loaded if not modules.is_module_loaded(module.name) then log.info("Unable to get event of type", type, "with module", module.name) return end -- Split the event type into two local split_type = modules.split_event_type(type) if not split_type then log.warn("Unable to get event template for event", type, "and module", module.name) return end log.trace("Returning", split_type[2], "for module", split_type[1]) -- Return the defined event from the specific module return modules.loaded_modules[module.name].events.defined[split_type[2]] end --- Creates a deep copy of the `modules.base_event` event and returns it with a custom type and referrer. --- @param module neorg.module A reference to the module invoking the function. --- @param name string A relative path to a valid event template. --- @return neorg.event function modules.define_event(module, name) -- Create a copy of the base event and override the values with ones specified by the user local new_event = { type = "core.base_event", split_type = {}, content = nil, referrer = nil, broadcast = true, cursor_position = {}, filename = "", filehead = "", line_content = "", buffer = 0, window = 0, mode = "", } if name then new_event.type = module.name .. ".events." .. name end new_event.referrer = module.name return new_event end --- Returns a copy of the event template provided by a module. --- @param module neorg.module A reference to the module invoking the function --- @param type string A full path to a valid event type (e.g. `core.module.events.some_event`) --- @param content table|any? The content of the event, can be anything from a string to a table to whatever you please. --- @param ev? table The original event data. --- @return neorg.event? # New event. function modules.create_event(module, type, content, ev) -- Get the module that contains the event local module_name = modules.split_event_type(type)[1] -- Retrieve the template from module.events.defined local event_template = modules.get_event_template(modules.loaded_modules[module_name] or { name = "" }, type) if not event_template then log.warn("Unable to create event of type", type, ". Returning nil...") return end -- Make a deep copy here - we don't want to override the actual base table! local new_event = vim.deepcopy(event_template) new_event.type = type new_event.content = content new_event.referrer = module.name -- Override all the important values new_event.split_type = assert(modules.split_event_type(type)) new_event.filename = vim.fn.expand("%:t") --[[@as string]] new_event.filehead = vim.fn.expand("%:p:h") --[[@as string]] local bufid = ev and ev.buf or vim.api.nvim_get_current_buf() local winid = assert(vim.fn.bufwinid(bufid)) if winid == -1 then winid = vim.api.nvim_get_current_win() end new_event.cursor_position = vim.api.nvim_win_get_cursor(winid) local row_1b = new_event.cursor_position[1] new_event.line_content = vim.api.nvim_buf_get_lines(bufid, row_1b - 1, row_1b, true)[1] new_event.referrer = module.name new_event.broadcast = true new_event.buffer = bufid new_event.window = winid new_event.mode = vim.api.nvim_get_mode().mode return new_event end --- Sends an event to all subscribed modules. The event contains the filename, filehead, cursor position and line content as a bonus. --- @param event neorg.event An event, usually created by `modules.create_event()`. --- @param callback function? A callback to be invoked after all events have been asynchronously broadcast function modules.broadcast_event(event, callback) -- Broadcast the event to all modules if not event.split_type then log.error("Unable to broadcast event of type", event.type, "- invalid event name") return end -- Let the callback handler know of the event callbacks.handle_callbacks(event) -- Loop through all the modules for _, current_module in pairs(modules.loaded_modules) do -- If the current module has any subscribed events and if it has a subscription bound to the event's module name then if current_module.events.subscribed and current_module.events.subscribed[event.split_type[1]] then -- Check whether we are subscribed to the event type local evt = current_module.events.subscribed[event.split_type[1]][event.split_type[2]] if evt ~= nil and evt == true then -- Run the on_event() for that module current_module.on_event(event) end end end -- Because the broadcasting of events is async we allow the event broadcaster to provide a callback -- TODO: deprecate if callback then callback() end end --- Instead of broadcasting to all loaded modules, `send_event()` only sends to one module. --- @param recipient string The name of a loaded module that will be the recipient of the event. --- @param event neorg.event An event, usually created by `modules.create_event()`. function modules.send_event(recipient, event) -- If the recipient is not loaded then there's no reason to send an event to it if not modules.is_module_loaded(recipient) then log.warn("Unable to send event to module", recipient, "- the module is not loaded.") return end -- Set the broadcast variable to false since we're not invoking broadcast_event() event.broadcast = false -- Let the callback handler know of the event callbacks.handle_callbacks(event) -- Get the recipient module and check whether it's subscribed to our event local mod = modules.loaded_modules[recipient] if mod.events.subscribed and mod.events.subscribed[event.split_type[1]] then local evt = mod.events.subscribed[event.split_type[1]][event.split_type[2]] -- If it is then trigger the module's on_event() function if evt ~= nil and evt == true then mod.on_event(event) end end end return modules ================================================ FILE: lua/neorg/core/utils.lua ================================================ local configuration = require("neorg.core.config") local log = require("neorg.core.log") ---@class neorg.core.utils local utils = {} local version = vim.version() -- TODO: Move to a more local scope --- A version agnostic way to call the neovim treesitter query parser --- @param language string # Language to use for the query --- @param query_string string # Query in s-expr syntax --- @return vim.treesitter.Query # Parsed query function utils.ts_parse_query(language, query_string) if vim.treesitter.query.parse then return vim.treesitter.query.parse(language, query_string) else ---@diagnostic disable-next-line return vim.treesitter.parse_query(language, query_string) end end --- An OS agnostic way of querying the current user --- @return string username function utils.get_username() local current_os = configuration.os_info if not current_os then return "" end if current_os == "linux" or current_os == "mac" or current_os == "wsl" then return os.getenv("USER") or "" elseif current_os == "windows" then return os.getenv("username") or "" end return "" end --- Returns an array of strings, the array being a list of languages that Neorg can inject. ---@param values boolean If set to true will return an array of strings, if false will return a key-value table. ---@return string[]|table function utils.get_language_list(values) local regex_files = {} local ts_files = {} -- Search for regex files in syntax and after/syntax. -- Its best if we strip out anything but the ft name. for _, lang in pairs(vim.api.nvim_get_runtime_file("syntax/*.vim", true)) do local lang_name = vim.fn.fnamemodify(lang, ":t:r") table.insert(regex_files, lang_name) end for _, lang in pairs(vim.api.nvim_get_runtime_file("after/syntax/*.vim", true)) do local lang_name = vim.fn.fnamemodify(lang, ":t:r") table.insert(regex_files, lang_name) end -- Search for available parsers for _, parser in pairs(vim.api.nvim_get_runtime_file("parser/*.so", true)) do local parser_name = assert(vim.fn.fnamemodify(parser, ":t:r")) ts_files[parser_name] = true end local ret = {} for _, syntax in pairs(regex_files) do if ts_files[syntax] then ret[syntax] = { type = "treesitter" } else ret[syntax] = { type = "syntax" } end end return values and vim.tbl_keys(ret) or ret end --- Gets a list of shorthands for a given language. --- @param reverse_lookup boolean Whether to create a reverse lookup for the table. --- @return LanguageList function utils.get_language_shorthands(reverse_lookup) ---@class LanguageList local langs = { ["bash"] = { "sh", "zsh" }, ["c_sharp"] = { "csharp", "cs" }, ["clojure"] = { "clj" }, ["cmake"] = { "cmake.in" }, ["commonlisp"] = { "cl" }, ["cpp"] = { "hpp", "cc", "hh", "c++", "h++", "cxx", "hxx" }, ["dockerfile"] = { "docker" }, ["erlang"] = { "erl" }, ["fennel"] = { "fnl" }, ["fortran"] = { "f90", "f95" }, ["go"] = { "golang" }, ["godot"] = { "gdscript" }, ["gomod"] = { "gm" }, ["haskell"] = { "hs" }, ["java"] = { "jsp" }, ["javascript"] = { "js", "jsx" }, ["julia"] = { "julia-repl" }, ["kotlin"] = { "kt" }, ["python"] = { "py", "gyp" }, ["ruby"] = { "rb", "gemspec", "podspec", "thor", "irb" }, ["rust"] = { "rs" }, ["supercollider"] = { "sc" }, ["typescript"] = { "ts" }, ["verilog"] = { "v" }, ["yaml"] = { "yml" }, } -- TODO: `vim.tbl_add_reverse_lookup` deprecated: NO ALTERNATIVES -- GOOD JOB CORE DEVS -- return reverse_lookup and vim.tbl_add_reverse_lookup(langs) or langs ---@diagnostic disable-line end --- Checks whether Neovim is running at least at a specific version. --- @param major number The major release of Neovim. --- @param minor number The minor release of Neovim. --- @param patch number The patch number (in case you need it). --- @return boolean # Whether Neovim is running at the same or a higher version than the one given. function utils.is_minimum_version(major, minor, patch) if major ~= version.major then return major < version.major end if minor ~= version.minor then return minor < version.minor end if patch ~= version.patch then return patch < version.patch end return true end --- Parses a version string like "0.4.2" and provides back a table like { major = , minor = , patch = } --- @param version_string string The input string. --- @return table? # The parsed version string, or `nil` if a failure occurred during parsing. function utils.parse_version_string(version_string) if not version_string then return end -- Define variables that split the version up into 3 slices local split_version, versions, ret = vim.split(version_string, ".", { plain = true }), { "major", "minor", "patch" }, { major = 0, minor = 0, patch = 0 } -- If the sliced version string has more than 3 elements error out if #split_version > 3 then log.warn( "Attempt to parse version:", version_string, "failed - too many version numbers provided. Version should follow this layout: .." ) return end -- Loop through all the versions and check whether they are valid numbers. If they are, add them to the return table for i, ver in ipairs(versions) do if split_version[i] then local num = tonumber(split_version[i]) if not num then log.warn("Invalid version provided, string cannot be converted to integral type.") return end ret[ver] = num end end return ret end --- Custom Neorg notifications. Wrapper around `vim.notify`. --- @param msg string Message to send. --- @param log_level integer? Log level in `vim.log.levels`. function utils.notify(msg, log_level) vim.notify(msg, log_level, { title = "Neorg" }) end --- Opens up an array of files and runs a callback for each opened file. --- @param files (string|PathlibPath)[] An array of files to open. --- @param callback fun(buffer: integer, filename: string) The callback to invoke for each file. function utils.read_files(files, callback) for _, file in ipairs(files) do file = tostring(file) local bufnr = vim.uri_to_bufnr(vim.uri_from_fname(file)) local should_delete = not vim.api.nvim_buf_is_loaded(bufnr) vim.fn.bufload(bufnr) callback(bufnr, file) if should_delete then vim.api.nvim_buf_delete(bufnr, { force = true }) end end end -- following https://gist.github.com/kylechui/a5c1258cd2d86755f97b10fc921315c3 function utils.set_operatorfunc(f) utils._neorg_operatorfunc = f vim.go.operatorfunc = "v:lua.require'neorg'.utils._neorg_operatorfunc" end function utils.wrap_dotrepeat(callback) return function(...) if vim.api.nvim_get_mode().mode == "i" then callback(...) return end local args = { ... } utils.set_operatorfunc(function() callback(unpack(args)) end) vim.cmd("normal! g@l") end end --- Truncate input string to fit inside the `col_limit` when displayed. Takes non-ascii chars into account. --- @param str string The string to limit. --- @param col_limit integer `str` will be cut so that when displayed, the display length does not exceed this limit. --- @return string # Substring of input str function utils.truncate_by_cell(str, col_limit) if str and str:len() == vim.api.nvim_strwidth(str) then return vim.fn.strcharpart(str, 0, col_limit) end local short = vim.fn.strcharpart(str, 0, col_limit) if vim.api.nvim_strwidth(short) > col_limit then while vim.api.nvim_strwidth(short) > col_limit do short = vim.fn.strcharpart(short, 0, vim.fn.strchars(short) - 1) end end return short end return utils ================================================ FILE: lua/neorg/external/log.lua ================================================ ================================================ FILE: lua/neorg/health.lua ================================================ return { check = function() local config = require("neorg.core").config.user_config local modules = require("neorg.core.modules") vim.health.start("Neorg Configuration") if config.load == nil or vim.tbl_isempty(config.load) then vim.health.ok("Empty configuration provided: Neorg will load `core.defaults` by default.") elseif type(config.load) ~= "table" then vim.health.error("Invalid data type provided. `load` table should be a dictionary of modules!") else vim.health.info("Checking `load` table...") for key, value in pairs(config.load) do if type(key) ~= "string" then vim.health.error( string.format( "Invalid data type provided within `load` table! Expected a module name (e.g. `core.defaults`), got a %s instead.", type(key) ) ) elseif not modules.load_module(key) then vim.health.warn( string.format( "You are attempting to load a module `%s` which is not recognized by Neorg at this time. You may receive an error upon launching Neorg.", key ) ) elseif type(value) ~= "table" then vim.health.error( string.format( "Invalid data type provided within `load` table for module `%s`! Expected module data (e.g. `{ config = { ... } }`), got a %s instead.", key, type(key) ) ) elseif value.config and type(value.config) ~= "table" then vim.health.error( string.format( "Invalid data type provided within data table for module `%s`! Expected configuration data (e.g. `config = { ... }`), but `config` was set to a %s instead.", key, type(key) ) ) elseif #vim.tbl_keys(value) > 1 and value.config ~= nil then vim.health.warn( string.format( "Unexpected extra data provided to module `%s` - each module only expects a `config` table to be provided, nothing else.", key ) ) elseif (#vim.tbl_keys(value) > 0 and value.config == nil) or #vim.tbl_keys(value) > 1 then vim.health.warn( string.format( "Misplaced configuration data for module `%s` - it seems like you forgot to put your module configuration inside a `config = {}` table?", key ) ) else vim.health.ok(string.format("Module declaration for `%s` is well-formed", key)) end end -- TODO(vhyrro): Check the correctness of the logger table too if config.logger == nil or vim.tbl_isempty(config.logger) then vim.health.ok("Default configuration for logger provided, Neorg will not output debug info.") end end vim.health.start("Neorg Dependencies") if vim.fn.executable("luarocks") then vim.health.ok("`luarocks` is installed.") else vim.health.error( "`luarocks` not installed on your system! Please consult the Neorg README for installation instructions." ) end vim.health.start("Neorg Keybinds") modules.load_module("core.keybinds") local keybinds = modules.get_module("core.keybinds") local keybinds_config = modules.get_module_config("core.keybinds") assert(keybinds and keybinds_config, "keybinds module missing") if keybinds_config.default_keybinds then local key_healthcheck = keybinds.health() if key_healthcheck.preset_exists then vim.health.info(string.format("Neorg is configured to use keybind preset `%s`", keybinds_config.preset)) else vim.health.error( string.format( "Invalid configuration found: preset `%s` does not exist! Did you perhaps make a typo?", keybinds_config.preset ) ) return end for remap_key, remap_rhs in vim.spairs(key_healthcheck.remaps) do vim.health.ok( string.format( "Action `%s` (bound to `%s` by default) has been remapped to something else in your configuration.", remap_rhs, remap_key ) ) end local ok = true for conflict_key, rhs in vim.spairs(key_healthcheck.conflicts) do vim.health.warn( string.format( "Key `%s` conflicts with a key bound by the user. Neorg will not bind this key.", conflict_key ), string.format("consider mapping `%s` to a different key than the one bound by Neorg.", rhs) ) ok = false end if ok then vim.health.ok("No keybind conflicts found.") end else vim.health.ok("Neorg is not configured to set any default keybinds.") end end, } ================================================ FILE: lua/neorg/init.lua ================================================ --- @brief [[ --- This file marks the beginning of the entire plugin. It's here that everything fires up and starts pumping. --- @brief ]] local neorg = require("neorg.core") local config, log, modules, utils = neorg.config, neorg.log, neorg.modules, neorg.utils --- @module "neorg.core.config" --- Initializes Neorg. Parses the supplied user configuration, initializes all selected modules and adds filetype checking for `.norg`. --- @param cfg neorg.configuration.user? A table that reflects the structure of `config.user_config`. --- @see config.user_config --- @see neorg.configuration.user function neorg.setup(cfg) -- Ensure that we are running Neovim 0.10+ assert(utils.is_minimum_version(0, 10, 0), "Neorg requires at least Neovim version 0.10 to operate!") -- If the user supplied no configuration then generate a default one (assume the user wants the defaults) cfg = cfg or { load = { ["core.defaults"] = {}, }, } -- If no `load` table was passed whatsoever then assume the user wants the default ones. -- If the user explicitly sets `load = {}` in their configs then that means they do not want -- any modules loaded. -- -- We check for nil specifically because some users might think `load = false` is a valid thing. -- With the explicit check `load = false` will issue an error. if cfg.load == nil then cfg.load = { ["core.defaults"] = {}, } end config.user_config = vim.tbl_deep_extend("force", config.user_config, cfg) -- Create a new global instance of the neorg logger. log.new(config.user_config.logger or log.get_default_config(), true) -- If the file we have entered has a `.norg` extension: if vim.fn.expand("%:e") == "norg" or not config.user_config.lazy_loading then -- Then boot up the environment. neorg.org_file_entered(false) else -- Else listen for a BufAdd event for `.norg` files and fire up the Neorg environment. vim.api.nvim_create_user_command("NeorgStart", function() vim.cmd.delcommand("NeorgStart") neorg.org_file_entered(true) end, {}) vim.api.nvim_create_autocmd("BufAdd", { pattern = "norg", callback = function() neorg.org_file_entered(false) end, }) end end --- This function gets called upon entering a .norg file and loads all of the user-defined modules. --- @param manual boolean If true then the environment was kickstarted manually by the user. --- @param arguments string? A list of arguments in the format of "key=value other_key=other_value". function neorg.org_file_entered(manual, arguments) -- Extract the module list from the user config local module_list = config.user_config and config.user_config.load or {} -- If we have already started Neorg or if we haven't defined any modules to load then bail if config.started or not module_list or vim.tbl_isempty(module_list) then return end -- If the user has defined a post-load hook then execute it if config.user_config.hook then config.user_config.hook(manual, arguments) end -- If Neorg was loaded manually (through `:NeorgStart`) then set this flag to true config.manual = manual -- If the user has supplied any Neorg environment variables -- then parse those here if arguments and arguments:len() > 0 then for key, value in arguments:gmatch("([%w%W]+)=([%w%W]+)") do config.arguments[key] = value end end -- Go through each defined module and grab its config for name, module in pairs(module_list) do -- Apply the config config.modules[name] = vim.tbl_deep_extend("force", config.modules[name] or {}, module.config or {}) end -- After all config are merged proceed to actually load the modules local load_module = modules.load_module for name, _ in pairs(module_list) do -- If it could not be loaded then halt if not load_module(name) then log.warn("Recovering from error...") modules.loaded_modules[name] = nil end end -- Goes through each loaded module and invokes neorg_post_load() for _, module in pairs(modules.loaded_modules) do module.neorg_post_load() end -- Set this variable to prevent Neorg from loading twice config.started = true -- Lets the entire Neorg environment know that Neorg has started! modules.broadcast_event({ type = "core.started", split_type = { "core", "started" }, filename = "", filehead = "", cursor_position = { 0, 0 }, referrer = "core", line_content = "", broadcast = true, buffer = vim.api.nvim_get_current_buf(), window = vim.api.nvim_get_current_win(), mode = vim.fn.mode(), }) -- Sometimes external plugins prefer hooking in to an autocommand vim.api.nvim_exec_autocmds("User", { pattern = "NeorgStarted", }) end --- Returns whether or not Neorg is loaded --- @return boolean function neorg.is_loaded() return config.started end return neorg ================================================ FILE: lua/neorg/modules/core/autocommands/module.lua ================================================ --[[ file: Autocommands summary: Handles the creation and management of Neovim's autocommands. description: Handles the creation and management of Neovim's autocommands. internal: true --- This internal module exposes functionality for subscribing to autocommands and performing actions based on those autocommands. ###### NOTE: This module will be soon deprecated, and it's favourable to use the `vim.api*` functions instead. In your `module.setup()`, make sure to require `core.autocommands` (`requires = { "core.autocommands" }`) Afterwards in a function of your choice that gets called *after* core.autocommmands gets intialized (e.g. `load()`): ```lua module.load = function() module.required["core.autocommands"].enable_autocommand("VimLeavePre") -- Substitute VimLeavePre for any valid neovim autocommand end ``` Afterwards, be sure to subscribe to the event: ```lua module.events.subscribed = { ["core.autocommands"] = { vimleavepre = true } } ``` Upon receiving an event, it will come in this format: ```lua { type = "core.autocommands.events.", broadcast = true } ``` --]] local neorg = require("neorg.core") local log, modules = neorg.log, neorg.modules local module = modules.create("core.autocommands") --- This function gets invoked whenever a core.autocommands enabled autocommand is triggered. Note that this function should be only used internally ---@param name string #The name of the autocommand that was just triggered ---@param triggered_from_norg boolean #If true, that means we have received this event as part of a *.norg autocommand ---@param ev? table the original event data function _neorg_module_autocommand_triggered(name, triggered_from_norg, ev) local event = modules.create_event(module, name, { norg = triggered_from_norg }, ev) assert(event) modules.broadcast_event(event) end -- A convenience wrapper around modules.define_event_event module.private.autocmd_base = function(name) return modules.define_event(module, name) end ---@class core.autocommands module.public = { --- By default, all autocommands are disabled for performance reasons. To enable them, use this command. If an invalid autocmd is given nothing happens. ---@param autocmd string #The relative name of the autocommand to enable ---@param dont_isolate boolean? #Defaults to false. Specifies whether the autocommand should run globally (* instead of in Neorg files (*.norg) enable_autocommand = function(autocmd, dont_isolate) dont_isolate = dont_isolate or false autocmd = autocmd:lower() local subscribed_autocommand = module.events.subscribed["core.autocommands"][autocmd] if subscribed_autocommand ~= nil then vim.cmd("augroup Neorg") if dont_isolate and vim.fn.exists("#Neorg#" .. autocmd .. "#*") == 0 then vim.api.nvim_create_autocmd(autocmd, { callback = function(ev) _neorg_module_autocommand_triggered("core.autocommands.events." .. autocmd, false, ev) end, }) elseif vim.fn.exists("#Neorg#" .. autocmd .. "#*.norg") == 0 then vim.api.nvim_create_autocmd(autocmd, { pattern = "*.norg", callback = function(ev) _neorg_module_autocommand_triggered("core.autocommands.events." .. autocmd, true, ev) end, }) end vim.cmd("augroup END") module.events.subscribed["core.autocommands"][autocmd] = true end end, version = "0.0.8", } -- All the subscribeable events for core.autocommands module.events.subscribed = { ["core.autocommands"] = { bufadd = false, bufdelete = false, bufenter = false, buffilepost = false, buffilepre = false, bufhidden = false, bufleave = false, bufmodifiedset = false, bufnew = false, bufnewfile = false, bufreadpost = false, bufreadcmd = false, bufreadpre = false, bufunload = false, bufwinenter = false, bufwinleave = false, bufwipeout = false, bufwrite = false, bufwritecmd = false, bufwritepost = false, chaninfo = false, chanopen = false, cmdundefined = false, cmdlinechanged = false, cmdlineenter = false, cmdlineleave = false, cmdwinenter = false, cmdwinleave = false, colorscheme = false, colorschemepre = false, completechanged = false, completedonepre = false, completedone = false, cursorhold = false, cursorholdi = false, cursormoved = false, cursormovedi = false, diffupdated = false, dirchanged = false, fileappendcmd = false, fileappendpost = false, fileappendpre = false, filechangedro = false, exitpre = false, filechangedshell = false, filechangedshellpost = false, filereadcmd = false, filereadpost = false, filereadpre = false, filetype = false, filewritecmd = false, filewritepost = false, filewritepre = false, filterreadpost = false, filterreadpre = false, filterwritepost = false, filterwritepre = false, focusgained = false, focuslost = false, funcundefined = false, uienter = false, uileave = false, insertchange = false, insertcharpre = false, textyankpost = false, insertenter = false, insertleavepre = false, insertleave = false, menupopup = false, optionset = false, quickfixcmdpre = false, quickfixcmdpost = false, quitpre = false, remotereply = false, sessionloadpost = false, shellcmdpost = false, signal = false, shellfilterpost = false, sourcepre = false, sourcepost = false, sourcecmd = false, spellfilemissing = false, stdinreadpost = false, stdinreadpre = false, swapexists = false, syntax = false, tabenter = false, tableave = false, tabnew = false, tabnewentered = false, tabclosed = false, termopen = false, termenter = false, termleave = false, termclose = false, termresponse = false, textchanged = false, textchangedi = false, textchangedp = false, user = false, usergettingbored = false, vimenter = false, vimleave = false, vimleavepre = false, vimresized = false, vimresume = false, vimsuspend = false, winclosed = false, winenter = false, winleave = false, winnew = false, winscrolled = false, }, } -- All the autocommand definitions module.events.defined = { bufadd = module.private.autocmd_base("bufadd"), bufdelete = module.private.autocmd_base("bufdelete"), bufenter = module.private.autocmd_base("bufenter"), buffilepost = module.private.autocmd_base("buffilepost"), buffilepre = module.private.autocmd_base("buffilepre"), bufhidden = module.private.autocmd_base("bufhidden"), bufleave = module.private.autocmd_base("bufleave"), bufmodifiedset = module.private.autocmd_base("bufmodifiedset"), bufnew = module.private.autocmd_base("bufnew"), bufnewfile = module.private.autocmd_base("bufnewfile"), bufreadpost = module.private.autocmd_base("bufreadpost"), bufreadcmd = module.private.autocmd_base("bufreadcmd"), bufreadpre = module.private.autocmd_base("bufreadpre"), bufunload = module.private.autocmd_base("bufunload"), bufwinenter = module.private.autocmd_base("bufwinenter"), bufwinleave = module.private.autocmd_base("bufwinleave"), bufwipeout = module.private.autocmd_base("bufwipeout"), bufwrite = module.private.autocmd_base("bufwrite"), bufwritecmd = module.private.autocmd_base("bufwritecmd"), bufwritepost = module.private.autocmd_base("bufwritepost"), chaninfo = module.private.autocmd_base("chaninfo"), chanopen = module.private.autocmd_base("chanopen"), cmdundefined = module.private.autocmd_base("cmdundefined"), cmdlinechanged = module.private.autocmd_base("cmdlinechanged"), cmdlineenter = module.private.autocmd_base("cmdlineenter"), cmdlineleave = module.private.autocmd_base("cmdlineleave"), cmdwinenter = module.private.autocmd_base("cmdwinenter"), cmdwinleave = module.private.autocmd_base("cmdwinleave"), colorscheme = module.private.autocmd_base("colorscheme"), colorschemepre = module.private.autocmd_base("colorschemepre"), completechanged = module.private.autocmd_base("completechanged"), completedonepre = module.private.autocmd_base("completedonepre"), completedone = module.private.autocmd_base("completedone"), cursorhold = module.private.autocmd_base("cursorhold"), cursorholdi = module.private.autocmd_base("cursorholdi"), cursormoved = module.private.autocmd_base("cursormoved"), cursormovedi = module.private.autocmd_base("cursormovedi"), diffupdated = module.private.autocmd_base("diffupdated"), dirchanged = module.private.autocmd_base("dirchanged"), fileappendcmd = module.private.autocmd_base("fileappendcmd"), fileappendpost = module.private.autocmd_base("fileappendpost"), fileappendpre = module.private.autocmd_base("fileappendpre"), filechangedro = module.private.autocmd_base("filechangedro"), exitpre = module.private.autocmd_base("exitpre"), filechangedshell = module.private.autocmd_base("filechangedshell"), filechangedshellpost = module.private.autocmd_base("filechangedshellpost"), filereadcmd = module.private.autocmd_base("filereadcmd"), filereadpost = module.private.autocmd_base("filereadpost"), filereadpre = module.private.autocmd_base("filereadpre"), filetype = module.private.autocmd_base("filetype"), filewritecmd = module.private.autocmd_base("filewritecmd"), filewritepost = module.private.autocmd_base("filewritepost"), filewritepre = module.private.autocmd_base("filewritepre"), filterreadpost = module.private.autocmd_base("filterreadpost"), filterreadpre = module.private.autocmd_base("filterreadpre"), filterwritepost = module.private.autocmd_base("filterwritepost"), filterwritepre = module.private.autocmd_base("filterwritepre"), focusgained = module.private.autocmd_base("focusgained"), focuslost = module.private.autocmd_base("focuslost"), funcundefined = module.private.autocmd_base("funcundefined"), uienter = module.private.autocmd_base("uienter"), uileave = module.private.autocmd_base("uileave"), insertchange = module.private.autocmd_base("insertchange"), insertcharpre = module.private.autocmd_base("insertcharpre"), textyankpost = module.private.autocmd_base("textyankpost"), insertenter = module.private.autocmd_base("insertenter"), insertleavepre = module.private.autocmd_base("insertleavepre"), insertleave = module.private.autocmd_base("insertleave"), menupopup = module.private.autocmd_base("menupopup"), optionset = module.private.autocmd_base("optionset"), quickfixcmdpre = module.private.autocmd_base("quickfixcmdpre"), quickfixcmdpost = module.private.autocmd_base("quickfixcmdpost"), quitpre = module.private.autocmd_base("quitpre"), remotereply = module.private.autocmd_base("remotereply"), sessionloadpost = module.private.autocmd_base("sessionloadpost"), shellcmdpost = module.private.autocmd_base("shellcmdpost"), signal = module.private.autocmd_base("signal"), shellfilterpost = module.private.autocmd_base("shellfilterpost"), sourcepre = module.private.autocmd_base("sourcepre"), sourcepost = module.private.autocmd_base("sourcepost"), sourcecmd = module.private.autocmd_base("sourcecmd"), spellfilemissing = module.private.autocmd_base("spellfilemissing"), stdinreadpost = module.private.autocmd_base("stdinreadpost"), stdinreadpre = module.private.autocmd_base("stdinreadpre"), swapexists = module.private.autocmd_base("swapexists"), syntax = module.private.autocmd_base("syntax"), tabenter = module.private.autocmd_base("tabenter"), tableave = module.private.autocmd_base("tableave"), tabnew = module.private.autocmd_base("tabnew"), tabnewentered = module.private.autocmd_base("tabnewentered"), tabclosed = module.private.autocmd_base("tabclosed"), termopen = module.private.autocmd_base("termopen"), termenter = module.private.autocmd_base("termenter"), termleave = module.private.autocmd_base("termleave"), termclose = module.private.autocmd_base("termclose"), termresponse = module.private.autocmd_base("termresponse"), textchanged = module.private.autocmd_base("textchanged"), textchangedi = module.private.autocmd_base("textchangedi"), textchangedp = module.private.autocmd_base("textchangedp"), user = module.private.autocmd_base("user"), usergettingbored = module.private.autocmd_base("usergettingbored"), vimenter = module.private.autocmd_base("vimenter"), vimleave = module.private.autocmd_base("vimleave"), vimleavepre = module.private.autocmd_base("vimleavepre"), vimresized = module.private.autocmd_base("vimresized"), vimresume = module.private.autocmd_base("vimresume"), vimsuspend = module.private.autocmd_base("vimsuspend"), winclosed = module.private.autocmd_base("winclosed"), winenter = module.private.autocmd_base("winenter"), winleave = module.private.autocmd_base("winleave"), winnew = module.private.autocmd_base("winnew"), winscrolled = module.private.autocmd_base("winscrolled"), } module.examples = { ["Binding to an Autocommand"] = function() local mymodule = modules.create("my.module") mymodule.setup = function() return { success = true, requires = { "core.autocommands", -- Be sure to require the module! }, } end mymodule.load = function() -- Enable an autocommand (in this case InsertLeave) module.required["core.autocommands"].enable_autocommand("InsertLeave") end -- Listen for any incoming events mymodule.on_event = function(event) -- If it's the event we're looking for then do something! if event.type == "core.autocommands.events.insertleave" then log.warn("We left insert mode!") end end mymodule.events.subscribed = { ["core.autocommands"] = { insertleave = true, -- Be sure to listen in for this event! }, } return mymodule end, } return module ================================================ FILE: lua/neorg/modules/core/clipboard/code-blocks/module.lua ================================================ --[[ file: Clipboard-Code-Blocks title: Comfortable Code Copying in Neorg summary: Removes beginning whitespace from text copied from code blocks. embed: https://user-images.githubusercontent.com/76052559/216775085-7e808dbd-4985-49fa-b4c2-069b9782b300.gif --- The `code-blocks` module removes leading whitespace when copying from an `@code` tag, allowing for easy pasting into external applications. To use it, simply highlight some code within an `@code` block and paste it elsewhere! This functionality will **only** work if the selection is inside the `@code` section, excluding the `@code` and `@end` portion itself. If the conditions are not met, the content is copied normally, preserving all indentation. --]] local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.clipboard.code-blocks") module.load = function() modules.await("core.clipboard", function(clipboard) clipboard.add_callback("ranged_verbatim_tag_content", function(node, content, position) -- TODO: Handle visual/visual line/visual block modes -- The end of "ranged_tag_content" spans one line too many if position["end"][1] > node:end_() - 1 then return end -- Check if the start of the selection that was made is worth cutting off. local _, indentation = node:start() for i, line in ipairs(content) do if i == 1 then local amount_to_cut_off = position["start"][2] - indentation if amount_to_cut_off < 0 then content[i] = line:sub(-amount_to_cut_off + 1) end goto continue end content[i] = line:sub(indentation + 1) ::continue:: end return content end, true) end) end return module ================================================ FILE: lua/neorg/modules/core/clipboard/module.lua ================================================ --[[ file: Clipboard title: Quality of Life Features for the Clipboard summary: A module to manipulate and interact with the user's clipboard. internal: true --- The clipboard module is a minimal and generic module allowing to overwrite or add special behaviour to the `y` (yank) keybind in Neovim. --]] local neorg = require("neorg.core") local lib, modules = neorg.lib, neorg.modules local module = modules.create("core.clipboard") ---@type core.integrations.treesitter local ts module.setup = function() return { requires = { "core.integrations.treesitter", }, } end module.load = function() ts = module.required["core.integrations.treesitter"] vim.api.nvim_create_autocmd("TextYankPost", { callback = function(data) if vim.api.nvim_get_option_value("filetype", { buf = data.buf }) ~= "norg" or vim.v.event.operator ~= "y" then return end local range = { vim.api.nvim_buf_get_mark(data.buf, "["), vim.api.nvim_buf_get_mark(data.buf, "]") } range[1][1] = range[1][1] - 1 range[2][1] = range[2][1] - 1 for i = range[1][1], range[2][1] do local node = ts.get_first_node_on_line(data.buf, i) while node and node:parent() do if module.private.callbacks[node:type()] then local register = vim.fn.getreg(assert(vim.v.register)) vim.fn.setreg( vim.v.register, lib.filter(module.private.callbacks[node:type()], function(_, callback) if callback.strict and (range[1][1] < i or range[2][1] > node:end_()) then return end return callback.cb( node, vim.split(assert(register --[[@as string]]), "\n", { plain = true, -- TODO: This causes problems in places -- where you actually want to copy -- newlines. trimempty = true, }), { start = range[1], ["end"] = range[2], current = { i, range[1][2] }, } ) end) or register, vim.v.event.regtype ---@diagnostic disable-line ) return end node = node:parent() end end end, }) end module.private = { callbacks = {}, } --- @class core.clipboard module.public = { add_callback = function(node_type, func, strict) module.private.callbacks[node_type] = module.private.callbacks[node_type] or {} table.insert(module.private.callbacks[node_type], { cb = func, strict = strict }) end, } return module ================================================ FILE: lua/neorg/modules/core/completion/module.lua ================================================ --[[ file: Completion title: Get completions in Neorg files summary: A wrapper to interface with several different completion engines. --- This module is an intermediary between Neorg and the completion engine of your choice. After setting up this module (this usually just involves setting the `engine` field in the [configuration](#configuration) section), please read the corresponding wiki page for the engine you selected ([`nvim-cmp`](@core.integrations.nvim-cmp) [`coq_nvim`](@core.integrations.coq_nvim) or [`nvim-compe`](@core.integrations.nvim-compe)) to complete setup. Completions are provided in the following cases (examples in (), `|` represents the cursor location): - TODO items (`- (|`) - @ tags (`@|`) - \# tags (`#|`) - file path links (`{:|`) provides workspace relative paths (`:$/workspace/relative/path:`) - header links (`{*|`) - fuzzy header links (`{#|`) - footnotes (`{^|`) - file path + header links (`{:path:*|`) - file path + fuzzy header links (`{:path:#|`) - file path + footnotes (`{:path:^|`) - anchor names (`[|`) - link names (`{}[|`) Header completions will show only valid headers at the current level in the current or specified file. All link completions are smart about closing `:` and `}`. --]] local neorg = require("neorg.core") local Path = require("pathlib") local log, modules, utils = neorg.log, neorg.modules, neorg.utils local dirutils, dirman, link_utils, ts local module = modules.create("core.completion") module.config.public = { -- The engine to use for completion. -- -- Possible values: -- - [`"nvim-cmp"`](@core.integrations.nvim-cmp) -- - [`"coq_nvim"`](@core.integrations.coq_nvim) -- - [`"nvim-compe"`](@core.integrations.nvim-compe) -- - `{ module_name = "external.lsp-completion" }` this must be used with -- [neorg-interim-ls](https://github.com/benlubas/neorg-interim-ls) and can provide -- completions through a shim Language Server. This allows users without an auto complete -- plugin to still get Neorg completions engine = nil, -- The identifier for the Neorg source. name = "[Neorg]", } module.setup = function() return { success = true, requires = { "core.dirman", "core.dirman.utils", "core.integrations.treesitter", "core.links" }, } end ---@class neorg.completion_engine ---@field create_source function module.private = { ---@type neorg.completion_engine engine = nil, --- Get a list of all norg files in current workspace. Returns { workspace_path, norg_files } --- @return { [1]: PathlibPath, [2]: PathlibPath[]|nil }|nil get_norg_files = function() local current_workspace = dirman.get_current_workspace() local norg_files = dirman.get_norg_files(current_workspace[1]) return { current_workspace[2], norg_files } end, --- Get the closing characters for a link completion --- @param context table --- @param colon boolean should there be a closing colon? --- @return string "", ":", or ":}" depending on what's needed get_closing_chars = function(context, colon) local offset = 1 local closing_colon = "" if colon then closing_colon = ":" if string.sub(context.full_line, context.char + offset, context.char + offset) == ":" then closing_colon = "" offset = 2 end end local closing_brace = "}" if string.sub(context.full_line, context.char + offset, context.char + offset) == "}" then closing_brace = "" end return closing_colon .. closing_brace end, --- query all the linkable items in a given buffer/file for a given link type ---@param source number | string | PathlibPath bufnr or file path ---@param link_type "generic" | "definition" | "footnote" | string get_linkables = function(source, link_type) local query_str = link_utils.get_link_target_query_string(link_type) local norg_parser, iter_src = ts.get_ts_parser(source) if not norg_parser then return {} end local norg_tree = norg_parser:parse()[1] local query = vim.treesitter.query.parse("norg", query_str) local links = {} for id, node in query:iter_captures(norg_tree:root(), iter_src, 0, -1) do local capture = query.captures[id] if capture == "title" then local original_title = ts.get_node_text(node, iter_src) if original_title then local title = original_title:gsub("\\", "") title = title:gsub("%s+", " ") title = title:gsub("^%s+", "") table.insert(links, { original_title = original_title, title = title, node = node, }) end end end return links end, generate_file_links = function(context, _prev, _saved, _match) local res = {} local files = module.private.get_norg_files() if not files or not files[2] then return {} end local closing_chars = module.private.get_closing_chars(context, true) for _, file in pairs(files[2]) do if not file:samefile(Path.new(vim.api.nvim_buf_get_name(0))) then local rel = file:relative_to(files[1], false) if rel and rel:len() > 0 then local link = "$/" .. rel:with_suffix(""):tostring() .. closing_chars table.insert(res, link) end end end return res end, --- Generate list of autocompletion suggestions for links --- @param context table --- @param source number | string | PathlibPath --- @param node_type string --- @return string[] suggestions = function(context, source, node_type) local leading_whitespace = " " if context.before_char == " " then leading_whitespace = "" end local links = module.private.get_linkables(source, node_type) local closing_chars = module.private.get_closing_chars(context, false) return vim.iter(links) :map(function(x) return leading_whitespace .. x.title .. closing_chars end) :totable() end, --- All the things that you can link to (`{#|}` completions) local_link_targets = function(context, _prev, _saved, _match) return module.private.suggestions(context, 0, "generic") end, local_heading_links = function(context, _prev, _saved, match) local heading_level = match[2] and #match[2] return module.private.suggestions(context, 0, ("heading%d"):format(heading_level)) end, foreign_heading_links = function(context, _prev, _saved, match) local file = match[1] local heading_level = match[2] and #match[2] if file then file = dirutils.expand_pathlib(file) return module.private.suggestions(context, file, ("heading%d"):format(heading_level)) end return {} end, foreign_generic_links = function(context, _prev, _saved, match) local file = match[1] if file then file = dirutils.expand_pathlib(file) return module.private.suggestions(context, file, "generic") end return {} end, local_footnote_links = function(context, _prev, _saved, _match) return module.private.suggestions(context, 0, "footnote") end, foreign_footnote_links = function(context, _prev, _saved, match) local file = match[2] if match[2] then file = dirutils.expand_pathlib(file) return module.private.suggestions(context, file, "footnote") end return {} end, --- The node context for normal norg (ie. not in a code block) normal_norg = function(current, previous, _, _) -- If no previous node exists then try verifying the current node instead if not previous then return current and (current:type() ~= "translation_unit" or current:type() == "document") or false end -- If the previous node is not tag parameters or the tag name -- (i.e. we are not inside of a tag) then show auto completions return previous:type() ~= "tag_parameters" and previous:type() ~= "tag_name" end, } ---Suggest common link names for the given link. Suggests: --- - target name if the link point to a heading/footer/etc. --- - metadata `title` field --- - file description ---@return string[] module.private.foreign_link_names = function(_context, _prev, _saved, match) local file, target = match[2], match[3] local path = dirutils.expand_pathlib(file) local meta = ts.get_document_metadata(path) local suggestions = {} if meta then table.insert(suggestions, meta.title) table.insert(suggestions, meta.description) end if target ~= "" then table.insert(suggestions, target) end return suggestions end ---provide suggestions for anchors that are already defined in the document ---@return string[] module.private.anchor_suggestions = function(_context, _prev, _saved, _match) local suggestions = {} local anchor_query_string = [[ (anchor_definition (link_description text: (paragraph) @anchor_name )) ]] ts.execute_query(anchor_query_string, function(query, id, node, _metadata) local capture_name = query.captures[id] if capture_name == "anchor_name" then table.insert(suggestions, ts.get_node_text(node, 0)) end end, 0) return suggestions end --- suggest the link target name ---@return string[] module.private.local_link_names = function(_context, _prev, _saved, match) local target = match[2] if target then target = target:gsub("^%s+", "") target = target:gsub("%s+$", "") end return { target } end module.load = function() -- If we have not defined an engine then bail if not module.config.public.engine then log.error("No engine specified, aborting...") return end -- check if a custom completion module is provided if type(module.config.public.engine) == "table" and module.config.public.engine["module_name"] then local completion_module = module.config.public.engine["module_name"] modules.load_module_as_dependency(completion_module, module.name, {}) module.private.engine = modules.get_module(completion_module) elseif module.config.public.engine == "nvim-compe" and modules.load_module("core.integrations.nvim-compe") then -- If our engine is compe then attempt to load the integration module for nvim-compe modules.load_module_as_dependency("core.integrations.nvim-compe", module.name, {}) module.private.engine = modules.get_module("core.integrations.nvim-compe") elseif module.config.public.engine == "nvim-cmp" and modules.load_module("core.integrations.nvim-cmp") then modules.load_module_as_dependency("core.integrations.nvim-cmp", module.name, {}) module.private.engine = modules.get_module("core.integrations.nvim-cmp") elseif module.config.public.engine == "coq_nvim" and modules.load_module("core.integrations.coq_nvim") then modules.load_module_as_dependency("core.integrations.coq_nvim", module.name, {}) module.private.engine = modules.get_module("core.integrations.coq_nvim") else log.error("Unable to load completion module -", module.config.public.engine, "is not a recognized engine.") return end dirutils = module.required["core.dirman.utils"] dirman = module.required["core.dirman"] link_utils = module.required["core.links"] ---@type core.integrations.treesitter ts = module.required["core.integrations.treesitter"] -- Set a special function in the integration module to allow it to communicate with us module.private.engine.invoke_completion_engine = function(context) ---@diagnostic disable-line return module.public.complete(context) ---@diagnostic disable-line -- TODO: type error workaround end -- Create the integration engine's source module.private.engine.create_source({ completions = module.config.public.completions, }) end ---@class core.completion module.public = { -- Define completions completions = { { -- Create a new completion (for `@|tags`) -- Define the regex that should match in order to proceed regex = "^%s*@(%w*)", -- If regex can be matched, this item then gets verified via TreeSitter's AST node = module.private.normal_norg, -- The actual elements to show if the above tests were true complete = { "table", "code", "image", "embed", "document", }, -- Additional options to pass to the completion engine options = { type = "Tag", completion_start = "@", }, -- We might have matched the top level item, but can we match it with any -- more precision? Descend down the rabbit hole and try to more accurately match -- the line. descend = { -- The cycle continues { regex = "document%.%w*", complete = { "meta", }, options = { type = "Tag", }, descend = {}, }, { -- Define a regex (gets appended to parent's regex) regex = "code%s+%w*", -- No node variable, we don't need that sort of check here complete = utils.get_language_list(true), -- Extra options options = { type = "Language", }, -- Don't descend any further, we've narrowed down our match descend = {}, }, { regex = "export%s+%w*", complete = utils.get_language_list(true), options = { type = "Language", }, descend = {}, }, { regex = "tangle%s+%w*", complete = { "", }, options = { type = "Property", }, }, { regex = "image%s+%w*", complete = { "jpeg", "png", "svg", "jfif", "exif", }, options = { type = "Format", }, }, { regex = "embed%s+%w*", complete = { "video", "image", }, options = { type = "Embed", }, }, }, }, { -- `#|tags` regex = "^%s*%#(%w*)", complete = { "comment", "ordered", "time.due", "time.start", "contexts", "waiting.for", }, options = { type = "Tag", }, descend = {}, }, { -- `@|end` tags regex = "^%s*@e?n?", node = function(_, previous) if not previous then return false end return previous:type() == "tag_parameters" or previous:type() == "tag_name" end, complete = { "end", }, options = { type = "Directive", completion_start = "@", }, }, { -- Detached Modifier Extensions `- (`, `* (`, etc. regex = "^%s*[%-*$~^]+%s+%(", complete = { { "( ) ", label = "( ) (undone)" }, { "(-) ", label = "(-) (pending)" }, { "(x) ", label = "(x) (done)" }, { "(_) ", label = "(_) (cancelled)" }, { "(!) ", label = "(!) (important)" }, { "(+) ", label = "(+) (recurring)" }, { "(=) ", label = "(=) (on hold)" }, { "(?) ", label = "(?) (uncertain)" }, }, options = { type = "TODO", pre = function() local sub = vim.api.nvim_get_current_line():gsub("^(%s*%-+%s+%(%s*)%)", "%1") if sub then vim.api.nvim_set_current_line(sub) end end, completion_start = "-", }, }, { -- links for file paths `{:|` regex = "^.*{:([^:}]*)", node = module.private.normal_norg, complete = module.private.generate_file_links, options = { type = "File", completion_start = "{", }, }, { -- links that have a file path, suggest any heading from the file `{:...:#|}` regex = "^.*{:(.*):#[^}]*", complete = module.private.foreign_generic_links, node = module.private.normal_norg, options = { type = "Reference", completion_start = "#", }, }, { -- links that have a file path, suggest direct headings from the file `{:...:*|}` regex = "^.*{:(.*):(%*+)[^}]*", complete = module.private.foreign_heading_links, node = module.private.normal_norg, options = { type = "Reference", completion_start = "*", }, }, { -- # links to headings in the current file `{#|}` regex = "^.*{#[^}]*", -- complete = module.private.generate_local_heading_links, complete = module.private.local_link_targets, node = module.private.normal_norg, options = { type = "Reference", completion_start = "#", }, }, { -- * links to headings in current file `{*|}` regex = "^(.*){(%*+)[^}]*", -- the first capture group is a nothing group so that match[2] is reliably the heading -- level or nil if there's no heading level. complete = module.private.local_heading_links, node = module.private.normal_norg, options = { type = "Reference", completion_start = "*", }, }, { -- ^ footnote links in the current file `{^|}` regex = "^(.*){%^[^}]*", complete = module.private.local_footnote_links, node = module.private.normal_norg, options = { type = "Reference", completion_start = "^", }, }, { -- ^ footnote links in another file `{:path:^|}` regex = "^(.*){:(.*):%^[^}]*", complete = module.private.foreign_footnote_links, node = module.private.normal_norg, options = { type = "Reference", completion_start = "^", }, }, { -- foreign link name suggestions `{:path:target}[|]` regex = "^(.*){:([^:]*):[#$*%^]* ([^}]*)}%[", complete = module.private.foreign_link_names, node = module.private.normal_norg, options = { type = "Reference", completion_start = "[", }, }, { -- local link name suggestions `{target}[|]` for `#`, `$`, `^`, `*` link targets regex = "^(.*){[#$*%^]+ ([^}]*)}%[", complete = module.private.local_link_names, node = module.private.normal_norg, options = { type = "Reference", completion_start = "[", }, }, { -- complete anchor names that exist in the current buffer ` [|` regex = { "^(.*)[^}]%[", "^%[", }, complete = module.private.anchor_suggestions, node = module.private.normal_norg, options = { type = "Reference", completion_start = "[", }, }, }, --- Parses the public completion table and attempts to find all valid matches ---@param context table #The context provided by the integration engine ---@param prev table? #The previous table of completions - used for descent ---@param saved string? #The saved regex in the form of a string, used to concatenate children nodes with parent nodes' regexes complete = function(context, prev, saved) -- If the save variable wasn't passed then set it to an empty string saved = saved or "" -- If we haven't defined any explicit table to read then read the public completions table local completions = prev or module.public.completions -- Loop through every completion for _, completion_data in ipairs(completions) do -- If the completion data has a regex variable if completion_data.regex then local regexes if type(completion_data.regex) == "string" then regexes = { completion_data.regex } elseif type(completion_data.regex) == "table" then ---@diagnostic disable-next-line: cast-local-type regexes = completion_data.regex else break end local match = {} -- Attempt to match the current line before the cursor with any of the regex -- expressions in the list, first one to succeed is used ---@diagnostic disable-next-line: param-type-mismatch for _, regex in ipairs(regexes) do match = { context.line:match(saved .. regex .. "$") } if not vim.tbl_isempty(match) then break end end -- If our match was successful if not vim.tbl_isempty(match) then -- Construct a variable that will be returned on a successful match local items = type(completion_data.complete) == "table" and completion_data.complete or completion_data.complete(context, prev, saved, match) local ret_completions = { items = items, options = completion_data.options or {} } -- Set the match variable for the integration module ret_completions.match = match -- If the completion data has a node variable then attempt to match the current node too! if completion_data.node then -- If the type of completion data we're dealing with is a string then attempt to parse it if type(completion_data.node) == "string" then -- Split the completion node string down every pipe character local split = vim.split(completion_data.node --[[@as string]], "|") -- Check whether the first character of the string is an exclamation mark -- If this is present then it means we're looking for a node that *isn't* the one we specify local negate = split[1]:sub(0, 1) == "!" -- If we are negating then remove the leading exclamation mark so it doesn't interfere if negate then split[1] = split[1]:sub(2) end -- If we have a second split (i.e. in the string "tag_name|prev" this would be the "prev" string) if split[2] then -- Is our other value "prev"? If so, compare the current node in the syntax tree with the previous node if split[2] == "prev" then -- Get the previous node local current_node = vim.treesitter.get_node() if not current_node then return { items = {}, options = {} } end local previous_node = ts.get_previous_node(current_node, true, true) -- If the previous node is nil if not previous_node then -- If we have specified a negation then that means our tag type doesn't match the previous tag's type, -- which is good! That means we can return our completions if negate then return ret_completions end -- Otherwise continue on with the loop goto continue end -- If we haven't negated and the previous node type is equal to the one we specified then return completions if not negate and previous_node:type() == split[1] then return ret_completions -- Otherwise, if we want to negate and if the current node type is not equal to the one we specified -- then also return completions - it means the match was successful elseif negate and previous_node:type() ~= split[1] then return ret_completions else -- Otherwise just continue with the loop goto continue end -- Else if our second split is equal to "next" then it's time to inspect the next node in the AST elseif split[2] == "next" then -- Grab the next node local current_node = vim.treesitter.get_node() if not current_node then return { items = {}, options = {} } end local next_node = ts.get_next_node(current_node, true, true) -- If it's nil if not next_node then -- If we want to negate then return completions - the comparison was unsuccessful, which is what we wanted if negate then return ret_completions end -- Or just continue goto continue end -- If we are not negating and the node values match then return completions if not negate and next_node:type() == split[1] then return ret_completions -- If we are negating and then values don't match then also return completions elseif negate and next_node:type() ~= split[1] then return ret_completions else -- Else keep look through the completion table to see whether we can find another match goto continue end end else -- If we haven't defined a split (no pipe was found) then compare the current node if vim.treesitter.get_node():type() == split[1] then -- If we're not negating then return completions if not negate then return ret_completions else -- Else continue goto continue end end end -- If our completion data type is not a string but rather it is a function then elseif type(completion_data.node) == "function" then -- Grab all the necessary variables (current node, previous node, next node) local current_node = vim.treesitter.get_node() -- The file is blank, return completions if not current_node then return ret_completions end local next_node = ts.get_next_node(current_node, true, true) local previous_node = ts.get_previous_node(current_node, true, true) -- Execute the callback function with all of our parameters. -- If it returns true then that means the match was successful, and so return completions if completion_data.node(current_node, previous_node, next_node, ts) then return ret_completions end -- If no completions were found, try looking whether we can descend any further down the syntax tree. -- Maybe we can find something extra there? if completion_data.descend then -- Recursively call complete() with the nested table local descent = module.public.complete( context, completion_data.descend, saved .. completion_data.regex ) -- If the returned completion items actually hold some data (i.e. a match was found) then return those matches if not vim.tbl_isempty(descent.items) then return descent end end -- Else just don't bother and continue goto continue end end -- If none of the checks matched, then we can conclude that only the regex variable was defined, -- and since that was matched properly, we can return all completions. return ret_completions -- If the regex for the current line wasn't matched then attempt to descend further down, -- similarly to what we did earlier elseif completion_data.descend then -- Recursively call function with new parameters local descent = module.public.complete(context, completion_data.descend, saved .. completion_data.regex) -- If we had some completions from that function then return those completions if not vim.tbl_isempty(descent.items) then return descent end end end ::continue:: end -- If absolutely no matches were found return empty data (no completions) return { items = {}, options = {} } end, } return module ================================================ FILE: lua/neorg/modules/core/concealer/module.lua ================================================ --[[ file: Concealer title: Display Markup as Icons, not Text description: The concealer module converts verbose markup elements into beautified icons for your viewing pleasure. summary: Enhances the basic Neorg experience by using icons instead of text. embed: https://user-images.githubusercontent.com/76052559/216767027-726b451d-6da1-4d09-8fa4-d08ec4f93f54.png --- "Concealing" is the process of hiding away from plain sight. When writing raw Norg, long strings like `***** Hello` or `$$ Definition` can be distracting and sometimes unpleasant when sifting through large notes. To reduce the amount of cognitive load required to "parse" Norg documents with your own eyes, this module masks, or sometimes completely hides many categories of markup. The concealer depends on [Nerd Fonts >=v3.0.1](https://github.com/ryanoasis/nerd-fonts/releases/latest) to be installed on your system. This module respects `:h conceallevel` and `:h concealcursor`. Setting the wrong values for these options can make it look like this module isn't working. --]] -- utils to be refactored local neorg = require("neorg.core") local log, modules, utils = neorg.log, neorg.modules, neorg.utils local function in_range(k, l, r_ex) return l <= k and k < r_ex end local function is_concealing_on_row_range(mode, conceallevel, concealcursor, current_row_0b, row_start_0b, row_end_0bex) if conceallevel < 1 then return false elseif not in_range(current_row_0b, row_start_0b, row_end_0bex) then return true else return (concealcursor:find(mode) ~= nil) end end local function table_extend_in_place(tbl, tbl_ext) for k, v in pairs(tbl_ext) do tbl[k] = v end end local function get_node_position_and_text_length(bufid, node) local row_start_0b, col_start_0b = node:range() -- FIXME parser: multi_definition_suffix, weak_paragraph_delimiter should not span across lines -- assert(row_start_0b == row_end_0bin, row_start_0b .. "," .. row_end_0bin) local text = vim.treesitter.get_node_text(node, bufid) local past_end_offset_1b = text:find("%s") or text:len() + 1 return row_start_0b, col_start_0b, (past_end_offset_1b - 1) end local function get_header_prefix_node(header_node) local first_child = header_node:child(0) assert(first_child:type() == header_node:type() .. "_prefix") return first_child end local function get_line_length(bufid, row_0b) return vim.api.nvim_strwidth(vim.api.nvim_buf_get_lines(bufid, row_0b, row_0b + 1, true)[1]) end --- end utils local module = modules.create("core.concealer", { "preset_basic", "preset_varied", "preset_diamond", }) module.setup = function() return { success = true, requires = { "core.autocommands", "core.integrations.treesitter", }, } end module.private = { ns_icon = vim.api.nvim_create_namespace("neorg-conceals"), ns_prettify_flag = vim.api.nvim_create_namespace("neorg-conceals.prettify-flag"), rerendering_scheduled_bufids = {}, enabled = true, cursor_record = {}, } local function set_mark(bufid, row_0b, col_0b, text, highlight, ext_opts) local ns_icon = module.private.ns_icon local opt = { virt_text = { { text, highlight } }, virt_text_pos = "overlay", virt_text_win_col = nil, hl_group = nil, conceal = nil, id = nil, end_row = row_0b, end_col = col_0b, hl_eol = nil, virt_text_hide = true, hl_mode = "combine", virt_lines = nil, virt_lines_above = nil, virt_lines_leftcol = nil, ephemeral = nil, right_gravity = nil, end_right_gravity = nil, priority = nil, strict = nil, -- default true sign_text = nil, sign_hl_group = nil, number_hl_group = nil, line_hl_group = nil, cursorline_hl_group = nil, spell = nil, ui_watched = nil, invalidate = true, } if ext_opts then table_extend_in_place(opt, ext_opts) end vim.api.nvim_buf_set_extmark(bufid, ns_icon, row_0b, col_0b, opt) end local function table_get_default_last(tbl, index) return tbl[index] or tbl[#tbl] end local function get_ordered_index(bufid, prefix_node) -- TODO: calculate levels in one pass, since treesitter API implementation seems to have ridiculously high complexity local _, _, level = get_node_position_and_text_length(bufid, prefix_node) local header_node = prefix_node:parent() -- TODO: fix parser: `(ERROR)` on standalone prefix not followed by text, like `- ` -- assert(header_node:type() .. "_prefix" == prefix_node:type()) local sibling = header_node:prev_named_sibling() local count = 1 while sibling and (sibling:type() == header_node:type()) do local _, _, sibling_level = get_node_position_and_text_length(bufid, get_header_prefix_node(sibling)) if sibling_level < level then break elseif sibling_level == level then count = count + 1 end sibling = sibling:prev_named_sibling() end return count, (sibling or header_node:parent()) end local function tbl_reverse(tbl) local result = {} for i = 1, #tbl do result[i] = tbl[#tbl - i + 1] end return result end local function tostring_lowercase(n) local t = {} while n > 0 do t[#t + 1] = string.char(0x61 + (n - 1) % 26) n = math.floor((n - 1) / 26) end return table.concat(t):reverse() end local roman_numerals = { { "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix" }, { "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc" }, { "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm" }, { "m", "mm", "mmm" }, } local function tostring_roman_lowercase(n) if n >= 4000 then -- too large to render return end local result = {} local i = 1 while n > 0 do result[#result + 1] = roman_numerals[i][n % 10] n = math.floor(n / 10) i = i + 1 end return table.concat(tbl_reverse(result)) end local ordered_icon_table = { ["0"] = function(i) return tostring(i - 1) end, ["1"] = function(i) return tostring(i) end, ["a"] = function(i) return tostring_lowercase(i) end, ["A"] = function(i) return tostring_lowercase(i):upper() end, ["i"] = function(i) return tostring_roman_lowercase(i) end, ["I"] = function(i) return tostring_roman_lowercase(i):upper() end, ["Ⅰ"] = { "Ⅰ", "Ⅱ", "Ⅲ", "Ⅳ", "Ⅴ", "Ⅵ", "Ⅶ", "Ⅷ", "Ⅸ", "Ⅹ", "Ⅺ", "Ⅻ", }, ["ⅰ"] = { "ⅰ", "ⅱ", "ⅲ", "ⅳ", "ⅴ", "ⅵ", "ⅶ", "ⅷ", "ⅸ", "ⅹ", "ⅺ", "ⅻ", }, ["⒈"] = { "⒈", "⒉", "⒊", "⒋", "⒌", "⒍", "⒎", "⒏", "⒐", "⒑", "⒒", "⒓", "⒔", "⒕", "⒖", "⒗", "⒘", "⒙", "⒚", "⒛", }, ["⑴"] = { "⑴", "⑵", "⑶", "⑷", "⑸", "⑹", "⑺", "⑻", "⑼", "⑽", "⑾", "⑿", "⒀", "⒁", "⒂", "⒃", "⒄", "⒅", "⒆", "⒇", }, ["①"] = { "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩", "⑪", "⑫", "⑬", "⑭", "⑮", "⑯", "⑰", "⑱", "⑲", "⑳", }, ["⒜"] = { "⒜", "⒝", "⒞", "⒟", "⒠", "⒡", "⒢", "⒣", "⒤", "⒥", "⒦", "⒧", "⒨", "⒩", "⒪", "⒫", "⒬", "⒭", "⒮", "⒯", "⒰", "⒱", "⒲", "⒳", "⒴", "⒵", }, ["Ⓐ"] = { "Ⓐ", "Ⓑ", "Ⓒ", "Ⓓ", "Ⓔ", "Ⓕ", "Ⓖ", "Ⓗ", "Ⓘ", "Ⓙ", "Ⓚ", "Ⓛ", "Ⓜ", "Ⓝ", "Ⓞ", "Ⓟ", "Ⓠ", "Ⓡ", "Ⓢ", "Ⓣ", "Ⓤ", "Ⓥ", "Ⓦ", "Ⓧ", "Ⓨ", "Ⓩ", }, ["ⓐ"] = { "ⓐ", "ⓑ", "ⓒ", "ⓓ", "ⓔ", "ⓕ", "ⓖ", "ⓗ", "ⓘ", "ⓙ", "ⓚ", "ⓛ", "ⓜ", "ⓝ", "ⓞ", "ⓟ", "ⓠ", "ⓡ", "ⓢ", "ⓣ", "ⓤ", "ⓥ", "ⓦ", "ⓧ", "ⓨ", "ⓩ", }, } local memoized_ordered_icon_generator = {} local function format_ordered_icon(pattern, index) if type(pattern) == "function" then return pattern(index) end local gen = memoized_ordered_icon_generator[pattern] if gen then return gen(index) end for char_one, number_table in pairs(module.config.public.ordered_icons) do local l, r = pattern:find(char_one:find("%w") and "%f[%w]" .. char_one .. "%f[%W]" or char_one) if l then gen = function(index_) local icon = type(number_table) == "function" and number_table(index_) or number_table[index_] return icon and pattern:sub(1, l - 1) .. icon .. pattern:sub(r + 1) end break end end gen = gen or function(_) end memoized_ordered_icon_generator[pattern] = gen return gen(index) end local superscript_digits = { ["0"] = "⁰", ["1"] = "¹", ["2"] = "²", ["3"] = "³", ["4"] = "⁴", ["5"] = "⁵", ["6"] = "⁶", ["7"] = "⁷", ["8"] = "⁸", ["9"] = "⁹", ["-"] = "⁻", } ---@class core.concealer module.public = { icon_renderers = { on_left = function(config, bufid, node) if not config.icon then return end local row_0b, col_0b, len = get_node_position_and_text_length(bufid, node) local text = (" "):rep(len - 1) .. config.icon set_mark(bufid, row_0b, col_0b, text, config.highlight) end, multilevel_on_right = function(is_ordered) return function(config, bufid, node) if not config.icons then return end local row_0b, col_0b, len = get_node_position_and_text_length(bufid, node) local icon_pattern = table_get_default_last(config.icons, len) if not icon_pattern then return end local icon = not is_ordered and icon_pattern or format_ordered_icon(icon_pattern, get_ordered_index(bufid, node)) if not icon then return end local text = (" "):rep(len - 1) .. icon local _, first_unicode_end = text:find("[%z\1-\127\194-\244][\128-\191]*", len) local highlight = config.highlights and table_get_default_last(config.highlights, len) set_mark(bufid, row_0b, col_0b, text:sub(1, first_unicode_end), highlight) if vim.fn.strcharlen(text) > len then set_mark(bufid, row_0b, col_0b + len, text:sub(first_unicode_end + 1), highlight, { virt_text_pos = "inline", }) end end end, footnote_concealed = function(config, bufid, node) local link_title_node = node:next_named_sibling() local link_title = vim.treesitter.get_node_text(link_title_node, bufid) if config.numeric_superscript and link_title:match("^[-0-9]+$") then local t = {} for i = 1, #link_title do local d = link_title:sub(i, i) table.insert(t, superscript_digits[d]) end local superscripted_title = table.concat(t) local row_start_0b, col_start_0b, _, _ = link_title_node:range() local highlight = config.title_highlight set_mark(bufid, row_start_0b, col_start_0b, superscripted_title, highlight) end end, ---@param node TSNode quote_concealed = function(config, bufid, node) if not config.icons then return end local prefix = node:named_child(0) local row_0b, col_0b, len = get_node_position_and_text_length(bufid, prefix) local last_icon, last_highlight for _, child in ipairs(node:field("content")) do local row_last_0b, col_last_0b = child:end_() -- Sometimes the parser overshoots to the next newline, breaking -- the range. -- To counteract this we correct the overshoot. if col_last_0b == 0 then row_last_0b = row_last_0b - 1 end for line = row_0b, row_last_0b do if get_line_length(bufid, line) > len then for col = 1, len do if config.icons[col] ~= nil then last_icon = config.icons[col] end if not last_icon then goto continue end last_highlight = config.highlights[col] or last_highlight set_mark(bufid, line, col_0b + (col - 1), last_icon, last_highlight) ::continue:: end end end end end, fill_text = function(config, bufid, node) if not config.icon then return end local row_0b, col_0b, len = get_node_position_and_text_length(bufid, node) local text = config.icon:rep(len) set_mark(bufid, row_0b, col_0b, text, config.highlight) end, fill_multiline_chop2 = function(config, bufid, node) if not config.icon then return end local row_start_0b, col_start_0b, row_end_0bin, col_end_0bex = node:range() for i = row_start_0b, row_end_0bin do local l = i == row_start_0b and col_start_0b + 1 or 0 local r_ex = i == row_end_0bin and col_end_0bex - 1 or get_line_length(bufid, i) set_mark(bufid, i, l, config.icon:rep(r_ex - l), config.highlight) end end, render_horizontal_line = function(config, bufid, node) if not config.icon then return end local row_start_0b, col_start_0b, _, col_end_0bex = node:range() local render_col_start_0b = config.left == "here" and col_start_0b or 0 local opt_textwidth = vim.bo[bufid].textwidth local render_col_end_0bex = config.right == "textwidth" and (opt_textwidth > 0 and opt_textwidth or 79) or vim.api.nvim_win_get_width(assert(vim.fn.bufwinid(bufid))) local len = math.max(col_end_0bex - col_start_0b, render_col_end_0bex - render_col_start_0b) set_mark(bufid, row_start_0b, render_col_start_0b, config.icon:rep(len), config.highlight) end, render_code_block = function(config, bufid, node) local tag_name = vim.treesitter.get_node_text(node:named_child(0), bufid) if not (tag_name == "code" or tag_name == "embed") then return end local row_start_0b, col_start_0b, row_end_0bin = node:range() assert(row_start_0b < row_end_0bin) local conceal_on = (vim.wo.conceallevel >= 2) and config.conceal if conceal_on then for _, row_0b in ipairs({ row_start_0b, row_end_0bin }) do vim.api.nvim_buf_set_extmark( bufid, module.private.ns_icon, row_0b, 0, { end_col = get_line_length(bufid, row_0b), conceal = "" } ) end end if conceal_on or config.content_only then row_start_0b = row_start_0b + 1 row_end_0bin = row_end_0bin - 1 end local line_lengths = {} local max_len = config.min_width or 0 for row_0b = row_start_0b, row_end_0bin do local len = get_line_length(bufid, row_0b) if len > max_len then max_len = len end table.insert(line_lengths, len) end local to_eol = (config.width ~= "content") for row_0b = row_start_0b, row_end_0bin do local len = line_lengths[row_0b - row_start_0b + 1] local mark_col_start_0b = math.max(0, col_start_0b - config.padding.left) local mark_col_end_0bex = max_len + config.padding.right local priority = 101 if len >= mark_col_start_0b then vim.api.nvim_buf_set_extmark(bufid, module.private.ns_icon, row_0b, mark_col_start_0b, { end_row = row_0b + 1, hl_eol = to_eol, hl_group = config.highlight, hl_mode = "blend", virt_text = not to_eol and { { (" "):rep(mark_col_end_0bex - len), config.highlight } } or nil, virt_text_pos = "overlay", virt_text_win_col = len, spell = config.spell_check, priority = priority, }) else vim.api.nvim_buf_set_extmark(bufid, module.private.ns_icon, row_0b, len, { end_row = row_0b + 1, hl_eol = to_eol, hl_group = config.highlight, hl_mode = "blend", virt_text = { { (" "):rep(mark_col_start_0b - len) }, { not to_eol and (" "):rep(mark_col_end_0bex - mark_col_start_0b) or "", config.highlight }, }, virt_text_pos = "overlay", virt_text_win_col = len, spell = config.spell_check, priority = priority, }) end end end, }, icon_removers = { quote = function(_, bufid, node) for _, content in ipairs(node:field("content")) do local end_row, end_col = content:end_() -- This counteracts the issue where a quote can span onto the next -- line, even though it shouldn't. if end_col == 0 then end_row = end_row - 1 end vim.api.nvim_buf_clear_namespace(bufid, module.private.ns_icon, (content:start()), end_row + 1) end end, }, } module.config.public = { -- Which icon preset to use. -- -- The currently available icon presets are: -- - "basic" - use a mixture of icons (includes cute flower icons!) -- - "diamond" - use diamond shapes for headings -- - "varied" - use a mix of round and diamond shapes for headings; no cute flower icons though :( icon_preset = "basic", -- If true, Neorg will enable folding by default for `.norg` documents. -- You may use the inbuilt Neovim folding options like `foldnestmax`, -- `foldlevelstart` and others to then tune the behaviour to your liking. -- -- Set to `false` if you do not want Neorg setting anything. folds = true, -- When set to `auto`, Neorg will open all folds when opening new documents if `foldlevel` is 0. -- When set to `always`, Neorg will always open all folds when opening new documents. -- When set to `never`, Neorg will not do anything. init_open_folds = "auto", -- Provide custom ordered icons for ordered lists. -- - keys are a string matched against the first character of the values in `icons.ordered.icons` -- - value are either a list of string icons to use (one for each index), or a function that -- takes the index and returns the string value ordered_icons = ordered_icon_table, -- Configuration for icons. -- -- This table contains the full configuration set for each icon, including -- its query (where to be placed), render functions (how to be placed) and -- characters to use. -- -- For most use cases, the only values that you should be changing is the `icon`/`icons` field. -- `icon` is a string, while `icons` is a table of strings for multilevel elements like -- headings, lists, and quotes. -- -- To disable part of the config, replace the table with `false`, or prepend `false and` to it. -- For example: `done = false` or `done = false and { ... }`. icons = { todo = { done = { icon = "󰄬", nodes = { "todo_item_done" }, render = module.public.icon_renderers.on_left, }, pending = { icon = "󰥔", nodes = { "todo_item_pending" }, render = module.public.icon_renderers.on_left, }, undone = { icon = " ", nodes = { "todo_item_undone" }, render = module.public.icon_renderers.on_left, }, uncertain = { icon = "", nodes = { "todo_item_uncertain" }, render = module.public.icon_renderers.on_left, }, on_hold = { icon = "", nodes = { "todo_item_on_hold" }, render = module.public.icon_renderers.on_left, }, cancelled = { icon = "", nodes = { "todo_item_cancelled" }, render = module.public.icon_renderers.on_left, }, recurring = { icon = "↺", nodes = { "todo_item_recurring" }, render = module.public.icon_renderers.on_left, }, urgent = { icon = "⚠", nodes = { "todo_item_urgent" }, render = module.public.icon_renderers.on_left, }, }, list = { icons = { "•" }, nodes = { "unordered_list1_prefix", "unordered_list2_prefix", "unordered_list3_prefix", "unordered_list4_prefix", "unordered_list5_prefix", "unordered_list6_prefix", }, render = module.public.icon_renderers.multilevel_on_right(false), }, ordered = { icons = { "1.", "A.", "a.", "(1)", "I.", "i." }, nodes = { "ordered_list1_prefix", "ordered_list2_prefix", "ordered_list3_prefix", "ordered_list4_prefix", "ordered_list5_prefix", "ordered_list6_prefix", }, render = module.public.icon_renderers.multilevel_on_right(true), }, quote = { icons = { "│" }, nodes = { "quote1", "quote2", "quote3", "quote4", "quote5", "quote6", }, highlights = { "@neorg.quotes.1.prefix", "@neorg.quotes.2.prefix", "@neorg.quotes.3.prefix", "@neorg.quotes.4.prefix", "@neorg.quotes.5.prefix", "@neorg.quotes.6.prefix", }, render = module.public.icon_renderers.quote_concealed, clear = module.public.icon_removers.quote, }, heading = { icons = { "◉", "◎", "○", "✺", "▶", "⤷" }, highlights = { "@neorg.headings.1.prefix", "@neorg.headings.2.prefix", "@neorg.headings.3.prefix", "@neorg.headings.4.prefix", "@neorg.headings.5.prefix", "@neorg.headings.6.prefix", }, nodes = { "heading1_prefix", "heading2_prefix", "heading3_prefix", "heading4_prefix", "heading5_prefix", "heading6_prefix", concealed = { "link_target_heading1", "link_target_heading2", "link_target_heading3", "link_target_heading4", "link_target_heading5", "link_target_heading6", }, }, render = module.public.icon_renderers.multilevel_on_right(false), }, definition = { single = { icon = "≡", nodes = { "single_definition_prefix", concealed = { "link_target_definition" } }, render = module.public.icon_renderers.on_left, }, multi_prefix = { icon = "⋙ ", nodes = { "multi_definition_prefix" }, render = module.public.icon_renderers.on_left, }, multi_suffix = { icon = "⋘ ", nodes = { "multi_definition_suffix" }, render = module.public.icon_renderers.on_left, }, }, footnote = { single = { icon = "⁎", -- When set to true, footnote link with numeric title will be -- concealed to superscripts. numeric_superscript = true, title_highlight = "@neorg.footnotes.title", nodes = { "single_footnote_prefix", concealed = { "link_target_footnote" } }, render = module.public.icon_renderers.on_left, render_concealed = module.public.icon_renderers.footnote_concealed, }, multi_prefix = { icon = "⁑ ", nodes = { "multi_footnote_prefix" }, render = module.public.icon_renderers.on_left, }, multi_suffix = { icon = "⁑ ", nodes = { "multi_footnote_suffix" }, render = module.public.icon_renderers.on_left, }, }, delimiter = { weak = { icon = "⟨", highlight = "@neorg.delimiters.weak", nodes = { "weak_paragraph_delimiter" }, render = module.public.icon_renderers.fill_text, }, strong = { icon = "⟪", highlight = "@neorg.delimiters.strong", nodes = { "strong_paragraph_delimiter" }, render = module.public.icon_renderers.fill_text, }, horizontal_line = { icon = "─", highlight = "@neorg.delimiters.horizontal_line", nodes = { "horizontal_line" }, -- The starting position of horizontal lines: -- - "window": the horizontal line starts from the first column, reaching the left of the window -- - "here": the horizontal line starts from the node column left = "here", -- The ending position of horizontal lines: -- - "window": the horizontal line ends at the last column, reaching the right of the window -- - "textwidth": the horizontal line ends at column `textwidth` or 79 when it's set to zero right = "window", render = module.public.icon_renderers.render_horizontal_line, }, }, markup = { spoiler = { icon = "•", highlight = "@neorg.markup.spoiler", nodes = { "spoiler" }, render = module.public.icon_renderers.fill_multiline_chop2, }, }, -- Options that control the behaviour of code block dimming -- (placing a darker background behind `@code` tags). code_block = { -- If true will only dim the content of the code block (without the -- `@code` and `@end` lines), not the entirety of the code block itself. content_only = true, -- The width to use for code block backgrounds. -- -- When set to `fullwidth` (the default), will create a background -- that spans the width of the buffer. -- -- When set to `content`, will only span as far as the longest line -- within the code block. width = "fullwidth", -- When set to a number, the code block background will be at least -- this many chars wide. Useful in conjunction with `width = "content"` min_width = nil, -- Additional padding to apply to either the left or the right. Making -- these values negative is considered undefined behaviour (it is -- likely to work, but it's not officially supported). padding = { left = 0, right = 0, }, -- If `true` will conceal (hide) the `@code` and `@end` portion of the code -- block. conceal = false, -- If `false` will disable spell check on code blocks when 'spell' option is switched on. spell_check = true, nodes = { "ranged_verbatim_tag" }, highlight = "@neorg.tags.ranged_verbatim.code_block", render = module.public.icon_renderers.render_code_block, insert_enabled = true, }, }, } local function pos_eq(pos1, pos2) return (pos1.x == pos2.x) and (pos1.y == pos2.y) end local function pos_le(pos1, pos2) return pos1.x < pos2.x or (pos1.x == pos2.x and pos1.y <= pos2.y) end -- local function pos_lt(pos1, pos2) -- return pos1.x < pos2.x or (pos1.x == pos2.x and pos1.y < pos2.y) -- end local function remove_extmarks(bufid, pos_start_0b_0b, pos_end_0bin_0bex) assert(pos_le(pos_start_0b_0b, pos_end_0bin_0bex)) if pos_eq(pos_start_0b_0b, pos_end_0bin_0bex) then return end local ns_icon = module.private.ns_icon for _, result in ipairs( vim.api.nvim_buf_get_extmarks( bufid, ns_icon, { pos_start_0b_0b.x, pos_start_0b_0b.y }, { pos_end_0bin_0bex.x - ((pos_end_0bin_0bex.y == 0) and 1 or 0), pos_end_0bin_0bex.y - 1 }, {} ) ) do local extmark_id = result[1] -- TODO: Optimize -- local node_pos_0b_0b = { x = result[2], y = result[3] } -- assert( -- pos_le(pos_start_0b_0b, node_pos_0b_0b) and pos_le(node_pos_0b_0b, pos_end_0bin_0bex), -- ("start=%s, end=%s, node=%s"):format( -- vim.inspect(pos_start_0b_0b), -- vim.inspect(pos_end_0bin_0bex), -- vim.inspect(node_pos_0b_0b) -- ) -- ) vim.api.nvim_buf_del_extmark(bufid, ns_icon, extmark_id) end end local function is_inside_example(_) -- TODO: waiting for parser fix return false end local function should_skip_prettify(mode, current_row_0b, node, config, row_start_0b, row_end_0bex) local result if config.insert_enabled then result = false elseif (mode == "i") and in_range(current_row_0b, row_start_0b, row_end_0bex) then result = true elseif is_inside_example(node) then result = true else result = false end return result end local function query_get_nodes(query, document_root, bufid, row_start_0b, row_end_0bex) local result = {} local concealed_node_ids = {} for id, node in query:iter_captures(document_root, bufid, row_start_0b, row_end_0bex) do if node:missing() then goto continue end if query.captures[id] == "icon-concealed" then concealed_node_ids[node:id()] = true end table.insert(result, node) ::continue:: end return result, concealed_node_ids end local function check_min(xy, x_new, y_new) if (x_new < xy.x) or (x_new == xy.x and y_new < xy.y) then xy.x = x_new xy.y = y_new end end local function check_max(xy, x_new, y_new) if (x_new > xy.x) or (x_new == xy.x and y_new > xy.y) then xy.x = x_new xy.y = y_new end end local function add_prettify_flag_line(bufid, row) local ns_prettify_flag = module.private.ns_prettify_flag vim.api.nvim_buf_set_extmark(bufid, ns_prettify_flag, row, 0, {}) end local function add_prettify_flag_range(bufid, row_start_0b, row_end_0bex) for row = row_start_0b, row_end_0bex - 1 do add_prettify_flag_line(bufid, row) end end local function remove_prettify_flag_on_line(bufid, row_0b) -- TODO: optimize local ns_prettify_flag = module.private.ns_prettify_flag vim.api.nvim_buf_clear_namespace(bufid, ns_prettify_flag, row_0b, row_0b + 1) end local function remove_prettify_flag_range(bufid, row_start_0b, row_end_0bex) -- TODO: optimize local ns_prettify_flag = module.private.ns_prettify_flag vim.api.nvim_buf_clear_namespace(bufid, ns_prettify_flag, row_start_0b, row_end_0bex) end local function remove_prettify_flag_all(bufid) remove_prettify_flag_range(bufid, 0, -1) end local function get_visible_line_range(winid) local row_start_1b = vim.fn.line("w0", winid) local row_end_1b = vim.fn.line("w$", winid) return (row_start_1b - 1), row_end_1b end local function get_parsed_query_lazy() if module.private.prettify_query then return module.private.prettify_query end local keys = { "config", "icons" } local function traverse_config(config, f) if config == false then return end if config.nodes then f(config) return end if type(config) ~= "table" then log.warn(("unsupported icon config: %s = %s"):format(table.concat(keys, "."), config)) return end local key_pos = #keys + 1 for key, sub_config in pairs(config) do keys[key_pos] = key traverse_config(sub_config, f) keys[key_pos] = nil end end local config_by_node_name = {} local queries = { "[" } traverse_config(module.config.public.icons, function(config) for _, node_type in ipairs(config.nodes) do table.insert(queries, ("(%s)@icon"):format(node_type)) config_by_node_name[node_type] = config end for _, node_type in ipairs(config.nodes.concealed or {}) do table.insert(queries, ("(%s)@icon-concealed"):format(node_type)) config_by_node_name[node_type] = config end end) table.insert(queries, "]") local query_combined = table.concat(queries, " ") module.private.prettify_query = utils.ts_parse_query("norg", query_combined) assert(module.private.prettify_query) module.private.config_by_node_name = config_by_node_name return module.private.prettify_query end local function prettify_range(bufid, row_start_0b, row_end_0bex) -- in case there's undo/removal garbage -- TODO: optimize row_end_0bex = math.min(row_end_0bex + 1, vim.api.nvim_buf_line_count(bufid)) local treesitter_module = module.required["core.integrations.treesitter"] local document_root = treesitter_module.get_document_root(bufid) assert(document_root) local nodes, concealed_node_ids = query_get_nodes(get_parsed_query_lazy(), document_root, bufid, row_start_0b, row_end_0bex) local winid = vim.fn.bufwinid(bufid) assert(winid > 0) local current_row_0b = vim.api.nvim_win_get_cursor(winid)[1] - 1 local current_mode = vim.api.nvim_get_mode().mode local conceallevel = vim.wo[winid].conceallevel local concealcursor = vim.wo[winid].concealcursor assert(document_root) for _, node in ipairs(nodes) do local node_row_start_0b, node_col_start_0b, node_row_end_0bin, node_col_end_0bex = node:range() local node_row_end_0bex = node_row_end_0bin + 1 local config = module.private.config_by_node_name[node:type()] if config.clear then config:clear(bufid, node) else local pos_start_0b_0b, pos_end_0bin_0bex = { x = node_row_start_0b, y = node_col_start_0b }, { x = node_row_end_0bin, y = node_col_end_0bex } check_min(pos_start_0b_0b, node:start()) check_max(pos_end_0bin_0bex, node:end_()) remove_extmarks(bufid, pos_start_0b_0b, pos_end_0bin_0bex) end remove_prettify_flag_range(bufid, node_row_start_0b, node_row_end_0bex) add_prettify_flag_range(bufid, node_row_start_0b, node_row_end_0bex) if should_skip_prettify(current_mode, current_row_0b, node, config, node_row_start_0b, node_row_end_0bex) then goto continue end local has_conceal = ( concealed_node_ids[node:id()] and (not config.check_conceal or config.check_conceal(node)) and is_concealing_on_row_range( current_mode, conceallevel, concealcursor, current_row_0b, node_row_start_0b, node_row_end_0bex ) ) if has_conceal then if config.render_concealed then config:render_concealed(bufid, node) end else config:render(bufid, node) end ::continue:: end end local function render_window_buffer(bufid) local ns_prettify_flag = module.private.ns_prettify_flag local winid = vim.fn.bufwinid(bufid) local row_start_0b, row_end_0bex = get_visible_line_range(winid) local prettify_flags_0b = vim.api.nvim_buf_get_extmarks( bufid, ns_prettify_flag, { row_start_0b, 0 }, { row_end_0bex - 1, -1 }, {} ) local row_nomark_start_0b, row_nomark_end_0bin local i_flag = 1 for i = row_start_0b, row_end_0bex - 1 do while i_flag <= #prettify_flags_0b and i > prettify_flags_0b[i_flag][2] do i_flag = i_flag + 1 end if i_flag <= #prettify_flags_0b and i == prettify_flags_0b[i_flag][2] then i_flag = i_flag + 1 else assert(i < (prettify_flags_0b[i_flag] and prettify_flags_0b[i_flag][2] or row_end_0bex)) row_nomark_start_0b = row_nomark_start_0b or i row_nomark_end_0bin = i end end assert((row_nomark_start_0b == nil) == (row_nomark_end_0bin == nil)) if row_nomark_start_0b then prettify_range(bufid, row_nomark_start_0b, row_nomark_end_0bin + 1) end end local function render_all_scheduled_and_done() for bufid, _ in pairs(module.private.rerendering_scheduled_bufids) do if vim.fn.bufwinid(bufid) >= 0 then render_window_buffer(bufid) end end module.private.rerendering_scheduled_bufids = {} end local function schedule_rendering(bufid) local not_scheduled = vim.tbl_isempty(module.private.rerendering_scheduled_bufids) module.private.rerendering_scheduled_bufids[bufid] = true if not_scheduled then vim.schedule(render_all_scheduled_and_done) end end local function mark_line_changed(bufid, row_0b) remove_prettify_flag_on_line(bufid, row_0b) schedule_rendering(bufid) end local function mark_line_range_changed(bufid, row_start_0b, row_end_0bex) remove_prettify_flag_range(bufid, row_start_0b, row_end_0bex) schedule_rendering(bufid) end local function mark_all_lines_changed(bufid) if not module.private.enabled then return end remove_prettify_flag_all(bufid) schedule_rendering(bufid) end local function clear_all_extmarks(bufid) local ns_icon = module.private.ns_icon local ns_prettify_flag = module.private.ns_prettify_flag vim.api.nvim_buf_clear_namespace(bufid, ns_icon, 0, -1) vim.api.nvim_buf_clear_namespace(bufid, ns_prettify_flag, 0, -1) end local function get_table_default_empty(tbl, key) if not tbl[key] then tbl[key] = {} end return tbl[key] end local function update_cursor(event) local cursor_record = get_table_default_empty(module.private.cursor_record, event.buffer) cursor_record.row_0b = event.cursor_position[1] - 1 cursor_record.col_0b = event.cursor_position[2] cursor_record.line_content = event.line_content end local function handle_init_event(event) assert(vim.api.nvim_win_is_valid(event.window)) update_cursor(event) local function on_line_callback( tag, bufid, _changedtick, ---@diagnostic disable-line -- TODO: type error workaround row_start_0b, _row_end_0bex, ---@diagnostic disable-line -- TODO: type error workaround row_updated_0bex, _n_byte_prev ---@diagnostic disable-line -- TODO: type error workaround ) assert(tag == "lines") if not module.private.enabled then return end mark_line_range_changed(bufid, row_start_0b, row_updated_0bex) end local attach_succeeded = vim.api.nvim_buf_attach(event.buffer, true, { on_lines = on_line_callback }) assert(attach_succeeded) local language_tree = vim.treesitter.get_parser(event.buffer, "norg") if not language_tree then log.error("Failed to get parser for language norg in buffer " .. event.buffer) return end local bufid = event.buffer -- used for detecting non-local (multiline) changes, like spoiler / code block -- TODO: exemption in certain cases, for example when changing only heading followed by pure texts, -- in which case all its descendants would be unnecessarily re-concealed. local function on_changedtree_callback(ranges) -- TODO: abandon if too large for i = 1, #ranges do local range = ranges[i] local row_start_0b = range[1] local row_end_0bex = range[3] + 1 remove_prettify_flag_range(bufid, row_start_0b, row_end_0bex) end end language_tree:register_cbs({ on_changedtree = on_changedtree_callback }) mark_all_lines_changed(event.buffer) if module.config.public.folds and vim.api.nvim_win_is_valid(event.window) and vim.api.nvim_buf_is_valid(event.buffer) then vim.api.nvim_buf_call(event.buffer, function() -- NOTE(vhyrro): `vim.wo` only supports `wo[winid][0]`, -- hence the `buf_call` here. local wo = vim.wo[event.window][0] wo.foldmethod = "expr" wo.foldexpr = vim.treesitter.foldexpr and "v:lua.vim.treesitter.foldexpr()" or "nvim_treesitter#foldexpr()" wo.foldtext = "" local init_open_folds = module.config.public.init_open_folds local function open_folds() vim.cmd("normal! zR") end if init_open_folds == "always" then open_folds() elseif init_open_folds == "never" then -- luacheck:ignore 542 -- do nothing else if init_open_folds ~= "auto" then log.warn('"init_open_folds" must be "auto", "always", or "never"') end if wo.foldlevel == 0 then open_folds() end end end) end end local function handle_insert_toggle(event) mark_line_changed(event.buffer, event.cursor_position[1] - 1) end local function handle_insertenter(event) handle_insert_toggle(event) end local function handle_insertleave(event) handle_insert_toggle(event) end local function handle_toggle_prettifier(event) -- FIXME: module.private.enabled should be a map from bufid to boolean module.private.enabled = not module.private.enabled if module.private.enabled then mark_all_lines_changed(event.buffer) else module.private.rerendering_scheduled_bufids[event.buffer] = nil clear_all_extmarks(event.buffer) end end local function is_same_line_movement(event) -- some operations like dd / u cannot yet be listened reliably -- below is our best approximation local cursor_record = module.private.cursor_record return ( cursor_record and cursor_record.row_0b == event.cursor_position[1] - 1 and cursor_record.col_0b ~= event.cursor_position[2] and cursor_record.line_content == event.line_content ) end local function handle_cursor_moved(event) -- reveal/conceal when conceallevel>0 -- also triggered when dd / u if not is_same_line_movement(event) then local cursor_record = module.private.cursor_record[event.buffer] if cursor_record then -- leaving previous line, conceal it if necessary mark_line_changed(event.buffer, cursor_record.row_0b) end -- entering current line, conceal it if necessary local current_row_0b = event.cursor_position[1] - 1 mark_line_changed(event.buffer, current_row_0b) end update_cursor(event) end local function handle_cursor_moved_i(event) return handle_cursor_moved(event) end local function handle_winscrolled(event) schedule_rendering(event.buffer) end local function handle_filetype(event) handle_init_event(event) end local event_handlers = { ["core.neorgcmd.events.core.concealer.toggle"] = handle_toggle_prettifier, -- ["core.autocommands.events.bufnewfile"] = handle_init_event, ["core.autocommands.events.filetype"] = handle_filetype, ["core.autocommands.events.bufreadpost"] = handle_init_event, ["core.autocommands.events.insertenter"] = handle_insertenter, ["core.autocommands.events.insertleave"] = handle_insertleave, ["core.autocommands.events.cursormoved"] = handle_cursor_moved, ["core.autocommands.events.cursormovedi"] = handle_cursor_moved_i, ["core.autocommands.events.winscrolled"] = handle_winscrolled, } module.on_event = function(event) if event.referrer == "core.autocommands" and vim.bo[event.buffer].ft ~= "norg" then return end if (not module.private.enabled) and (event.type ~= "core.neorgcmd.events.core.concealer.toggle") then return end return event_handlers[event.type](event) end module.load = function() local icon_preset = module.imported[module.name .. ".preset_" .. module.config.public.icon_preset].config.private["icon_preset_" .. module.config.public.icon_preset] if not icon_preset then log.error( ("Unable to load icon preset '%s' - such a preset does not exist"):format(module.config.public.icon_preset) ) return end module.config.public = vim.tbl_deep_extend("force", module.config.public, { icons = icon_preset }, module.config.custom or {}) -- module.required["core.autocommands"].enable_autocommand("BufNewFile") module.required["core.autocommands"].enable_autocommand("FileType", true) module.required["core.autocommands"].enable_autocommand("BufReadPost") module.required["core.autocommands"].enable_autocommand("InsertEnter") module.required["core.autocommands"].enable_autocommand("InsertLeave") module.required["core.autocommands"].enable_autocommand("CursorMoved") module.required["core.autocommands"].enable_autocommand("CursorMovedI") module.required["core.autocommands"].enable_autocommand("WinScrolled", true) modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ ["toggle-concealer"] = { name = "core.concealer.toggle", args = 0, condition = "norg", }, }) end) vim.api.nvim_create_autocmd("OptionSet", { pattern = "conceallevel", callback = function() local bufid = vim.api.nvim_get_current_buf() if vim.bo[bufid].ft ~= "norg" then return end mark_all_lines_changed(bufid) end, }) end module.events.subscribed = { ["core.autocommands"] = { -- bufnewfile = true, filetype = true, bufreadpost = true, insertenter = true, insertleave = true, cursormoved = true, cursormovedi = true, winscrolled = true, }, ["core.neorgcmd"] = { ["core.concealer.toggle"] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/concealer/preset_basic/module.lua ================================================ local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.concealer.preset_basic") module.config.private.icon_preset_basic = {} return module ================================================ FILE: lua/neorg/modules/core/concealer/preset_diamond/module.lua ================================================ local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.concealer.preset_diamond") module.config.private.icon_preset_diamond = { heading = { icons = { "◈", "◇", "◆", "⋄", "❖", "⟡" }, }, footnote = { single = { icon = "†", }, multi_prefix = { icon = "‡ ", }, multi_suffix = { icon = "‡ ", }, }, } return module ================================================ FILE: lua/neorg/modules/core/concealer/preset_varied/module.lua ================================================ local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.concealer.preset_varied") module.config.private.icon_preset_varied = { heading = { icons = { "◉", "◆", "✿", "○", "▶", "⤷" }, }, footnote = { single = { icon = "", }, multi_prefix = { icon = " ", }, multi_suffix = { icon = " ", }, }, } return module ================================================ FILE: lua/neorg/modules/core/defaults/module.lua ================================================ --[[ file: Defaults summary: Metamodule for storing the most necessary modules. internal: true --- This file contains all of the most important modules that any user would want to have a "just works" experience. Individual entries can be disabled via the "disable" flag: ```lua load = { ["core.defaults"] = { config = { disable = { -- module list goes here "core.autocommands", "core.itero", }, }, }, } ``` --]] local neorg = require("neorg.core") local modules = neorg.modules return modules.create_meta( "core.defaults", "core.autocommands", "core.clipboard", "core.clipboard.code-blocks", "core.esupports.hop", "core.esupports.indent", "core.esupports.metagen", "core.integrations.treesitter", "core.itero", "core.journal", "core.keybinds", "core.looking-glass", "core.neorgcmd", "core.pivot", "core.promo", "core.qol.toc", "core.qol.todo_items", "core.storage", "core.tangle", "core.tempus", "core.todo-introspector", "core.ui.calendar" ) ================================================ FILE: lua/neorg/modules/core/dirman/module.lua ================================================ --[[ file: Dirman title: The Most Critical Component of any Organized Workflow description: The `dirman` module handles different collections of notes in separate directories. summary: This module is be responsible for managing directories full of .norg files. --- `core.dirman` provides other modules the ability to see which directories the user is in, where each note collection is stored and how to interact with it. When writing notes, it is often crucial to have notes on a certain topic be isolated from notes on another topic. Dirman achieves this with a concept of "workspaces", which are named directories full of `.norg` notes. To use `core.dirman`, simply load up the module in your configuration and specify the directories you would like to be managed for you: ```lua require('neorg').setup { load = { ["core.defaults"] = {}, ["core.dirman"] = { config = { workspaces = { my_ws = "~/neorg", -- Format: = my_other_notes = "~/work/notes", }, index = "index.norg", -- The name of the main (root) .norg file } } } } ``` To query the current workspace, run `:Neorg workspace`. To set the workspace, run `:Neorg workspace `. ### Changing the Current Working Directory After a recent update `core.dirman` will no longer change the current working directory after switching workspace. To get the best experience it's recommended to set the `autochdir` Neovim option. ### Create a new note (in lua) You can use dirman to create new notes in your workspaces. ```lua local dirman = require('neorg').modules.get_module("core.dirman") dirman.create_file("my_file", "my_ws", { no_open = false, -- open file after creation? force = false, -- overwrite file if exists metadata = {} -- key-value table for metadata fields }) ``` ## Keybinds This module exposes the following keybinds (see [`core.keybinds`](@core.keybinds) for instructions on mapping them): - `neorg.dirman.new-note` - Create a new note in the current workspace, prompt for name --]] local Path = require("pathlib") local neorg = require("neorg.core") local log, modules, utils = neorg.log, neorg.modules, neorg.utils ---@type core.dirman.utils, core.ui local dirman_utils, ui local module = modules.create("core.dirman") module.setup = function() return { success = true, requires = { "core.autocommands", "core.ui", "core.storage", "core.dirman.utils" }, } end module.load = function() -- Go through every workspace and expand special symbols like ~ for name, workspace_location in pairs(module.config.public.workspaces) do -- module.config.public.workspaces[name] = vim.fn.expand(vim.fn.fnameescape(workspace_location)) ---@diagnostic disable-line -- TODO: type error workaround module.config.public.workspaces[name] = Path(workspace_location):resolve():to_absolute() end dirman_utils = module.required["core.dirman.utils"] ui = module.required["core.ui"] vim.keymap.set("", "(neorg.dirman.new-note)", module.public.new_note) -- Used to detect when we've entered a buffer with a potentially different cwd module.required["core.autocommands"].enable_autocommand("BufEnter", true) modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ index = { args = 0, name = "dirman.index", }, }) end) -- Synchronize core.neorgcmd autocompletions module.public.sync() local default_workspace = module.public.get_default_workspace() if module.config.public.open_last_workspace and vim.fn.argc(-1) == 0 then if module.config.public.open_last_workspace == "default" then if not default_workspace then log.warn( 'Configuration error in `core.dirman`: the `open_last_workspace` option is set to "default", but no default workspace is provided in the `default_workspace` configuration variable. Defaulting to opening the last known workspace.' ) module.public.set_last_workspace() return end module.public.open_workspace(default_workspace) else module.public.set_last_workspace() end elseif default_workspace then module.public.set_workspace(default_workspace) end end module.config.public = { -- The list of active Neorg workspaces. -- -- There is always an inbuilt workspace called `default`, whose location is -- set to the Neovim current working directory on boot. ---@type table workspaces = { default = require("pathlib").cwd(), }, -- The name for the index file. -- -- The index file is the "entry point" for all of your notes. index = "index.norg", -- The default workspace to set whenever Neovim starts. -- If a function, will be called with the current workspace and should resolve to a valid workspace name default_workspace = nil, -- Whether to open the last workspace's index file when `nvim` is executed -- without arguments. -- -- May also be set to the string `"default"`, due to which Neorg will always -- open up the index file for the workspace defined in `default_workspace`. open_last_workspace = false, -- Whether to use core.ui.text_popup for `dirman.new.note` event. -- if `false`, will use vim's default `vim.ui.input` instead. use_popup = true, } module.private = { ---@type { [1]: string, [2]: PathlibPath } current_workspace = { "default", Path.cwd() }, } ---@class core.dirman module.public = { ---@return table get_workspaces = function() return module.config.public.workspaces end, ---@return string[] get_workspace_names = function() return vim.tbl_keys(module.config.public.workspaces) end, --- If present retrieve a workspace's path by its name, else returns nil ---@param name string #The name of the workspace ---@return PathlibPath get_workspace = function(name) return module.config.public.workspaces[name] end, --- @return { [1]: string, [2]: PathlibPath } get_current_workspace = function() return module.private.current_workspace end, --- The default workspace, may be set dynamically based on cwd ---@return string? # Should evaluate to a valid workspace name get_default_workspace = function() if type(module.config.public.default_workspace) == "function" then return module.config.public.default_workspace() end return module.config.public.default_workspace end, --- Sets the workspace to the one specified (if it exists) and broadcasts the workspace_changed event ---@param ws_name string #The name of a valid namespace we want to switch to ---@return boolean #True if the workspace is set correctly, false otherwise set_workspace = function(ws_name) -- Grab the workspace location local workspace = module.config.public.workspaces[ws_name] -- Create a new object describing our new workspace local new_workspace = { ws_name, workspace } -- If the workspace does not exist then error out if not workspace then log.warn("Unable to set workspace to", workspace, "- that workspace does not exist") return false end -- Create the workspace directory if not already present workspace:mkdir(Path.const.o755, true) -- Cache the current workspace local current_ws = vim.deepcopy(module.private.current_workspace) -- Set the current workspace to the new workspace object we constructed module.private.current_workspace = new_workspace if ws_name ~= "default" then module.required["core.storage"].store("last_workspace", ws_name) end -- Broadcast the workspace_changed event with all the necessary information modules.broadcast_event( assert( modules.create_event( module, "core.dirman.events.workspace_changed", { old = current_ws, new = new_workspace } ) ) ) return true end, --- Dynamically defines a new workspace if the name isn't already occupied and broadcasts the workspace_added event ---@return boolean True if the workspace is added successfully, false otherwise ---@param workspace_name string #The unique name of the new workspace ---@param workspace_path string|PathlibPath #A full path to the workspace root add_workspace = function(workspace_name, workspace_path) -- If the module already exists then bail if module.config.public.workspaces[workspace_name] then return false end workspace_path = Path(workspace_path):resolve():to_absolute() -- Set the new workspace and its path accordingly module.config.public.workspaces[workspace_name] = workspace_path -- Broadcast the workspace_added event with the newly added workspace as the content modules.broadcast_event( assert( modules.create_event(module, "core.dirman.events.workspace_added", { workspace_name, workspace_path }) ) ) -- Sync autocompletions so the user can see the new workspace module.public.sync() return true end, --- If the file we opened is within a workspace directory, returns the name of the workspace, else returns nil get_workspace_match = function() -- Cache the current working directory module.config.public.workspaces.default = Path.cwd() local file = Path(vim.fn.expand("%:p")) -- Name of matching workspace. Falls back to "default" local ws_name = "default" -- Store the depth of the longest match local longest_match = 0 -- Find a matching workspace for workspace, location in pairs(module.config.public.workspaces) do if workspace ~= "default" then if file:is_relative_to(location) and location:depth() > longest_match then ws_name = workspace longest_match = location:depth() end end end return ws_name end, --- Uses the `get_workspace_match()` function to determine the root of the workspace based on the --- current working directory, then changes into that workspace set_closest_workspace_match = function() -- Get the closest workspace match local ws_match = module.public.get_workspace_match() -- If that match exists then set the workspace to it! if ws_match then module.public.set_workspace(ws_match) else -- Otherwise try to reset the workspace to the default module.public.set_workspace("default") end end, --- Updates completions for the :Neorg command sync = function() -- Get all the workspace names local workspace_names = module.public.get_workspace_names() -- Add the command to core.neorgcmd so it can be used by the user! modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ workspace = { max_args = 1, name = "dirman.workspace", complete = { workspace_names }, }, }) end) end, ---@class core.dirman.create_file_opts ---@field no_open? boolean do not open the file after creation? ---@field force? boolean overwrite file if it already exists? ---@field metadata? core.esupports.metagen.metadata metadata fields, if provided inserts metadata - an empty table uses default values --- Takes in a path (can include directories) and creates a .norg file from that path ---@param path string|PathlibPath a path to place the .norg file in ---@param workspace? string workspace name ---@param opts? core.dirman.create_file_opts additional options create_file = function(path, workspace, opts) opts = opts or {} -- Grab the current workspace's full path local fullpath if workspace ~= nil then fullpath = module.public.get_workspace(workspace) else fullpath = module.public.get_current_workspace()[2] end if fullpath == nil then log.error("Error in fetching workspace path") return end local destination = (fullpath / path):add_suffix(".norg") -- Generate parents just in case destination:parent_assert():mkdir(Path.const.o755 + 4 * math.pow(8, 4), true) -- 40755(oct) -- Create or overwrite the file local fd = destination:fs_open(opts.force and "w" or "a", Path.const.o644, false) if fd then vim.loop.fs_close(fd) end -- Broadcast file creation event local bufnr = module.public.get_file_bufnr(destination:tostring()) modules.broadcast_event( assert(modules.create_event(module, "core.dirman.events.file_created", { buffer = bufnr, opts = opts })) ) if not opts.no_open then -- Begin editing that newly created file vim.cmd("e " .. destination:cmd_string() .. "| w") end end, --- Takes in a workspace name and a path for a file and opens it ---@param workspace_name string #The name of the workspace to use ---@param path string|PathlibPath #A path to open the file (e.g directory/filename.norg) open_file = function(workspace_name, path) local workspace = module.public.get_workspace(workspace_name) if workspace == nil then return end vim.cmd("e " .. (workspace / path):cmd_string() .. " | w") end, --- Reads the neorg_last_workspace.txt file and loads the cached workspace from there set_last_workspace = function() -- Attempt to open the last workspace cache file in read-only mode local storage = modules.get_module("core.storage") if not storage then log.trace("Module `core.storage` not loaded, refusing to load last user's workspace.") return end local last_workspace = storage.retrieve("last_workspace") last_workspace = type(last_workspace) == "string" and last_workspace or module.public.get_default_workspace() or "" local workspace_path = module.public.get_workspace(last_workspace) if not workspace_path then log.trace("Unable to switch to workspace '" .. last_workspace .. "'. The workspace does not exist.") return end -- If we were successful in switching to that workspace then begin editing that workspace's index file if module.public.set_workspace(last_workspace) then vim.cmd("e " .. (workspace_path / module.public.get_index()):cmd_string()) utils.notify("Last Workspace -> " .. workspace_path) end end, --- Checks for file existence by supplying a full path in `filepath` ---@param filepath string|PathlibPath file_exists = function(filepath) return Path(filepath):exists() end, --- Get the bufnr for a `filepath` (full path) ---@param filepath string|PathlibPath get_file_bufnr = function(filepath) if module.public.file_exists(filepath) then local uri = vim.uri_from_fname(tostring(filepath)) return vim.uri_to_bufnr(uri) end end, --- Returns a list of all files relative path from a `workspace_name` ---@param workspace_name string ---@return PathlibPath[]|nil get_norg_files = function(workspace_name) local res = {} local workspace = module.public.get_workspace(workspace_name) if not workspace then return end for path in workspace:fs_iterdir(true, 20) do if path:is_file(true) and path:suffix() == ".norg" then table.insert(res, path) end end return res end, --- Sets the current workspace and opens that workspace's index file ---@param workspace string #The name of the workspace to open open_workspace = function(workspace) -- If we have, then query that workspace local ws_match = module.public.get_workspace(workspace) -- If the workspace does not exist then give the user a nice error and bail if not ws_match then log.error('Unable to switch to workspace - "' .. workspace .. '" does not exist') return end -- Set the workspace to the one requested module.public.set_workspace(workspace) -- If we're switching to a workspace that isn't the default workspace then enter the index file if workspace ~= "default" then vim.cmd("e " .. (ws_match / module.public.get_index()):cmd_string()) end end, --- Touches a file in workspace ---@param path string|PathlibPath ---@param workspace string touch_file = function(path, workspace) vim.validate({ path = { path, "string", "table" }, workspace = { workspace, "string" }, }) local ws_match = module.public.get_workspace(workspace) if not workspace then return false end return (ws_match / path):touch(Path.const.o644, true) end, get_index = function() return module.config.public.index end, new_note = function() if module.config.public.use_popup then ui.create_prompt("NeorgNewNote", "New Note: ", function(text) -- Create the file that the user has entered module.public.create_file(text) end, { center_x = true, center_y = true, }, { width = 25, height = 1, row = 10, col = 0, }) else vim.ui.input({ prompt = "New Note: " }, function(text) if text ~= nil and #text > 0 then module.public.create_file(text) end end) end end, ---Is the file a part of the given workspace? ---@param file PathlibPath ---@param workspace_name string? workspace or current ws when nil ---@return boolean in_workspace = function(file, workspace_name) local ws_path if not workspace_name then ws_path = module.private.current_workspace[2] else ws_path = module.public.get_workspace(workspace_name) end return not not file:match("^" .. ws_path) end, } module.on_event = function(event) -- If somebody has executed the :Neorg workspace command then if event.type == "core.neorgcmd.events.dirman.workspace" then -- Have we supplied an argument? if event.content[1] then module.public.open_workspace(event.content[1]) vim.schedule(function() local new_workspace = module.public.get_workspace(event.content[1]) if not new_workspace then return end utils.notify("New Workspace: " .. event.content[1] .. " -> " .. new_workspace) end) else -- No argument supplied, simply print the current workspace -- Query the current workspace local current_ws = module.public.get_current_workspace() -- Nicely print it. We schedule_wrap here because people with a configured logger will have this message -- silenced by other trace logs vim.schedule(function() utils.notify("Current Workspace: " .. current_ws[1] .. " -> " .. current_ws[2]) end) end end -- If somebody has executed the :Neorg index command then if event.type == "core.neorgcmd.events.dirman.index" then local current_ws = module.public.get_current_workspace() local index_path = current_ws[2] / module.public.get_index() if vim.fn.filereadable(index_path:tostring("/")) == 0 then if current_ws[1] == "default" then utils.notify(table.concat({ "Index file cannot be created in 'default' workspace to avoid confusion.", "If this is intentional, manually create an index file beforehand to use this command.", }, " ")) return end if not index_path:touch(Path.const.o644, true) then utils.notify( table.concat({ "Unable to create '", module.public.get_index(), "' in the current workspace - are your filesystem permissions set correctly?", }), vim.log.levels.WARN ) return end end dirman_utils.edit_file(index_path:cmd_string()) return end end module.events.defined = { workspace_changed = modules.define_event(module, "workspace_changed"), workspace_added = modules.define_event(module, "workspace_added"), workspace_cache_empty = modules.define_event(module, "workspace_cache_empty"), file_created = modules.define_event(module, "file_created"), } module.events.subscribed = { ["core.autocommands"] = { bufenter = true, }, ["core.dirman"] = { workspace_changed = true, }, ["core.neorgcmd"] = { ["dirman.workspace"] = true, ["dirman.index"] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/dirman/tests.lua ================================================ local tests = require("neorg.tests") local Path = require("pathlib") describe("core.dirman tests", function() local dirman = tests .neorg_with("core.dirman", { workspaces = { test = "./test-workspace", }, }).modules .get_module("core.dirman") describe("workspace-related functions", function() it("properly expands workspace paths", function() assert.same(dirman.get_workspaces(), { default = Path.cwd(), test = Path.cwd() / "test-workspace", }) end) it("properly sets and retrieves workspaces", function() assert.is_true(dirman.set_workspace("test")) assert.equal(dirman.get_current_workspace()[1], "test") end) it("properly creates and writes files", function() local ws_path = (Path.cwd() / "test-workspace") dirman.create_file("example-file", "test", { no_open = true, }) finally(function() vim.fn.delete(ws_path:tostring(), "rf") end) assert.equal(vim.fn.filereadable((ws_path / "example-file.norg"):tostring()), 1) end) end) end) ================================================ FILE: lua/neorg/modules/core/dirman/utils/module.lua ================================================ --[[ file: Dirman-Utils summary: A set of utilities for the `core.dirman` module. internal: true --- This internal submodule implements some basic utility functions for [`core.dirman`](@core.dirman). Currently the only exposed API function is `expand_path`, which takes a path like `$name/my/location` and converts `$name` into the full path of the workspace called `name`. --]] local Path = require("pathlib") local neorg = require("neorg.core") local log, modules = neorg.log, neorg.modules local module = neorg.modules.create("core.dirman.utils") ---@class core.dirman.utils module.public = { ---Resolve `$/path/to/file` and return the real path ---@param path string | PathlibPath # path ---@param raw_path boolean? # If true, returns resolved path, otherwise, returns resolved path and append ".norg" ---@param host_file string | PathlibPath | nil file the link resides in, if the link is relative, this file is used instead of the current file ---@return PathlibPath?, boolean? # Resolved path. If path does not start with `$` or not absolute, adds relative from current file. expand_pathlib = function(path, raw_path, host_file) local relative = false if not host_file then host_file = vim.fn.expand("%:p") end local filepath = Path(path) -- Expand special chars like `$` local custom_workspace_path = filepath:match("^%$([^/\\]*)[/\\]") if custom_workspace_path then ---@type core.dirman? local dirman = modules.get_module("core.dirman") if not dirman then log.error(table.concat({ "Unable to jump to link with custom workspace: `core.dirman` is not loaded.", "Please load the module in order to get workspace support.", }, " ")) return end -- If the user has given an empty workspace name (i.e. `$/myfile`) if custom_workspace_path:len() == 0 then filepath = dirman.get_current_workspace()[2] / filepath:relative_to(Path("$")) else -- If the user provided a workspace name (i.e. `$my-workspace/myfile`) local workspace = dirman.get_workspace(custom_workspace_path) if not workspace then local msg = "Unable to expand path: workspace '%s' does not exist" log.warn(string.format(msg, custom_workspace_path)) return end filepath = workspace / filepath:relative_to(Path("$" .. custom_workspace_path)) end elseif filepath:is_relative() then relative = true local this_file = Path(host_file):absolute() filepath = this_file:parent_assert() / filepath else filepath = filepath:absolute() end -- requested to expand norg file if not raw_path then if type(path) == "string" and (path:sub(#path) == "/" or path:sub(#path) == "\\") then -- if path ends with `/`, it is an invalid request! log.error(table.concat({ "Norg file location cannot point to a directory.", string.format("Current link points to '%s'", path), "which ends with a `/`.", }, " ")) return end filepath = filepath:add_suffix(".norg") end return filepath, relative end, ---Call attempt to edit a file, catches and suppresses the error caused by a swap file being ---present. Re-raises other errors via log.error ---@param path string | PathlibPath edit_file = function(path) local ok, err = pcall(vim.cmd.edit, tostring(path)) if not ok then -- Vim:E325 is the swap file error, in which case, a lengthy message already shows to -- the user, and we don't have to crash out of this function (which creates a long and -- misleading error message). if err and not err:match("Vim:E325") then log.error("Failed to edit file %s. Error:\n%s"):format(path, err) end end end, ---Resolve `$/path/to/file` and return the real path -- NOTE: Use `expand_pathlib` which returns a PathlibPath object instead. --- ---\@deprecate Use `expand_pathlib` which returns a PathlibPath object instead. TODO: deprecate this <2024-03-27> ---@param path string|PathlibPath # path ---@param raw_path boolean? # If true, returns resolved path, otherwise, returns resolved path and append ".norg" ---@return string? # Resolved path. If path does not start with `$` or not absolute, adds relative from current file. expand_path = function(path, raw_path) local res = module.public.expand_pathlib(path, raw_path) return res and res:tostring() or nil end, } return module ================================================ FILE: lua/neorg/modules/core/esupports/hop/module.lua ================================================ --[[ file: Esupports-Hop title: Follow Various Link Locations description: `esupport.hop` handles the process of dealing with links so you don't have to summary: "Hop" between Neorg links, following them with a single keypress. --- The hop module serves to provide an easy way to follow and fix broken links with a single keypress. By default, pressing `` in normal mode under a link will attempt to follow said link. If the link location is found, you will be taken to the destination - if it is not, you will be prompted with a set of actions that you can perform on the broken link. ## Keybinds This module exposes the following keybinds (see [`core.keybinds`](@core.keybinds) for instructions on mapping them): - `neorg.esupports.hop.hop-link` - Follow the link under the cursor, seeks forward - `neorg.esupports.hop.hop-link.vsplit` - Same, but open the link in a vertical split - `neorg.esupports.hop.hop-link.tab-drop` - Same as hop-link, but open the link in a new tab; if the destination is already open in an existing tab then just navigate to that tab (check :help :drop) - `neorg.esupports.hop.hop-link.drop` - Same as hop-link, but navigate to the buffer if the destination is already open in an existing buffer (check :help :drop) --]] local neorg = require("neorg.core") local config, lib, log, modules, utils = neorg.config, neorg.lib, neorg.log, neorg.modules, neorg.utils local module = modules.create("core.esupports.hop") ---@type core.ui, core.integrations.treesitter, core.links, core.dirman.utils local ui, ts, links, dirman_utils module.setup = function() return { success = true, requires = { "core.integrations.treesitter", "core.ui", "core.dirman.utils", "core.links", }, } end module.load = function() ui = module.required["core.ui"] links = module.required["core.links"] dirman_utils = module.required["core.dirman.utils"] ts = module.required["core.integrations.treesitter"] vim.keymap.set("", "(neorg.esupports.hop.hop-link)", module.public.hop_link) vim.keymap.set("", "(neorg.esupports.hop.hop-link.vsplit)", lib.wrap(module.public.hop_link, "vsplit")) vim.keymap.set("", "(neorg.esupports.hop.hop-link.drop)", lib.wrap(module.public.hop_link, "drop")) vim.keymap.set("", "(neorg.esupports.hop.hop-link.tab-drop)", lib.wrap(module.public.hop_link, "tab-drop")) end module.config.public = { -- If true, will attempt to find a link further than your cursor on the current line, -- even if your cursor is not over the link itself. lookahead = true, -- This value determines the strictness of fuzzy matching when trying to fix a link. -- Zero means only exact matches will be found, and higher values mean more lenience. -- -- `0.5` is the optimal default value, and it is recommended to keep this option as-is. fuzzing_threshold = 0.5, -- List of strings specifying which filetypes to open in an external application, -- should the user want to open a link to such a file. external_filetypes = {}, } local function xy_le(x0, y0, x1, y1) return x0 < x1 or (x0 == x1 and y0 <= y1) end local function range_contains(r_out, r_in) return xy_le(r_out.row_start, r_out.column_start, r_in.row_start, r_in.column_start) and xy_le(r_in.row_end, r_in.column_end, r_out.row_end, r_out.column_end) end ---@alias LinkType ---|"url" ---|"generic" ---|"external_file" ---|"definition" ---|"timestamp" ---|"footnote" ---|"heading1" ---|"heading2" ---|"heading3" ---|"heading4" ---|"heading5" ---|"heading6" ---|"line_number" ---|"wiki" ---@class (exact) Link ---@field link_node TSNode The treesitter node of the link. ---@field link_file_text string? A provided path, if any. ---@field link_type LinkType? The type of link that was provided. ---@field link_location_text string? The target title/URL of the link. ---@field link_description string? The description of the link, if provided. ---@alias LinkTargetType --- |"buffer" --- |"external_app" --- |"external_file" --- |"wiki" --- |"calendar" ---@class LinkTarget ---@field original_title string The title of the link that points to this target. ---@field node TSNode The node of the target. ---@field type LinkTargetType The type of target that was located. ---@field buffer number The buffer ID in which the target was found. ---@field uri? string For external links ---@field line? number Link number ---@field date? osdate For calendar links ---@field path? string | PathlibPath ---@class (exact) PotentialLinkFixes ---@field similarity number The similarity of this candidate to the current title of the link. ---@field text string The title of the candidate link title (will replace the existing link target). ---@field node TSNode The node the fixed link points to. ---@class core.esupports.hop module.public = { --- Follow link from a specific node ---@param node TSNode ---@param open_mode string? if not nil, will open a new split with the split mode defined (vsplitr...) or new tab (mode="tab") or with external app (mode="external") ---@param parsed_link Link A table of link information gathered from parse_link() follow_link = function(node, open_mode, parsed_link) if node:type() == "anchor_declaration" then local located_anchor_declaration = module.public.locate_anchor_declaration_target(node) if not located_anchor_declaration then return end local range = ts.get_node_range(located_anchor_declaration.node) vim.cmd([[normal! m`]]) vim.api.nvim_win_set_cursor(0, { range.row_start + 1, range.column_start }) return end if not parsed_link then log.warn("Please parse your link before calling this function.") return end local located_link_information = module.public.locate_link_target(parsed_link) local function os_open_link(link_location) local o = {} if config.os_info == "windows" then o.command = "rundll32.exe" o.args = { "url.dll,FileProtocolHandler", link_location } else o.args = { link_location } if config.os_info == "linux" then o.command = "xdg-open" elseif config.os_info == "mac" then o.command = "open" elseif config.os_info == "wsl2" then o.command = "wslview" -- The file uri should be decoded when being transformed to a unix path. -- The decoding step is temporarily missing from wslview (https://github.com/wslutilities/wslu/issues/295), -- so we work around the problem by doing the transformation before invoking wslview. o.args[1] = vim.uri_to_fname(link_location) elseif config.os_info == "wsl" then o.command = "explorer.exe" end end require("plenary.job"):new(o):start() end local function open_split() if open_mode then if open_mode == "vsplit" then vim.cmd("vsplit") elseif open_mode == "split" then vim.cmd("split") elseif open_mode == "tab" then vim.cmd("tabnew") end end end local function jump_to_line(line) local status, _ = pcall(vim.api.nvim_win_set_cursor, 0, { line, 0 }) if not status then log.error("Failed to jump to line:", line, "- make sure the line number exists!") end end if located_link_information then if open_mode == "external" then os_open_link(located_link_information.uri or located_link_information.path) return end lib.match(located_link_information.type)({ -- Filter the currently unsupported link types and let the user know that they do not work wiki = function() vim.notify( "Neorg doesn't support wiki links yet, please use a more specific link type instead.", vim.log.levels.WARN ) end, -- If we're dealing with a URI, simply open the URI in the user's preferred method external_app = function() os_open_link(located_link_information.uri) end, -- If we're dealing with an external file, open it up in another Neovim buffer (unless otherwise applicable) external_file = function() open_split() dirman_utils.edit_file(located_link_information.path) if located_link_information.line then jump_to_line(located_link_information.line) end end, buffer = function() if open_mode ~= "tab-drop" and open_mode ~= "drop" then open_split() end if located_link_information.buffer ~= vim.api.nvim_get_current_buf() then if open_mode == "tab-drop" then vim.cmd("tab drop " .. vim.api.nvim_buf_get_name(located_link_information.buffer)) elseif open_mode == "drop" then vim.cmd("drop " .. vim.api.nvim_buf_get_name(located_link_information.buffer)) else vim.api.nvim_set_option_value("buflisted", true, { buf = located_link_information.buffer }) vim.api.nvim_set_current_buf(located_link_information.buffer) end end if located_link_information.line then jump_to_line(located_link_information.line) return end if located_link_information.node then local range = ts.get_node_range(located_link_information.node) vim.cmd([[normal! m`]]) vim.api.nvim_win_set_cursor(0, { range.row_start + 1, range.column_start }) return end end, calendar = function() local calendar = modules.get_module("core.ui.calendar") if not calendar then log.error("`core.ui.calendar` is not loaded! Unable to open timestamp.") return end local tempus = modules.get_module("core.tempus") if not tempus then log.error("`core.tempus` is not loaded! Unable to parse timestamp.") return end local buffer = vim.api.nvim_get_current_buf() calendar.select_date({ date = located_link_information.date, callback = function(input) local start_row, start_col, end_row, end_col = located_link_information.node:range() vim.api.nvim_buf_set_text( buffer, start_row, start_col, end_row, end_col, { "{@ " .. tostring(tempus.to_date(input, false)) .. "}" } ) end, }) end, }) return end local link_not_found_buf = ui.create_split("link-not-found") if link_not_found_buf == nil then return end local selection = ui.begin_selection(link_not_found_buf) :listener({ "", }, function(self) self:destroy() end) :apply({ warning = function(self, text) return self:text("WARNING: " .. text, "@text.warning") end, desc = function(self, text) return self:text(text, "@comment") end, }) selection :title("Link not found - what do we do now?") :blank() :text("There are a few actions that you can perform whenever a link cannot be located.", "Normal") :text("Press one of the available keys to perform your desired action.") :blank() :desc("The most common action will be to try and fix the link.") :desc("Fixing the link will perform a fuzzy search on every item of the same type in the file") :desc("and make the link point to the closest match:") :flag("f", "Attempt to fix the link", function() local similarities = module.private.fix_link_strict(parsed_link) if not similarities or vim.tbl_isempty(similarities) then return end module.private.write_fixed_link(node, parsed_link, similarities) ---@diagnostic disable-line -- TODO: type error workaround end) :blank() :desc("Does the same as the above keybind, however doesn't limit matches to those") :desc("defined by the link type. This means that even if the link points to a level 1") :desc("heading this fixing algorithm will be able to match any other item type:") :flag("F", "Attempt to fix the link (loose fuzzing)", function() local similarities = module.private.fix_link_loose(parsed_link) if not similarities or vim.tbl_isempty(similarities) then return end module.private.write_fixed_link(node, parsed_link, similarities, true) end) :blank() :warning("The below flags currently do not work, this is a beta build.") :desc("Instead of fixing the link you may actually want to create the target:") :flag("a", "Place target above current link parent") :flag("b", "Place target below current link parent") end, --- Locate a `link` or `anchor` node under the cursor ---@return TSNode? #A `link` or `anchor` node if present under the cursor, else `nil` extract_link_node = function() local current_node = vim.treesitter.get_node() if not current_node then return end local found_node = ts.find_parent(current_node, { "link", "anchor_declaration", "anchor_definition" }) if not found_node then found_node = (module.config.public.lookahead and module.public.lookahead_link_node()) end return found_node end, --- Attempts to locate a `link` or `anchor` node after the cursor on the same line ---@return TSNode? #A `link` or `anchor` node if present on the current line, else `nil` lookahead_link_node = function() local line = vim.api.nvim_get_current_line() local current_cursor_pos = vim.api.nvim_win_get_cursor(0) local current_line = current_cursor_pos[1] local index = current_cursor_pos[2] local resulting_node while not resulting_node do local next_square_bracket = line:find("%[", index) local next_curly_bracket = line:find("{", index) local smaller_value if not next_square_bracket and not next_curly_bracket then return elseif not next_square_bracket and next_curly_bracket then smaller_value = next_curly_bracket elseif next_square_bracket and not next_curly_bracket then smaller_value = next_square_bracket else smaller_value = (next_square_bracket < next_curly_bracket and next_square_bracket or next_curly_bracket) end vim.api.nvim_win_set_cursor(0, { current_line, smaller_value - 1, }) local node_under_cursor = vim.treesitter.get_node() if not node_under_cursor then return end if vim.tbl_contains({ "link_location", "link_description" }, node_under_cursor:type()) then resulting_node = node_under_cursor:parent() end index = index + 1 end return resulting_node end, --- Locates the node that an anchor is pointing to ---@param anchor_decl_node TSNode #A valid anchor declaration node ---@return LinkTarget? #The target of the link if it was found. locate_anchor_declaration_target = function(anchor_decl_node) if not anchor_decl_node:named_child(0) then return end local target = ts.get_node_text(anchor_decl_node:named_child(0):named_child(0)):gsub("[%s\\]", "") local query_str = [[ (anchor_definition (link_description text: (paragraph) @text ) ) ]] local document_root = ts.get_document_root() if not document_root then return end local query = utils.ts_parse_query("norg", query_str) for id, node in query:iter_captures(document_root, 0) do local capture = query.captures[id] if capture == "text" then local original_title = ts.get_node_text(node) local title = original_title:gsub("[%s\\]", "") if title:lower() == target:lower() then return { original_title = original_title, node = node, } end end end end, --- Converts a link node into a table of data related to the link ---@param link_node TSNode #The link node that was found by e.g. `extract_link_node()` ---@param buf number? #The buffer to parse the link in ---@return Link? #A table of data about the link parse_link = function(link_node, buf) buf = buf or 0 if not link_node or not vim.tbl_contains({ "link", "anchor_definition" }, link_node:type()) then return end local query_text = [[ [ (link (link_location file: ( (link_file_text) @link_file_text )? type: [ (link_target_url) (link_target_generic) (link_target_external_file) (link_target_definition) (link_target_timestamp) (link_target_footnote) (link_target_heading1) (link_target_heading2) (link_target_heading3) (link_target_heading4) (link_target_heading5) (link_target_heading6) (link_target_line_number) (link_target_wiki) ]? @link_type text: (paragraph)? @link_location_text ) (link_description text: (paragraph) @link_description )? ) (anchor_definition (link_description text: (paragraph) @link_description ) (link_location file: ( (link_file_text) @link_file_text )? type: [ (link_target_url) (link_target_generic) (link_target_external_file) (link_target_definition) (link_target_timestamp) (link_target_footnote) (link_target_heading1) (link_target_heading2) (link_target_heading3) (link_target_heading4) (link_target_heading5) (link_target_heading6) (link_target_wiki) ]? @link_type text: (paragraph)? @link_location_text ) ) ] ]] ---@type TSNode? local document_root = ts.get_document_root(buf) if not document_root then return end ---@type vim.treesitter.Query local query = utils.ts_parse_query("norg", query_text) local range = ts.get_node_range(link_node) ---@type Link local parsed_link_information = { link_node = link_node, } for id, node in query:iter_captures(document_root, buf, range.row_start, range.row_end + 1) do local capture = query.captures[id] local capture_node_range = ts.get_node_range(node) -- Check whether the node captured node is in bounds. -- There are certain rare cases where incorrect nodes would be parsed. if range_contains(range, capture_node_range) then local extract_node_text = lib.wrap(ts.get_node_text, node) parsed_link_information[capture] = parsed_link_information[capture] or lib.match(capture)({ link_file_text = extract_node_text, link_type = lib.wrap(string.sub, node:type(), string.len("link_target_") + 1), link_location_text = extract_node_text, link_description = extract_node_text, _ = function() log.error("Unknown capture type encountered when parsing link:", capture) end, }) end end return parsed_link_information end, --- Locate the target that a link points to ---@param parsed_link_information Link #A table returned by `parse_link()` ---@return LinkTarget #A table containing data about the link target locate_link_target = function(parsed_link_information) --- A pointer to the target buffer we will be parsing. -- This may change depending on the target file the user gave. local buf_pointer = vim.api.nvim_get_current_buf() -- Check whether our target is from a different file if parsed_link_information.link_file_text then local expanded_link_text = dirman_utils.expand_path(parsed_link_information.link_file_text) if expanded_link_text ~= vim.fn.expand("%:p") then -- We are dealing with a foreign file buf_pointer = vim.uri_to_bufnr("file://" .. expanded_link_text) end if not parsed_link_information.link_type then return { type = "buffer", original_title = nil, node = nil, buffer = buf_pointer, } end end return lib.match(parsed_link_information.link_type)({ -- Wiki links are currently unsupported, so we simply forward the link type wiki = function() return { type = "wiki" } end, url = function() return { type = "external_app", uri = parsed_link_information.link_location_text } end, external_file = function() -- There has to be a link location present for a link to be recognized as an external file, -- therefore we can safely assert here. local destination = assert(parsed_link_information.link_location_text) local path, line = string.match(destination, "^(.*):(%d+)$") if line then destination = path line = tonumber(line) end destination = ( vim.tbl_contains({ "/", "~" }, destination:sub(1, 1)) and "" or (vim.fn.expand("%:p:h") .. "/") ) .. destination return lib.match(vim.fn.fnamemodify(destination, ":e"))({ [{ "jpg", "jpeg", "png", "pdf" }] = { type = "external_app", uri = vim.uri_from_fname(vim.fn.expand(destination)), }, [module.config.public.external_filetypes] = { type = "external_app", uri = vim.uri_from_fname(vim.fn.expand(destination)), }, _ = function() return { type = "external_file", path = vim.fn.fnamemodify(destination, ":p"), line = line, } end, }) end, line_number = function() return { type = "buffer", buffer = buf_pointer, line = tonumber(parsed_link_information.link_location_text), } end, timestamp = function() local tempus = modules.get_module("core.tempus") if not tempus then log.error("`core.tempus` is not loaded! Unable to parse timestamp.") return {} end local parsed_date = tempus.parse_date(parsed_link_information.link_location_text) if type(parsed_date) == "string" then log.error("[ERROR]:", parsed_date) return {} end return { type = "calendar", date = tempus.to_lua_date(parsed_date), node = parsed_link_information.link_node, } end, _ = function() local query_str = links.get_link_target_query_string(parsed_link_information.link_type) local document_root = ts.get_document_root(buf_pointer) if not document_root then return end local query = utils.ts_parse_query("norg", query_str) for id, node in query:iter_captures(document_root, buf_pointer) do local capture = query.captures[id] if capture == "title" then local original_title = ts.get_node_text(node, buf_pointer) if original_title then local title = original_title:gsub("[%s\\]", "") local target = parsed_link_information.link_location_text:gsub("[%s\\]", "") if title:lower() == target:lower() then return { type = "buffer", original_title = original_title, node = node, buffer = buf_pointer, } end end end end end, } --[[@as table]]) end, hop_link = function(split_mode) local link_node_at_cursor = module.public.extract_link_node() if not link_node_at_cursor then log.trace("No link under cursor.") return end local parsed_link = module.public.parse_link(link_node_at_cursor) if not parsed_link then log.trace("Failed to parse link", vim.inspect(link_node_at_cursor)) return end module.public.follow_link(link_node_at_cursor, split_mode, parsed_link) end, } module.private = { --- Damerau-levenstein implementation calculate_similarity = function(lhs, rhs) -- https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance local str1 = lhs local str2 = rhs local matrix = {} local cost -- build matrix for i = 0, #str1 do matrix[i] = {} matrix[i][0] = i end for j = 0, #str2 do matrix[0][j] = j end for j = 1, #str2 do for i = 1, #str1 do if str1:sub(i, i) == str2:sub(j, j) then cost = 0 else cost = 1 end matrix[i][j] = math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost) if i > 1 and j > 1 and str1:sub(i, i) == str2:sub(j - 1, j - 1) and str1:sub(i - 1, i - 1) == str2:sub(j, j) then matrix[i][j] = math.min(matrix[i][j], matrix[i - 2][j - 2] + cost) end end end return matrix[#str1][#str2] / ( (#str1 + #str2) + (function() local index = 1 local ret = 0 while index < #str1 do if str1:sub(index, index):lower() == str2:sub(index, index):lower() then ret = ret + 0.2 end index = index + 1 end return ret end)() ) end, --- Fuzzy fixes a link with a loose type checking query ---@param parsed_link_information Link #A table as returned by `parse_link()` ---@return PotentialLinkFixes[]? #A table of similarities (fuzzed items) fix_link_loose = function(parsed_link_information) local generic_query = [[ [(_ [(strong_carryover_set (strong_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name"))) (weak_carryover_set (weak_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name")))]? title: (paragraph_segment) @title) (inline_link_target (paragraph) @title)] ]] return module.private.fix_link(parsed_link_information, generic_query) end, --- Fuzzy fixes a link with a strict type checking query ---@param parsed_link_information Link #A table as returned by `parse_link()` ---@return PotentialLinkFixes[]? #A table of similarities (fuzzed items) fix_link_strict = function(parsed_link_information) local query = lib.match(parsed_link_information.link_type)({ generic = [[ [(_ [(strong_carryover_set (strong_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name"))) (weak_carryover_set (weak_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name")))]? title: (paragraph_segment) @title) (inline_link_target (paragraph) @title)] ]], [{ "definition", "footnote" }] = string.format( [[ (%s_list (strong_carryover_set (strong_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name")))? . [(single_%s (weak_carryover_set (weak_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name")))? (single_%s_prefix) title: (paragraph_segment) @title) (multi_%s (weak_carryover_set (weak_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name")))? (multi_%s_prefix) title: (paragraph_segment) @title)]) ]], lib.reparg(parsed_link_information.link_type, 5) ), _ = string.format( [[ (%s [(strong_carryover_set (strong_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name"))) (weak_carryover_set (weak_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name")))]? (%s_prefix) title: (paragraph_segment) @title) ]], lib.reparg(parsed_link_information.link_type, 2) ), }) return module.private.fix_link(parsed_link_information, query) end, --- Query all similar targets that a link could be pointing to ---@param parsed_link_information table #A table as returned by `parse_link()` ---@param query_str string #The query to be used during the search ---@return PotentialLinkFixes[]? #A table of similarities (fuzzed items) fix_link = function(parsed_link_information, query_str) local buffer = vim.api.nvim_get_current_buf() if parsed_link_information.link_file_text then local expanded_link_text = dirman_utils.expand_path(parsed_link_information.link_file_text) if expanded_link_text ~= vim.fn.expand("%:p") then -- We are dealing with a foreign file buffer = vim.uri_to_bufnr("file://" .. expanded_link_text) end end local query = utils.ts_parse_query("norg", query_str) local document_root = ts.get_document_root(buffer) if not document_root then return end ---@type PotentialLinkFixes[] local similarities = {} for id, node in query:iter_captures(document_root, buffer) do local capture_name = query.captures[id] if capture_name == "title" then local text = ts.get_node_text(node, buffer) local similarity = module.private.calculate_similarity(parsed_link_information.link_location_text, text) -- If our match is similar enough then add it to the list if similarity < module.config.public.fuzzing_threshold then table.insert(similarities, { similarity = similarity, text = text, node = node:parent() }) end end end if vim.tbl_isempty(similarities) then utils.notify("Sorry, Neorg couldn't fix that link.", vim.log.levels.WARN) end table.sort(similarities, function(lhs, rhs) return lhs.similarity < rhs.similarity end) return similarities end, --- Writes a link that was fixed through fuzzing into the buffer ---@param link_node TSNode #The treesitter node of the link, extracted by e.g. `extract_link_node()` ---@param parsed_link_information Link #A table as returned by `parse_link()` ---@param similarities PotentialLinkFixes[] #The table of similarities as returned by `fix_link_*()` ---@param force_type boolean #If true will forcefully overwrite the link type to the target type as well (e.g. would convert `#` -> `*`) write_fixed_link = function(link_node, parsed_link_information, similarities, force_type) local most_similar = similarities[1] if not link_node or not most_similar then return end local range = ts.get_node_range(link_node) local prefix = lib.when( parsed_link_information.link_type == "generic" and not force_type, "#", lib.match(most_similar.node:type())({ heading1 = "*", heading2 = "**", heading3 = "***", heading4 = "****", heading5 = "*****", heading6 = "******", single_definition = "$", multi_definition = "$", single_footnote = "^", multi_footnote = "^", _ = "#", }) ) .. " " local function callback(replace) vim.api.nvim_buf_set_text( 0, range.row_start, range.column_start, range.row_end, range.column_end, { replace } ) end callback( "{" .. lib.when( parsed_link_information.link_file_text --[[@as boolean]], lib.lazy_string_concat(":", parsed_link_information.link_file_text, ":"), "" ) .. prefix .. most_similar.text .. "}" .. lib.when( parsed_link_information.link_description --[[@as boolean]], lib.lazy_string_concat("[", parsed_link_information.link_description, "]"), "" ) ) end, } return module ================================================ FILE: lua/neorg/modules/core/esupports/indent/module.lua ================================================ --[[ file: Indent title: Formatting on the Fly summary: A set of instructions for Neovim to indent Neorg documents. --- `core.esupports.indent` uses Norg's format to unambiguously determine the indentation level for the current line. The indent calculation is aided by [treesitter](@core.integrations.treesitter), which means that the quality of indents is "limited" by the quality of the produced syntax tree, which will get better and better with time. To reindent a file, you may use the inbuilt Neovim `=` operator. Indent levels are also calculated as you type, but may not be entirely correct due to incomplete syntax trees (if you find any such examples, then file an issue!). It is also noteworthy that indents add the indentation level to the beginning of the line and doesn't carry on the indentation level from the previous heading, meaning that if both heading1 and heading2 have an indentation level of 4, heading2 will not be indented an additional 4 spaces from heading1. --]] local neorg = require("neorg.core") local lib, modules = neorg.lib, neorg.modules local module = modules.create("core.esupports.indent") ---@type core.integrations.treesitter local ts module.setup = function() return { requires = { "core.integrations.treesitter", "core.autocommands", }, } end ---@class core.esupports.indent module.public = { indentexpr = function(buf, line, node) line = line or (vim.v.lnum - 1) node = node or ts.get_first_node_on_line(buf, line) if not node then return 0 end local indent_data = module.config.public.indents[node:type()] or module.config.public.indents._ if not indent_data then return 0 end local _, initial_indent = node:start() local indent = 0 for _, modifier in ipairs(indent_data.modifiers or {}) do if module.config.public.modifiers[modifier] then local ret = module.config.public.modifiers[modifier](buf, node, line, initial_indent) ---@diagnostic disable-line -- TODO: type error workaround if ret ~= 0 then indent = ret end end end local line_len = (vim.api.nvim_buf_get_lines(buf, line, line + 1, true)[1] or ""):len() -- Ensure that the cursor is within the `norg` language local current_lang = vim.treesitter.get_parser(buf, "norg"):language_for_range({ line, line_len, line, line_len, }) -- If it isn't then fall back to `nvim-treesitter`'s indent instead. if current_lang:lang() ~= "norg" then -- If we're in a ranged tag then apart from providing nvim-treesitter indents also make sure -- to account for the indentation level of the tag itself. if node:type() == "ranged_verbatim_tag_content" then local lnum = line local start = node:range() while lnum > start do if vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1]:match("^%s*$") then lnum = lnum - 1 else return vim.fn["nvim_treesitter#indent"]() end end return ts.get_node_range(node:parent()).column_start + vim.fn["nvim_treesitter#indent"]() else return vim.fn["nvim_treesitter#indent"]() end end -- Check if the code is within a verbatim block local ranged_tag = ts.find_parent(node, "ranged_verbatim_tag_content") indent_data = ranged_tag and module.config.public.indents[ranged_tag:type()] or indent_data -- Indents can be a static value, so account for that here if type(indent_data.indent) == "number" then -- If the indent is -1 then let Neovim indent instead of us if indent_data.indent == -1 then return -1 end local new_indent = indent + indent_data.indent + (module.config.public.tweaks[node:type()] or 0) if (not module.config.public.dedent_excess) and new_indent <= initial_indent then return initial_indent end return new_indent end local calculated_indent = indent_data.indent(buf, node, line, indent, initial_indent) or 0 if calculated_indent == -1 then return -1 end local new_indent = indent + calculated_indent + (module.config.public.tweaks[node:type()] or 0) if (not module.config.public.dedent_excess) and new_indent <= initial_indent then return initial_indent end return new_indent end, ---re-evaluate the indent expression for each line in the range, and apply the new indentation ---@param buffer number ---@param row_start number 0 based ---@param row_end number 0 based exclusive reindent_range = function(buffer, row_start, row_end) for i = row_start, row_end - 1 do local indent_level = module.public.indentexpr(buffer, i) module.public.buffer_set_line_indent(buffer, i, indent_level) end end, ---Set the indent of the given line to the new value ---@param buffer number ---@param start_row number 0 based ---@param new_indent number buffer_set_line_indent = function(buffer, start_row, new_indent) local line = vim.api.nvim_buf_get_lines(buffer, start_row, start_row + 1, true)[1] if line:match("^%s*$") or new_indent == -1 then return end local leading_whitespace = line:match("^%s*"):len() vim.api.nvim_buf_set_text(buffer, start_row, 0, start_row, leading_whitespace, { (" "):rep(new_indent) }) end, } module.config.public = { -- The table of indentations. -- -- This table describes a set of node types and how they should be indented -- when encountered in the syntax tree. -- -- It also allows for certain nodes to be given properties (modifiers), which -- can additively stack indentation given more complex circumstances. indents = { -- Default behaviour for every other node not explicitly defined. _ = { modifiers = { "under-headings" }, indent = 0, }, -- Indent behaviour for paragraph segments (lines of text). ["paragraph_segment"] = { modifiers = { "under-headings", "under-nestable-detached-modifiers" }, indent = 0, }, -- Indent behaviour for strong paragraph delimiters. -- -- The indentation of these should be determined based on the heading level -- that it is a part of. Since the `strong_paragraph_delimiter` node isn't actually -- a child of the previous heading in the syntax tree some extra work is required to -- make it indent as expected. ["strong_paragraph_delimiter"] = { indent = function(buf, _, line, _, _) local node = ts.get_first_node_on_line(buf, vim.fn.prevnonblank(line) - 1) if not node then return 0 end return ts.get_node_range(node:type():match("heading%d") and node:named_child(1) or node).column_start end, }, -- Indent behaviour for headings. -- -- In "idiomatic norg", headings should not be indented. ["heading1"] = { indent = 0, }, -- Indent behaviour for headings. -- -- In "idiomatic norg", headings should not be indented. ["heading2"] = { indent = 0, }, -- Indent behaviour for headings. -- -- In "idiomatic norg", headings should not be indented. ["heading3"] = { indent = 0, }, -- Indent behaviour for headings. -- -- In "idiomatic norg", headings should not be indented. ["heading4"] = { indent = 0, }, -- Indent behaviour for headings. -- -- In "idiomatic norg", headings should not be indented. ["heading5"] = { indent = 0, }, -- Indent behaviour for headings. -- -- In "idiomatic norg", headings should not be indented. ["heading6"] = { indent = 0, }, -- Ranged tag contents' indentation should be calculated by Neovim itself. ["ranged_verbatim_tag_content"] = { indent = -1, }, -- `@end` tags should always be indented as far as the beginning `@` ranged verbatim tag. ["ranged_verbatim_tag_end"] = { modifiers = { "ranged-tag-end" }, indent = 0, }, -- `|end` tags should always be indented as far as the beginning `|` ranged tag. ["ranged_tag_end"] = { modifiers = { "ranged-tag-end" }, indent = 0, }, -- `=end` tags should always be indented as far as the beginning `=` ranged tag. ["macro_tag_end"] = { modifiers = { "ranged-tag-end" }, indent = 0, }, }, -- Apart from indents, modifiers may also be defined. -- -- These are repeatable instructions for nodes that share common traits. modifiers = { -- For any object that can exist under headings ["under-headings"] = function(_, node) local heading = ts.find_parent(node:parent(), "heading%d") if not heading then return 0 end local child = heading:named_child(1) if not child then return 0 end return ts.get_node_range(child).column_start end, -- For any object that should be indented under a list ["under-nestable-detached-modifiers"] = function(_, node) local list = ts.find_parent(node, { "unordered_list1", "unordered_list2", "unordered_list3", "unordered_list4", "unordered_list5", "unordered_list6", "ordered_list1", "ordered_list2", "ordered_list3", "ordered_list4", "ordered_list5", "ordered_list6", "quote1", "quote2", "quote3", "quote4", "quote5", "quote6", }) if not list or not list:named_child(1) then return 0 end if list:named_child(1):type() == "detached_modifier_extension" then local child = list:named_child(2) if not child then return 0 end return ts.get_node_range(child).column_start + ts.get_node_text(list:named_child(2)):match("^%s*"):len() end local child = list:named_child(1) if not child then return 0 end return ts.get_node_range(child).column_start end, -- For any ranged tag end that should always be indented as far as the beginning of the ranged tag ["ranged-tag-end"] = function(_, node) return ts.get_node_range(node:parent()).column_start end, }, -- Tweaks are user defined `node_name` => `indent_level` mappings, -- allowing the user to overwrite the indentation level for certain nodes. -- -- Nodes can be found via treesitter's `:InspectTree`. For example, -- indenting an unordered list can be done with `unordered_list2 = 4` tweaks = {}, -- When true, will reformat the current line every time you press `` (Enter). format_on_enter = true, -- When true, will reformat the current line every time you press `` (i.e. every -- time you leave insert mode). format_on_escape = true, -- When false will not dedent nodes, only indent them. This means that if a node -- is indented too much to the right, it will not be touched. It will only be indented -- if the node is to the left of the expected indentation level. -- -- Useful when writing documentation in the style of vimdoc, where content is indented -- heavily to the right in comparison to the default Neorg style. dedent_excess = true, } module.load = function() module.required["core.autocommands"].enable_autocommand("BufEnter") ts = module.required["core.integrations.treesitter"] end module.on_event = function(event) if event.type == "core.autocommands.events.bufenter" and event.content.norg then vim.api.nvim_set_option_value( "indentexpr", ("v:lua.require'neorg'.modules.get_module('core.esupports.indent').indentexpr(%d)"):format(event.buffer), { buf = event.buffer } ) local indentkeys = "o,O,*,*" .. lib.when(module.config.public.format_on_enter, ",*", "") .. lib.when(module.config.public.format_on_escape, ",*", "") vim.api.nvim_set_option_value("indentkeys", indentkeys, { buf = event.buffer }) end end module.events.subscribed = { ["core.autocommands"] = { bufenter = true, }, } return module ================================================ FILE: lua/neorg/modules/core/esupports/metagen/module.lua ================================================ --[[ file: Metagen title: Manually Writing Metadata? No Thanks description: The metagen module automatically places relevant metadata at the top of your `.norg` files. summary: A Neorg module for generating document metadata automatically. --- The metagen module exposes two commands - `:Neorg inject-metadata` and `:Neorg update-metadata`. - The `inject-metadata` command will remove any existing metadata and overwrite it with fresh information. - The `update-metadata` preserves existing info, updating things like the `updated` fields (when the file was last edited) as well as a few other non-destructive fields. --]] local neorg = require("neorg.core") ---@type neorg.configuration, neorg.modules, neorg.core.utils local config, modules, utils, lib = neorg.config, neorg.modules, neorg.utils, neorg.lib local module = modules.create("core.esupports.metagen") local function get_timezone_offset() -- http://lua-users.org/wiki/TimeZon -- return the timezone offset in seconds, as it was on the time given by ts -- Eric Feliksik local utcdate = os.date("!*t", 0) local localdate = os.date("*t", 0) localdate.isdst = false -- this is the trick return os.difftime(os.time(localdate), os.time(utcdate)) ---@diagnostic disable-line -- TODO: type error workaround end local function get_timestamp() -- generate a ISO-8601 timestamp -- example: 2023-09-05T09:09:11-0500 -- local timezone_config = module.config.public.timezone if timezone_config == "utc" then return os.date("!%Y-%m-%dT%H:%M:%S+0000") elseif timezone_config == "implicit-local" then return os.date("%Y-%m-%dT%H:%M:%S") else -- assert(timezone_config == "local") local tz_offset = get_timezone_offset() local h, m = math.modf(tz_offset / 3600) return os.date("%Y-%m-%dT%H:%M:%S") .. string.format("%+.4d", h * 100 + m * 60) end end local function get_author() local author_config = module.config.public.author if author_config == nil or author_config == "" then return utils.get_username() else return author_config end end -- The default template found in the config for this module. local default_template = { -- The title field generates a title for the file based on the filename. { "title", function() return vim.fn.expand("%:p:t:r") end, }, -- The description field is always kept empty for the user to fill in. { "description", "" }, -- The authors field is taken from config or autopopulated by querying the current user's system username. { "authors", get_author, }, -- The categories field is always kept empty for the user to fill in. { "categories", "" }, -- The created field is populated with the current date as returned by `os.date`. { "created", get_timestamp, }, -- When creating fresh, new metadata, the updated field is populated the same way -- as the `created` date. { "updated", get_timestamp, }, -- The version field determines which Norg version was used when -- the file was created. { "version", function() return config.norg_version end, }, } -- For all of the currently configured template entries, fall back to default handling -- if the configuration omits the handler function. This allows an end user to specify -- they want an entry in the generated metadata but they do not want to override the -- default value for that entry by adding a singleton (like { "description" }) to the -- template. local function fill_template_defaults() local function match_first(comparand) return function(_key, value) ---@diagnostic disable-line -- TODO: type error workaround if value[1] == comparand then return value else return nil end end end module.config.public.template = vim.iter(module.config.public.template) :map(function(elem) if not elem[2] then return lib.filter(default_template, match_first(elem[1])) end return elem end) :totable() end module.setup = function() return { requires = { "core.autocommands", "core.integrations.treesitter" } } end module.config.public = { -- One of "none", "auto" or "empty" -- - "none" generates no metadata -- - "auto" generates metadata if it is not present -- - "empty" generates metadata only for new files/buffers. type = "none", -- Whether updated date field should be automatically updated on save if required update_date = true, -- How to generate a tabulation inside the `@document.meta` tag tab = "", -- Custom delimiter between tag and value delimiter = ": ", -- Custom template to use for generating content inside `@document.meta` tag -- The template is a list of lists, each defining a key-value pair of metadata -- -- Example: -- ``` -- template = { -- -- Default field name without a value will fall back to the default behavior -- { "title" }, -- -- Set a custom value for "authors" field -- { "authors", "Vhyrro" }, -- -- Fields can be set by lua functions -- { -- "categories", -- function() -- return {"Category-1", "Category-2"} -- end -- } -- } -- ``` template = default_template, -- Custom author name that overrides default value if not nil or empty -- Default value is autopopulated by querying the current user's system username. author = "", -- Timezone information in the timestamps -- - "utc" the timestamp is in UTC+0 -- - "local" the timestamp is in the local timezone -- - "implicit-local" like "local", but the timezone information is omitted from the timestamp timezone = "local", -- Whether or not to call `:h :undojoin` just before changing the timestamp in -- `update_metadata`. This will make your undo key undo the last change before writing the file -- in addition to the timestamp change. This will move your cursor to the top of the file. For -- users with an autosave plugin, this option must be paired with keybinds for undo/redo to -- avoid problems with undo tree branching: -- ```lua -- vim.keymap.set("n", "u", function() -- require("neorg.modules").get_module("core.esupports.metagen").skip_next_update() -- local k = vim.api.nvim_replace_termcodes("u", true, false, true) -- vim.api.nvim_feedkeys(k, 'n', false) -- end) -- vim.keymap.set("n", "", function() -- require("neorg.modules").get_module("core.esupports.metagen").skip_next_update() -- local k = vim.api.nvim_replace_termcodes("", true, false, true) -- vim.api.nvim_feedkeys(k, 'n', false) -- end) -- ``` undojoin_updates = false, } module.private = { buffers = {}, listen_event = "none", skip_next_update = false, } ---@class core.esupports.metagen module.public = { --- Returns true if there is a `@document.meta` tag in the current document ---@param buf number #The buffer to check in ---@return boolean,table #Whether the metadata was present, and the range of the metadata node is_metadata_present = function(buf) local query = utils.ts_parse_query( "norg", [[ (ranged_verbatim_tag (tag_name) @name (#eq? @name "document.meta") ) @meta ]] ) local root = module.required["core.integrations.treesitter"].get_document_root(buf) if not root then return false, { range = { 0, 0 }, node = nil, } end local _, found = query:iter_matches(root, buf)() local range = { 0, 0 } if not found then return false, { range = range, node = nil, } end local metadata_node = nil for id, nodes in pairs(found) do local name = query.captures[id] -- node is a list in nvim 0.11+ ---@type TSNode local node if vim.islist(nodes) then node = nodes[1] end if name == "meta" then metadata_node = node range[1], _, range[2], _ = node:range() range[2] = range[2] + 2 end end return true, { range = range, node = metadata_node, } end, --- Skip the next call to update_metadata skip_next_update = function() module.private.skip_next_update = true end, ---@class core.esupports.metagen.metadata ---@field title? function|string the title of the note ---@field description? function|string the description of the note ---@field authors? function|string the authors of the note ---@field categories? function|string the categories of the note ---@field created? function|string a timestamp of creation time for the note ---@field updated? function|string a timestamp of last time the note was updated ---@field version? function|string the neorg version --- Creates the metadata contents from the provided metadata table (defaulting to the configuration's template). ---@param buf number #The buffer to query potential data from ---@param metadata? core.esupports.metagen.metadata #Table of metadata, overrides defaults if present ---@return table #A table of strings that can be directly piped to `nvim_buf_set_lines` construct_metadata = function(buf, metadata) local template = module.config.public.template local whitespace = type(module.config.public.tab) == "function" and module.config.public.tab() or module.config.public.tab local delimiter = type(module.config.public.delimiter) == "function" and module.config.public.delimiter() or module.config.public.delimiter local result = { "@document.meta", } for _, data in ipairs(template) do if metadata and metadata[data[1]] then -- override with data from metadata table data = { data[1], metadata[data[1]] } end local lines = whitespace .. data[1] .. delimiter .. tostring(type(data[2]) == "function" and data[2]() or data[2]) for _, line in ipairs(vim.split(lines, "\n")) do table.insert(result, line) end end table.insert(result, "@end") if vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1]:len() > 0 then table.insert(result, "") end return result end, --- Inject the metadata into a buffer ---@param buf number #The number of the buffer to inject the metadata into ---@param force? boolean #Whether to forcefully override existing metadata ---@param metadata? core.esupports.metagen.metadata #Table of metadata data, overrides defaults if present inject_metadata = function(buf, force, metadata) local present, data = module.public.is_metadata_present(buf) if force or not present then local constructed_metadata = module.public.construct_metadata(buf, metadata) vim.api.nvim_buf_set_lines(buf, data.range[1], data.range[2], false, constructed_metadata) end end, update_metadata = function(buf) if module.private.skip_next_update then module.private.skip_next_update = false return end local present = module.public.is_metadata_present(buf) if not present then return end local modified = vim.api.nvim_get_option_value("modified", { buf = buf }) if not modified then return end -- Extract the root node of the norg_meta language -- This process should be abstracted into a core.integrations.treesitter -- function. local languagetree = vim.treesitter.get_parser(buf, "norg") if not languagetree then return end local meta_root = nil for _, tree in pairs(languagetree:children()) do if tree:lang() ~= "norg_meta" or meta_root then goto continue end local meta_tree = tree:parse()[1] if not meta_tree then goto continue end meta_root = meta_tree:root() ::continue:: end if not meta_root then return end -- Capture current date from config local current_date = "" for _, val in ipairs(module.config.public.template) do if val[1] == "updated" then current_date = val[2]() end end local query = utils.ts_parse_query( "norg_meta", [[ (pair (key) @_key (#eq? @_key "updated") (value) @updated) ]] ) for id, node in query:iter_captures(meta_root, buf) do local capture = query.captures[id] if capture == "updated" then local date = module.required["core.integrations.treesitter"].get_node_text(node) if date ~= current_date then local range = module.required["core.integrations.treesitter"].get_node_range(node) if module.config.public.undojoin_updates then vim.cmd.undojoin() end vim.api.nvim_buf_set_text( buf, range.row_start, range.column_start, range.row_end, range.column_end, { current_date } ) end end end end, } module.load = function() -- combine user-defined template with defaults fill_template_defaults() modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ ["inject-metadata"] = { args = 0, name = "inject-metadata", condition = "norg", }, ["update-metadata"] = { args = 0, name = "update-metadata", condition = "norg", }, }) end) if module.config.public.type == "auto" then module.required["core.autocommands"].enable_autocommand("BufEnter") module.private.listen_event = "bufenter" elseif module.config.public.type == "empty" then module.required["core.autocommands"].enable_autocommand("BufNewFile") module.private.listen_event = "bufnewfile" end if module.config.public.update_date then vim.api.nvim_create_autocmd("BufWritePre", { pattern = "*.norg", callback = function() module.public.update_metadata(vim.api.nvim_get_current_buf()) end, desc = "Update updated date metadata field in norg documents", }) end end module.on_event = function(event) if event.type == ("core.autocommands.events." .. module.private.listen_event) and event.content.norg and vim.api.nvim_buf_is_loaded(event.buffer) and vim.api.nvim_get_option_value("modifiable", { buf = event.buffer }) and not module.private.buffers[event.buffer] and not vim.startswith(event.filehead, "neorg://") -- Do not inject metadata on displays created by neorg by default then module.public.inject_metadata(event.buffer) module.private.buffers[event.buffer] = true elseif event.type == "core.neorgcmd.events.inject-metadata" then module.public.inject_metadata(event.buffer, true) module.private.buffers[event.buffer] = true elseif event.type == "core.neorgcmd.events.update-metadata" then module.public.update_metadata(event.buffer) module.private.buffers[event.buffer] = true elseif event.type == "core.dirman.events.file_created" then if event.content.opts.metadata then module.public.inject_metadata(event.content.buffer, true, event.content.opts.metadata) end end end module.events.subscribed = { ["core.autocommands"] = { bufenter = true, bufnewfile = true, bufwritepre = true, }, ["core.neorgcmd"] = { ["inject-metadata"] = true, ["update-metadata"] = true, }, ["core.dirman"] = { ["file_created"] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/export/html/module.lua ================================================ --[[ file: HTML-Export title: Neorg's HTML Exporter summary: Interface for `core.export` to allow exporting to HTML. --- This module exists as an interface for `core.export` to export `.norg` files to HTML. As a user the only reason you would ever have to touch this module is to configure *how* you'd like your markdown to be exported (i.e. do you want to support certain extensions during the export). To learn more about configuration, consult the [relevant section](#configuration). --]] -- TODO: One day this module will need to be restructured or maybe even rewritten. -- It's not atrocious, but there are a lot of moving parts that make it difficult to understand -- from another person's perspective. Some cleanup and rethinking of certain implementation -- details will be necessary. local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.export.html") ---@type core.dirman, core.esupports.hop local dirman, hop module.setup = function() return { success = true, requires = { "core.esupports.hop", "core.dirman", }, } end --- Enumeration of different stackk types ---@enum StackKey local StackKey = { LIST = "list", BLOCK_QUOTE = "blockquote", SPAN = "span", } --- Enumeration of differnete link target types. --- @enum HeadingType local HeadingType = { HEADING1 = "h1", HEADING2 = "h2", HEADING3 = "h3", HEADING4 = "h4", HEADING5 = "h5", HEADING6 = "h6", } --- @class Location --- @field file string --- @field text string --- @field type HeadingType --- --- @class FragmentArgs --- @field type string --- @field text string --> Generic Utility Functions --- Escapes unsafe characters in the string ---@param text string string being escaped ---@return string local function html_escape(text) local escaped_text = text:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"):gsub('"', """):gsub("'", "'") return escaped_text end --- Applies HTML escaping to a word node ---@param word string ---@return table local function escape_word(word) return { output = html_escape(word), } end --- Adds opening tag and pushes closing tag onto stack to be popped in a recollector. ---@param tag string ---@param level number ---@param stack_key StackKey ---@return fun(_: any, _: any, state: table): table local function nest_tag(tag, level, stack_key) return function(text, _, state) if not state.nested_tag_stacks[stack_key] then state.nested_tag_stacks[stack_key] = {} end local attributes = "" if stack_key == StackKey.SPAN then attributes = ' id="generic-' .. text:lower():gsub("<", ""):gsub(">", ""):gsub(" ", "") .. '" ' end local output = "" local opening_tag = "\n<" .. tag .. attributes .. ">\n" local closing_tag = "\n\n" while level > #state.nested_tag_stacks[stack_key] do output = output .. opening_tag table.insert(state.nested_tag_stacks[stack_key], closing_tag) end while level < #state.nested_tag_stacks[stack_key] do output = output .. table.remove(state.nested_tag_stacks[stack_key]) end return { output = output, keep_descending = true, } end end --- Recollects tags by popping them off the stack and appending them to the --- output ---@param stack_key StackKey ---@return fun(output: table, state: table): table local function nested_tag_recollector(stack_key) return function(output, state) local suffix = "" local closing_tag = table.remove(state.nested_tag_stacks[stack_key]) while closing_tag do suffix = suffix .. closing_tag closing_tag = table.remove(state.nested_tag_stacks[stack_key]) end table.insert(output, suffix) return output end end --- Return true when a given stack key is empty ---@param state table ---@param stack_key StackKey ---@return boolean local function is_stack_empty(state, stack_key) return not state.nested_tag_stacks[stack_key] or #state.nested_tag_stacks[stack_key] == 0 end ---@param heading_type HeadingType ---@param target_type LinkType ---@return fun(): table local function heading(heading_type, target_type) return function() return { output = "
\n", keep_descending = true, state = { heading = heading_type, target_type = target_type, }, } end end ---Appends the closing p tag when required ---@param output table ---@param state table ---@return table local function add_closing_p_tag(output, state) if not state.link and is_stack_empty(state, StackKey.LIST) and is_stack_empty(state, StackKey.SPAN) then local new_output = { "\n

\n" } for _, value in ipairs(output) do table.insert(new_output, value) end table.insert(new_output, "\n

\n") output = new_output end return output end ---Appends a given tag to the output ---@param tag string ---@param cleanup? fun(state) ---@return fun(output: table, state: table): table local function add_closing_tag(tag, cleanup) return function(output, state) table.insert(output, tag) if cleanup then cleanup(state) end return output end end ---Builds a link and adds it to the output givne recollected data in the state table. ---@return fun(_: any, state: table): table local function wrap_anchor() return function(output, state) local link_builder = module.config.public.link_builders.link_builder local href if state.link then href = link_builder(state.link) else href = "" end local content if #output > 0 and output[1] ~= "" then content = output[1] else content = state.link.link_text end output = { '', content, "", } state.link = nil return output end end local function set_link(_, node) local link = hop.parse_link(node, 0) return { keep_descending = true, state = { link = link, }, } end ---Just keeps swimming ---@param state_or_fn? table|fun(): table ---@return fun(): table local function keep_descending(state_or_fn) return function() local state if type(state_or_fn) == "function" then state = state_or_fn() else state = state_or_fn end return { output = "", keep_descending = true, state = state, } end end ---@param output table ---@return table local function recollect_footnote(output, state) local title = table.remove(output, 1) .. table.remove(output, 1) local content = table.concat(output) local footnote_number = #state.footnotes + 1 table.insert(state.footnotes, { title = title, content = content, number = footnote_number }) return { '[' .. footnote_number .. "]", } end ---@return fun(text: string, node: TSNode): table local function ranged_verbatim_tag_content() return function(text, node) local _, start_column = node:range() local indent = "" local i = 0 while i < start_column do indent = indent .. " " i = i + 1 end return { output = "", state = { tag_content = indent .. text, tag_indent_level = start_column, }, } end end local function init_state() return { todo = nil, tag_params = {}, tag_close = nil, heading = nil, ranged_tag_indentation_level = 0, is_url = false, nested_tag_stacks = {}, anchors = {}, link = nil, footnotes = {}, } end ---@param text string ---@param state table ---@return table local function paragraph_segment(text, _, state) local output = "\n" local fragment_builder = module.config.public.link_builders.fragment_builder if state.heading then output = "<" .. state.heading .. ' id="' .. fragment_builder({ type = state.target_type, text = text }) .. '">' -- Add span to support generic link targets output = output .. '' end local todo = "" if state.todo then todo = '' state.todo = nil end return { output = output .. todo, keep_descending = true, } end ---@param node TSNode ---@return string local function get_opening_tag(_, node) local parent_type = node:parent():type() local tag = module.private.open_close_tags[parent_type] if type(tag) == "table" then return "<" .. tag.tag .. ' class="' .. tag.class .. '">' elseif tag then return "<" .. tag .. ">" else return "" end end ---@param node TSNode ---@return string local function get_closing_tag(_, node) local parent_type = node:parent():type() local tag = module.private.open_close_tags[parent_type] if type(tag) == "table" then return "" elseif tag then return "" else return "" end end ---@param text string ---@return table local function add_tag_name(text) return { output = "", state = { tag_name = text, }, } end ---@param text string ---@param state table ---@return table local function add_tag_param(text, _, state) local tag_params = table.insert(state.tag_params, text) return { output = "", state = { tag_params = tag_params, }, } end ---@param output table ---@param state table ---@return table local function add_closing_segement_tags(output, state) if state.heading then table.insert(output, "") state.heading = nil state.target_type = nil end return output end ---@param output table ---@param state table ---@return table local function apply_ranged_tag_handlers(output, state) local name = state.tag_name local content = state.tag_content local params = state.tag_params local ranged_tag_handler = module.config.public.ranged_tag_handler[name] or module.private.ranged_tag_handler[name] or module.private.ranged_tag_handler["comment"] table.insert(output, ranged_tag_handler(params, content, state.tag_indent_level)) state.tag_name = "" state.tag_params = {} state.tag_content = "" state.tag_indent_level = 0 return output end local function build_footnote(footnote) return table.concat({ '\n
', '\n
\n', footnote.number, "\n
", '\n
\n', footnote.title, "\n
", '\n
\n', footnote.content, "\n
", "\n
", "\n", }) end local function get_anchor(_, node, _) local target = hop.locate_anchor_declaration_target(node) local link = nil local link_desription = nil local anchor_definition = nil if target then link_desription = target.node:parent() end if link_desription then anchor_definition = link_desription:parent() end if anchor_definition then link = hop.parse_link(anchor_definition, 0) end return { keep_descending = true, state = { link = link, }, } end module.load = function() dirman = module.required["core.dirman"] hop = module.required["core.esupports.hop"] end module.config.public = { --- If you'd like to modify the way specific range tabs are handled. For --- example if you wanted to translate document.meta into use-case specific --- HTML, you could so here (see: module.private[ranged_tag_handler""] for --- examples). ranged_tag_handler = {}, -- Used by the exporter to know what extension to use -- when creating HTML files. -- The default is recommended, although you can change it. extension = "html", link_builders = { --- Function handler for building just the fragment. The fragment is the part --- of the URL that comes after the "#" and it's used for linking to specific --- IDs within a file. ---@param args FragmentArgs ---@return string fragment_builder = function(args) if args.type == "external_file" or args.type == "url" then -- External links and target URLs don't have target support by default. return "" end local text = args.text or "" return args.type .. "-" .. text:lower():gsub(" ", "") end, -- Function handler for building just the path URL path. ---@param link Link ---@return string path_builder = function(link) local file = link.link_file_text or "" if file:match("%$/") then local workspace_path = "/" local current_workspace = dirman.get_current_workspace() if current_workspace then workspace_path = "/" .. current_workspace[1] .. "/" end return (file:gsub("%$/", workspace_path):gsub(".norg", ".html")) elseif #file > 0 then return (file:gsub("%$", "/"):gsub(".norg", ".html")) else return "" end end, --- Function handler for building the entire link. If you change this handler --- you'll need to change ---@param link Link ---@return string link_builder = function(link) if link.link_type == "external_file" then local file = link.link_location_text or "" return "file://" .. file:gsub(" ", "") end if link.link_type == "url" then return link.link_location_text end local fragment_builder = module.config.public.link_builders.fragment_builder local path_builder = module.config.public.link_builders.path_builder return path_builder(link) .. "#" .. fragment_builder({ type = link.link_type, text = link.link_location_text }) end, }, } module.private = { ranged_tag_handler = { ["code"] = function(params, content, indent_level) local language = params[1] or "" local indent_regex = "^" .. string.rep("%s", indent_level) local lines_of_code = {} for line in string.gmatch(content, "[^\n]+") do local normalized_line = line:gsub(indent_regex, "") table.insert(lines_of_code, normalized_line) end local code_block = html_escape(table.concat(lines_of_code, "\n")) return '\n
\n\n' .. code_block .. "\n\n
\n" end, ["comment"] = function(_, content) return "\n\n" end, }, open_close_tags = { ["bold"] = "b", ["italic"] = "i", ["underline"] = "u", ["strikethrough"] = "s", ["spoiler"] = { tag = "span", class = "spoiler" }, ["verbatim"] = { tag = "code", class = "verbatim" }, ["superscript"] = "sup", ["subscript"] = "sub", ["inline_math"] = { tag = "code", class = "inline-math" }, }, } --- @class core.export.html module.public = { export = { init_state = init_state, functions = { ["_word"] = escape_word, ["_space"] = escape_word, ["_open"] = get_opening_tag, ["_close"] = get_closing_tag, ["_begin"] = "", ["_end"] = "", ["escape_sequence"] = keep_descending(), ["any_char"] = true, ["paragraph_segment"] = paragraph_segment, ["paragraph"] = keep_descending(), ["heading1"] = heading(HeadingType.HEADING1, "heading1"), ["heading2"] = heading(HeadingType.HEADING2, "heading2"), ["heading3"] = heading(HeadingType.HEADING3, "heading3"), ["heading4"] = heading(HeadingType.HEADING4, "heading4"), ["heading5"] = heading(HeadingType.HEADING5, "heading5"), ["heading6"] = heading(HeadingType.HEADING6, "heading6"), ["inline_link_target"] = nest_tag("span", 1, StackKey.SPAN), ["unordered_list1"] = nest_tag("ul", 1, StackKey.LIST), ["unordered_list2"] = nest_tag("ul", 2, StackKey.LIST), ["unordered_list3"] = nest_tag("ul", 3, StackKey.LIST), ["unordered_list4"] = nest_tag("ul", 4, StackKey.LIST), ["unordered_list5"] = nest_tag("ul", 5, StackKey.LIST), ["unordered_list6"] = nest_tag("ul", 6, StackKey.LIST), ["unordered_list1_prefix"] = "\n
  • \n", ["unordered_list2_prefix"] = "\n
  • \n", ["unordered_list3_prefix"] = "\n
  • \n", ["unordered_list4_prefix"] = "\n
  • \n", ["unordered_list5_prefix"] = "\n
  • \n", ["unordered_list6_prefix"] = "\n
  • \n", ["ordered_list1"] = nest_tag("ol", 1, StackKey.LIST), ["ordered_list2"] = nest_tag("ol", 2, StackKey.LIST), ["ordered_list3"] = nest_tag("ol", 3, StackKey.LIST), ["ordered_list4"] = nest_tag("ol", 4, StackKey.LIST), ["ordered_list5"] = nest_tag("ol", 5, StackKey.LIST), ["ordered_list6"] = nest_tag("ol", 6, StackKey.LIST), ["ordered_list1_prefix"] = "\n
  • \n", ["ordered_list2_prefix"] = "\n
  • \n", ["ordered_list3_prefix"] = "\n
  • \n", ["ordered_list4_prefix"] = "\n
  • \n", ["ordered_list5_prefix"] = "\n
  • \n", ["ordered_list6_prefix"] = "\n
  • \n", ["quote1"] = nest_tag("blockquote", 1, StackKey.BLOCK_QUOTE), ["quote2"] = nest_tag("blockquote", 2, StackKey.BLOCK_QUOTE), ["quote3"] = nest_tag("blockquote", 3, StackKey.BLOCK_QUOTE), ["quote4"] = nest_tag("blockquote", 4, StackKey.BLOCK_QUOTE), ["quote5"] = nest_tag("blockquote", 5, StackKey.BLOCK_QUOTE), ["quote6"] = nest_tag("blockquote", 6, StackKey.BLOCK_QUOTE), ["tag_parameters"] = keep_descending(function() return { tag_params = {} } end), ["tag_name"] = add_tag_name, ["tag_param"] = add_tag_param, ["ranged_verbatim_tag_content"] = ranged_verbatim_tag_content(), ["todo_item_done"] = keep_descending({ todo = "done" }), ["todo_item_undone"] = keep_descending({ todo = "undone" }), ["todo_item_pending"] = keep_descending({ todo = "pending" }), ["todo_item_urgent"] = keep_descending({ todo = "urgent" }), ["todo_item_cancelled"] = keep_descending({ todo = "cancelled" }), ["todo_item_recurring"] = keep_descending({ todo = "recurring" }), ["todo_item_on_hold"] = keep_descending({ todo = "on_hold" }), ["todo_item_uncertain"] = keep_descending({ todo = "uncertain" }), ["single_footnote"] = keep_descending(), ["multi_footnote"] = keep_descending(), ["link"] = set_link, ["anchor_definition"] = set_link, ["anchor_declaration"] = get_anchor, ["strong_carryover"] = "", ["weak_carryover"] = "", -- [UNSUPPORTED] Infirm Tags are not currently supported, TS parsing -- is returning unexpected ranges for .image tag, specically "http:" -- gets included as a param and then the rest of hte URL is moved to -- the following paragraph as content. ["infirm_tag"] = "", }, recollectors = { ["paragraph"] = add_closing_p_tag, ["paragraph_segment"] = add_closing_segement_tags, ["link"] = wrap_anchor(), ["anchor_definition"] = wrap_anchor(), ["anchor_declaration"] = wrap_anchor(), ["generic_list"] = nested_tag_recollector(StackKey.LIST), ["quote"] = nested_tag_recollector(StackKey.BLOCK_QUOTE), ["inline_link_target"] = nested_tag_recollector(StackKey.SPAN), ["heading1"] = add_closing_tag("\n
  • \n"), ["heading2"] = add_closing_tag("\n\n"), ["heading3"] = add_closing_tag("\n\n"), ["heading4"] = add_closing_tag("\n\n"), ["heading5"] = add_closing_tag("\n\n"), ["heading6"] = add_closing_tag("\n\n"), ["unordered_list1"] = add_closing_tag("\n\n"), ["unordered_list2"] = add_closing_tag("\n\n"), ["unordered_list3"] = add_closing_tag("\n\n"), ["unordered_list4"] = add_closing_tag("\n\n"), ["unordered_list5"] = add_closing_tag("\n\n"), ["unordered_list6"] = add_closing_tag("\n\n"), ["ordered_list1"] = add_closing_tag("\n\n"), ["ordered_list2"] = add_closing_tag("\n\n"), ["ordered_list3"] = add_closing_tag("\n\n"), ["ordered_list4"] = add_closing_tag("\n\n"), ["ordered_list5"] = add_closing_tag("\n\n"), ["ordered_list6"] = add_closing_tag("\n\n"), ["ranged_verbatim_tag_end"] = apply_ranged_tag_handlers, ["single_footnote"] = recollect_footnote, ["multi_footnote"] = recollect_footnote, }, cleanup = function(output, state) if #state.footnotes > 0 then output = output .. "\n
    \n" end for _, footnote in ipairs(state.footnotes) do output = output .. "\n" .. build_footnote(footnote) end return output end, }, } return module ================================================ FILE: lua/neorg/modules/core/export/markdown/module.lua ================================================ --[[ file: Markdown-Export title: Neorg's Markdown Exporter summary: Interface for `core.export` to allow exporting to markdown. --- This module exists as an interface for `core.export` to export `.norg` files to Markdown. As a user the only reason you would ever have to touch this module is to configure *how* you'd like your markdown to be exported (i.e. do you want to support certain extensions during the export). To learn more about configuration, consult the [relevant section](#configuration). --]] -- TODO: One day this module will need to be restructured or maybe even rewritten. -- It's not atrocious, but there are a lot of moving parts that make it difficult to understand -- from another person's perspective. Some cleanup and rethinking of certain implementation -- details will be necessary. local neorg = require("neorg.core") local lib, modules = neorg.lib, neorg.modules local module = modules.create("core.export.markdown") module.setup = function() return { success = true, requires = { "core.integrations.treesitter", }, } end local last_parsed_link_location = "" --> Generic Utility Functions local function unordered_list_prefix(level) return function() return { output = string.rep(" ", (level - 1) * 4) .. "- ", keep_descending = true, state = { weak_indent = ((level - 1) * 4) + 2, }, } end end local function ordered_list_prefix(level) return function(_, node, state) state.ordered_list_level[level] = state.ordered_list_level[level] + 1 state.weak_indent = ((level - 1) * 4) + 3 + (tostring(state.ordered_list_level[level]):len() - 1) local parent = node:parent() local prev_node = parent:prev_named_sibling() -- If the previous node from the current parent (`ordered_list`) isn't another ordered -- list node, the list was split and the current count should be restarted if prev_node == nil or prev_node:type() ~= parent:type() then state.ordered_list_level[level] = 1 end return { output = string.rep(" ", (level - 1) * 4) .. tostring(state.ordered_list_level[level]) .. ". ", keep_descending = true, state = state, } end end local function todo_item_extended(replace_text) return function(_, node, state) if not node:parent():parent():type():match("_list%d$") then return end return { output = module.config.public.extensions["todo-items-extended"] and replace_text or nil, state = { weak_indent = state.weak_indent + replace_text:len(), }, } end end local function get_metadata_array_prefix(node, state) return node:parent():type() == "array" and string.rep(" ", state.indent) .. "- " or "" end local function handle_metadata_literal(text, node, state) -- If the parent is an array, we need to indent it and add the `- ` prefix. Otherwise, there will be a key right before which will take care of indentation return get_metadata_array_prefix(node, state) .. text .. "\n" end local function update_indent(value) return function(_, _, state) return { state = { indent = state.indent + value, }, } end end --> Recollector Utility Functions local function todo_item_recollector() return function(output) return output[2] ~= "(_) " and output end end local function handle_heading_newlines() return function(output, _, node, ts) local prev = ts.get_previous_node(node, true, true) if prev and not vim.tbl_contains({ "_line_break", "_paragraph_break" }, prev:type()) and ((prev:end_()) + 1) ~= (node:start()) then output[1] = "\n" .. output[1] end if output[3] then output[3] = output[3] .. "\n" end return output end end local function handle_metadata_composite_element(empty_element) return function(output, state, node) if vim.tbl_isempty(output) then return { get_metadata_array_prefix(node, state), empty_element, "\n" } end local parent = node:parent():type() if parent == "array" then -- If the parent is an array, we need to splice an extra `-` prefix to the first element output[1] = output[1]:sub(1, state.indent) .. "-" .. output[1]:sub(state.indent + 2) elseif parent == "pair" then -- If the parent is a pair, the first element should be on the next line output[1] = "\n" .. output[1] end return output end end --- module.load = function() if module.config.public.extensions == "all" then module.config.public.extensions = { "todo-items-basic", "todo-items-pending", "todo-items-extended", "definition-lists", "mathematics", "metadata", "latex", } end module.config.public.extensions = lib.to_keys(module.config.public.extensions, {}) end module.config.public = { -- Any extensions you may want to use when exporting to markdown. By -- default no extensions are loaded (the exporter is commonmark compliant). -- You can also set this value to `"all"` to enable all extensions. -- The full extension list is: `todo-items-basic`, `todo-items-pending`, `todo-items-extended`, -- `definition-lists`, `mathematics`, `metadata` and `latex`. extensions = {}, -- Data about how to render mathematics. -- The default is recommended as it is the most common, although certain flavours -- of markdown use different syntax. mathematics = { -- Inline mathematics are represented `$like this$`. inline = { start = "$", ["end"] = "$", }, -- Block-level mathematics are represented as such: -- -- ```md -- $$ -- \frac{3, 2} -- $$ -- ``` block = { start = "$$", ["end"] = "$$", }, }, -- Data about how to render metadata -- There are a few ways to render metadata blocks, but this is the most -- common. metadata = { start = "---", ["end"] = "---", -- Is usually also "..." }, -- Used by the exporter to know what extension to use -- when creating markdown files. -- The default is recommended, although you can change it. extension = "md", } --- @class core.export.markdown module.public = { export = { init_state = function() return { weak_indent = 0, indent = 0, ordered_list_level = { 0, 0, 0, 0, 0, 0, }, tag_indent = 0, tag_close = nil, ranged_tag_indentation_level = 0, is_url = false, footnote_count = 0, } end, functions = { ["single_footnote"] = function(_, node, state) state["footnote_count"] = state["footnote_count"] + 1 for nd in node:iter_children() do if nd:type() == "paragraph" then local n = state["footnote_count"] return "[^" .. n .. "]\n\n\n[^" .. n .. "]: " .. module.required["core.integrations.treesitter"].get_node_text(nd) end end return "" end, ["_word"] = true, ["_space"] = true, ["_line_break"] = function(_, node, state) local next_sibling = node:next_sibling() return "\n" .. ((next_sibling and next_sibling:type() == "paragraph_segment" and string.rep( " ", state.weak_indent )) or "") .. string.rep(" ", state.indent) end, ["_paragraph_break"] = function(newlines, _, state) return { output = string.rep("\n\n", newlines:len()) .. string.rep(" ", state.indent), state = { weak_indent = 0, ordered_list_level = { 0, 0, 0, 0, 0, 0 }, }, } end, ["_segment"] = function(text, node, state) return string.rep(" ", state.indent + (({ node:range() })[2] - state.ranged_tag_indentation_level)) .. text end, ["heading1_prefix"] = "# ", ["heading2_prefix"] = "## ", ["heading3_prefix"] = "### ", ["heading4_prefix"] = "#### ", ["heading5_prefix"] = "##### ", ["heading6_prefix"] = "###### ", ["_open"] = function(_, node) local type = node:parent():type() if type == "bold" then return "**" elseif type == "italic" then return "_" elseif type == "underline" then return "" elseif type == "strikethrough" then return "~~" elseif type == "spoiler" then return "|" elseif type == "verbatim" then return "`" elseif type == "superscript" then return "" elseif type == "subscript" then return "" elseif type == "inline_comment" then return "" elseif type == "inline_math" and module.config.public.extensions["mathematics"] then return module.config.public.mathematics.inline["end"] end end, ["_begin"] = function(text, node) local type = node:parent():type() if type == "link_location" then return text == "{" and "(" elseif type == "link_description" then return "[" end end, ["_end"] = function(text, node) local type = node:parent():type() if type == "link_location" then return text == "}" and ")" elseif type == "link_description" then return "]" end end, ["link_file_text"] = function(text) return vim.uri_from_fname(text .. ".md"):sub(string.len("file://") + 1) end, ["link_target_url"] = function() return { state = { is_url = true, }, } end, ["escape_sequence"] = function(text) local escaped_char = text:sub(-1) return escaped_char:match("%p") and text or escaped_char end, ["unordered_list1_prefix"] = unordered_list_prefix(1), ["unordered_list2_prefix"] = unordered_list_prefix(2), ["unordered_list3_prefix"] = unordered_list_prefix(3), ["unordered_list4_prefix"] = unordered_list_prefix(4), ["unordered_list5_prefix"] = unordered_list_prefix(5), ["unordered_list6_prefix"] = unordered_list_prefix(6), ["ordered_list1_prefix"] = ordered_list_prefix(1), ["ordered_list2_prefix"] = ordered_list_prefix(2), ["ordered_list3_prefix"] = ordered_list_prefix(3), ["ordered_list4_prefix"] = ordered_list_prefix(4), ["ordered_list5_prefix"] = ordered_list_prefix(5), ["ordered_list6_prefix"] = ordered_list_prefix(6), ["tag_parameters"] = function(text, _, state) if state.ignore_tag_parameters then state.ignore_tag_parameters = nil return { output = "", state = state, } end return text end, ["tag_name"] = function(text, node, _, _) local _, tag_start_column = node:range() if text == "code" then return { output = "```", state = { -- Minus one to account for the `@` tag_indent = tag_start_column - 1, tag_close = "```", }, } elseif text == "comment" then return { output = "", }, } elseif text == "table" then return { output = "", state = { tag_indent = tag_start_column - 1, tag_close = "", }, } elseif text == "math" and module.config.public.extensions["mathematics"] then return { output = module.config.public.mathematics.block["start"], state = { tag_indent = tag_start_column - 1, tag_close = module.config.public.mathematics.block["end"], }, } elseif text == "document.meta" then local allows_metadata = module.config.public.extensions["metadata"] return { output = allows_metadata and module.config.public.metadata["start"] or nil, state = { tag_indent = tag_start_column - 1, tag_close = allows_metadata and module.config.public.metadata["end"] or nil, is_meta = true, }, } elseif text == "embed" and node:next_named_sibling() and vim.tbl_contains( { "markdown", "html", module.config.public.extensions["latex"] and "latex" or nil }, module.required["core.integrations.treesitter"].get_node_text(node:next_named_sibling()) ) then return { state = { tag_indent = tag_start_column - 1, tag_close = "", ignore_tag_parameters = true, }, } end return { state = { ignore_tag_parameters = true, tag_close = nil, }, } end, ["ranged_verbatim_tag_content"] = function(text, node, state) if state.is_meta then state.is_meta = false if module.config.public.extensions["metadata"] then return { keep_descending = true, state = { parse_as = "norg_meta", }, } else return end end local _, ranged_tag_content_column_start = node:range() local split_text = vim.split(text, "\n") split_text[1] = string.rep(" ", ranged_tag_content_column_start - state.tag_indent) .. split_text[1] for i = 2, #split_text do split_text[i] = split_text[i]:sub(state.tag_indent + 1) end return state.tag_close and (table.concat(split_text, "\n") .. "\n") end, ["ranged_verbatim_tag_end"] = function(_, _, state) local tag_close = state.tag_close state.tag_close = nil return tag_close end, ["quote1_prefix"] = true, ["quote2_prefix"] = true, ["quote3_prefix"] = true, ["quote4_prefix"] = true, ["quote5_prefix"] = true, ["quote6_prefix"] = true, ["todo_item_done"] = function(_, node, state) if not node:parent():parent():type():match("_list%d$") then return end return { output = module.config.public.extensions["todo-items-basic"] and "[x]", state = { weak_indent = state.weak_indent + 4, }, } end, ["todo_item_undone"] = function(_, node, state) if not node:parent():parent():type():match("_list%d$") then return end return { output = module.config.public.extensions["todo-items-basic"] and "[ ]", state = { weak_indent = state.weak_indent + 4, }, } end, ["todo_item_pending"] = function(_, node, state) if not node:parent():parent():type():match("_list%d$") then return end return { output = module.config.public.extensions["todo-items-pending"] and "[*]", state = { weak_indent = state.weak_indent + 4, }, } end, ["todo_item_urgent"] = todo_item_extended("[ ]"), ["todo_item_cancelled"] = todo_item_extended("[_]"), ["todo_item_recurring"] = todo_item_extended("[ ]"), ["todo_item_on_hold"] = todo_item_extended("[ ]"), ["todo_item_uncertain"] = todo_item_extended("[ ]"), ["single_definition_prefix"] = function() return module.config.public.extensions["definition-lists"] and ": " end, ["multi_definition_prefix"] = function(_, _, state) if not module.config.public.extensions["definition-lists"] then return end return { output = ": ", state = { indent = state.indent + 2, }, } end, ["multi_definition_suffix"] = function(_, _, state) if not module.config.public.extensions["definition-lists"] then return end return { state = { indent = state.indent - 2, }, } end, ["_prefix"] = function(_, node) return { state = { ranged_tag_indentation_level = ({ node:range() })[2], }, } end, ["capitalized_word"] = function(text, node) if node:parent():type() == "insertion" then if text == "Image" then return "![" end end end, ["strong_carryover"] = "", ["weak_carryover"] = "", ["key"] = function(text, _, state) return string.rep(" ", state.indent) .. (text == "authors" and "author" or text) end, [":"] = ": ", ["["] = update_indent(2), ["]"] = update_indent(-2), ["{"] = update_indent(2), ["}"] = update_indent(-2), ["string"] = handle_metadata_literal, ["number"] = handle_metadata_literal, ["horizontal_line"] = "___", }, recollectors = { ["link_location"] = function(output, state) last_parsed_link_location = output[#output - 1] if state.is_url then state.is_url = false return output end table.insert(output, #output - 1, "#") output[#output - 1] = output[#output - 1]:lower():gsub("-", " "):gsub("%p+", ""):gsub("%s+", "-") return output end, ["link"] = function(output) return { output[2] or ("[" .. last_parsed_link_location .. "]"), output[1], } end, ["ranged_verbatim_tag"] = function(output) if output[2] and output[2]:match("^[ \t]+$") then table.remove(output, 2) end return output end, ["unordered_list1"] = todo_item_recollector(), ["unordered_list2"] = todo_item_recollector(), ["unordered_list3"] = todo_item_recollector(), ["unordered_list4"] = todo_item_recollector(), ["unordered_list5"] = todo_item_recollector(), ["unordered_list6"] = todo_item_recollector(), ["single_definition"] = function(output) return { output[2], output[3], output[1], output[4], } end, ["multi_definition"] = function(output) output[3] = output[3]:gsub("^\n+ ", "\n") .. output[1] table.remove(output, 1) return output end, -- TODO ["insertion"] = function(output) if output[1] == "![" then table.insert(output, 1, "\n") local split = vim.split(output[3], "/", { plain = true }) table.insert(output, 3, (split[#split]:match("^(.+)%..+$") or split[#split]) .. "](") table.insert(output, ")\n") end return output end, ["heading1"] = handle_heading_newlines(), ["heading2"] = handle_heading_newlines(), ["heading3"] = handle_heading_newlines(), ["heading4"] = handle_heading_newlines(), ["heading5"] = handle_heading_newlines(), ["heading6"] = handle_heading_newlines(), ["object"] = handle_metadata_composite_element("{}"), ["array"] = handle_metadata_composite_element("[]"), }, cleanup = function() last_parsed_link_location = "" end, }, } return module ================================================ FILE: lua/neorg/modules/core/export/module.lua ================================================ --[[ file: Exporting-Files title: Convert Neorg Files to other Filetypes with `core.export` summary: Exports Neorg documents into any other supported filetype. --- When sending files to people not invested in Neorg it's nice to be able to use a format that *they* use. The `core.export` module is a backend module providing functionality to write your own exporter for any given filetype. All export functionality is provided by the `:Neorg export` command. To export the currently opened buffer to another file format, you should use the `:Neorg export to-file` command. The command takes the following arguments: - `path` - the path to export to. Examples are: `my-file.md`, `~/output.md`. If the second argument is not provided Neorg will try to infer the filetype to convert to through the file extension. - `filetype` (optional) - the filetype to export to. Useful if you want to use a non-standard extension, or if the filetype you're using cannot be inferred automatically. Note that this filetype *must* be a filetype that Neovim itself provides and/or understands, i.e. `md` or `markd` is not a valid filetype, however `markdown` is. Neorg also supports exporting a directory of files: this is where the `:Neorg export directory` command comes into play. It takes 3 arguments: - `directory` - the directory to export - `filetype` - the filetype to export to - `output-dir` (optional) - a custom output directory to use. If not provided will fall back to `config.public.export_dir` (see [configuration](#configuration)). And if you just want to export a snippet from a single document, you can use `:Neorg export to-clipboard `. Filetype is required, at the time of writing the only option is "markdown". This copies the range given to the command to the `+` register. --]] local neorg = require("neorg.core") local lib, log, modules, utils = neorg.lib, neorg.log, neorg.modules, neorg.utils local module = modules.create("core.export") module.setup = function() return { success = true, requires = { "core.integrations.treesitter", }, } end ---@type core.integrations.treesitter local ts module.load = function() ts = module.required["core.integrations.treesitter"] modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ export = { args = 1, condition = "norg", subcommands = { ["to-file"] = { min_args = 1, max_args = 2, name = "export.to-file", }, ["directory"] = { min_args = 2, max_args = 3, name = "export.directory", }, ["to-clipboard"] = { args = 1, name = "export.to-clipboard", }, }, }, }) end) end module.config.public = { -- The directory to export to when running `:Neorg export directory`. -- The string can be formatted with the special keys: `` and ``. export_dir = "/-export", } ---@class core.export module.public = { --- Returns a module that can handle conversion from `.norg` to the target filetype ---@param ftype string #The filetype to export to ---@return table?,table? #The export module and its configuration, else nil get_converter = function(ftype) if not modules.is_module_loaded("core.export." .. ftype) then if not modules.load_module("core.export." .. ftype) then return end end return modules.get_module("core.export." .. ftype), modules.get_module_config("core.export." .. ftype) end, ---export part of a buffer ---@param buffer number ---@param start_row number 1 indexed ---@param end_row number 1 indexed, inclusive ---@param filetype string ---@return string? content, string? extension exported content as a string, and the extension ---used for the export export_range = function(buffer, start_row, end_row, filetype) local converter = module.private.get_converter_checked(filetype) if not converter then return end local content = vim.iter(vim.api.nvim_buf_get_lines(buffer, start_row - 1, end_row, false)):join("\n") local root = ts.get_document_root(content) if not root then return end return module.public.export_from_root(root, converter, content) end, --- Takes a buffer and exports it to a specific file ---@param buffer number #The buffer ID to read the contents from ---@param filetype string #A Neovim filetype to specify which language to export to ---@return string?, string? #The entire buffer parsed, converted and returned as a string, as well as the extension used for the export. export = function(buffer, filetype) local converter, converter_config = module.private.get_converter_checked(filetype) if not converter or not converter_config then return end local document_root = ts.get_document_root(buffer) if not document_root then return end local content = module.public.export_from_root(document_root, converter, buffer) return content, converter_config.extension end, ---Do the work of exporting the given TS node via the given converter ---@param root TSNode ---@param converter table ---@param source number | string ---@return string export_from_root = function(root, converter, source) -- Initialize the state. The state is a table that exists throughout the entire duration -- of the export, and can be used to e.g. retain indent levels and/or keep references. local state = converter.export.init_state and converter.export.init_state() or {} --- Descends down a node and its children ---@param start table #The TS node to begin at ---@return string #The exported/converted node as a string local function descend(start) -- We do not want to parse erroneous nodes, so we skip them instead if start:type() == "ERROR" then return "" end local output = {} for node in start:iter_children() do -- See if there is a conversion function for the specific node type we're dealing with local exporter = converter.export.functions[node:type()] if exporter then -- The value of `exporter` can be of 3 different types: -- a function, in which case it should be executed -- a boolean (true), which signifies to use the content of the node as-is without changing anything -- a string, in which case every time the node is encountered it will always be converted to a static value if type(exporter) == "function" then -- An exporter function can return output string or table with 3 values: -- `output` - the converted text -- `keep_descending` - if true will continue to recurse down the current node's children despite the current -- node already being parsed -- `state` - a modified version of the state that then gets merged into the main state table local result = exporter(vim.treesitter.get_node_text(node, source), node, state, ts) if type(result) == "table" then state = result.state and vim.tbl_extend("force", state, result.state) or state if result.output then table.insert(output, result.output) end if result.keep_descending then if state.parse_as then node = ts.get_document_root( "\n" .. vim.treesitter.get_node_text(node, source), state.parse_as ) if not node then goto continue end state.parse_as = nil end local ret = descend(node) if ret then table.insert(output, ret) end end elseif type(result) == "string" then table.insert(output, result) end elseif exporter == true then table.insert(output, ts.get_node_text(node, source)) else table.insert(output, exporter) end else -- If no exporter exists for the current node then keep descending local ret = descend(node) if ret then table.insert(output, ret) end end ::continue:: end -- Recollectors exist to collect all the converted children nodes of a parent node -- and to optionally rearrange them into a new layout. Consider the following Neorg markup: -- $ Term -- Definition -- The markdown version looks like this: -- Term -- : Definition -- Without a recollector such a conversion wouldn't be possible, as by simply converting each -- node individually you'd end up with: -- : Term -- Definition -- -- The recollector can encounter a `definition` node, see the nodes it is made up of ({ ": ", "Term", "Definition" }) -- and rearrange its components to { "Term", ": ", "Definition" } to then achieve the desired result. local recollector = converter.export.recollectors[start:type()] return recollector and table.concat(recollector(output, state, start, ts) or {}) or (not vim.tbl_isempty(output) and table.concat(output)) end local output = descend(root) -- Every converter can also come with a `cleanup` function that performs some final tweaks to the output string return converter.export.cleanup and converter.export.cleanup(output, state) or output end, } module.private = { ---get the converter for the given filetype ---@param filetype string ---@return table?, table? get_converter_checked = function(filetype) local converter, converter_config = module.public.get_converter(filetype) if not converter or not converter_config then log.error("Unable to export file - did not find exporter for filetype '" .. filetype .. "'.") return end -- Each converter must have a `extension` field in its public config -- This is done to do a backwards lookup, e.g. `markdown` uses the `.md` file extension. if not converter_config.extension then log.error( "Unable to export file - exporter for filetype '" .. filetype .. "' did not return a preferred extension. The exporter is unable to infer extensions." ) return end return converter, converter_config end, } ---@param event neorg.event module.on_event = function(event) if event.type == "core.neorgcmd.events.export.to-file" then -- Syntax: Neorg export to-file file.extension forced-filetype? -- Example: Neorg export to-file my-custom-file markdown local filepath = vim.fn.expand(event.content[1]) local filetype = event.content[2] or vim.filetype.match({ filename = filepath }) local exported = module.public.export(event.buffer, filetype) vim.loop.fs_open(filepath, "w", 438, function(err, fd) assert(not err, lib.lazy_string_concat("Failed to open file '", filepath, "' for export: ", err)) assert(fd) vim.loop.fs_write(fd, exported, 0, function(werr) assert(not werr, lib.lazy_string_concat("Failed to write to file '", filepath, "' for export: ", werr)) end) vim.schedule(lib.wrap(utils.notify, "Successfully exported 1 file!")) end) elseif event.type == "core.neorgcmd.events.export.to-clipboard" then -- Syntax: Neorg export to-clipboard filetype -- Example: Neorg export to-clipboard markdown local filetype = event.content[1] local data = event.content.data local exported = module.public.export_range(event.buffer, data.line1, data.line2, filetype) vim.fn.setreg("+", exported, "l") elseif event.type == "core.neorgcmd.events.export.directory" then local path = event.content[3] and vim.fn.expand(event.content[3]) or module.config.public.export_dir :gsub("", event.content[2]) :gsub("", event.content[1]) vim.fn.mkdir(path, "p") -- The old value of `eventignore` is stored here. This is done because the eventignore -- value is set to ignore BufEnter events before loading all the Neorg buffers, as they can mistakenly -- activate the concealer, which not only slows down performance notably but also causes errors. ---@diagnostic disable-next-line: undefined-field local old_event_ignore = table.concat(vim.opt.eventignore:get(), ",") vim.loop.fs_scandir(event.content[1], function(err, handle) assert(not err, lib.lazy_string_concat("Failed to scan directory '", event.content[1], "': ", err)) assert(handle) local file_counter, parsed_counter = 0, 0 while true do local name, type = vim.loop.fs_scandir_next(handle) if not name then break end if type == "file" and vim.endswith(name, ".norg") then file_counter = file_counter + 1 local function check_counters() parsed_counter = parsed_counter + 1 if parsed_counter >= file_counter then vim.schedule( lib.wrap(utils.notify, string.format("Successfully exported %d files!", file_counter)) ) end end vim.schedule(function() local filepath = vim.fn.expand(event.content[1]) .. "/" .. name vim.opt.eventignore = "BufEnter" local buffer = assert(vim.fn.bufadd(filepath)) vim.fn.bufload(buffer) vim.opt.eventignore = old_event_ignore local exported, extension = module.public.export(buffer, event.content[2]) vim.api.nvim_buf_delete(buffer, { force = true }) if not exported then check_counters() return end local write_path = path .. "/" .. name:gsub("%.%a+$", "." .. extension) vim.loop.fs_open(write_path, "w+", 438, function(fs_err, fd) assert( not fs_err, lib.lazy_string_concat("Failed to open file '", write_path, "' for export: ", fs_err) ) assert(fd) vim.loop.fs_write(fd, exported, 0, function(werr) assert( not werr, lib.lazy_string_concat( "Failed to write to file '", write_path, "' for export: ", werr ) ) check_counters() end) end) end) end end end) end vim.api.nvim_exec_autocmds("User", { pattern = "NeorgExportComplete", }) end module.events.subscribed = { ["core.neorgcmd"] = { ["export.to-file"] = true, ["export.to-clipboard"] = true, ["export.directory"] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/fs/module.lua ================================================ --[[ file: Filesystem title: Module for Filesystem Operations summary: A cross-platform set of utilities to traverse filesystems. internal: true --- `core.fs` is a small module providing functionality to perform common operations safely on arbitrary filesystems. --]] local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.fs") ---@class core.fs module.public = { directory_map = function(path, callback) for name, type in vim.fs.dir(path) do if type == "directory" then module.public.directory_map(table.concat({ path, "/", name }), callback) else callback(name, type, path) end end end, --- Recursively copies a directory from one path to another ---@param old_path string #The path to copy ---@param new_path string #The new location. This function will not --- succeed if the directory already exists. ---@return boolean #If true, the directory copying succeeded copy_directory = function(old_path, new_path) local file_permissions = tonumber("744", 8) local ok, err = vim.loop.fs_mkdir(new_path, file_permissions) if not ok then return ok, err ---@diagnostic disable-line -- TODO: type error workaround end for name, type in vim.fs.dir(old_path) do if type == "file" then ok, err = vim.loop.fs_copyfile(table.concat({ old_path, "/", name }), table.concat({ new_path, "/", name })) if not ok then return ok, err ---@diagnostic disable-line -- TODO: type error workaround end elseif type == "directory" and not vim.endswith(new_path, name) then ok, err = module.public.copy_directory( table.concat({ old_path, "/", name }), table.concat({ new_path, "/", name }) ) if not ok then return ok, err ---@diagnostic disable-line -- TODO: type error workaround end end end return true, nil ---@diagnostic disable-line -- TODO: type error workaround end, } return module ================================================ FILE: lua/neorg/modules/core/highlights/module.lua ================================================ --[[ file: Core-Highlights title: No Colour Means no Productivity summary: Manages your highlight groups with this module. internal: true --- `core.highlights` maps all possible highlight groups available throughout Neorg under a single tree of highlights: `@neorg.*`. --]] local neorg = require("neorg.core") local lib, log, modules = neorg.lib, neorg.log, neorg.modules local module = modules.create("core.highlights") --[[ --]] module.config.public = { -- The TS highlights for each Neorg type. -- -- The `highlights` table is a large collection of nested trees. At the leaves of each of these -- trees is the final highlight to apply to that tree. For example: `"+@comment"` tells Neorg to -- link to an existing highlight group `@comment` (denoted by the `+` prefix). When no prefix is -- found, the string is treated as arguments passed to `:highlight`, for example: `gui=bold -- fg=#000000`. -- -- Nested trees concatenate, thus: -- ```lua -- tags = { -- ranged_verbatim = { -- begin = "+@comment", -- }, -- } -- ``` -- matches the highlight group: -- ```lua -- @neorg.tags.ranged_verbatim.begin -- ``` -- and converts into the following command: -- ```vim -- highlight! link @neorg.tags.ranged_verbatim.begin @comment -- ``` highlights = { -- Highlights displayed in Neorg selection window popups. selection_window = { heading = "+@annotation", arrow = "+@none", key = "+@module", keyname = "+@constant", nestedkeyname = "+@string", }, -- Highlights displayed in all sorts of tag types. -- -- These include: `@`, `.`, `|`, `#`, `+` and `=`. tags = { -- Highlights for the `@` verbatim tags. ranged_verbatim = { begin = "+@keyword", ["end"] = "+@keyword", name = { [""] = "+@none", delimiter = "+@none", word = "+@keyword", }, parameters = "+@type", document_meta = { key = "+@variable.member", value = "+@string", number = "+@number", trailing = "+@keyword.repeat", title = "+@markup.heading", description = "+@label", authors = "+@annotation", categories = "+@keyword", created = "+@number.float", updated = "+@number.float", version = "+@number.float", object = { bracket = "+@punctuation.bracket", }, array = { bracket = "+@punctuation.bracket", value = "+@none", }, }, }, -- Highlights for the carryover (`#`, `+`) tags. carryover = { begin = "+@label", name = { [""] = "+@none", word = "+@label", delimiter = "+@none", }, parameters = "+@string", }, -- Highlights for the content of any tag named `comment`. -- -- Most prominent use case is for the `#comment` carryover tag. comment = { content = "+@comment", }, }, -- Highlights for each individual heading level. headings = { ["1"] = { title = "+@attribute", prefix = "+@attribute", }, ["2"] = { title = "+@label", prefix = "+@label", }, ["3"] = { title = "+@constant", prefix = "+@constant", }, ["4"] = { title = "+@string", prefix = "+@string", }, ["5"] = { title = "+@label", prefix = "+@label", }, ["6"] = { title = "+@constructor", prefix = "+@constructor", }, }, -- In case of errors in the syntax tree, use the following highlight. error = "+Error", -- Highlights for definitions (`$ Definition`). definitions = { prefix = "+@punctuation.delimiter", suffix = "+@punctuation.delimiter", title = "+@markup.strong", content = "+@markup.italic", }, -- Highlights for footnotes (`^ My Footnote`). footnotes = { prefix = "+@punctuation.delimiter", suffix = "+@punctuation.delimiter", title = "+@markup.strong", content = "+@markup.italic", }, -- Highlights for TODO items. -- -- This strictly covers the `( )` component of any detached modifier. In other words, these -- highlights only bother with highlighting the brackets and the content within, but not the -- object containing the TODO item itself. todo_items = { undone = "+@punctuation.delimiter", pending = "+@module", done = "+@string", on_hold = "+@comment.note", cancelled = "+NonText", urgent = "+@comment.error", uncertain = "+@boolean", recurring = "+@keyword.repeat", }, -- Highlights for all the possible levels of ordered and unordered lists. lists = { unordered = { prefix = "+@markup.list" }, ordered = { prefix = "+@keyword.repeat" }, }, -- Highlights for all the possible levels of quotes. quotes = { ["1"] = { prefix = "+@punctuation.delimiter", content = "+@punctuation.delimiter", }, ["2"] = { prefix = "+Blue", content = "+Blue", }, ["3"] = { prefix = "+Yellow", content = "+Yellow", }, ["4"] = { prefix = "+Red", content = "+Red", }, ["5"] = { prefix = "+Green", content = "+Green", }, ["6"] = { prefix = "+Brown", content = "+Brown", }, }, -- Highlights for the anchor syntax: `[name]{location}`. anchors = { declaration = { [""] = "+@markup.link.label", delimiter = "+NonText", }, definition = { delimiter = "+NonText", }, }, links = { description = { [""] = "+@markup.link.url", delimiter = "+NonText", }, file = { [""] = "+@comment", delimiter = "+NonText", }, location = { delimiter = "+NonText", url = "+@markup.link.url", generic = { [""] = "+@type", prefix = "+@type", }, external_file = { [""] = "+@label", prefix = "+@label", }, marker = { [""] = "+@neorg.markers.title", prefix = "+@neorg.markers.prefix", }, definition = { [""] = "+@neorg.definitions.title", prefix = "+@neorg.definitions.prefix", }, footnote = { [""] = "+@neorg.footnotes.title", prefix = "+@neorg.footnotes.prefix", }, heading = { ["1"] = { [""] = "+@neorg.headings.1.title", prefix = "+@neorg.headings.1.prefix", }, ["2"] = { [""] = "+@neorg.headings.2.title", prefix = "+@neorg.headings.2.prefix", }, ["3"] = { [""] = "+@neorg.headings.3.title", prefix = "+@neorg.headings.3.prefix", }, ["4"] = { [""] = "+@neorg.headings.4.title", prefix = "+@neorg.headings.4.prefix", }, ["5"] = { [""] = "+@neorg.headings.5.title", prefix = "+@neorg.headings.5.prefix", }, ["6"] = { [""] = "+@neorg.headings.6.title", prefix = "+@neorg.headings.6.prefix", }, }, }, }, -- Highlights for inline markup. -- -- This is all the highlights like `bold`, `italic` and so on. markup = { bold = { [""] = "+@markup.strong", delimiter = "+NonText", }, italic = { [""] = "+@markup.italic", delimiter = "+NonText", }, underline = { [""] = "+@markup.underline", delimiter = "+NonText", }, strikethrough = { [""] = "+@markup.strikethrough", delimiter = "+NonText", }, spoiler = { [""] = "+@comment.error", delimiter = "+NonText", }, subscript = { [""] = "+@label", delimiter = "+NonText", }, superscript = { [""] = "+@number", delimiter = "+NonText", }, variable = { [""] = "+@function.macro", delimiter = "+NonText", }, verbatim = { delimiter = "+NonText", }, inline_comment = { delimiter = "+NonText", }, inline_math = { [""] = "+@markup.math", delimiter = "+NonText", }, free_form_delimiter = "+NonText", }, -- Highlights for all the delimiter types. These include: -- - `---` - the weak delimiter -- - `===` - the strong delimiter -- - `___` - the horizontal rule delimiters = { strong = "+@punctuation.delimiter", weak = "+@punctuation.delimiter", horizontal_line = "+@punctuation.delimiter", }, -- Inline modifiers. -- -- This includes: -- - `~` - the trailing modifier -- - All link characters (`{`, `}`, `[`, `]`, `<`, `>`) -- - The escape character (`\`) modifiers = { link = "+NonText", escape = "+@type", }, -- Rendered Latex, this will dictate the foreground color of latex images rendered via -- core.latex.renderer rendered = { latex = "+Normal", }, }, -- Handles the dimming of certain highlight groups. -- -- It sometimes is favourable to use an existing highlight group, -- but to dim or brighten it a little bit. -- -- To do so, you may use this table, which, similarly to the `highlights` table, -- will concatenate nested trees to form a highlight group name. -- -- The difference is, however, that the leaves of the tree are a table, not a single string. -- This table has three possible fields: -- - `reference` - which highlight to use as reference for the dimming. -- - `percentage` - by how much to darken the reference highlight. This value may be between -- `-100` and `100`, where negative percentages brighten the reference highlight, whereas -- positive values dim the highlight by the given percentage. dim = { tags = { ranged_verbatim = { code_block = { reference = "Normal", percentage = 15, affect = "background", }, }, }, markup = { verbatim = { reference = "Normal", percentage = 20, }, inline_comment = { reference = "Normal", percentage = 40, }, }, }, } module.setup = function() return { success = true, requires = { "core.autocommands" } } end module.load = function() module.required["core.autocommands"].enable_autocommand("BufEnter") module.required["core.autocommands"].enable_autocommand("FileType") module.required["core.autocommands"].enable_autocommand("ColorScheme", true) module.public.trigger_highlights() vim.api.nvim_create_autocmd({ "FileType", "ColorScheme" }, { callback = module.public.trigger_highlights, }) end ---@class core.highlights module.public = { --- Reads the highlights configuration table and applies all defined highlights trigger_highlights = function() -- NOTE(vhyrro): This code was added here to work around oddities related to nvim-treesitter. -- This code, with modern nvim-treesitter versions, will probably not break as harshly. -- This code should be removed as soon as possible. -- -- do -- local query = require("nvim-treesitter.query") -- if not query.has_highlights("norg") then -- query.invalidate_query_cache() -- if not query.has_highlights("norg") then -- log.error( -- "nvim-treesitter has no available highlights for norg! Ensure treesitter is properly loaded in your config." -- ) -- end -- end -- if vim.bo.filetype == "norg" then -- require("nvim-treesitter.highlight").attach(vim.api.nvim_get_current_buf(), "norg") -- end -- end --- Recursively descends down the highlight configuration and applies every highlight accordingly ---@param highlights table #The table of highlights to descend down ---@param callback fun(hl_name: string, highlight: table, prefix: string): boolean? #A callback function to be invoked for every highlight. If it returns true then we should recurse down the table tree further ---@diagnostic disable-line -- TODO: type error workaround ---@param prefix string #Should be only used by the function itself, acts as a "savestate" so the function can keep track of what path it has descended down local function descend(highlights, callback, prefix) -- Loop through every highlight defined in the provided table for hl_name, highlight in pairs(highlights) do -- If the callback returns true then descend further down the table tree if callback(hl_name, highlight, prefix) then descend(highlight, callback, prefix .. "." .. hl_name) end end end -- Begin the descent down the public highlights configuration table descend(module.config.public.highlights, function(hl_name, highlight, prefix) -- If the type of highlight we have encountered is a table -- then recursively descend down it as well if type(highlight) == "table" then return true end -- Trim any potential leading and trailing whitespace highlight = vim.trim(highlight) -- Check whether we are trying to link to an existing hl group -- by checking for the existence of the + sign at the front local is_link = highlight:sub(1, 1) == "+" local full_highlight_name = "@neorg" .. prefix .. (hl_name:len() > 0 and ("." .. hl_name) or "") local does_hl_exist = lib.inline_pcall(vim.api.nvim_exec, "highlight " .. full_highlight_name, true) ---@diagnostic disable-line -- TODO: type error workaround -- If we are dealing with a link then link the highlights together (excluding the + symbol) if is_link then -- If the highlight already exists then assume the user doesn't want it to be -- overwritten if does_hl_exist and does_hl_exist:len() > 0 and not does_hl_exist:match("xxx%s+cleared") then return end vim.api.nvim_set_hl(0, full_highlight_name, { link = highlight:sub(2), }) else -- Otherwise simply apply the highlight options the user provided -- If the highlight already exists then assume the user doesn't want it to be -- overwritten if does_hl_exist and does_hl_exist:len() > 0 then return end -- We have to use vim.cmd here vim.cmd({ cmd = "highlight", args = { full_highlight_name, highlight }, bang = true, }) end end, "") -- Begin the descent down the dimming configuration table descend(module.config.public.dim, function(hl_name, highlight, prefix) -- If we don't have a percentage value then keep traversing down the table tree if not highlight.percentage then return true end local full_highlight_name = "@neorg" .. prefix .. (hl_name:len() > 0 and ("." .. hl_name) or "") local does_hl_exist = lib.inline_pcall(vim.api.nvim_exec, "highlight " .. full_highlight_name, true) ---@diagnostic disable-line -- TODO: type error workaround -- If the highlight already exists then assume the user doesn't want it to be -- overwritten if does_hl_exist and does_hl_exist:len() > 0 and not does_hl_exist:match("xxx%s+cleared") then return end -- Apply the dimmed highlight vim.api.nvim_set_hl(0, full_highlight_name, { [highlight.affect == "background" and "bg" or "fg"] = module.public.dim_color( module.public.get_attribute( highlight.reference or full_highlight_name, highlight.affect or "foreground" ), highlight.percentage ), }) end, "") end, --- Takes in a table of highlights and applies them to the current buffer ---@param highlights table #A table of highlights add_highlights = function(highlights) module.config.public.highlights = vim.tbl_deep_extend("force", module.config.public.highlights, highlights or {}) module.public.trigger_highlights() end, --- Takes in a table of items to dim and applies the dimming to them ---@param dim table #A table of items to dim add_dim = function(dim) module.config.public.dim = vim.tbl_deep_extend("force", module.config.public.dim, dim or {}) module.public.trigger_highlights() end, --- Assigns all Neorg* highlights to `clear` clear_highlights = function() --- Recursively descends down the highlight configuration and clears every highlight accordingly ---@param highlights table #The table of highlights to descend down ---@param prefix string #Should be only used by the function itself, acts as a "savestate" so the function can keep track of what path it has descended down local function descend(highlights, prefix) -- Loop through every defined highlight for hl_name, highlight in pairs(highlights) do -- If it is a table then recursively traverse down it! if type(highlight) == "table" then descend(highlight, hl_name) else -- Otherwise we're dealing with a string -- Hence we should clear the highlight vim.cmd("highlight! clear Neorg" .. prefix .. hl_name) end end end -- Begin the descent descend(module.config.public.highlights, "") end, -- NOTE: Shamelessly taken and tweaked a little from akinsho's nvim-bufferline: -- https://github.com/akinsho/nvim-bufferline.lua/blob/fec44821eededceadb9cc25bc610e5114510a364/lua/bufferline/colors.lua -- <3 get_attribute = function(name, attribute) -- Attempt to get the highlight local success, hl = pcall(vim.api.nvim_get_hl_by_name, name, true) ---@diagnostic disable-line -- TODO: type error workaround -- If we were successful and if the attribute exists then return it if success and hl[attribute] then return bit.tohex(hl[attribute], 6) else -- Else log the message in a regular info() call, it's not an insanely important error log.info("Unable to grab highlight for attribute", attribute, " - full error:", hl) end return "NONE" end, hex_to_rgb = function(hex_colour) return tonumber(hex_colour:sub(1, 2), 16), tonumber(hex_colour:sub(3, 4), 16), tonumber(hex_colour:sub(5), 16) end, dim_color = function(colour, percent) if colour == "NONE" then return colour end local function alter(attr) return math.floor(attr * (100 - percent) / 100) end local r, g, b = module.public.hex_to_rgb(colour) if not r or not g or not b then return "NONE" end return string.format("#%02x%02x%02x", math.min(alter(r), 255), math.min(alter(g), 255), math.min(alter(b), 255)) end, -- END of shamelessly ripped off akinsho code } module.events.subscribed = { ["core.autocommands"] = { colorscheme = true, bufenter = true, }, } return module ================================================ FILE: lua/neorg/modules/core/integrations/coq_nvim/module.lua ================================================ --[[ file: Coq_nvim title: Integrating Neorg with `coq_nvim` summary: A module for integrating coq_nvim with Neorg. internal: true --- This module works with the [`core.completion`](@core.completion) module to attempt to provide intelligent completions. Note that integrations like this are second-class citizens and may not work in 100% of scenarios. If they don't then please file a bug report! --]] local neorg = require("neorg.core") local log, modules = neorg.log, neorg.modules local module = modules.create("core.integrations.coq_nvim") module.private = { ---@param map table new_uid = function(map) vim.validate({ map = { map, "table" }, }) local key ---@type integer|nil while true do if not key or map[key] then key = math.floor(math.random() * 10000) else return key end end end, } module.load = function() local success = pcall(require, "coq") if not success then log.fatal("coq_nvim not found, aborting...") return end end ---@class core.integrations.coq_nvim : neorg.completion_engine module.public = { create_source = function() module.private.completion_item_mapping = { Directive = vim.lsp.protocol.CompletionItemKind.Keyword, Tag = vim.lsp.protocol.CompletionItemKind.Keyword, Language = vim.lsp.protocol.CompletionItemKind.Property, TODO = vim.lsp.protocol.CompletionItemKind.Event, Property = vim.lsp.protocol.CompletionItemKind.Property, Format = vim.lsp.protocol.CompletionItemKind.Property, Embed = vim.lsp.protocol.CompletionItemKind.Property, Reference = vim.lsp.protocol.CompletionItemKind.Reference, File = vim.lsp.protocol.CompletionItemKind.File, } -- luacheck: push ignore 111 -- luacheck: push ignore 112 -- luacheck: push ignore 113 COQsources = COQsources or {} ---@diagnostic disable undefined-global COQsources[module.private.new_uid(COQsources)] = { name = "Neorg", fn = function(args, callback) if vim.bo.filetype ~= "norg" then return callback() end local row, col = unpack(args.pos) local line = args.line local before_char = line:sub(#line, #line) -- Neorg requires a nvim-compe like context nvim-compe defines -- the following fields. Not all of them are used by Neorg, but -- they are left here for future reference local context = { start_offset = nil, char = col + 1, before_char = before_char, line = line, column = nil, buffer = nil, line_number = row + 1, previous_context = nil, full_line = line, } local completion_cache = module.public.invoke_completion_engine(context) local prev = line:match("({.*)$") if completion_cache.options.pre then completion_cache.options.pre(context) end local completions = vim.deepcopy(completion_cache.items) for index, element in ipairs(completions) do -- coq_nvim requries at least 2 exact prefix characters by -- default. Users could chagnge this setting, but instead -- we are providing the start of the match (for links) so -- coq_nvim doesnt' filter our results local word = (prev or "") .. element local label = (prev or "") .. element if type(element) == "table" then word = element[1] label = element.label end completions[index] = { word = word, label = label, kind = module.private.completion_item_mapping[completion_cache.options.type], } end callback({ isIncomplete = false, items = completions, }) end, } -- luacheck: pop -- luacheck: pop -- luacheck: pop end, } return module ================================================ FILE: lua/neorg/modules/core/integrations/image/module.lua ================================================ --[[ file: Image title: Images Directly within Neovim. description: The `image` module uses various terminal backends to display images within your Neovim instance. summary: Module for interacting with and managing images in the terminal. internal: true --- `core.integrations.image` is an internal module that wraps image.nvim, exposing methods to display images in neovim. --]] local neorg = require("neorg.core") local module = neorg.modules.create("core.integrations.image") module.load = function() local success, image = pcall(require, "image") assert(success, "Unable to load image.nvim plugin") module.private.image = image module.private.image_utils = require("image.utils") end module.private = { image = nil, } module.public = { new_image = function(buffernr, png_path, position, window, scale, virtual_padding) local image = require("image").from_file(png_path, { window = window, buffer = buffernr, inline = true, -- let image.nvim track the position for us with_virtual_padding = virtual_padding, x = position.column_start, y = position.row_start + (virtual_padding and 1 or 0), height = scale, }) return image end, ---Render an image or list of images ---@param images any[] render = function(images) for _, limage in pairs(images) do limage.image:render({ y = limage.range[1], x = limage.range[2] }) end end, clear = function(images) for _, limage in pairs(images) do limage.image:clear() end end, clear_at_cursor = function(images, row) local cleared = {} for id, limage in pairs(images) do local image = limage.image if image.geometry.y == row then image:clear() table.insert(cleared, id) end end return cleared end, ---Compute the image's rendered width/height without rendering ---@param image any an image.nvim image ---@param limit { width: number, height: number } ---@return { width: number, height: number } image_size = function(image, limit) limit = limit or {} local term_size = require("image.utils.term").get_size() local gopts = image.global_state.options local true_size = { width = math.min( math.floor(image.image_width / term_size.cell_width), -- max image size (images don't scale up past their true size) gopts.max_width or math.huge, -- image.nvim configured max size limit.width or math.huge -- latex-renderer configured max size ), height = math.min( math.floor(image.image_height / term_size.cell_height), gopts.max_height or math.huge, limit.height or math.huge ), } local width, height = module.private.image_utils.math.adjust_to_aspect_ratio( term_size, image.image_width, image.image_height, true_size.width, true_size.height ) return { width = math.ceil(width), height = math.ceil(height) } end, } return module ================================================ FILE: lua/neorg/modules/core/integrations/nvim-cmp/module.lua ================================================ --[[ file: Nvim-Cmp title: Integrating Neorg with `nvim-cmp` summary: A module for integrating nvim-cmp with Neorg. internal: true --- This module works with the [`core.completion`](@core.completion) module to attempt to provide intelligent completions. Note that integrations like this are second-class citizens and may not work in 100% of scenarios. If they don't then please file a bug report! After setting up `core.completion` with the `engine` set to `nvim-cmp`, make sure to also set up "neorg" as a source in `nvim-cmp`: ```lua sources = { { name = "neorg" }, }, ``` --]] local neorg = require("neorg.core") local log, modules = neorg.log, neorg.modules local module = modules.create("core.integrations.nvim-cmp") module.private = { source = {}, cmp = {}, completions = {}, } module.load = function() local success, cmp = pcall(require, "cmp") if not success then log.fatal("nvim-cmp not found, aborting...") return end module.private.cmp = cmp end ---@class core.integrations.nvim-cmp : neorg.completion_engine module.public = { create_source = function() module.private.completion_item_mapping = { Directive = module.private.cmp.lsp.CompletionItemKind.Keyword, Tag = module.private.cmp.lsp.CompletionItemKind.Keyword, Language = module.private.cmp.lsp.CompletionItemKind.Property, TODO = module.private.cmp.lsp.CompletionItemKind.Event, Property = module.private.cmp.lsp.CompletionItemKind.Property, Format = module.private.cmp.lsp.CompletionItemKind.Property, Embed = module.private.cmp.lsp.CompletionItemKind.Property, Reference = module.private.cmp.lsp.CompletionItemKind.Reference, File = module.private.cmp.lsp.CompletionItemKind.File, } module.private.source.new = function() return setmetatable({}, { __index = module.private.source }) end function module.private.source.complete(_, request, callback) local abstracted_context = module.public.create_abstracted_context(request) local completion_cache = module.public.invoke_completion_engine(abstracted_context) if completion_cache.options.pre then completion_cache.options.pre(abstracted_context) end local completions = vim.deepcopy(completion_cache.items) for index, element in ipairs(completions) do local word = element local label = element if type(element) == "table" then word = element[1] label = element.label end completions[index] = { word = word, label = label, kind = module.private.completion_item_mapping[completion_cache.options.type], } end callback(completions) end function module.private.source:get_trigger_characters() return { "@", "-", "(", " ", ".", ":", "#", "*", "^", "[" } end function module.private.source:is_available() return vim.bo.filetype == "norg" end module.private.cmp.register_source("neorg", module.private.source) end, create_abstracted_context = function(request) return { start_offset = request.offset, char = request.context.cursor.character, before_char = request.completion_context.triggerCharacter, line = request.context.cursor_before_line, column = request.context.cursor.col, buffer = request.context.bufnr, line_number = request.context.cursor.line, previous_context = { line = request.context.prev_context.cursor_before_line, column = request.context.prev_context.cursor.col, start_offset = request.offset, }, full_line = request.context.cursor_line, } end, invoke_completion_engine = function(context) error("`invoke_completion_engine` must be set from outside.") assert(context) return {} end, } return module ================================================ FILE: lua/neorg/modules/core/integrations/nvim-compe/module.lua ================================================ --[[ file: Nvim-Compe title: Integrating Neorg with `nvim-compe` summary: A module for integrating nvim-compe with Neorg. internal: true --- WARNING: This module is **deprecated**, and no further support for the module will be provided. That does not mean it will not work, however any bugs will not be dealt with by the main Neorg team. A module for integrating nvim-compe with Neorg. Works with [`core.completion`](@core.completion) to provide intelligent completions. --]] local neorg = require("neorg.core") local log, modules = neorg.log, neorg.modules local module = modules.create("core.integrations.nvim-compe") -- Define some private data that's not supposed to be seen module.private = { source = {}, compe = {}, completions = {}, } module.load = function() -- Code to test the existence of nvim-compe local success, compe = pcall(require, "compe") if not success then log.fatal("nvim-compe not found, aborting...") return end module.private.compe = compe end ---@class core.integrations.nvim-compe : neorg.completion_engine module.public = { ---@param user_data table #A table of user data to supply to the source upon creation create_source = function(user_data) user_data = user_data or {} local data = { name = "[Neorg]", priority = 998, sort = false, dup = 0, } data = vim.tbl_deep_extend("force", data, user_data) -- Define functions for nvim-compe module.private.source.new = function() return setmetatable({}, { __index = module.private.source }) end -- Return metadata for nvim-compe to use module.private.source.get_metadata = function() return { priority = data.priority, sort = data.sort, dup = data.dup, filetypes = { "norg" }, menu = data.name, } end -- Used to determine whether or not to provide completions, simply invokes the public determine function module.private.source.determine = function(_, context) return module.public.determine(context) end -- Used to actually provide completions, simply invokes the public sibling function module.private.source.complete = function(_, context) module.public.complete(context) end -- Invoked whenever a completion is confirmed, calls the public confirm() function module.private.source.confirm = function(_, context) module.public.confirm(context) ---@diagnostic disable-line -- TODO: type error workaround end -- Actually register the nvim-compe source module.private.compe.register_source("neorg", module.private.source) end, --- Looks at the cursor position and tries to determine whether we should provide any completions ---@param context table #The context provided by nvim-compe determine = function(context) -- Abstract away the context to a completion engine agnostic format local abstracted_context = module.public.create_abstracted_context(context) -- Update the current completion cache with the data returned by core.completion module.private.completion_cache = module.public.invoke_completion_engine(abstracted_context) ---@diagnostic disable-line -- TODO: type error workaround -- If we haven't returned any items to complete via that function then return an empty table, -- symbolizing a lack of completions if vim.tbl_isempty(module.private.completion_cache.items) then return {} end -- If the current completion that was found has a pre() function then invoke that if module.private.completion_cache.options.pre then module.private.completion_cache.options.pre(abstracted_context) end -- Reverse the current line, this is used for a reverse find() call local reversed = vim.trim(context.before_line):reverse() -- Find any occurrence of whitespace local last_whitespace = reversed:find("%s") --[[ This bit is a bit crazy, however here's the gist of it: It checks the current cursor position and the last occurrence of whitespace in the string to provide completions even if a part of that completion is already present. Say we have: @ Compe would have no problem with this and would provide completions instantly, but say we have (| means the current cursor pos): @t|ab And i try pressing C to edit till the end of the line, I'm then left with: @t If I were to try typing here I wouldn't get any completions, because compe wouldn't understand that @t is part of the completion. This below bit of code makes sure that it *does* understand and that it *does* detect properly. --]] last_whitespace = last_whitespace and last_whitespace - 1 or (function() local found = module.private.completion_cache.options.completion_start and reversed:find(module.private.completion_cache.options.completion_start) return found and found - 1 or 0 end)() return { keyword_pattern_offset = 0, trigger_character_offset = context.col - last_whitespace } end, --- Once the completion candidates have been collected from the determine() function it's time to display them ---@param context table #A context as provided by nvim-compe complete = function(context) -- If the completion cache is empty for some reason then don't do anything if vim.tbl_isempty(module.private.completion_cache.items) then return end -- Grab a copy of the completions (important, because if it's not copied then values get overwritten) local completions = vim.deepcopy(module.private.completion_cache.items) -- Go through each element and convert it into a format that nvim-compe understands for index, element in ipairs(completions) do completions[index] = { word = element, kind = module.private.completion_cache.options.type } end -- Display the completions context.callback({ items = completions, }) end, confirm = function() -- If the defined completion has a post function then invoke it if module.private.completion_cache.options.post then module.private.completion_cache.options.post() end -- Reset the completion cache module.private.completion_cache = {} end, --- Returns a new context based off of nvim-compe's "proprietary" context and converts it into a universal context ---@param context table #A context as provided by nvim-compe create_abstracted_context = function(context) return { start_offset = context.start_offset, char = context.char, before_char = context.before_char, line = context.before_line, column = context.col, buffer = context.bufnr, line_number = context.lnum, previous_context = { line = context.prev_context.before_line, column = context.prev_context.col, start_offset = context.prev_context.start_offset, }, full_line = context.line, } end, invoke_completion_engine = function(context) error("`invoke_completion_engine` must be set from outside.") assert(context) return {} end, } return module ================================================ FILE: lua/neorg/modules/core/integrations/otter/module.lua ================================================ --[[ file: Otter title: LSP Features in Code Cells description: Integrates with otter.nvim to get LSP features directly in norg code cells. --- From the otter README: > Otter.nvim provides lsp features and a code completion source for code embedded in other documents. This includes: - auto completion - diagnostics - hover - rename symbol - go to definition - go to references - go to type definition - range formatting (not document formatting) ## Setup You need to install otter.nvim and make sure it's loaded before Neorg, you can configure it yourself by reading [the Otter.nvim README](https://github.com/jmbuhr/otter.nvim). You do not need to configure `handle_leading_whitespace`, neorg will do that for you. If you want auto complete, make sure you add `"otter"` as a source to nvim-cmp: ```lua sources = { -- ... other sources { name = "otter" }, -- ... other sources } ``` ## Commands - `:Neorg otter enable` - enable otter in the current buffer - `:Neorg otter disable` - disable otter in the current buffer --]] local neorg = require("neorg.core") local module = neorg.modules.create("core.integrations.otter") module.setup = function() return { success = pcall(require, "otter"), requires = { "core.neorgcmd", }, } end local otter module.load = function() local ok ok, otter = pcall(require, "otter") assert(ok, "[Neorg] Failed to load otter.nvim") -- This will not interfere with other otter.nvim configuration otter.setup({ handle_leading_whitespace = true }) if module.config.public.auto_start then local group = vim.api.nvim_create_augroup("neorg.integrations.otter", {}) vim.api.nvim_create_autocmd({ "BufReadPost" }, { desc = "Activate Otter on Buf Enter", pattern = "*.norg", group = group, callback = function(_) module.public.activate() end, }) end module.required["core.neorgcmd"].add_commands_from_table({ otter = { args = 1, name = "otter", condition = "norg", subcommands = { enable = { args = 0, name = "otter.enable", }, disable = { args = 0, name = "otter.disable", }, }, }, }) end module.events.subscribed = { ["core.neorgcmd"] = { ["otter.enable"] = true, ["otter.disable"] = true, }, } module.on_event = function(event) if module.private[event.split_type[2]] then module.private[event.split_type[2]](event) end end module.config.public = { -- list of languages that otter will try to start a language server for. -- `nil` means all languages languages = nil, -- Automatically start Otter when a norg buffer is opened auto_start = true, completion = { -- enable/disable Otter autocomplete enabled = true, }, diagnostics = { -- enable/disable Otter diagnostics enabled = true, }, } module.private = { ["otter.enable"] = function(_) module.public.activate() end, ["otter.disable"] = function(_) module.public.deactivate() end, } module.public = { ---Activate otter in the current buffer, includes setting buffer keymaps activate = function() otter.activate( module.config.public.languages, module.config.public.completion.enabled, module.config.public.diagnostics.enabled, nil -- or a query... ) end, ---Deactivate otter in the current buffer, including unsetting buffer keymaps deactivate = function() otter.deactivate(module.config.public.completion.enabled, module.config.public.diagnostics.enabled) end, } return module ================================================ FILE: lua/neorg/modules/core/integrations/treesitter/module.lua ================================================ --[[ file: Treesitter-Integration title: Snazzy Treesitter Integration summary: A module designed to integrate Treesitter into Neorg. embed: https://user-images.githubusercontent.com/76052559/151668244-9805afc4-8c50-4925-85ec-1098aff5ede6.gif internal: true --- ## Keybinds This module exposes the following keybinds (see [`core.keybinds`](@core.keybinds) for instructions on mapping them): - `neorg.treesitter.next.heading` - jump to the next heading - `neorg.treesitter.next.link` - jump to the next link - `neorg.treesitter.previous.heading` - jump to the previous heading - `neorg.treesitter.previous.link` - jump to the previous link --]] local neorg = require("neorg.core") local lib, log, modules, utils = neorg.lib, neorg.log, neorg.modules, neorg.utils local module = modules.create("core.integrations.treesitter") module.private = { link_query = [[ (link) @next-segment (anchor_declaration) @next-segment (anchor_definition) @next-segment ]], heading_query = [[ [ (heading1 title: (paragraph_segment) @next-segment ) (heading2 title: (paragraph_segment) @next-segment ) (heading3 title: (paragraph_segment) @next-segment ) (heading4 title: (paragraph_segment) @next-segment ) (heading5 title: (paragraph_segment) @next-segment ) (heading6 title: (paragraph_segment) @next-segment ) ] ]], } module.setup = function() return { success = true, requires = { "core.highlights" } } end module.load = function() if module.config.public.configure_parsers then -- luacheck: push ignore -- compat: nvim-treesitter master requires the extra function call, main does not local parser_configs = require("nvim-treesitter.parsers") if parser_configs.get_parser_configs then parser_configs = parser_configs.get_parser_configs() end parser_configs.norg = { install_info = module.config.public.parser_configs.norg, } parser_configs.norg_meta = { install_info = module.config.public.parser_configs.norg_meta, } modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ ["sync-parsers"] = { args = 0, name = "sync-parsers", }, }) end) -- luacheck: pop vim.api.nvim_create_autocmd("BufEnter", { pattern = "*.norg", once = true, callback = function() module.public.parser_path = vim.api.nvim_get_runtime_file("parser/norg.so", false)[1] if module.public.parser_path then return end if module.config.public.install_parsers then require("nvim-treesitter.install").commands.TSInstallSync["run!"]("norg", "norg_meta") module.public.parser_path = vim.api.nvim_get_runtime_file("parser/norg.so", false)[1] else assert( false, "Neorg's parser is not installed! Run `:Neorg sync-parsers` to install it, then restart Neovim." ) end end, }) end vim.keymap.set( "", "(neorg.treesitter.next.heading)", lib.wrap(module.public.goto_next_query_match, module.private.heading_query) ) vim.keymap.set( "", "(neorg.treesitter.next.link)", lib.wrap(module.public.goto_next_query_match, module.private.link_query) ) vim.keymap.set( "", "(neorg.treesitter.previous.heading)", lib.wrap(module.public.goto_previous_query_match, module.private.heading_query) ) vim.keymap.set( "", "(neorg.treesitter.previous.link)", lib.wrap(module.public.goto_previous_query_match, module.private.link_query) ) end module.config.public = { --- If true will auto-configure the parsers to use the recommended setup. -- Set to false only if you know what you're doing, or if the setting messes -- with your personal configuration. configure_parsers = true, --- If true will automatically install Norg parsers if they are not present. install_parsers = true, --- Configurations for each parser as required by `nvim-treesitter`. -- If you would like to tweak your parser configs you may do so here. parser_configs = { -- Configuration for the mainline norg parser. norg = { url = "https://github.com/nvim-neorg/tree-sitter-norg", files = { "src/parser.c", "src/scanner.cc" }, revision = "6348056b999f06c2c7f43bb0a5aa7cfde5302712", use_makefile = true, }, -- Configuration for the metadata parser (used to parse the contents -- of `@document.meta` blocks). norg_meta = { url = "https://github.com/nvim-neorg/tree-sitter-norg-meta", files = { "src/parser.c" }, revision = "a479d1ca05848d0b51dd25bc9f71a17e0108b240", use_makefile = true, }, }, } ---@class core.integrations.treesitter module.public = { parser_path = nil, --- Jumps to the next match of a query in the current buffer ---@param query_string string Query with `@next-segment` captures goto_next_query_match = function(query_string) local cursor = vim.api.nvim_win_get_cursor(0) local line_number, col_number = cursor[1], cursor[2] local document_root = module.public.get_document_root(0) if not document_root then return end local next_match_query = utils.ts_parse_query("norg", query_string) for id, node in next_match_query:iter_captures(document_root, 0, line_number - 1, -1) do if next_match_query.captures[id] == "next-segment" then local start_line, start_col = node:range() -- start_line is 0-based; increment by one so we can compare it to the 1-based line_number start_line = start_line + 1 -- Skip node if it's inside a closed fold if not vim.tbl_contains({ -1, start_line }, vim.fn.foldclosed(start_line)) then goto continue end -- Find and go to the first matching node that starts after the current cursor position. if (start_line == line_number and start_col > col_number) or start_line > line_number then module.public.goto_node(node) return end end ::continue:: end end, --- Jumps to the previous match of a query in the current buffer ---@param query_string string Query with `@next-segment` captures goto_previous_query_match = function(query_string) local cursor = vim.api.nvim_win_get_cursor(0) local line_number, col_number = cursor[1], cursor[2] local document_root = module.public.get_document_root(0) if not document_root then return end local previous_match_query = utils.ts_parse_query("norg", query_string) local final_node = nil for id, node in previous_match_query:iter_captures(document_root, 0, 0, line_number) do if previous_match_query.captures[id] == "next-segment" then local start_line, _, _, end_col = node:range() -- start_line is 0-based; increment by one so we can compare it to the 1-based line_number start_line = start_line + 1 -- Skip node if it's inside a closed fold if not vim.tbl_contains({ -1, start_line }, vim.fn.foldclosed(start_line)) then goto continue end -- Find the last matching node that ends before the current cursor position. if start_line < line_number or (start_line == line_number and end_col < col_number) then final_node = node end end ::continue:: end if final_node then module.public.goto_node(final_node) end end, --- Gets all nodes of a given type from the AST ---@param node_type string #The type of node to filter out ---@param opts? table #A table of two options: `buf` and `ft`, for the buffer and format to use respectively. get_all_nodes = function(node_type, opts) local result = {} opts = opts or {} if not opts.buf then opts.buf = 0 end if not opts.ft then opts.ft = "norg" end -- Do we need to go through each tree? lol vim.treesitter.get_parser(opts.buf, opts.ft):for_each_tree(function(tree) table.insert(result, module.public.search_tree(tree, node_type)) end) return vim.iter(result):flatten():totable() end, ---Gets all nodes of a given type from the AST ---@param node_type string #The type of node to filter out ---@param path string path to the file to parse ---@param filetype string? file type of the file or `norg` if omitted get_all_nodes_in_file = function(node_type, path, filetype) path = vim.fs.normalize(path) if not filetype then filetype = "norg" end local contents = io.open(path, "r"):read("*a") local tree = vim.treesitter.get_string_parser(contents, filetype):parse()[1] if not (tree or tree.root) then return {} end return module.public.search_tree(tree, node_type) end, search_tree = function(tree, node_type) local result = {} local root = tree:root() --- Recursively searches for a node of a given type ---@param node TSNode #The starting point for the search local function descend(node) -- Iterate over all children of the node and try to match their type for child, _ in node:iter_children() do if child:type() == node_type then table.insert(result, child) else -- If no match is found try descending further down the syntax tree for _, child_node in ipairs(descend(child) or {}) do table.insert(result, child_node) end end end end descend(root) return result end, --- Executes function callback on each child node of the root ---@param callback function ---@param ts_tree any #Optional syntax tree ---@diagnostic disable-line -- TODO: type error workaround tree_map = function(callback, ts_tree) local tree = ts_tree or vim.treesitter.get_parser(0, "norg"):parse()[1] local root = tree:root() for child, _ in root:iter_children() do callback(child) end end, --- Executes callback on each child recursive ---@param callback function Executes with each node as parameter, can return false to stop recursion ---@param ts_tree any #Optional syntax tree ---@diagnostic disable-line -- TODO: type error workaround tree_map_rec = function(callback, ts_tree) local tree = ts_tree or vim.treesitter.get_parser(0, "norg"):parse()[1] local root = tree:root() local function descend(start) for child, _ in start:iter_children() do local stop_descending = callback(child) if not stop_descending then descend(child) end end end descend(root) end, get_node_text = function(node, source) if not node then return "" end -- when source is the string contents of the file if type(source) == "string" then local _, _, start_bytes = node:start() local _, _, end_bytes = node:end_() return string.sub(source, start_bytes + 1, end_bytes) end source = source or 0 local start_row, start_col = node:start() local end_row, end_col = node:end_() local eof_row = vim.api.nvim_buf_line_count(source) if end_row >= eof_row then end_row = eof_row - 1 end_col = -1 end if start_row >= eof_row then return "" end local lines = vim.api.nvim_buf_get_text(source, start_row, start_col, end_row, end_col, {}) return table.concat(lines, "\n") end, --- Get the range of a TSNode as an LspRange ---@param node TSNode ---@return lsp.Range node_to_lsp_range = function(node) local start_line, start_col, end_line, end_col = node:range() return { start = { line = start_line, character = start_col }, ["end"] = { line = end_line, character = end_col }, } end, --- Swap two nodes in the buffer. Ignores newlines at the end of the node ---@param node1 TSNode ---@param node2 TSNode ---@param bufnr number ---@param cursor_to_second boolean move the cursor to the start of the second node (default false) swap_nodes = function(node1, node2, bufnr, cursor_to_second) if not node1 or not node2 then return end local range1 = module.public.node_to_lsp_range(node1) local range2 = module.public.node_to_lsp_range(node2) local _text1 = module.public.get_node_text(node1, bufnr) local _text2 = module.public.get_node_text(node2, bufnr) if not _text1 or not _text2 then return end local text1 = vim.split(_text1, "\n") local text2 = vim.split(_text2, "\n") ---remove trailing blank lines from the text, and update the corresponding range appropriately ---@param text string[] ---@param range table local function remove_trailing_blank_lines(text, range) local end_line_offset = 0 while text[#text] == "" do text[#text] = nil end_line_offset = end_line_offset + 1 end range["end"] = { character = string.len(text[#text]), line = range["end"].line - end_line_offset, } if #text == 1 then -- ie. start and end lines are equal range["end"].character = range["end"].character + range.start.character end end remove_trailing_blank_lines(text1, range1) remove_trailing_blank_lines(text2, range2) local edit1 = { range = range1, newText = table.concat(text2, "\n") } local edit2 = { range = range2, newText = table.concat(text1, "\n") } vim.lsp.util.apply_text_edits({ edit1, edit2 }, bufnr, "utf-8") if cursor_to_second then -- set jump location vim.cmd("normal! m'") local char_delta = 0 local line_delta = 0 if range1["end"].line < range2.start.line or (range1["end"].line == range2.start.line and range1["end"].character <= range2.start.character) then line_delta = #text2 - #text1 end if range1["end"].line == range2.start.line and range1["end"].character <= range2.start.character then if line_delta ~= 0 then --- why? --correction_after_line_change = -range2.start.character --text_now_before_range2 = #(text2[#text2]) --space_between_ranges = range2.start.character - range1["end"].character --char_delta = correction_after_line_change + text_now_before_range2 + space_between_ranges --- Equivalent to: char_delta = #text2[#text2] - range1["end"].character -- add range1.start.character if last line of range1 (now text2) does not start at 0 if range1.start.line == range2.start.line + line_delta then char_delta = char_delta + range1.start.character end else char_delta = #text2[#text2] - #text1[#text1] end end vim.api.nvim_win_set_cursor( vim.api.nvim_get_current_win(), { range2.start.line + 1 + line_delta, range2.start.character + char_delta } ) end end, --- Returns the first node of given type if present ---@deprecated use get_first_node_recursive instead ---@param type string #The type of node to search for ---@param buf number #The buffer to search in ---@param parent userdata #The node to start searching in get_first_node = function(type, buf, parent) if not buf then buf = 0 end local function iterate(parent_node) for child, _ in parent_node:iter_children() do if child:type() == type then return child end end end if parent then return iterate(parent) end vim.treesitter.get_parser(buf, "norg"):for_each_tree(function(tree) -- FIXME: this return value doesn't do what the original author thinks it does -- Iterate over all top-level children and attempt to find a match return iterate(tree:root()) ---@diagnostic disable-line -- TODO: type error workaround end) end, --- Recursively attempts to locate a node of a given type ---@param type string #The type of node to look for ---@param opts {buf: number?, ft: string?, parent: TSNode?} # Buffer, filetype (for TS parsing), ---parent, defaults to root node ---@return TSNode? get_first_node_recursive = function(type, opts) opts = opts or {} local result if not opts.buf then opts.buf = 0 end if not opts.ft then opts.ft = "norg" end -- Do we need to go through each tree? lol vim.treesitter.get_parser(opts.buf, opts.ft):for_each_tree(function(tree) -- Get the root for that tree local root if opts.parent then root = opts.parent else root = tree:root() end if not root then return end --- Recursively searches for a node of a given type ---@param node TSNode #The starting point for the search local function descend(node) -- Iterate over all children of the node and try to match their type for child, _ in node:iter_children() do if child:type() == type then return child else -- If no match is found try descending further down the syntax tree local descent = descend(child) if descent then return descent end end end return nil end result = result or descend(root) end) return result end, --- Given a node this function will break down the AST elements and return the corresponding text for certain nodes --- @param tag_node TSNode - a node of type tag/carryover_tag --- @param throw boolean? - when true, throw an error instead of logging and returning on failure get_tag_info = function(tag_node, throw) if not tag_node or not vim.tbl_contains( { "ranged_tag", "ranged_verbatim_tag", "weak_carryover", "strong_carryover" }, tag_node:type() ) then return nil end local start_row, start_column, end_row, end_column = tag_node:range() local attributes = {} local resulting_name, params, content = {}, {}, {} local content_start_column = 0 -- Iterate over all children of the tag node for child, _ in tag_node:iter_children() do -- If we are dealing with a weak/strong attribute set then parse that set if vim.endswith(child:type(), "_carryover_set") then for subchild in child:iter_children() do if vim.endswith(subchild:type(), "_carryover") then local meta = module.public.get_tag_info(subchild, throw) table.insert(attributes, meta) end end elseif child:type() == "tag_name" then -- If we're dealing with the tag name then append the text of the tag_name node to this table table.insert(resulting_name, vim.split(module.public.get_node_text(child), "\n")[1]) elseif child:type() == "tag_parameters" then table.insert(params, vim.split(module.public.get_node_text(child), "\n")[1]) elseif child:type() == "ranged_verbatim_tag_content" then -- If we're dealing with tag content then retrieve that content content = vim.split(module.public.get_node_text(child), "\n") _, content_start_column = child:range() end end for i, line in ipairs(content) do if i == 1 then if content_start_column < start_column then local error_msg = string.format( "Unable to query information about tag on line %d: content is indented less than tag start!", start_row + 1 ) if throw then error(error_msg) else log.error(error_msg) return nil end end content[i] = string.rep(" ", content_start_column - start_column) .. line else content[i] = line:sub(1 + start_column) end end content[#content] = nil return { name = table.concat(resulting_name, "."), parameters = params, content = content, attributes = vim.fn.reverse(attributes), start = { row = start_row, column = start_column }, ["end"] = { row = end_row, column = end_column }, } end, --- Gets the range of the given node ---@param node TSNode ---@return { row_start: number, column_start: number, row_end: number, column_end: number } range get_node_range = function(node) if not node then return { row_start = 0, column_start = 0, row_end = 0, column_end = 0, } end local rs, cs, re, ce = lib.when(type(node) == "table", function() local brs, bcs, _, _ = node[1]:range() local _, _, ere, ece = node[#node]:range() return brs, bcs, ere, ece end, function() local a, b, c, d = node:range() return a, b, c, d end) return { row_start = rs, column_start = cs, row_end = re, column_end = ce, } end, --- Extracts the document root from the current document or from the string ---@param src number|string|nil The number of the buffer to extract or string with code ---@param filetype string? #The filetype of the buffer or the string with code ---@return TSNode? #The root node of the document get_document_root = function(src, filetype) filetype = filetype or "norg" local parser if type(src) == "string" then parser = vim.treesitter.get_string_parser(src, filetype) else if src ~= 0 and src ~= nil then vim.fn.bufload(src) end parser = vim.treesitter.get_parser(src or 0, filetype) end if not parser then return end local tree = parser:parse()[1] if not tree or not tree:root() then return end return tree:root() end, --- Attempts to find a parent of a node recursively ---@param node TSNode #The node to start at ---@param types table|string #If `types` is a table, this function will attempt to match any of the types present in the table. -- If the type is a string, the function will attempt to pattern match the `types` value with the node type. ---@return TSNode? find_parent = function(node, types) ---@type TSNode? local _node = node while _node do if type(types) == "string" then if _node:type():match(types) then return _node end elseif vim.tbl_contains(types, _node:type()) then return _node end _node = _node:parent() end end, --- Retrieves the first node at a specific line ---@param buf number #The buffer to search in (0 for current) ---@param line number #The line number (0-indexed) to get the node from the same line as `line`. ---@param stop_type string|table? #Don't recurse to the provided type(s) ---@return TSNode|nil #The first node on `line` get_first_node_on_line = function(buf, line, stop_type) if type(stop_type) == "string" then stop_type = { stop_type } end local document_root = module.public.get_document_root(buf) if not document_root then return end local first_char = (vim.api.nvim_buf_get_lines(buf, line, line + 1, true)[1] or ""):match("^(%s+)[^%s]") first_char = first_char and first_char:len() or 0 local descendant = document_root:descendant_for_range(line, first_char, line, first_char + 1) if not descendant then return end while descendant:parent() and (descendant:parent():start()) == line and descendant:parent():symbol() ~= document_root:symbol() ---@diagnostic disable-line -- TODO: type error workaround do local parent = descendant:parent() if parent and stop_type and vim.tbl_contains(stop_type, parent:type()) then break end if not parent then return end descendant = parent end return descendant end, ---get document's metadata ---@param source number | string | PathlibPath ---@param no_trim boolean? ---@return table? get_document_metadata = function(source, no_trim) source = source or 0 local norg_parser, iter_src = module.public.get_ts_parser(source) if not norg_parser then return end local norg_tree = norg_parser:parse()[1] if not norg_tree then return end local function trim(value) return no_trim and value or vim.trim(value) end local result = {} local function parse_data(node, src) return lib.match(node:type())({ string = function() return trim(module.public.get_node_text(node, src)) end, number = function() return tonumber(module.public.get_node_text(node, src)) end, array = function() local resulting_array = {} for child in node:iter_children() do if child:named() then local parsed_data = parse_data(child, src) if parsed_data then table.insert(resulting_array, parsed_data) end end end return resulting_array end, object = function() local resulting_object = {} for child in node:iter_children() do if not child:named() or child:type() ~= "pair" then goto continue end local key = child:named_child(0) local value = child:named_child(1) if not key then goto continue end local key_content = trim(module.public.get_node_text(key, src)) resulting_object[key_content] = (value and parse_data(value, src) or vim.NIL) ::continue:: end return resulting_object end, }) end local norg_query = utils.ts_parse_query( "norg", [[ (document (ranged_verbatim_tag (tag_name) @tag_name (ranged_verbatim_tag_content) @tag_content ) ) ]] ) local meta_query = utils.ts_parse_query( "norg_meta", [[ (metadata (pair (key) @key (value) @value ) ) ]] ) local meta_node for id, node in norg_query:iter_captures(norg_tree:root(), iter_src) do if norg_query.captures[id] == "tag_name" then local tag_name = trim(module.public.get_node_text(node, iter_src)) if tag_name == "document.meta" then meta_node = node:next_named_sibling() or vim.NIL break end end end if not meta_node then return result end local meta_source = module.public.get_node_text(meta_node, iter_src) local norg_meta_parser = vim.treesitter.get_string_parser(meta_source, "norg_meta") local norg_meta_tree = norg_meta_parser:parse()[1] if not norg_meta_tree then return end for id, node in meta_query:iter_captures(norg_meta_tree:root(), meta_source) do if meta_query.captures[id] == "key" then local key = trim(module.public.get_node_text(node, meta_source)) local val if key == "title" then -- force title's value as string type val = trim(module.public.get_node_text(node:next_named_sibling(), meta_source)) else val = node:next_named_sibling() and parse_data(node:next_named_sibling(), meta_source) or vim.NIL end result[key] = val end end return result end, --- Parses a query and automatically executes it for Norg ---@param query_string string #The query string ---@param callback function #The callback to execute with all values returned by ---`Query:iter_captures()`. When callback returns true, this function returns early ---@param source number | string | PathlibPath #buf number, or file path or 0 for current buffer ---@param start number? #The start line for the query ---@param finish number? #The end line for the query execute_query = function(query_string, callback, source, start, finish) local query = utils.ts_parse_query("norg", query_string) local norg_parser, iter_src = module.public.get_ts_parser(source) if not norg_parser then return false end local root = norg_parser:parse()[1]:root() for id, node, metadata in query:iter_captures(root, iter_src, start, finish) do if callback(query, id, node, metadata) == true then return true end end return true end, ---Create a norg TS parser from the given source ---@param source string | number | PathlibPath file path or buf number or 0 for current buffer ---@return vim.treesitter.LanguageTree? norg_parser ---@return string | number iter_src the corresponding source that you must pass to ---`iter_query()`, either the full file text, or the buffer number get_ts_parser = function(source) local norg_parser local iter_src if type(source) ~= "string" and type(source) ~= "number" then source = tostring(source) end if type(source) == "string" then -- check if the file is open; use the buffer contents if it is if vim.fn.bufnr(source) ~= -1 then ---@diagnostic disable-line source = vim.uri_to_bufnr(vim.uri_from_fname(source)) else iter_src = io.open(source, "r"):read("*a") norg_parser = vim.treesitter.get_string_parser(iter_src, "norg") end end if type(source) == "number" then if source == 0 then source = vim.api.nvim_get_current_buf() end norg_parser = vim.treesitter.get_parser(source, "norg") iter_src = source end return norg_parser, iter_src end, } --[[ -- attribution notice: -- The below public functions are originally licensed under Apache v2 taken from: -- https://github.com/nvim-treesitter/nvim-treesitter/blob/master/lua/nvim-treesitter/ts_utils.lua --]] -- Get previous node with same parent ---@param node TSNode ---@param allow_switch_parents? boolean allow switching parents if first node ---@param allow_previous_parent? boolean allow previous parent if first node and previous parent without children module.public.get_previous_node = function(node, allow_switch_parents, allow_previous_parent) local destination_node ---@type TSNode? local parent = node:parent() if not parent then return end local found_pos = 0 for i = 0, parent:named_child_count() - 1, 1 do if parent:named_child(i) == node then found_pos = i break end end if 0 < found_pos then destination_node = parent:named_child(found_pos - 1) elseif allow_switch_parents then local previous_node = module.public.get_previous_node(parent) if previous_node and previous_node:named_child_count() > 0 then destination_node = previous_node:named_child(previous_node:named_child_count() - 1) elseif previous_node and allow_previous_parent then destination_node = previous_node end end return destination_node end module.public.goto_node = function(node, goto_end, avoid_set_jump) if not node then return end if not avoid_set_jump then vim.cmd("normal! m'") end local range = module.public.get_node_range(node) ---@type table local position if not goto_end then position = { range.row_start, range.column_start } else position = { range.row_end, range.column_end } end -- Enter visual mode if we are in operator pending mode -- If we don't do this, it will miss the last character. local mode = vim.api.nvim_get_mode() if mode.mode == "no" then vim.cmd("normal! v") end vim.api.nvim_win_set_cursor(0, { position[1] + 1, position[2] }) end -- Get next node with same parent ---@param node TSNode ---@param allow_switch_parents? boolean allow switching parents if last node ---@param allow_next_parent? boolean allow next parent if last node and next parent without children module.public.get_next_node = function(node, allow_switch_parents, allow_next_parent) local destination_node ---@type TSNode? local parent = node:parent() if not parent then return end local found_pos = 0 for i = 0, parent:named_child_count() - 1, 1 do if parent:named_child(i) == node then found_pos = i break end end if parent:named_child_count() > found_pos + 1 then destination_node = parent:named_child(found_pos + 1) elseif allow_switch_parents then local next_node = module.public.get_next_node(parent) if next_node and next_node:named_child_count() > 0 then destination_node = next_node:named_child(0) elseif next_node and allow_next_parent then destination_node = next_node end end return destination_node end module.on_event = function(event) if event.split_type[2] == "sync-parsers" then local install = require("nvim-treesitter.install") install.commands.TSInstallSync["run!"]("norg") install.commands.TSInstallSync["run!"]("norg_meta") end end module.events.subscribed = { ["core.neorgcmd"] = { ["sync-parsers"] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/integrations/truezen/module.lua ================================================ --[[ file: Truezen-Integration title: A TrueZen integration for Neorg summary: Integrates the TrueZen module for use within Neorg. internal: true --- This is a basic wrapper around truezen that allows one to toggle the atraxis mode programmatically. --]] local neorg = require("neorg.core") local modules, log = neorg.modules, neorg.log local module = modules.create("core.integrations.truezen") module.setup = function() local success, truezen = pcall(require, "true-zen") if not success then log.warn("Could not find module: `true-zen`. Please ensure you have true-zen installed.") return { success = false } end module.private.truezen = truezen end module.private = { truezen = nil, } ---@class core.integrations.truezen module.public = { toggle_ataraxis = function() vim.cmd(":TZAtaraxis") end, } return module ================================================ FILE: lua/neorg/modules/core/integrations/zen_mode/module.lua ================================================ --[[ file: ZenMode-Integration title: An integration for `zen-mode` summary: Integrates and exposes the functionality of `zen-mode` in Neorg. internal: true --- This is a basic wrapper around `zen_mode` that allows one to toggle the zen mode programatically. --]] local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.integrations.zen_mode") module.setup = function() local success, zen_mode = pcall(require, "zen_mode") if not success then return { success = false } end module.private.zen_mode = zen_mode end module.private = { zen_mode = nil, } ---@class core.integrations.zen_mode module.public = { toggle = function() vim.cmd(":ZenMode") end, } return module ================================================ FILE: lua/neorg/modules/core/itero/module.lua ================================================ --[[ file: Itero title: Fast List/Heading Continuation description: Fluidness is key, after all. summary: Module designed to continue lists, headings and other iterables. embed: https://user-images.githubusercontent.com/76052559/216777858-14e2036e-acc5-4276-aa7d-9a8a8ba549ba.gif --- `core.itero` is a rather small and simple module designed to assist in the creation of many lists, headings and other repeatable (iterable) items. By default, the key that is used to iterate on an item is `` (Alt + Enter). If you want to change the bind, remap the `neorg.itero.next-iteration` event. Begin by writing an initial item you'd like to iterate (in this instance, and unordered list item): ```md - Hello World! ``` With your cursor in insert mode at the end of the line, pressing the keybind will continue the item at whatever nesting level it is currently at (where `|` is the new cursor position): ```md - Hello World! - | ``` The same can also be done for headings: ```md * Heading 1 * | ``` This functionality is commonly paired with the [`core.promo`](@core.promo) module to then indent/dedent the item under the cursor with the `` and `` bindings. --]] local neorg = require("neorg.core") local lib, log, modules = neorg.lib, neorg.log, neorg.modules local module = modules.create("core.itero") module.setup = function() return { requires = { "core.integrations.treesitter", }, } end module.config.public = { -- A list of lua patterns detailing what treesitter nodes can be "iterated". -- Usually doesn't need to be changed, unless you want to disable some -- items from being iterable. iterables = { "unordered_list%d", "ordered_list%d", "heading%d", "quote%d", }, -- Which item types to retain extensions for. -- -- If the item you are currently iterating has an extension (e.g. `( )`, `(x)` etc.), -- then the following items will also have an extension (by default `( )`) attached -- to them automatically. retain_extensions = { ["unordered_list%d"] = true, ["ordered_list%d"] = true, }, } module.config.private = { stop_types = { "generic_list", "quote", }, } module.load = function() vim.keymap.set("!", "(neorg.itero.next-iteration)", module.public.next_iteration_cr) end ---@class core.itero module.public = { next_iteration_cr = function() local cursor = vim.api.nvim_win_get_cursor(0) local buffer = vim.api.nvim_get_current_buf() local ts = module.required["core.integrations.treesitter"] local cursor_pos = cursor[1] - 1 local current = ts.get_first_node_on_line(buffer, cursor_pos, module.config.private.stop_types) if not current then log.error( "Treesitter seems to be high and can't properly grab the node under the cursor. Perhaps try again?" ) return end while current and current:parent() do if lib.filter(module.config.public.iterables, function(_, iterable) return current:type():match(table.concat({ "^", iterable, "$" })) and iterable or nil end) then break end current = current:parent() end if not current or current:type() == "document" then vim.api.nvim_feedkeys(vim.keycode(""), "n", false) return end local should_append_extension = lib.filter( module.config.public.retain_extensions, function(match, should_append) return current:type():match(match) and should_append or nil end ) and current:named_child(1) and current:named_child(1):type() == "detached_modifier_extension" local text_to_repeat = ts.get_node_text(current:named_child(0), buffer) local _, column = current:start() local is_on_nonempty_line = vim.api.nvim_buf_get_lines(buffer, cursor_pos, cursor_pos + 1, true)[1]:match("%S") if is_on_nonempty_line then cursor_pos = cursor_pos + 1 end vim.api.nvim_buf_set_lines( buffer, cursor_pos, cursor_pos + (is_on_nonempty_line and 0 or 1), true, { string.rep(" ", column) .. text_to_repeat .. (should_append_extension and "( ) " or "") } ) vim.api.nvim_win_set_cursor( 0, { cursor_pos + 1, column + text_to_repeat:len() + (should_append_extension and ("( ) "):len() or 0) } ) end, } return module ================================================ FILE: lua/neorg/modules/core/journal/module.lua ================================================ --[[ file: Journal title: Dear diary... description: The journal module allows you to take personal notes with zero friction. summary: Easily track a journal within Neorg. --- The journal module exposes a total of six commands. The first three, `:Neorg journal today|yesterday|tomorrow`, allow you to access entries for a given time relative to today. A file will be opened with the respective date as a `.norg` file. The fourth command, `:Neorg journal custom`, allows you to specify a custom date as an argument. The date must be formatted according to the `YYYY-mm-dd` format, e.g. `2023-01-01`. The `:Neorg journal template` command creates a template file which will be used as the base whenever a new journal entry is created. Last but not least, the `:Neorg journal toc open|update` commands open or create/update a Table of Contents file found in the root of the journal. This file contains links to all other journal entries, alongside their titles. --]] local neorg = require("neorg.core") local config, lib, log, modules = neorg.config, neorg.lib, neorg.log, neorg.modules local module = modules.create("core.journal") module.examples = { ["Changing TOC format to divide year in quarters"] = function() -- In your ["core.journal"] options, change toc_format to a function like this: require("neorg").setup({ load = { -- ... ["core.journal"] = { config = { -- ... toc_format = function(entries) -- Convert the entries into a certain format local output = {} local current_year local current_quarter local last_quarter local current_month for _, entry in ipairs(entries) do -- Don't print the year if it hasn't changed if not current_year or current_year < entry[1] then current_year = entry[1] current_month = nil table.insert(output, "* " .. current_year) end -- Check to which quarter the current month corresponds to if entry[2] <= 3 then current_quarter = 1 elseif entry[2] <= 6 then current_quarter = 2 elseif entry[2] <= 9 then current_quarter = 3 else current_quarter = 4 end -- If the current month corresponds to another quarter, print it if current_quarter ~= last_quarter then table.insert(output, "** Quarter " .. current_quarter) last_quarter = current_quarter end -- Don't print the month if it hasn't changed if not current_month or current_month < entry[2] then current_month = entry[2] table.insert(output, "*** Month " .. current_month) end -- Prints the file link table.insert(output, " " .. entry[4] .. string.format("[%s]", entry[5])) end return output end, -- ... }, }, }, }) end, } module.setup = function() return { success = true, requires = { "core.dirman", "core.integrations.treesitter", }, } end ---@type core.integrations.treesitter local ts module.load = function() ts = module.required["core.integrations.treesitter"] --[[@as core.integrations.treesitter]] if module.config.private.strategies[module.config.public.strategy] then module.config.public.strategy = module.config.private.strategies[module.config.public.strategy] end modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ journal = { min_args = 1, max_args = 2, subcommands = { tomorrow = { args = 0, name = "journal.tomorrow" }, yesterday = { args = 0, name = "journal.yesterday" }, today = { args = 0, name = "journal.today" }, custom = { max_args = 1, name = "journal.custom" }, -- format :yyyy-mm-dd template = { args = 0, name = "journal.template" }, toc = { args = 1, name = "journal.toc", subcommands = { open = { args = 0, name = "journal.toc.open" }, update = { args = 0, name = "journal.toc.update" }, }, }, }, }, }) end) end module.config.public = { -- Which workspace to use for the journal files, the default behaviour -- is to use the current workspace. -- -- It is recommended to set this to a static workspace, but the most optimal -- behaviour may vary from workflow to workflow. workspace = nil, -- The name for the folder in which the journal files are put. journal_folder = "journal", -- The strategy to use to create directories. -- May be "flat" (`2022-03-02.norg`), "nested" (`2022/03/02.norg`), -- a lua string with the format given to `os.date()` or a lua function -- that returns a lua string with the same format. strategy = "nested", -- The name of the template file to use when running `:Neorg journal template`. template_name = "template.norg", -- Whether to apply the template file to new journal entries. use_template = true, -- Formatter function used to generate the toc file. -- Receives a table that contains tables like { yy, mm, dd, link, title }. -- -- The function must return a table of strings. toc_format = nil, } module.config.private = { strategies = { flat = "%Y-%m-%d.norg", nested = "%Y" .. config.pathsep .. "%m" .. config.pathsep .. "%d.norg", }, } ---@class core.journal module.public = { version = "0.0.9", --- Opens a diary entry at the given time ---@param time? number #The time to open the journal entry at as returned by `os.time()` ---@param custom_date? string #A YYYY-mm-dd string that specifies a date to open the diary at instead open_diary = function(time, custom_date) -- TODO(vhyrro): Change this to use Norg dates! local workspace = module.config.public.workspace or module.required["core.dirman"].get_current_workspace()[1] local folder_name = module.config.public.journal_folder local template_name = module.config.public.template_name if custom_date then local year, month, day = custom_date:match("^(%d%d%d%d)-(%d%d)-(%d%d)$") if not year or not month or not day then log.error("Wrong date format: use YYYY-mm-dd") return end time = os.time({ year = year, month = month, day = day, }) end local path = os.date( type(module.config.public.strategy) == "function" and module.config.public.strategy(os.date("*t", time)) or module.config.public.strategy, time ) local workspace_path = module.required["core.dirman"].get_workspace(workspace) local journal_file_exists = module.required["core.dirman"].file_exists(workspace_path .. "/" .. folder_name .. config.pathsep .. path) module.required["core.dirman"].create_file(folder_name .. config.pathsep .. path, workspace) module.required["core.dirman"].create_file(folder_name .. config.pathsep .. path, workspace) if not journal_file_exists and module.config.public.use_template and module.required["core.dirman"].file_exists(workspace_path .. "/" .. folder_name .. "/" .. template_name) then vim.cmd("$read " .. workspace_path .. "/" .. folder_name .. "/" .. template_name .. "| 1d | w") end end, --- Opens a diary entry for tomorrow's date diary_tomorrow = function() module.public.open_diary(os.time() + 24 * 60 * 60) end, --- Opens a diary entry for yesterday's date diary_yesterday = function() module.public.open_diary(os.time() - 24 * 60 * 60) end, --- Opens a diary entry for today's date diary_today = function() module.public.open_diary() end, create_template = function() local workspace = module.config.public.workspace local folder_name = module.config.public.journal_folder local template_name = module.config.public.template_name module.required["core.dirman"].create_file( folder_name .. config.pathsep .. template_name, workspace or module.required["core.dirman"].get_current_workspace()[1] ) end, --- Opens the toc file open_toc = function() local workspace = module.config.public.workspace or module.required["core.dirman"].get_current_workspace()[1] local index = modules.get_module_config("core.dirman").index local folder_name = module.config.public.journal_folder -- If the toc exists, open it, if not, create it if module.required["core.dirman"].file_exists(folder_name .. config.pathsep .. index) then module.required["core.dirman"].open_file(workspace, folder_name .. config.pathsep .. index) else module.public.create_toc() end end, --- Creates or updates the toc file create_toc = function() local workspace = module.config.public.workspace or module.required["core.dirman"].get_current_workspace()[1] local index = modules.get_module_config("core.dirman").index local workspace_path = module.required["core.dirman"].get_workspace(workspace) local workspace_name_for_links = module.config.public.workspace or "" local folder_name = module.config.public.journal_folder -- Each entry is a table that contains tables like { yy, mm, dd, link, title } local toc_entries = {} -- Get a filesystem handle for the files in the journal folder -- path is for each subfolder local get_fs_handle = function(path) path = path or "" local handle = vim.loop.fs_scandir(workspace_path .. config.pathsep .. folder_name .. config.pathsep .. path) if type(handle) ~= "userdata" then error(lib.lazy_string_concat("Failed to scan directory '", workspace, path, "': ", handle)) end return handle end ---Gets the title from the metadata of a file, must be called in a vim.schedule ---@param file PathlibPath | string ---@return string? local get_title = function(file) local path = workspace_path / folder_name / file local meta if vim.fn.bufexists(tostring(path)) == 1 then local buf = vim.fn.bufloaded(vim.fn.bufnr(tostring(path))) meta = ts.get_document_metadata(buf) else meta = ts.get_document_metadata(path) end if meta then return meta.title end end vim.loop.fs_scandir(workspace_path .. config.pathsep .. folder_name .. config.pathsep, function(err, handle) assert(not err, lib.lazy_string_concat("Unable to generate TOC for directory '", folder_name, "' - ", err)) while true do -- Name corresponds to either a YYYY-mm-dd.norg file, or just the year ("nested" strategy) local name, type = vim.loop.fs_scandir_next(handle) ---@diagnostic disable-line -- TODO: type error workaround if not name then break end -- Handle nested entries if type == "directory" then local years_handle = get_fs_handle(name) while true do -- mname is the month local mname, mtype = vim.loop.fs_scandir_next(years_handle) if not mname then break end if mtype == "directory" then local months_handle = get_fs_handle(name .. config.pathsep .. mname) while true do -- dname is the day local dname, dtype = vim.loop.fs_scandir_next(months_handle) if not dname then break end -- If it's a .norg file, also ensure it is a day entry if dtype == "file" and string.match(dname, "%d%d%.norg") then -- Split the file name local file = vim.split(dname, ".", { plain = true }) vim.schedule(function() -- Get the title from the metadata, else, it just defaults to the name of the file local title = get_title( name .. config.pathsep .. mname .. config.pathsep .. dname ) or file[1] -- Insert a new entry table.insert(toc_entries, { tonumber(name), tonumber(mname), tonumber(file[1]), "{:$" .. workspace_name_for_links .. config.pathsep .. module.config.public.journal_folder .. config.pathsep .. name .. config.pathsep .. mname .. config.pathsep .. file[1] .. ":}", title, }) end) end end end end end -- Handles flat entries -- If it is a .norg file, but it's not any user generated file. -- The match is here to avoid handling files made by the user, like a template file, or -- the toc file if type == "file" and string.match(name, "%d+-%d+-%d+%.norg") then -- Split yyyy-mm-dd to a table local file = vim.split(name, ".", { plain = true }) local parts = vim.split(file[1], "-") -- Convert the parts into numbers for k, v in pairs(parts) do parts[k] = tonumber(v) ---@diagnostic disable-line -- TODO: type error workaround end vim.schedule(function() -- Get the title from the metadata, else, it just defaults to the name of the file local title = get_title(name) or parts[3] -- And insert a new entry that corresponds to the file table.insert(toc_entries, { parts[1], parts[2], parts[3], "{:$" .. workspace_name_for_links .. config.pathsep .. module.config.public.journal_folder .. config.pathsep .. file[1] .. ":}", title, }) end) end end vim.schedule(function() -- Gets a default format for the entries local format = module.config.public.toc_format or function(entries) local months_text = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", } -- Convert the entries into a certain format to be written local output = {} local current_year local current_month for _, entry in ipairs(entries) do -- Don't print the year and month if they haven't changed if not current_year or current_year < entry[1] then current_year = entry[1] current_month = nil table.insert(output, "* " .. current_year) end if not current_month or current_month < entry[2] then current_month = entry[2] table.insert(output, "** " .. months_text[current_month]) end -- Prints the file link table.insert(output, " " .. entry[4] .. string.format("[%s]", entry[5])) end return output end module.required["core.dirman"].create_file( folder_name .. config.pathsep .. index, workspace or module.required["core.dirman"].get_current_workspace()[1] ) -- The current buffer now must be the toc file, so we set our toc entries there vim.api.nvim_buf_set_lines(0, 0, -1, false, format(toc_entries)) vim.cmd("w") end) end) end, } module.on_event = function(event) if event.split_type[1] == "core.neorgcmd" then if event.split_type[2] == "journal.tomorrow" then module.public.diary_tomorrow() elseif event.split_type[2] == "journal.yesterday" then module.public.diary_yesterday() elseif event.split_type[2] == "journal.custom" then if not event.content[1] then local calendar = modules.get_module("core.ui.calendar") if not calendar then log.error("[ERROR]: `core.ui.calendar` is not loaded! Said module is required for this operation.") return end calendar.select_date({ callback = vim.schedule_wrap(function(osdate) module.public.open_diary( nil, string.format("%04d", osdate.year) .. "-" .. string.format("%02d", osdate.month) .. "-" .. string.format("%02d", osdate.day) ) end), }) else module.public.open_diary(nil, event.content[1]) end elseif event.split_type[2] == "journal.today" then module.public.diary_today() elseif event.split_type[2] == "journal.template" then module.public.create_template() elseif event.split_type[2] == "journal.toc.open" then module.public.open_toc() elseif event.split_type[2] == "journal.toc.update" then module.public.create_toc() end end end module.events.subscribed = { ["core.neorgcmd"] = { ["journal.yesterday"] = true, ["journal.tomorrow"] = true, ["journal.today"] = true, ["journal.custom"] = true, ["journal.template"] = true, ["journal.toc.update"] = true, ["journal.toc.open"] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/keybinds/module.lua ================================================ --[[ file: User-Keybinds title: The Language of Neorg description: `core.keybinds` manages mappings for operations on or in `.norg` files. summary: Module for managing keybindings with Neorg mode support. --- The `core.keybinds` module configures an out-of-the-box Neovim experience by providing a default set of keys. To disable default keybinds, see the next section. To remap the existing keys, see [here](https://github.com/nvim-neorg/neorg/wiki/User-Keybinds#remapping-keys). To find common problems, consult the [FAQ](https://github.com/nvim-neorg/neorg/wiki/User-Keybinds#faq). ### Disabling Default Keybinds By default when you load the `core.keybinds` module all keybinds will be enabled. If you would like to change this, be sure to set `default_keybinds` to `false`: ```lua ["core.keybinds"] = { config = { default_keybinds = false, }, } ``` ### Remapping Keys To understand how to effectively remap keys, one must understand how keybinds are set. Neorg binds actions to various `` mappings that look like so: `(neorg...`. To remap a key, simply map an action somewhere in your configuration: ```lua vim.keymap.set("n", "my-key-here", "(neorg.pivot.list.toggle)", {}) ``` Neorg will recognize that the key has been bound by you and not bind its own key. #### Binding Keys for Norg Files Only This approach has a downside - all of Neorg's keybinds are set on a per-buffer basis so that keybinds don't "overflow" into buffers you don't want them active in. When you map a key using `vim.keymap.set`, you set a global key which is always active, even in non-norg files. There are two ways to combat this: - Create a file under `/ftplugin/norg.lua`: ```lua vim.keymap.set("n", "my-key-here", "(neorg.pivot.list.toggle)", { buffer = true }) ``` - Create an autocommand using `vim.api.nvim_create_autocmd`: ```lua vim.api.nvim_create_autocmd("Filetype", { pattern = "norg", callback = function() vim.keymap.set("n", "my-key-here", "(neorg.pivot.list.toggle)", { buffer = true }) end, }) ``` Notice that in both situations a `{ buffer = true }` was supplied to the function. This way, your remapped keys will never interfere with other files. ### Discovering Keys A comprehensive list of all keybinds can be found on [this page!](https://github.com/nvim-neorg/neorg/wiki/Default-Keybinds) ## FAQ ### Some (or all) keybinds do not work Neorg refuses to bind keys when it knows they'll interfere with your configuration. Run `:checkhealth neorg` to see a full list of what keys Neorg has considered "conflicted" or "rebound". If you see that *all* of your keybinds are in conflict, you're likely using a plugin that is mapping to your local leader key. This is a known issue with older versions of `which-key.nvim`. Since version `3.0` of which-key the issue has been fixed - we recommend updating to the latest version to resolve the errors. --]] local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.keybinds") local bound_keys = {} module.load = function() if module.config.public.default_keybinds then local preset = module.private.presets[module.config.public.preset] assert(preset, string.format("keybind preset `%s` does not exist!", module.config.public.preset)) module.public.set_keys_for(false, preset.all) vim.api.nvim_create_autocmd("FileType", { pattern = "norg", callback = function(ev) module.public.set_keys_for(ev.buf, preset.norg) end, }) end end module.config.public = { -- Whether to enable the default keybinds. default_keybinds = true, -- Which keybind preset to use. -- Currently allows only a single value: `"neorg"`. preset = "neorg", } ---@class core.keybinds module.public = { --- Adds a set of default keys for Neorg to bind. --- Should be used exclusively by external modules wanting to provide their own default keymaps. ---@param name string The name of the preset to extend (allows for providing default keymaps for various presets) ---@param preset neorg.keybinds.preset The preset data itself. extend_preset = function(name, preset) local original_preset = assert(module.private.presets[name], "provided preset doesn't exist!") local function extend(a, b) for k, v in pairs(b) do if type(v) == "table" then if vim.islist(v) then vim.list_extend(a[k], v) else extend(a[k], v) end end a[k] = v end end extend(original_preset, preset) module.public.bind_norg_keys(vim.api.nvim_get_current_buf()) end, ---@param buffer number|boolean ---@param preset_subdata table set_keys_for = function(buffer, preset_subdata) for mode, keybinds in pairs(preset_subdata) do bound_keys[mode] = bound_keys[mode] or {} for _, keybind in ipairs(keybinds) do if vim.fn.hasmapto(keybind[2], mode, false) == 0 and vim.fn.mapcheck(keybind[1], mode, false):len() == 0 then local opts = vim.tbl_deep_extend("force", { buffer = buffer }, keybind.opts or {}) vim.keymap.set(mode, keybind[1], keybind[2], opts) bound_keys[mode][keybind[1]] = true end end end end, --- Checks the health of keybinds. Returns all remaps and all conflicts in a table. ---@return { preset_exists: boolean, remaps: table, conflicts: table } health = function() local preset = module.private.presets[module.config.public.preset] if not preset then return { preset_exists = false, } end local remaps = {} local conflicts = {} local function check_keys_for(data) for mode, keybinds in pairs(data) do for _, keybind in ipairs(keybinds) do if not bound_keys[mode] or not bound_keys[mode][keybind[1]] then if vim.fn.hasmapto(keybind[2], mode, false) ~= 0 then remaps[keybind[1]] = keybind[2] elseif vim.fn.mapcheck(keybind[1], mode, false):len() ~= 0 then conflicts[keybind[1]] = keybind[2] end end end end end check_keys_for(preset.all) check_keys_for(preset.norg) return { preset_exists = true, remaps = remaps, conflicts = conflicts, } end, } module.private = { -- TODO: Move these to the "vim" preset -- { "gd", "(neorg.esupports.hop.hop-link)", opts = { desc = "[neorg] Jump to Link" } }, -- { "gf", "(neorg.esupports.hop.hop-link)", opts = { desc = "[neorg] Jump to Link" } }, -- { "gF", "(neorg.esupports.hop.hop-link)", opts = { desc = "[neorg] Jump to Link" } }, presets = { ---@class neorg.keybinds.preset neorg = { all = { n = { -- Create a new `.norg` file to take notes in -- ^New Note { "nn", "(neorg.dirman.new-note)", opts = { desc = "[neorg] Create New Note" }, }, -- Create a Table of Contents -- ^Table of Contents { "gO", "Neorg toc", opts = { desc = "[neorg] Create Table of Contents" }, }, }, }, norg = { n = { -- Mark the task under the cursor as "undone" -- ^mark Task as Undone { "tu", "(neorg.qol.todo-items.todo.task-undone)", opts = { desc = "[neorg] Mark as Undone" }, }, -- Mark the task under the cursor as "pending" -- ^mark Task as Pending { "tp", "(neorg.qol.todo-items.todo.task-pending)", opts = { desc = "[neorg] Mark as Pending" }, }, -- Mark the task under the cursor as "done" -- ^mark Task as Done { "td", "(neorg.qol.todo-items.todo.task-done)", opts = { desc = "[neorg] Mark as Done" }, }, -- Mark the task under the cursor as "on-hold" -- ^mark Task as on Hold { "th", "(neorg.qol.todo-items.todo.task-on-hold)", opts = { desc = "[neorg] Mark as On Hold" }, }, -- Mark the task under the cursor as "cancelled" -- ^mark Task as Cancelled { "tc", "(neorg.qol.todo-items.todo.task-cancelled)", opts = { desc = "[neorg] Mark as Cancelled" }, }, -- Mark the task under the cursor as "recurring" -- ^mark Task as Recurring { "tr", "(neorg.qol.todo-items.todo.task-recurring)", opts = { desc = "[neorg] Mark as Recurring" }, }, -- Mark the task under the cursor as "important" -- ^mark Task as Important { "ti", "(neorg.qol.todo-items.todo.task-important)", opts = { desc = "[neorg] Mark as Important" }, }, -- Mark the task under the cursor as "ambiguous" -- ^mark Task as Ambiguous { "ta", "(neorg.qol.todo-items.todo.task-ambiguous)", opts = { desc = "[neorg] Mark as Ambiguous" }, }, -- Switch the task under the cursor between a select few states { "", "(neorg.qol.todo-items.todo.task-cycle)", opts = { desc = "[neorg] Cycle Task" }, }, -- Hop to the destination of the link under the cursor { "", "(neorg.esupports.hop.hop-link)", opts = { desc = "[neorg] Jump to Link" } }, -- Same as ``, except open the destination in a vertical split { "", "(neorg.esupports.hop.hop-link.vsplit)", opts = { desc = "[neorg] Jump to Link (Vertical Split)" }, }, -- Same as ``, except open the destination in a new tab -- If destination is already open in an existing tab, just navigate to it { "", "(neorg.esupports.hop.hop-link.tab-drop)", opts = { desc = "[neorg] Jump to Link (Tab Drop)" }, }, -- Promote an object non-recursively { ">.", "(neorg.promo.promote)", opts = { desc = "[neorg] Promote Object (Non-Recursively)" }, }, -- Demote an object non-recursively { "<,", "(neorg.promo.demote)", opts = { desc = "[neorg] Demote Object (Non-Recursively)" } }, -- Promote an object recursively { ">>", "(neorg.promo.promote.nested)", opts = { desc = "[neorg] Promote Object (Recursively)" }, }, -- Demote an object recursively { "<<", "(neorg.promo.demote.nested)", opts = { desc = "[neorg] Demote Object (Recursively)" }, }, -- Toggle a list from ordered <-> unordered -- ^List Toggle { "lt", "(neorg.pivot.list.toggle)", opts = { desc = "[neorg] Toggle (Un)ordered List" }, }, -- Invert all items in a list -- Unlike `lt`, inverting a list will respect mixed list -- items, instead of snapping all list types to a single one. -- ^List Invert { "li", "(neorg.pivot.list.invert)", opts = { desc = "[neorg] Invert (Un)ordered List" }, }, -- Insert a link to a date at the given position -- ^Insert Date { "id", "(neorg.tempus.insert-date)", opts = { desc = "[neorg] Insert Date" } }, -- Magnifies a code block to a separate buffer. -- ^Code Magnify { "cm", "(neorg.looking-glass.magnify-code-block)", opts = { desc = "[neorg] Magnify Code Block" }, }, }, i = { -- Promote an object recursively { "", "(neorg.promo.promote)", opts = { desc = "[neorg] Promote Object (Recursively)" }, }, -- Demote an object recursively { "", "(neorg.promo.demote)", opts = { desc = "[neorg] Demote Object (Recursively)" } }, -- Create an iteration of e.g. a list item { "", "(neorg.itero.next-iteration)", opts = { desc = "[neorg] Continue Object" } }, -- Insert a link to a date at the current cursor position -- ^Date { "", "(neorg.tempus.insert-date.insert-mode)", opts = { desc = "[neorg] Insert Date" }, }, }, v = { -- Promote objects in range { ">", "(neorg.promo.promote.range)", opts = { desc = "[neorg] Promote Objects in Range" } }, -- Demote objects in range { "<", "(neorg.promo.demote.range)", opts = { desc = "[neorg] Demote Objects in Range" } }, }, }, }, }, } return module ================================================ FILE: lua/neorg/modules/core/latex/renderer/module.lua ================================================ --[[ file: Core-Latex-Renderer title: Rendering LaTeX with image.nvim summary: An experimental module for rendering latex images inline. --- This is an experimental module that requires nvim 0.10+. It renders LaTeX snippets as images making use of the image.nvim plugin. By default, images are only rendered after running the command: `:Neorg render-latex`. Rendering can be disabled with `:Neorg render-latex disable` Requires: - The [image.nvim](https://github.com/3rd/image.nvim) neovim plugin. - `latex` executable in path with the following packages: - standalone - amsmath - amssymb - graphicx - `dvipng` executable in path (normally comes with LaTeX) There's a highlight group that controls the foreground color of the rendered latex: `@norg.rendered.latex`, configurable in `core.highlights` --]] local nio local neorg = require("neorg.core") local module = neorg.modules.create("core.latex.renderer") local modules = neorg.modules module.setup = function() return { requires = { "core.integrations.image", "core.integrations.treesitter", "core.autocommands", "core.neorgcmd", "core.highlights", }, } end module.config.public = { -- When true, images of rendered LaTeX will cover the source LaTeX they were produced from. -- Setting this value to false creates more lag, and can be buggy with large numbers of images. conceal = true, -- "Dots Per Inch" increasing this value will result in crisper images at the expense of -- performance dpi = 350, -- When true, images will render when a `.norg` buffer is entered render_on_enter = false, -- Module that renders the images. "core.integrations.image" makes use of image.nvim and is -- currently the only option renderer = "core.integrations.image", -- Don't re-render anything until 200ms after the buffer has stopped changing. Lowering will -- lead to a more seamless experience but will cause more temporary images to be created debounce_ms = 200, -- Only render latex snippets that are longer than this many chars. Escaped chars are removed -- spaces are counted, `$` and `$|`/`|$` are not (ie. `$\\int$` counts as 4 chars) min_length = 3, -- Make the images larger or smaller by adjusting the scale. Will not pad images with virtual -- text when `conceal = true`, so they can overlap text. Images will not be blown up larger than -- their true size, so images may still render one line tall. scale = 1, } ---@class Image ---@field path string -- and many other fields that I don't necessarily need ---@class MathRange ---@field image Image our limited representation of an image ---@field range Range4 last range of the math block. Updated based on the extmark ---@field snippet string cleaned latex snippet ---@field extmark_id number? when rendered, the extmark_id that belongs to this image ---@field real boolean tag ranges that are confirmed to still exist by TS ---Compute and set the foreground color string local function compute_foreground() local neorg_hi = neorg.modules.get_module("core.highlights") assert(neorg_hi, "Failed to load core.highlights") local hi = vim.api.nvim_get_hl(0, { name = "@neorg.rendered.latex", link = false }) if not vim.tbl_isempty(hi) then local r, g, b = neorg_hi.hex_to_rgb(("%06x"):format(hi.fg)) module.private.foreground = ("rgb %s %s %s"):format(r / 255., g / 255., b / 255.) else -- grey module.private.foreground = "rgb 0.5 0.5 0.5" end end module.load = function() local success, image = pcall(neorg.modules.get_module, module.config.public.renderer) assert(success, "Unable to load image module") nio = require("nio") -- compute the foreground color in rgb compute_foreground() ---@type string[] ids module.private.cleared_at_cursor = {} ---Image cache. latex snippet to file path ---@type table module.private.image_paths = {} ---@type table> module.private.latex_images = {} module.private.image_api = image module.private.extmark_ns = vim.api.nvim_create_namespace("neorg-latex-concealer") module.private.do_render = module.config.public.render_on_enter module.required["core.autocommands"].enable_autocommand("BufWinEnter") module.required["core.autocommands"].enable_autocommand("CursorMoved") module.required["core.autocommands"].enable_autocommand("TextChanged") module.required["core.autocommands"].enable_autocommand("TextChangedI") modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ ["render-latex"] = { name = "latex.render.render", min_args = 0, max_args = 1, subcommands = { enable = { args = 0, name = "latex.render.enable", }, disable = { args = 0, name = "latex.render.disable", }, toggle = { args = 0, name = "latex.render.toggle", }, }, condition = "norg", }, }) end) end ---Get the key for a given range ---@param range Range2 | Range4 module.private.get_key = function(range) return ("%d:%d"):format(range[1], range[2]) end ---@class core.latex.renderer module.public = { ---@async ---@param buf number async_latex_renderer = function(buf) -- Update all the limage keys to their new extmark locations ---@type table local new_limages = {} for _, limage in pairs(module.private.latex_images[buf] or {}) do if limage.extmark_id then local extmark = nio.api.nvim_buf_get_extmark_by_id(buf, module.private.extmark_ns, limage.extmark_id, {}) local new_key = module.private.get_key({ extmark[1], extmark[2] }) limage.real = false new_limages[new_key] = limage end end module.private.cleared_at_cursor = {} module.required["core.integrations.treesitter"].execute_query( [[ ( (inline_math) @latex (#offset! @latex 0 1 0 -1) ) ]], function(query, id, node) if query.captures[id] ~= "latex" then return end local original_snippet = module.required["core.integrations.treesitter"].get_node_text(node, nio.api.nvim_get_current_buf()) local clean_snippet = string.gsub(original_snippet, "^%$|", "$") clean_snippet = string.gsub(clean_snippet, "|%$$", "$") if clean_snippet == original_snippet then -- this is a normal math block, we need to remove leading `\` chars -- TODO: test that this regex is actually correct clean_snippet = string.gsub(clean_snippet, "\\(.)", "%1") end -- `- 2` for the two `$`s if string.len(clean_snippet) - 2 < module.config.public.min_length then return end local png_location = module.private.image_paths[clean_snippet] or module.public.async_generate_image(clean_snippet) if not png_location then return end module.private.image_paths[clean_snippet] = png_location local range = { node:range() } local key = module.private.get_key(range) -- If there's already an image at this location and it's the same snippet, don't do -- anything if new_limages[key] then if new_limages[key].snippet == clean_snippet then new_limages[key].range = range new_limages[key].real = true return end end local img = module.private.image_api.new_image( buf, png_location, module.required["core.integrations.treesitter"].get_node_range(node), nio.api.nvim_get_current_win(), module.config.public.scale, not module.config.public.conceal ) local existing_ext_id = new_limages[key] and new_limages[key].extmark_id new_limages[key] = { image = img, range = range, snippet = clean_snippet, real = true, extmark_id = existing_ext_id, } end, buf ) nio.scheduler() for key, limage in pairs(new_limages) do if not limage.real then module.private.image_api.clear({ [key] = limage }) if limage.extmark_id then nio.api.nvim_buf_del_extmark(0, module.private.extmark_ns, limage.extmark_id) end new_limages[key] = nil end end module.private.latex_images[buf] = new_limages end, ---Writes a latex snippet to a file and wraps it with latex headers so it will render nicely ---@async ---@param snippet string latex snippet (if it's math it should include the surrounding $$) ---@return string temp file path async_create_latex_document = function(snippet) local tempname = nio.fn.tempname() local tempfile = nio.file.open(tempname, "w") local content = table.concat({ "\\documentclass[6pt]{standalone}", "\\usepackage{amsmath}", "\\usepackage{amssymb}", "\\usepackage{graphicx}", "\\begin{document}", snippet, "\\end{document}", }, "\n") tempfile.write(content) tempfile.close() return tempname end, ---Returns a filepath where the rendered image sits ---@async ---@param snippet string the full latex snippet to convert to an image ---@return string | nil async_generate_image = function(snippet) local document_name = module.public.async_create_latex_document(snippet) if not document_name then return end local cwd = nio.fn.fnamemodify(document_name, ":h") local create_dvi = nio.process.run({ cmd = "latex", args = { "--interaction=nonstopmode", "--output-format=dvi", document_name, }, cwd = cwd, }) if not create_dvi or type(create_dvi) == "string" then return end local res = create_dvi.result() if res ~= 0 then return end local png_result = nio.fn.tempname() png_result = ("%s.png"):format(png_result) local dvipng = nio.process.run({ cmd = "dvipng", args = { "-D", module.config.public.dpi, "-T", "tight", "-bg", "Transparent", "-fg", module.private.foreground, "-o", png_result, document_name .. ".dvi", }, }) if not dvipng or type(dvipng) == "string" then return end res = dvipng.result() if res ~= 0 then return end return png_result end, ---Actually renders the images (along with any extmarks it needs) ---@param images table render_inline_math = function(images, buffer) local conceallevel = vim.api.nvim_get_option_value("conceallevel", { win = 0 }) local cursor_row = vim.api.nvim_win_get_cursor(0)[1] local conceal_on = conceallevel >= 2 and module.config.public.conceal -- Create all extmarks before rendering images b/c these extmarks will change the -- position of the images for _, limage in pairs(images) do local range = limage.range local ext_opts = { end_col = range[4], strict = false, invalidate = true, undo_restore = false, id = limage.extmark_id, -- if it exists, update it, else this is nil so it will create a new one } if module.config.public.conceal then local image = limage.image local predicted_image_dimensions = module.private.image_api.image_size(image, { height = module.config.public.scale }) if range[1] ~= cursor_row - 1 then ext_opts.virt_text = { { (" "):rep(predicted_image_dimensions.width) } } ext_opts.virt_text_pos = "inline" end end if conceal_on and range[1] ~= cursor_row - 1 then ext_opts.conceal = "" end limage.extmark_id = vim.api.nvim_buf_set_extmark(buffer, module.private.extmark_ns, range[1], range[2], ext_opts) end for key, limage in pairs(images) do local range = limage.range if conceal_on and range[1] == cursor_row - 1 then table.insert(module.private.cleared_at_cursor, key) module.private.image_api.clear({ limage }) if limage.extmark_id then vim.api.nvim_buf_set_extmark(buffer, module.private.extmark_ns, range[1], range[2], { virt_text = { { "" } }, }) end goto continue end module.private.image_api.render({ limage }) ::continue:: end end, } local running_proc = nil local render_timer = nil local function render_latex() local buf = vim.api.nvim_get_current_buf() if not module.private.do_render then if render_timer then render_timer:stop() render_timer:close() render_timer = nil end return end if not render_timer then render_timer = vim.uv.new_timer() end render_timer:start(module.config.public.debounce_ms, 0, function() render_timer:stop() render_timer:close() render_timer = nil if not running_proc then running_proc = nio.run( function() nio.scheduler() module.public.async_latex_renderer(buf) end, vim.schedule_wrap(function() module.public.render_inline_math(module.private.latex_images[buf] or {}, buf) running_proc = nil end) ) end end) end local function clear_at_cursor() local buf = vim.api.nvim_get_current_buf() if not module.private.do_render or render_timer then return end if module.config.public.conceal and module.private.latex_images[buf] ~= nil then local cleared = module.private.image_api.clear_at_cursor( module.private.latex_images[buf], vim.api.nvim_win_get_cursor(0)[1] - 1 ) for _, id in ipairs(cleared) do local limage = module.private.latex_images[buf][id] if limage.extmark_id then vim.api.nvim_buf_set_extmark(0, module.private.extmark_ns, limage.range[1], limage.range[2], { id = limage.extmark_id, end_col = limage.range[4], conceal = "", virt_text = { { "", "" } }, strict = false, }) end end local to_render = {} for _, key in ipairs(module.private.cleared_at_cursor) do if not vim.tbl_contains(cleared, key) then -- this image was cleared b/c it was at our cursor, and now it should be rendered again to_render[key] = module.private.latex_images[buf][key] end end local updated_positions = {} for _, limage in pairs(to_render) do if limage.extmark_id then local extmark = vim.api.nvim_buf_get_extmark_by_id( buf, module.private.extmark_ns, limage.extmark_id, { details = true } ) local range = { extmark[1], extmark[2], extmark[3].end_row, extmark[3].end_col } local new_key = module.private.get_key(range) updated_positions[new_key] = limage updated_positions[new_key].range = range end end module.public.render_inline_math(updated_positions, buf) module.private.cleared_at_cursor = cleared end end local function enable_rendering() module.private.do_render = true render_latex() end local function disable_rendering() module.private.do_render = false for buf, images in pairs(module.private.latex_images) do module.private.image_api.clear(images) vim.api.nvim_buf_clear_namespace(buf, module.private.extmark_ns, 0, -1) end module.private.latex_images = {} end local function toggle_rendering() if module.private.do_render then disable_rendering() else enable_rendering() end end local function show_hidden() local buf = vim.api.nvim_get_current_buf() if not module.private.do_render then return end module.private.image_api.render(module.private.latex_images[buf] or {}) end local function colorscheme_change() module.private.image_paths = {} if module.private.do_render then disable_rendering() module.private.latex_images = {} vim.schedule(function() compute_foreground() enable_rendering() end) else vim.schedule_wrap(compute_foreground)() end end local event_handlers = { ["core.neorgcmd.events.latex.render.render"] = enable_rendering, ["core.neorgcmd.events.latex.render.enable"] = enable_rendering, ["core.neorgcmd.events.latex.render.disable"] = disable_rendering, ["core.neorgcmd.events.latex.render.toggle"] = toggle_rendering, ["core.autocommands.events.bufreadpost"] = render_latex, ["core.autocommands.events.bufwinenter"] = show_hidden, ["core.autocommands.events.cursormoved"] = clear_at_cursor, ["core.autocommands.events.textchanged"] = render_latex, -- ["core.autocommands.events.textchangedi"] = render_latex, ["core.autocommands.events.insertleave"] = render_latex, ["core.autocommands.events.colorscheme"] = colorscheme_change, } module.on_event = function(event) if event.referrer == "core.autocommands" and vim.bo[event.buffer].ft ~= "norg" then return end return event_handlers[event.type]() end module.events.subscribed = { ["core.autocommands"] = { bufreadpost = module.config.public.render_on_enter, bufwinenter = true, cursormoved = true, textchanged = true, -- textchangedi = true, insertleave = true, colorscheme = true, }, ["core.neorgcmd"] = { ["latex.render.render"] = true, ["latex.render.enable"] = true, ["latex.render.disable"] = true, ["latex.render.toggle"] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/links/module.lua ================================================ --[[ file: Links title: Find links/target in the buffer description: Utility module to handle links/link targets in the buffer internal: true --- This module provides utility functions that are used to find links and their targets in the buffer. --]] local neorg = require("neorg.core") local lib, modules = neorg.lib, neorg.modules local module = modules.create("core.links") module.setup = function() return { success = true, } end ---@class core.links module.public = { -- TS query strings for different link targets ---@param link_type "generic" | "definition" | "footnote" | string get_link_target_query_string = function(link_type) return lib.match(link_type)({ generic = [[ [(_ [(strong_carryover_set (strong_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name"))) (weak_carryover_set (weak_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name")))]? title: (paragraph_segment) @title) (inline_link_target (paragraph) @title)] ]], [{ "definition", "footnote" }] = string.format( [[ (%s_list (strong_carryover_set (strong_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name")))? . [(single_%s (weak_carryover_set (weak_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name")))? (single_%s_prefix) title: (paragraph_segment) @title) (multi_%s (weak_carryover_set (weak_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name")))? (multi_%s_prefix) title: (paragraph_segment) @title)]) ]], lib.reparg(link_type, 5) ), _ = string.format( [[ (%s [(strong_carryover_set (strong_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name"))) (weak_carryover_set (weak_carryover name: (tag_name) @tag_name (tag_parameters) @title (#eq? @tag_name "name")))]? (%s_prefix) title: (paragraph_segment) @title) ]], lib.reparg(link_type, 2) ), }) end, } return module ================================================ FILE: lua/neorg/modules/core/looking-glass/module.lua ================================================ --[[ file: Looking-Glass title: Code Blocks + Superpowers description: The `core.looking-glass` module magnifies code blocks and allows you to edit them in a separate buffer. summary: Allows for editing of code blocks within a separate buffer. embed: https://user-images.githubusercontent.com/76052559/216782314-5d82907f-ea6c-44f9-9bd8-1675f1849358.gif --- The looking glass module provides a simple way to edit code blocks in an external buffer, which allows LSPs and other language-specific tools to kick in. If you would like LSP features in code blocks without having to magnify, you can use [`core.integrations.otter`](@core.integrations.otter). ## Keybinds This module exposes the following keybinds (see [`core.keybinds`](@core.keybinds) for instructions on mapping them): - `neorg.looking-glass.magnify-code-block` - magnify the code block under the cursor --]] local neorg = require("neorg.core") local modules, utils = neorg.modules, neorg.utils local module = modules.create("core.looking-glass") module.setup = function() return { success = true, requires = { "core.integrations.treesitter", "core.ui", }, } end module.load = function() vim.keymap.set("", "(neorg.looking-glass.magnify-code-block)", module.public.magnify_code_block) end ---@class core.looking-glass module.public = { sync_text_segment = function(source, source_window, source_start, source_end, target, target_window) -- Create a unique but deterministic namespace name for the code block local namespace = vim.api.nvim_create_namespace( "neorg/code-block-" .. tostring(source) .. tostring(source_start.row) .. tostring(source_end.row) ) -- Clear any leftover extmarks vim.api.nvim_buf_clear_namespace(source, namespace, 0, -1) -- Create two extmarks, one at the beginning of the code block and one at the end. -- This lets us track size changes of the code block (shrinking and enlarging) local start_extmark = vim.api.nvim_buf_set_extmark(source, namespace, source_start.row, source_start.column, {}) local end_extmark = vim.api.nvim_buf_set_extmark(source, namespace, source_end.row, source_end.column, {}) -- This autocommand handles the synchronization from the source buffer to the target buffer -- (from the code block to the split) vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { buffer = source, callback = function() if not vim.api.nvim_buf_is_loaded(target) or not vim.api.nvim_buf_is_loaded(source) then return true end local cursor_pos = vim.api.nvim_win_get_cursor(source_window) if vim.api.nvim_get_current_buf() ~= source then return end -- Get the positions of both the extmarks (this has to be in the schedule function else it returns -- outdated information). local extmark_begin = vim.api.nvim_buf_get_extmark_by_id(source, namespace, start_extmark, {}) local extmark_end = vim.api.nvim_buf_get_extmark_by_id(source, namespace, end_extmark, {}) -- Both extmarks will have the same row if the user deletes the whole code block. -- In other words, this is a method to detect when a code block has been deleted. if extmark_end[1] == extmark_begin[1] then vim.api.nvim_buf_delete(target, { force = true }) vim.api.nvim_buf_clear_namespace(source, namespace, 0, -1) return true end -- Make sure that the cursor is within bounds of the code block if cursor_pos[1] > extmark_begin[1] and cursor_pos[1] <= (extmark_end[1] + 1) then -- For extra information grab the current node under the cursor local current_node = vim.treesitter.get_node({ bufnr = source, ignore_injections = true }) if not current_node then vim.api.nvim_buf_delete(target, { force = true }) vim.api.nvim_buf_clear_namespace(source, namespace, 0, -1) return true end -- If we are within bounds of the code block but the current node type is not part of a ranged -- tag then it means the user malformed the code block in some way and we should bail if not module.required["core.integrations.treesitter"].find_parent( current_node, "^ranged_verbatim_tag.*" ) then vim.api.nvim_buf_delete(target, { force = true }) vim.api.nvim_buf_clear_namespace(source, namespace, 0, -1) return true end local lines = vim.api.nvim_buf_get_lines(source, extmark_begin[1] + 1, extmark_end[1], true) for i, line in ipairs(lines) do lines[i] = line:sub(extmark_begin[2] + 1) end -- Now that we have full information that we are in fact in a valid code block -- take the lines from within the code block and put them in the buffer vim.api.nvim_buf_set_lines(target, 0, -1, false, lines) local target_line_count = vim.api.nvim_buf_line_count(target) -- Set the cursor in the target window to the place the text is being changed. -- Useful to keep up with long ranges of text. -- -- This check exists as sometimes the cursor position can be larger than the size of the -- target buffer which causes errors. if cursor_pos[1] - extmark_begin[1] > target_line_count then vim.api.nvim_win_set_cursor(target_window, { target_line_count, cursor_pos[2] }) else -- Here we subtract the beginning extmark's row position from the current cursor position -- in order to create an offset that can be applied to the target buffer. vim.api.nvim_win_set_cursor( target_window, { cursor_pos[1] - extmark_begin[1] - 1, cursor_pos[2] } ) end end end, }) -- Target -> source binding -- This binding is much simpler, as it captures changes from the vertical split and applies them -- to the source code block. vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { buffer = target, callback = vim.schedule_wrap(function() -- Schedule wrap is needed for up-to-date extmark information local cursor_pos = vim.api.nvim_win_get_cursor(0) local extmark_begin = vim.api.nvim_buf_get_extmark_by_id(source, namespace, start_extmark, {}) local extmark_end = vim.api.nvim_buf_get_extmark_by_id(source, namespace, end_extmark, {}) local lines = vim.api.nvim_buf_get_lines(target, 0, -1, true) for i, line in ipairs(lines) do lines[i] = string.rep(" ", extmark_begin[2]) .. line end vim.api.nvim_buf_set_lines(source, extmark_begin[1] + 1, extmark_end[1], true, lines) vim.api.nvim_win_set_cursor( source_window, { cursor_pos[1] + 1 + extmark_begin[1], cursor_pos[2] + extmark_begin[2] } ) end), }) -- For when the user closes the target buffer or vertical split. vim.api.nvim_create_autocmd({ "BufDelete", "WinClosed" }, { buffer = target, once = true, callback = function() pcall(vim.api.nvim_buf_delete, target, { force = true }) vim.api.nvim_buf_clear_namespace(source, namespace, 0, -1) end, }) end, magnify_code_block = function() local buffer = vim.api.nvim_get_current_buf() local window = vim.api.nvim_get_current_win() local query = utils.ts_parse_query( "norg", [[ (ranged_verbatim_tag name: (tag_name) @_name (#any-of? @_name "code" "embed")) @tag ]] ) local document_root = module.required["core.integrations.treesitter"].get_document_root(buffer) if not document_root then return end --- Table containing information about the code block that is potentially under the cursor local code_block_info do local cursor_pos = vim.api.nvim_win_get_cursor(window) for id, node in query:iter_captures(document_root, buffer, cursor_pos[1] - 1, cursor_pos[1]) do local capture = query.captures[id] if capture == "tag" then local tag_info = module.required["core.integrations.treesitter"].get_tag_info(node) if not tag_info then utils.notify("Unable to magnify current code block :(", vim.log.levels.WARN) return end code_block_info = tag_info end end end -- If the query above failed then we know that the user isn't under a code block if not code_block_info then utils.notify("No code block found under cursor!", vim.log.levels.WARN) return end -- TODO: Make the vsplit location configurable (i.e. whether it spawns on the left or the right) local vsplit = module.required["core.ui"].create_vsplit( "code-block-" .. tostring(code_block_info.start.row) .. tostring(code_block_info["end"].row), -- This is done to make the name of the vsplit unique true, { filetype = (code_block_info.parameters[1] or "none"), }, { split = "left" } ) if not vsplit then utils.notify( "Unable to magnify current code block because our split didn't want to open :(", vim.log.levels.WARN ) return end -- Set the content of the target buffer to the content of the code block (initial synchronization) vim.api.nvim_buf_set_lines(vsplit, 0, -1, true, code_block_info.content) -- iterate over attributes and find the last row of them. local last_attribute = nil if code_block_info.attributes then last_attribute = code_block_info.attributes[1] for _, v in ipairs(code_block_info.attributes) do if v["end"].row > last_attribute["end"].row then last_attribute = v end end end local start = last_attribute and last_attribute["end"] or code_block_info.start module.public.sync_text_segment( buffer, window, start, code_block_info["end"], vsplit, vim.api.nvim_get_current_win() ) end, } return module ================================================ FILE: lua/neorg/modules/core/neorgcmd/commands/return/module.lua ================================================ --[[ file: Neorgcmd-return title: Provides the `:Neorg return` Command summary: Return to last location before entering Neorg. internal: true --- When executed (`:Neorg return`), all currently open `.norg` files are deleted from the buffer list, and the current workspace is set to "default". --]] local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.neorgcmd.commands.return") module.setup = function() return { success = true, requires = { "core.neorgcmd" } } end module.public = { neorg_commands = { ["return"] = { args = 0, name = "return", }, }, } module.on_event = function(event) if event.type == "core.neorgcmd.events.return" then -- Get all the buffers local buffers = vim.api.nvim_list_bufs() local to_delete = {} for buffer in vim.iter(buffers):rev() do if vim.fn.buflisted(buffer) == 1 then -- If the listed buffer we're working with has a .norg extension then remove it (not forcibly) if not vim.endswith(vim.api.nvim_buf_get_name(buffer), ".norg") then vim.api.nvim_win_set_buf(0, buffer) break else table.insert(to_delete, buffer) end end end for _, buffer in ipairs(to_delete) do vim.api.nvim_buf_delete(buffer, {}) end end end module.events.subscribed = { ["core.neorgcmd"] = { ["return"] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/neorgcmd/module.lua ================================================ --[[ file: Neorgcmd-Module title: Does the Heavy Lifting for the `:Neorg` Command summary: This module deals with handling everything related to the `:Neorg` command. internal: true --- This internal module handles everything there is for the `:Neorg` command to function. Different modules can define their own commands, completions and conditions on when they'd like these commands to be avaiable. For a full example on how to create your own command, it is recommended to read the `core.neorgcmd`'s `module.lua` file. At the beginning of the file is an examples table which walks you through the necessary steps. --]] local neorg = require("neorg.core") local log, modules = neorg.log, neorg.modules local module = modules.create("core.neorgcmd") module.examples = { ["Adding a Neorg command"] = function() -- In your module.setup(), make sure to require core.neorgcmd (requires = { "core.neorgcmd" }) -- Afterwards in a function of your choice that gets called *after* core.neorgcmd gets intialized e.g. load(): module.load = function() module.required["core.neorgcmd"].add_commands_from_table({ -- The name of our command my_command = { min_args = 1, -- Tells neorgcmd that we want at least one argument for this command max_args = 1, -- Tells neorgcmd we want no more than one argument args = 1, -- Setting this variable instead would be the equivalent of min_args = 1 and max_args = 1 -- This command is only avaiable within `.norg` files. -- This can also be a function(bufnr, is_in_an_norg_file) condition = "norg", subcommands = { -- Defines subcommands -- Repeat the definition cycle again my_subcommand = { args = 2, -- Force two arguments to be supplied -- The identifying name of this command -- Every "endpoint" must have a name associated with it name = "my.command", -- If your command takes in arguments versus -- subcommands you can make a table of tables with -- completion for those arguments here. -- This table is optional. complete = { { "first_completion1", "first_completion2" }, { "second_completion1", "second_completion2" }, }, -- We do not define a subcommands table here because we don't have any more subcommands -- Creating an empty subcommands table will cause errors so don't bother }, }, }, }) end -- Afterwards, you want to subscribe to the corresponding event: module.events.subscribed = { ["core.neorgcmd"] = { ["my.command"] = true, -- Has the same name as our "name" variable had in the "data" table }, } -- There's also another way to define your own custom commands that's a lot more automated. Such automation can be achieved -- by putting your code in a special directory. That directory is in core.neorgcmd.commands. Creating your modules in this directory -- will allow users to easily enable you as a "command module" without much hassle. -- To enable a command in the commands/ directory, do this: require("neorg").setup({ load = { ["core.neorgcmd"] = { config = { load = { "some.neorgcmd", -- The name of a valid command }, }, }, }, }) -- And that's it! You're good to go. -- Want to find out more? Read the wiki entry! https://github.com/nvim-neorg/neorg/wiki/Neorg-Command end, } module.load = function() -- Define the :Neorg command with autocompletion taking any number of arguments (-nargs=*) -- If the user passes no arguments or too few, we'll query them for the remainder using select_next_cmd_arg. vim.api.nvim_create_user_command("Neorg", module.private.command_callback, { nargs = "*", complete = module.private.generate_completions, range = 2, }) -- Loop through all the command modules we want to load and load them for _, command in ipairs(module.config.public.load) do -- If one of the command modules is "default" then load all the default modules if command == "default" then for _, default_command in ipairs(module.config.public.default) do module.public.add_commands_from_file(default_command) end end end end module.config.public = { -- A list of neorgcmd modules to load automatically. -- This feature will soon be deprecated, so it is not recommended to touch it. load = { "default", }, -- A list of default commands to load. -- -- This feature will soon be deprecated, so it is not recommended to touch it. default = { "return", }, } ---@class core.neorgcmd module.public = { -- The table containing all the functions. This can get a tad complex so I recommend you read the wiki entry neorg_commands = { module = { subcommands = { load = { args = 1, name = "module.load", }, list = { args = 0, name = "module.list", }, }, }, }, --- Recursively merges the contents of the module's config.public.funtions table with core.neorgcmd's module.config.public.neorg_commands table. ---@param module_name string #An absolute path to a loaded module with a module.config.public.neorg_commands table following a valid structure add_commands = function(module_name) local module_config = modules.get_module(module_name) if not module_config or not module_config.neorg_commands then return end module.public.neorg_commands = vim.tbl_extend("force", module.public.neorg_commands, module_config.neorg_commands) end, --- Recursively merges the provided table with the module.config.public.neorg_commands table. ---@param functions table #A table that follows the module.config.public.neorg_commands structure add_commands_from_table = function(functions) module.public.neorg_commands = vim.tbl_extend("force", module.public.neorg_commands, functions) end, --- Takes a relative path (e.g "list.modules") and loads it from the commands/ directory ---@param name string #The relative path of the module we want to load add_commands_from_file = function(name) -- Attempt to require the file local err, ret = pcall(require, "neorg.modules.core.neorgcmd.commands." .. name .. ".module") -- If we've failed bail out if not err then log.warn( "Could not load command", name, "for module core.neorgcmd - the corresponding module.lua file does not exist." ) return end -- Load the module from table modules.load_module_from_table(ret) end, --- Rereads data from all modules and rebuild the list of available autocompletions and commands sync = function() -- Loop through every loaded module and set up all their commands for _, mod in pairs(modules.loaded_modules) do if mod.public.neorg_commands then module.public.add_commands_from_table(mod.public.neorg_commands) end end end, --- Defines a custom completion function to use for `core.neorgcmd`. ---@param callback function The same function format as you would receive by being called by `:command -completion=customlist,v:lua.callback Neorg`. set_completion_callback = function(callback) module.private.generate_completions = callback end, } module.private = { --- Handles the calling of the appropriate function based on the command the user entered command_callback = function(data) local args = data.fargs local current_buf = vim.api.nvim_get_current_buf() local is_norg = vim.bo[current_buf].filetype == "norg" local function check_condition(condition) if condition == nil then return true end if condition == "norg" and not is_norg then return false end if type(condition) == "function" then return condition(current_buf, is_norg) end return condition end local ref = { subcommands = module.public.neorg_commands, } local argument_index = 0 for i, cmd in ipairs(args) do if not ref.subcommands or vim.tbl_isempty(ref.subcommands) then break end ref = ref.subcommands[cmd] if not ref then log.error( ("Error when executing `:Neorg %s` - such a command does not exist!"):format( table.concat(vim.list_slice(args, 1, i), " ") ) ) return elseif not check_condition(ref.condition) then log.error( ("Error when executing `:Neorg %s` - the command is currently disabled. Some commands will only become available under certain conditions, e.g. being within a `.norg` file!"):format( table.concat(vim.list_slice(args, 1, i), " ") ) ) return end argument_index = i end local argument_count = (#args - argument_index) if ref.args then ref.min_args = ref.args ref.max_args = ref.args elseif ref.min_args and not ref.max_args then ref.max_args = math.huge else ref.min_args = ref.min_args or 0 ref.max_args = ref.max_args or 0 end if #args == 0 or argument_count < ref.min_args then local completions = module.private.generate_completions(_, table.concat({ "Neorg ", data.args, " " })) module.private.select_next_cmd_arg(data.args, completions) return elseif argument_count > ref.max_args then log.error( ("Error when executing `:Neorg %s` - too many arguments supplied! The command expects %s argument%s."):format( data.args, ref.max_args == 0 and "no" or ref.max_args, ref.max_args == 1 and "" or "s" ) ) return end if not ref.name then log.error( ("Error when executing `:Neorg %s` - the ending command didn't have a `name` variable associated with it! This is an implementation error on the developer's side, so file a report to the author of the module."):format( data.args ) ) return end if not module.events.defined[ref.name] then module.events.defined[ref.name] = modules.define_event(module, ref.name) end local content = vim.list_slice(args, argument_index + 1) content["data"] = data modules.broadcast_event( assert(modules.create_event(module, table.concat({ "core.neorgcmd.events.", ref.name }), content)) ) end, --- This function returns all available commands to be used for the :Neorg command ---@param _ nil #Placeholder variable ---@param command string #Supplied by nvim itself; the full typed out command generate_completions = function(_, command) local current_buf = vim.api.nvim_get_current_buf() local is_norg = vim.api.nvim_get_option_value("filetype", { buf = current_buf }) == "norg" local function check_condition(condition) if condition == nil then return true end if condition == "norg" and not is_norg then return false end if type(condition) == "function" then return condition(current_buf, is_norg) end return condition end command = command:gsub("^%s*", "") local splitcmd = vim.list_slice( vim.split(command, " ", { plain = true, trimempty = true, }), 2 ) local ref = { subcommands = module.public.neorg_commands, } local last_valid_ref = ref local last_completion_level = 0 for _, cmd in ipairs(splitcmd) do if not ref or not check_condition(ref.condition) then break end ref = ref.subcommands or {} ref = ref[cmd] if ref then last_valid_ref = ref last_completion_level = last_completion_level + 1 end end if not last_valid_ref.subcommands and last_valid_ref.complete then if type(last_valid_ref.complete) == "function" then last_valid_ref.complete = last_valid_ref.complete(current_buf, is_norg) end if vim.endswith(command, " ") then local completions = last_valid_ref.complete[#splitcmd - last_completion_level + 1] or {} if type(completions) == "function" then completions = completions(current_buf, is_norg) or {} end return completions else local completions = last_valid_ref.complete[#splitcmd - last_completion_level] or {} if type(completions) == "function" then completions = completions(current_buf, is_norg) or {} end return vim.tbl_filter(function(key) return key:find(splitcmd[#splitcmd]) end, completions) end end -- TODO: Fix `:Neorg m ` giving invalid completions local keys = ref and vim.tbl_keys(ref.subcommands or {}) or ( vim.tbl_filter(function(key) return key:find(splitcmd[#splitcmd]) end, vim.tbl_keys(last_valid_ref.subcommands or {})) ) table.sort(keys) do local subcommands = (ref and ref.subcommands or last_valid_ref.subcommands) or {} return vim.tbl_filter(function(key) return check_condition(subcommands[key].condition) end, keys) end end, --- Queries the user to select next argument ---@param qargs table #A string of arguments previously supplied to the Neorg command ---@param choices table #all possible choices for the next argument select_next_cmd_arg = function(qargs, choices) local current = table.concat({ "Neorg ", qargs }) local query if vim.tbl_isempty(choices) then query = function(...) vim.ui.input(...) end else query = function(...) vim.ui.select(choices, ...) end end query({ prompt = current, }, function(choice) if choice ~= nil then vim.cmd(string.format("%s %s", current, choice)) end end) end, } module.neorg_post_load = module.public.sync module.on_event = function(event) if event.type == "core.neorgcmd.events.module.load" then local ok = pcall(modules.load_module, event.content[1]) if not ok then vim.notify(string.format("Module `%s` does not exist!", event.content[1]), vim.log.levels.ERROR, {}) end end if event.type == "core.neorgcmd.events.module.list" then local Popup = require("nui.popup") local module_list_popup = Popup({ position = "50%", size = { width = "50%", height = "80%" }, enter = true, buf_options = { filetype = "norg", modifiable = true, readonly = false, }, win_options = { conceallevel = 3, concealcursor = "nvi", }, }) module_list_popup:on("VimResized", function() module_list_popup:update_layout() end) local function close() module_list_popup:unmount() end module_list_popup:map("n", "", close, {}) module_list_popup:map("n", "q", close, {}) local lines = {} for name, _ in pairs(neorg.modules.loaded_modules) do table.insert(lines, "- `" .. name .. "`") end vim.api.nvim_buf_set_lines(module_list_popup.bufnr, 0, -1, true, lines) vim.bo[module_list_popup.bufnr].modifiable = false module_list_popup:mount() end end module.events.subscribed = { ["core.neorgcmd"] = { ["module.load"] = true, ["module.list"] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/pivot/module.lua ================================================ --[[ file: Pivot title: Ordered or Unordered? description: That ~~is~~ was the question. Now you no longer have to ask! summary: Toggles the type of list currently under the cursor. --- `core.pivot` allows you to switch (or pivot) between the two list types in Norg with the press of a button. ## Keybinds This module exposes the following keybinds (see [`core.keybinds`](@core.keybinds) for instructions on mapping them): - `neorg.pivot.list.toggle` (default binding: `lt` ["list toggle"]) - takes a list and, based on the opposite type of the first list item, inverts all the other items in that list. Does not respect mixed lists, all items in the list will be converted to the same type. - `neorg.pivot.list.invert` (default binding: `li` ["list invert"]) - same behaviour as the previous keybind, however respects mixed lists - unordered items will become ordered, whereas ordered items will become unordered. --]] local neorg = require("neorg.core") local log, modules, lib = neorg.log, neorg.modules, neorg.lib local module = modules.create("core.pivot") module.setup = function() return { requires = { "core.integrations.treesitter", }, } end module.load = function() vim.keymap.set("", "(neorg.pivot.list.toggle)", lib.wrap(module.public.change_list, false)) vim.keymap.set("", "(neorg.pivot.list.invert)", lib.wrap(module.public.change_list, true)) end module.private = { --- Return current node we are on, accounting for possible root of list ---@param bufnr integer ---@return TSNode? get_current_node = function(bufnr) local cursor = vim.api.nvim_win_get_cursor(0) local node = module.required["core.integrations.treesitter"].get_first_node_on_line(bufnr, cursor[1] - 1) -- if on root of the list we are actually interested in the first list item not the generic_list node if node and node:type() == "generic_list" then node = node:child(0) end return node end, ---@param node TSNode ---@return TSNode? get_parent_list = function(node) local parent = node:parent() if not parent then return end return module.required["core.integrations.treesitter"].find_parent(parent, { "generic_list", "unordered_list1", "unordered_list2", "unordered_list3", "unordered_list4", "unordered_list5", "unordered_list6", "ordered_list1", "ordered_list2", "ordered_list3", "ordered_list4", "ordered_list5", "ordered_list6", }) end, --- Returns the prefix the current list node should be toggled to ---@param node TSNode ---@return string get_target_prefix = function(node) local type = node:type():match("^un") and "~" or "-" local level = tonumber(node:type():match("ordered_list(%d)")) or 0 return type:rep(level) end, } ---@class core.pivot module.public = { ---@param invert boolean change_list = neorg.utils.wrap_dotrepeat(function(invert) local buffer = vim.api.nvim_get_current_buf() local node = module.private.get_current_node(buffer) if not node then log.error("No node found under the cursor! Make sure your cursor is in a list.") return end local parent_list = module.private.get_parent_list(node) if not parent_list then log.error("No list found under the cursor! `toggle-list-type` and `invert-list-type` only work for lists.") return end local first_child = parent_list:iter_children()() if not first_child then return end local target_prefix = module.private.get_target_prefix(node) for child in parent_list:iter_children() do if invert then target_prefix = module.private.get_target_prefix(child) end -- We loop over every subchild because list items may have attached -- weak carryover tags which we have to skip. for subchild in child:iter_children() do if subchild:type():match("_prefix$") then local line, col_start, _, col_end = subchild:range() vim.api.nvim_buf_set_text(buffer, line, col_start, line, col_end - 1, { target_prefix }) break end end end end), } return module ================================================ FILE: lua/neorg/modules/core/presenter/module.lua ================================================ --[[ file: Core-Presenter title: Powerpoint in Neorg description: The presenter module creates slideshows out of notes or documents. summary: Neorg module to create gorgeous presentation slides. --- The presenter module provides a special Neorg display that resembles an active slideshow presentation. To set it up, first be sure to set the `zen_mode` variable in the [configuration](#configuration). Afterwards, run `:Neorg presenter start` on any Norg file. The presenter will split up your file at each level 1 heading, and display each in a different slide. NOTE: This module is due for a rewrite. All of its behaviour is not fully documented here as it will be overwritten soon anyway. ## Keybinds This module exposes the following keybinds (see [`core.keybinds`](@core.keybinds) for instructions on mapping them): - `neorg.presenter.next-page` - go to next page - `neorg.presenter.previous-page` - go to previous page - `neorg.presenter.close` - close presentation view --]] local neorg = require("neorg.core") local log, modules = neorg.log, neorg.modules local module = modules.create("core.presenter") local api = vim.api module.setup = function() return { success = true, requires = { "core.queries.native", "core.integrations.treesitter", "core.ui", }, } end module.load = function() local error_loading = false if module.config.public.zen_mode == "truezen" then modules.load_module("core.integrations.truezen") elseif module.config.public.zen_mode == "zen-mode" then modules.load_module("core.integrations.zen_mode") else log.error("Unrecognized mode for 'zen_mode' option. Please check your presenter config") error_loading = true end if error_loading then return end vim.keymap.set("", "(neorg.presenter.next-page)", module.public.next_page) vim.keymap.set("", "(neorg.presenter.previous-page)", module.public.previous_page) vim.keymap.set("", "(neorg.presenter.close)", module.public.close) modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ presenter = { args = 1, condition = "norg", subcommands = { start = { args = 0, name = "presenter.start" }, close = { args = 0, name = "presenter.close" }, }, }, }) end) end module.config.public = { -- Zen mode plugin to use. Currenly suppported: -- -- - `zen-mode` - https://github.com/folke/zen-mode.nvim -- - `truezen` - https://github.com/Pocco81/TrueZen.nvim zen_mode = "", } module.private = { data = {}, nodes = {}, buf = nil, current_page = 1, remove_blanklines = function(t) local copy = t for k, _t in pairs(copy) do -- Stops at the first non-blankline text local found_non_blankline = false for i = #_t, 1, -1 do if not found_non_blankline then local value = _t[i] value = string.gsub(value, "%s*", "") if value == "" then table.remove(copy[k], i) else found_non_blankline = true end end end end return copy end, } ---@class core.presenter module.public = { version = "0.0.8", present = function() if module.private.buf then log.warn("Presentation already started") return end ---@type core.queries.native local queries = module.required["core.queries.native"] -- Get current file and check if it's a norg one local uri = vim.uri_from_bufnr(0) local fname = vim.uri_to_fname(uri) if string.sub(fname, -5, -1) ~= ".norg" then log.error("Not on a norg file") return end local tree = { { query = { "all", "heading1" }, recursive = true, }, } -- Free the text in memory after reading nodes queries.delete_content(0) local results = queries.query_nodes_from_buf(tree, 0) if vim.tbl_isempty(results) then log.warn("Could not generate the presenter mode (no heading1 present on this file)") return end module.private.nodes = results results = queries.extract_nodes(results, { all_lines = true }) results = module.private.remove_blanklines(results) -- This is a temporary fix because querying the heading1 nodes seems to query the next heading1 node too ! for _, res in pairs(results) do if vim.startswith(res[#res], "* ") then res[#res] = nil end end if module.config.public.zen_mode == "truezen" and modules.is_module_loaded("core.integrations.truezen") then modules.get_module("core.integrations.truezen").toggle_ataraxis() elseif module.config.public.zen_mode == "zen-mode" and modules.is_module_loaded("core.integrations.zen_mode") then modules.get_module("core.integrations.zen_mode").toggle() end -- Generate views selection popup local buffer = module.required["core.ui"].create_norg_buffer("Norg Presenter", "nosplit", nil, { keybinds = false }) if not buffer then return end api.nvim_set_option_value("modifiable", true, { buf = buffer }) api.nvim_buf_set_lines(buffer, 0, -1, false, results[1]) api.nvim_buf_call(buffer, function() vim.cmd("set scrolloff=999") end) api.nvim_set_option_value("modifiable", false, { buf = buffer }) module.private.buf = buffer module.private.data = results end, next_page = function() if vim.tbl_isempty(module.private.data) or not module.private.buf then return end if vim.tbl_count(module.private.data) == module.private.current_page then api.nvim_set_option_value("modifiable", true, { buf = module.private.buf }) api.nvim_buf_set_lines(module.private.buf, 0, -1, false, { "Press `next` again to close..." }) api.nvim_set_option_value("modifiable", false, { buf = module.private.buf }) module.private.current_page = module.private.current_page + 1 return elseif vim.tbl_count(module.private.data) < module.private.current_page then module.public.close() return end module.private.current_page = module.private.current_page + 1 api.nvim_set_option_value("modifiable", true, { buf = module.private.buf }) api.nvim_buf_set_lines(module.private.buf, 0, -1, false, module.private.data[module.private.current_page]) api.nvim_set_option_value("modifiable", false, { buf = module.private.buf }) end, previous_page = function() if vim.tbl_isempty(module.private.data) or not module.private.buf then return end if module.private.current_page == 1 then return end module.private.current_page = module.private.current_page - 1 api.nvim_set_option_value("modifiable", true, { buf = module.private.buf }) api.nvim_buf_set_lines(module.private.buf, 0, -1, false, module.private.data[module.private.current_page]) api.nvim_set_option_value("modifiable", false, { buf = module.private.buf }) end, close = function() if not module.private.buf then return end if module.config.public.zen_mode == "truezen" and modules.is_module_loaded("core.integrations.truezen") then modules.get_module("core.integrations.truezen").toggle_ataraxis() elseif module.config.public.zen_mode == "zen-mode" and modules.is_module_loaded("core.integrations.zen_mode") then modules.get_module("core.integrations.zen_mode").toggle() end api.nvim_buf_delete(module.private.buf, {}) module.private.data = {} module.private.current_page = 1 module.private.buf = nil module.private.nodes = {} end, } module.on_event = function(event) if event.split_type[1] == "core.neorgcmd" then if event.split_type[2] == "presenter.start" then module.public.present() end end end module.events.subscribed = { ["core.neorgcmd"] = { ["presenter.start"] = true, ["presenter.close"] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/promo/module.lua ================================================ --[[ file: Promo title: You have Received a Promotion! description: The `promo` module increases or decreases the nesting level of nestable items by repeating their characters. summary: Promotes or demotes nestable items within Neorg files. --- When dealing with Norg, it may sometimes be tedious to continually repeat a single character to increase your nesting level. For example, for a level 6 nested unordered list, you need to repeat the `-` character six times: ```norg ------ This is my item! ``` The `core.promo` module allows you to indent these object by utilizing the inbuilt Neovim keybinds: - `>>` - increase the indentation level for the current object (also dedents children) - `<<` - decrease the indentation level for the current object recursively (also dedents children) - `>.` - increase the indentation level for the current object (non-recursively) - `<,` - decrease the indentation level for the current object (non-recursively) In insert mode, you are also provided with two keybinds, also being Neovim defaults: - `` increase the indentation level for the current object - `` decrease the indentation level for the current object This module is commonly used with the [`core.itero`](@core.itero) module for an effective workflow. ## Keybinds This module exposes the following keybinds (see [`core.keybinds`](@core.keybinds) for instructions on mapping them): - `neorg.promo.promote` - Promote item on current line - `neorg.promo.promote.nested` - Promote current line and nested items - `neorg.promo.promote.range` - Promote all items in range - `neorg.promo.demote` - similar - `neorg.promo.demote.nested` - similar - `neorg.promo.demote.range` - similar --]] local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.promo") local indent module.setup = function() return { success = true, requires = { "core.integrations.treesitter", "core.esupports.indent", }, } end module.load = function() ---@type core.esupports.indent indent = module.required["core.esupports.indent"] vim.keymap.set({ "n", "i" }, "(neorg.promo.promote)", module.public.promote) vim.keymap.set({ "n", "i" }, "(neorg.promo.promote.nested)", module.public.promote_nested) vim.keymap.set({ "n", "i", "v" }, "(neorg.promo.promote.range)", module.public.promote_range) vim.keymap.set({ "n", "i" }, "(neorg.promo.demote)", module.public.demote) vim.keymap.set({ "n", "i" }, "(neorg.promo.demote.nested)", module.public.demote_nested) vim.keymap.set({ "n", "i", "v" }, "(neorg.promo.demote.range)", module.public.demote_range) end module.private = { types = { heading = { pattern = "^heading(%d)$", prefix = "*", }, unordered_list = { pattern = "^unordered_list(%d)$", prefix = "-", }, ordered_list = { pattern = "^ordered_list(%d)$", prefix = "~", }, quote = { pattern = "^quote(%d)$", prefix = ">", }, }, ignore_types = { "generic_list", "quote", }, get_line = function(buffer, target_row) return vim.api.nvim_buf_get_lines(buffer, target_row, target_row + 1, true)[1] end, get_promotable_node_prefix = function(node) for _, data in pairs(module.private.types) do if node:type():match(data.pattern) then return data.prefix end end end, promote_or_demote = function(buffer, mode, row, reindent_children, affect_children) -- Treesitter node helpers local function get_header_prefix_node(header_node) local first_child = header_node:child(0) assert(first_child:type() == header_node:type() .. "_prefix") return first_child end local function get_node_row_range(node) local row_start, _ = node:start() local row_end, _ = node:end_() return row_start, row_end end local function is_prefix_node(node) return node:type():match("_prefix$") ~= nil end local function get_prefix_position_and_level(prefix_node) assert(is_prefix_node(prefix_node)) local row_start, col_start, row_end, col_end = prefix_node:range() assert(row_start == row_end) assert(col_start + 2 <= col_end) return row_start, col_start, (col_end - col_start - 1) end local function is_quasi_prefix(target_row) local line = module.private.get_line(buffer, target_row) -- NOTE: This is a hardcoded check determined by the limitations of -- the first generation treesitter parser. return line:match("^%s*[%-~%*]+%s*$") end local function adjust_quasi_prefix(target_row, count) local line = module.private.get_line(buffer, target_row) local l, r = line:find("%S+") assert(l) assert(count ~= 0) if count > 0 then vim.api.nvim_buf_set_text(buffer, target_row, l - 1, target_row, l - 1, { line:sub(l, l):rep(count) }) else local level_remain = math.max(1, r - l + 1 + count) vim.api.nvim_buf_set_text(buffer, target_row, l - 1, target_row, r - level_remain, {}) end end local root_node = module.required["core.integrations.treesitter"].get_first_node_on_line( buffer, row, module.private.ignore_types ) local action_count = vim.v.count1 if not root_node or root_node:has_error() then if is_quasi_prefix(row) then adjust_quasi_prefix(row, action_count * (mode == "promote" and 1 or -1)) end return end local root_prefix_char = module.private.get_promotable_node_prefix(root_node) if not root_prefix_char then local n_space_diff = vim.bo.shiftwidth * action_count if mode == "demote" then n_space_diff = -n_space_diff end local current_visual_indent = vim.fn.indent(row + 1) local new_indent = math.max(0, current_visual_indent + n_space_diff) indent.buffer_set_line_indent(buffer, row, new_indent) return end local root_prefix_node = get_header_prefix_node(root_node) local _, _, root_level = get_prefix_position_and_level(root_prefix_node) local adjust_prefix if mode == "promote" then adjust_prefix = function(prefix_node) local prefix_row, prefix_col, _ = get_prefix_position_and_level(prefix_node) vim.api.nvim_buf_set_text( buffer, prefix_row, prefix_col, prefix_row, prefix_col, { root_prefix_char:rep(action_count) } ) end else action_count = math.min(action_count, root_level - 1) assert(action_count >= 0) if action_count == 0 then assert(root_level == 1) return end adjust_prefix = function(prefix_node) local prefix_row, prefix_col, level = get_prefix_position_and_level(prefix_node) assert(level > action_count) vim.api.nvim_buf_set_text(buffer, prefix_row, prefix_col, prefix_row, prefix_col + action_count, {}) end end if not affect_children then adjust_prefix(root_prefix_node) return end local function apply_recursive_normal(node, is_target, f) if not is_target(node) then return end f(node) for child in node:iter_children() do apply_recursive_normal(child, is_target, f) end end local function apply_recursive_verylow(node, is_target, f) local started = false local _, _, level = get_prefix_position_and_level(get_header_prefix_node(node)) f(node) for sibling in node:parent():iter_children() do if started then if not is_target(sibling) then break end local _, _, sibling_level = get_prefix_position_and_level(get_header_prefix_node(sibling)) if sibling_level <= level then break end f(sibling) end started = started or (sibling == node) end end local HEADING_VERYLOW_LEVEL = 6 local indent_targets = {} local apply_recursive = root_level < HEADING_VERYLOW_LEVEL and apply_recursive_normal or apply_recursive_verylow apply_recursive(root_node, function(node) return module.private.get_promotable_node_prefix(node) == root_prefix_char end, function(node) indent_targets[#indent_targets + 1] = node end) local indent_row_start, indent_row_end = get_node_row_range(root_node) if root_level >= HEADING_VERYLOW_LEVEL then local _, last_child_row_end = get_node_row_range(indent_targets[#indent_targets]) indent_row_end = math.max(indent_row_end, last_child_row_end) end for _, node in ipairs(indent_targets) do adjust_prefix(get_header_prefix_node(node)) end if not reindent_children then return end indent.reindent_range(buffer, indent_row_start, indent_row_end) end, } ---@class core.promo module.public = { promote = neorg.utils.wrap_dotrepeat(function() local buffer = vim.api.nvim_get_current_buf() local row = vim.api.nvim_win_get_cursor(0)[1] - 1 module.private.promote_or_demote(buffer, "promote", row, true, false) end), promote_nested = neorg.utils.wrap_dotrepeat(function() local buffer = vim.api.nvim_get_current_buf() local row = vim.api.nvim_win_get_cursor(0)[1] - 1 module.private.promote_or_demote(buffer, "promote", row, true, true) end), promote_range = neorg.utils.wrap_dotrepeat(function() local buffer = vim.api.nvim_get_current_buf() local start_pos = vim.api.nvim_buf_get_mark(buffer, "<") local end_pos = vim.api.nvim_buf_get_mark(buffer, ">") for i = start_pos[1], end_pos[1] do module.private.promote_or_demote(buffer, "promote", i - 1, false, false) end indent.reindent_range(buffer, start_pos[1] - 1, end_pos[1]) end), demote = neorg.utils.wrap_dotrepeat(function() local buffer = vim.api.nvim_get_current_buf() local row = vim.api.nvim_win_get_cursor(0)[1] - 1 module.private.promote_or_demote(buffer, "demote", row, true, false) end), demote_nested = neorg.utils.wrap_dotrepeat(function() local buffer = vim.api.nvim_get_current_buf() local row = vim.api.nvim_win_get_cursor(0)[1] - 1 module.private.promote_or_demote(buffer, "demote", row, true, true) end), demote_range = neorg.utils.wrap_dotrepeat(function() local buffer = vim.api.nvim_get_current_buf() local start_pos = vim.api.nvim_buf_get_mark(buffer, "<") local end_pos = vim.api.nvim_buf_get_mark(buffer, ">") for i = start_pos[1], end_pos[1] do module.private.promote_or_demote(buffer, "demote", i - 1, false, false) end indent.reindent_range(buffer, start_pos[1] - 1, end_pos[1]) end), } return module ================================================ FILE: lua/neorg/modules/core/qol/toc/module.lua ================================================ --[[ file: TOC title: A Bird's Eye View of Norg Documents description: The TOC module generates a table of contents for a given Norg buffer. summary: Generates a table of contents for a given Norg buffer. --- The TOC module exposes a single command - `:Neorg toc`. This command can be executed with one of three optional arguments: `left`, `right` and `qflist`. When `left` or `right` is supplied, the Table of Contents split is placed on that side of the screen. When the `qflist` argument is provided, the whole table of contents is sent to the Neovim quickfix list, should that be more convenient for you. When in the TOC view, `` can be pressed on any of the entries to move to that location in the respective Norg document. The TOC view updates automatically when switching buffers. --]] local neorg = require("neorg.core") local modules, utils, log = neorg.modules, neorg.utils, neorg.log local module = modules.create("core.qol.toc") module.setup = function() return { requires = { "core.integrations.treesitter", "core.ui" }, } end ---Track if the next TOC open was automatic. Used to determine if we should enter the TOC or not. local next_open_is_auto = false module.load = function() modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ toc = { name = "core.qol.toc", max_args = 1, condition = "norg", complete = { { "left", "right", "qflist" }, }, }, }) end) if module.config.public.auto_toc.open then vim.api.nvim_create_autocmd("BufWinEnter", { pattern = "*.norg", callback = function() vim.schedule(function() if vim.bo.filetype == "norg" then next_open_is_auto = true vim.cmd([[Neorg toc]]) end end) end, }) end end module.config.public = { -- Close the Table of Contents after an entry in the table is picked close_after_use = false, -- Width of the Table of Contents window will automatically fit its longest line, up to -- `max_width` fit_width = true, -- Max width of the ToC window when `fit_width = true` (in columns) max_width = 30, -- When set, the ToC window will always be this many cols wide. -- will override `fit_width` and ignore `max_width` fixed_width = nil, -- Enable `cursorline` in the ToC window, and sync the cursor position between ToC and content -- window sync_cursorline = true, -- Enter a ToC window opened manually (any ToC window not opened by auto_toc) enter = true, -- Options for automatically opening/entering the ToC window auto_toc = { -- Automatically open a ToC window when entering any `norg` buffer open = false, -- Enter an automatically opened ToC window enter = false, -- Automatically close the ToC window when there is no longer an open norg buffer close = true, -- Will exit nvim if the ToC is the last buffer on the screen, similar to help windows exit_nvim = true, }, } local ui_data_of_tabpage = {} local data_of_norg_buf = {} local toc_namespace local function upper_bound(array, v) -- assume array is sorted -- find index of first element in array that is > v local l = 1 local r = #array while l <= r do local m = math.floor((l + r) / 2) if v >= array[m] then l = m + 1 else r = m - 1 end end return l end local function get_target_location_under_cursor(ui_data) local ui_window = vim.fn.bufwinid(ui_data.buffer) local curline = vim.api.nvim_win_get_cursor(ui_window)[1] local offset = ui_data.start_lines.offset local extmark_lookup = data_of_norg_buf[ui_data.norg_buffer].extmarks[curline - offset] if not extmark_lookup then return end return vim.api.nvim_buf_get_extmark_by_id(ui_data.norg_buffer, toc_namespace, extmark_lookup, {}) end local toc_query ---@class core.qol.toc module.public = { parse_toc_macro = function(buffer) local toc, toc_name = false, nil local success = module.required["core.integrations.treesitter"].execute_query( [[ (infirm_tag (tag_name) @name (tag_parameters)? @parameters) ]], function(query, id, node) local capture_name = query.captures[id] if capture_name == "name" and module.required["core.integrations.treesitter"].get_node_text(node, buffer):lower() == "toc" then toc = true elseif capture_name == "parameters" and toc then toc_name = module.required["core.integrations.treesitter"].get_node_text(node, buffer) return true end end, buffer ) if not success then return end return toc_name end, generate_qflist = function(original_buffer) local prefix, title local qflist_data = {} local success = module.required["core.integrations.treesitter"].execute_query( [[ (_ . (_) @prefix . title: (paragraph_segment) @title) ]], function(query, id, node) local capture = query.captures[id] if capture == "prefix" then if node:type():match("_prefix$") then prefix = node else prefix = nil end title = nil elseif capture == "title" then title = node end if prefix and title then local prefix_text = module.required["core.integrations.treesitter"].get_node_text(prefix, original_buffer) local title_text = module.required["core.integrations.treesitter"].get_node_text(title, original_buffer) if prefix_text:sub(1, 1) ~= "*" and prefix_text:match("^%W%W") then prefix_text = table.concat({ prefix_text:sub(1, 1), " " }) end table.insert(qflist_data, { bufnr = original_buffer, lnum = (prefix:start()) + 1, text = table.concat({ prefix_text, title_text }), }) prefix, title = nil, nil end end, original_buffer ) if not success then return end return qflist_data end, -- Update ui cursor according to norg cursor update_cursor = function(ui_data) local norg_window = vim.fn.bufwinid(ui_data.norg_buffer) local norg_data = data_of_norg_buf[ui_data.norg_buffer] local ui_window = vim.fn.bufwinid(ui_data.buffer) assert(ui_window ~= -1) local current_row_1b = vim.fn.line(".", norg_window) if norg_data.last_row == current_row_1b then return end norg_data.last_row = current_row_1b local start_lines = ui_data.start_lines assert(start_lines) local current_toc_item_idx = upper_bound(start_lines, current_row_1b - 1) - 1 local current_toc_row = ( current_toc_item_idx == 0 and math.max(1, start_lines.offset) or current_toc_item_idx + start_lines.offset ) vim.api.nvim_win_set_cursor(ui_window, { current_toc_row, 0 }) end, update_toc = function(toc_title, ui_data, norg_buffer) local ui_buffer = ui_data.buffer ui_data.norg_buffer = norg_buffer if not vim.api.nvim_buf_is_valid(ui_buffer) then log.error("update_toc called with invalid ui buffer") return end vim.bo[ui_buffer].modifiable = true vim.api.nvim_buf_clear_namespace(norg_buffer, toc_namespace, 0, -1) table.insert(toc_title, "") vim.api.nvim_buf_set_lines(ui_buffer, 0, -1, true, toc_title) local norg_data = {} data_of_norg_buf[norg_buffer] = norg_data local extmarks = {} norg_data.extmarks = extmarks local offset = vim.api.nvim_buf_line_count(ui_buffer) local start_lines = { offset = offset } ui_data.start_lines = start_lines ---@type vim.treesitter.Query toc_query = toc_query or utils.ts_parse_query( "norg", [[ ( [(heading1_prefix) (heading2_prefix) (heading3_prefix) (heading4_prefix) (heading5_prefix) (heading6_prefix) ] @prefix state: (detached_modifier_extension . (_)@modifier)? title: (paragraph_segment) @title ) ]] ) local norg_root = module.required["core.integrations.treesitter"].get_document_root(norg_buffer) if not norg_root then return end local current_capture local heading_nodes = {} for id, node in toc_query:iter_captures(norg_root, norg_buffer) do local type = toc_query.captures[id] if type == "prefix" then current_capture = {} table.insert(heading_nodes, current_capture) end current_capture[type] = node end local heading_texts = {} for _, capture in ipairs(heading_nodes) do if capture.modifier and capture.modifier:type() == "todo_item_cancelled" then goto continue end local row_start_0b, col_start_0b, _, _ = capture.prefix:range() local _, _, row_end_0bin, col_end_0bex = capture.title:range() table.insert(start_lines, row_start_0b) table.insert( extmarks, vim.api.nvim_buf_set_extmark(norg_buffer, toc_namespace, row_start_0b, col_start_0b, {}) ) for _, line in ipairs( vim.api.nvim_buf_get_text(norg_buffer, row_start_0b, col_start_0b, row_end_0bin, col_end_0bex, {}) ) do table.insert(heading_texts, line) end ::continue:: end vim.api.nvim_buf_set_lines(ui_buffer, -1, -1, true, heading_texts) vim.bo[ui_buffer].modifiable = false vim.api.nvim_buf_set_keymap(ui_buffer, "n", "", "", { callback = function() local location = get_target_location_under_cursor(ui_data) if not location then return end local norg_window = vim.fn.bufwinid(norg_buffer) if norg_window == -1 then local toc_window = vim.fn.bufwinid(ui_data.buffer) local buf_width = nil if toc_window ~= -1 then buf_width = vim.api.nvim_win_get_width(toc_window) - module.private.get_toc_width(ui_data) if buf_width < 1 then buf_width = nil end end norg_window = vim.api.nvim_open_win(norg_buffer, true, { win = 0, vertical = true, width = buf_width }) else vim.api.nvim_set_current_win(norg_window) vim.api.nvim_set_current_buf(norg_buffer) end vim.api.nvim_win_set_cursor(norg_window, { location[1] + 1, location[2] }) if module.config.public.close_after_use then vim.api.nvim_buf_delete(ui_buffer, { force = true }) end end, }) if module.config.public.sync_cursorline then module.public.update_cursor(ui_data) end end, } module.private = { ---get the width of the ToC window ---@param ui_data table ---@return number get_toc_width = function(ui_data) if type(module.config.public.fixed_width) == "number" then return module.config.public.fixed_width end local max_virtcol_1bex = module.private.get_max_virtcol(ui_data.window) local current_winwidth = vim.api.nvim_win_get_width(ui_data.window) local new_winwidth = math.min(current_winwidth, module.config.public.max_width, max_virtcol_1bex - 1) return new_winwidth + 1 end, get_max_virtcol = function(win) local n_line = vim.fn.line("$", win) local result = 1 for i = 1, n_line do result = math.max(result, vim.fn.virtcol({ i, "$" }, false, win)) end return result end, } local function get_norg_ui(norg_buffer) local tabpage = vim.api.nvim_win_get_tabpage(vim.fn.bufwinid(norg_buffer)) return ui_data_of_tabpage[tabpage] end ---Guard an autocommand callback function with a check that the ToC is still open ---@param listener function ---@return function local function unlisten_if_closed(listener) return function(ev) if vim.tbl_isempty(ui_data_of_tabpage) then return true end local norg_buffer = ev.buf local ui_data = get_norg_ui(norg_buffer) if not ui_data or vim.fn.bufwinid(ui_data.buffer) == -1 then return end return listener(norg_buffer, ui_data) end end ---Create a split window and buffer for the table of contents. Set buffer and window options ---accordingly ---@param tabpage number ---@param split_dir "left" | "right" ---@param enter boolean ---@return table local function create_ui(tabpage, split_dir, enter) assert(tabpage == vim.api.nvim_get_current_tabpage()) toc_namespace = toc_namespace or vim.api.nvim_create_namespace("neorg/toc") local ui_buffer, ui_window = module.required["core.ui"].create_vsplit( ("toc-%d"):format(tabpage), enter, { ft = "norg" }, { split = split_dir, win = 0, style = "minimal" } ) local ui_wo = vim.wo[ui_window] ui_wo.scrolloff = 999 ui_wo.conceallevel = 0 ui_wo.foldmethod = "expr" ui_wo.foldexpr = "v:lua.vim.treesitter.foldexpr()" ui_wo.foldlevel = 99 ui_wo.winfixbuf = true ui_wo.winfixwidth = true if module.config.public.sync_cursorline then ui_wo.cursorline = true end local ui_data = { buffer = ui_buffer, tabpage = tabpage, window = ui_window, } ui_data_of_tabpage[tabpage] = ui_data return ui_data end --- should we enter the ToC window? local function enter_toc_win() local do_enter = module.config.public.enter if next_open_is_auto then do_enter = module.config.public.auto_toc.enter end return do_enter end module.on_event = function(event) if event.split_type[2] ~= module.name then return end local toc_title = vim.split(module.public.parse_toc_macro(event.buffer) or "Table of Contents", "\n") local norg_buffer = event.buffer if event.content and event.content[1] == "qflist" then local qflist = module.public.generate_qflist(event.buffer) if not qflist then utils.notify("An error occurred and the qflist could not be generated", vim.log.levels.WARN) return end vim.fn.setqflist(qflist, "r") vim.fn.setqflist({}, "a", { title = toc_title[1] }) vim.cmd.copen() return end local tabpage = vim.api.nvim_win_get_tabpage(vim.fn.bufwinid(norg_buffer)) if ui_data_of_tabpage[tabpage] then if norg_buffer == ui_data_of_tabpage[tabpage].buffer then return end module.public.update_toc(toc_title, ui_data_of_tabpage[tabpage], norg_buffer) if enter_toc_win() then vim.api.nvim_set_current_win(ui_data_of_tabpage[tabpage].window) end return end local ui_data = create_ui(tabpage, event.content[1] or "left", enter_toc_win()) next_open_is_auto = false module.public.update_toc(toc_title, ui_data_of_tabpage[tabpage], norg_buffer) if module.config.public.fit_width then vim.api.nvim_win_set_width(ui_data.window, module.private.get_toc_width(ui_data)) end local close_buffer_callback = function() -- Check if ui_buffer exists before deleting it if vim.api.nvim_buf_is_loaded(ui_data.buffer) then vim.api.nvim_buf_delete(ui_data.buffer, { force = true }) end ui_data_of_tabpage[tabpage] = nil end vim.keymap.set("n", "q", close_buffer_callback, { buffer = ui_data.buffer }) --- WinClosed matches against the win number as a string, not the buf number vim.api.nvim_create_autocmd("WinClosed", { pattern = tostring(ui_data.window), callback = close_buffer_callback, }) vim.api.nvim_create_autocmd("BufWritePost", { pattern = "*.norg", callback = unlisten_if_closed(function(buf, ui) toc_title = vim.split(module.public.parse_toc_macro(buf) or "Table of Contents", "\n") data_of_norg_buf[buf].last_row = nil -- invalidate cursor cache module.public.update_toc(toc_title, ui, buf) end), }) vim.api.nvim_create_autocmd("BufEnter", { pattern = "*.norg", callback = unlisten_if_closed(function(buf, ui) if buf == ui.buffer or buf == ui.norg_buffer then return end toc_title = vim.split(module.public.parse_toc_macro(buf) or "Table of Contents", "\n") module.public.update_toc(toc_title, ui, buf) end), }) -- Sync cursor: ToC -> content if module.config.public.sync_cursorline then -- Ignore the first (fake) CursorMoved coming together with BufEnter of the ToC buffer vim.api.nvim_create_autocmd("BufEnter", { buffer = ui_data.buffer, callback = function(_ev) ui_data.cursor_start_moving = false end, }) vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { buffer = ui_data.buffer, callback = function(ev) assert(ev.buf == ui_data.buffer) if vim.fn.bufwinid(ui_data.norg_buffer) == -1 then return end -- Ignore the first (fake) CursorMoved coming together with BufEnter of the ToC buffer if ui_data.cursor_start_moving then local location = get_target_location_under_cursor(ui_data) if location then local norg_window = vim.fn.bufwinid(ui_data.norg_buffer) vim.api.nvim_win_set_cursor(norg_window, { location[1] + 1, location[2] }) vim.api.nvim_buf_call(ui_data.norg_buffer, function() vim.cmd("normal! zz") end) end end ui_data.cursor_start_moving = true end, }) -- Sync cursor: content -> ToC vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { pattern = "*.norg", callback = unlisten_if_closed(function(buf, ui) if buf ~= ui.norg_buffer then return end if not data_of_norg_buf[buf] then -- toc not yet created because BufEnter is not yet triggered return end module.public.update_cursor(ui) end), }) -- When leaving the content buffer, add its last cursor position to jump list vim.api.nvim_create_autocmd("BufLeave", { pattern = "*.norg", callback = unlisten_if_closed(function(_norg_buffer, _ui_data) vim.cmd("normal! m'") end), }) end if module.config.public.auto_toc.exit_nvim then vim.api.nvim_create_autocmd("WinEnter", { buffer = ui_data.buffer, callback = unlisten_if_closed(function(_, _) vim.schedule(function() -- count the number of 'real' (non-floating) windows. This avoids noice popups -- and nvim notify popups causing nvim to stay open local real_windows = vim.iter(vim.api.nvim_list_wins()) :filter(function(win) return vim.api.nvim_win_get_config(win).relative == "" end) :totable() if #real_windows == 1 then vim.schedule(vim.cmd.q) end end) end), }) end if module.config.public.auto_toc.close then vim.api.nvim_create_autocmd("BufWinLeave", { pattern = "*.norg", callback = unlisten_if_closed(function(_buf, ui) vim.schedule(function() if vim.fn.winnr("$") > 1 then local win = vim.fn.bufwinid(ui.buffer) if win ~= -1 then vim.api.nvim_win_close(win, true) close_buffer_callback() end end end) end), }) end end module.events.subscribed = { ["core.neorgcmd"] = { [module.name] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/qol/todo_items/module.lua ================================================ --[[ file: Todo-Items title: Todo Item Swiss Army Knife summary: Module for implementing todo lists. --- This module handles the whole concept of toggling TODO items, as well as updating parent and/or children items alongside the current item. The following keybinds are exposed (with their default binds): - `(neorg.qol.todo-items.todo.task-done)` (`td`) - `(neorg.qol.todo-items.todo.task-undone)` (`tu`) - `(neorg.qol.todo-items.todo.task-pending)` (`tp`) - `(neorg.qol.todo-items.todo.task-on_hold)` (`th`) - `(neorg.qol.todo-items.todo.task-cancelled)` (`tc`) - `(neorg.qol.todo-items.todo.task-recurring)` (`tr`) - `(neorg.qol.todo-items.todo.task-important)` (`ti`) - `(neorg.qol.todo-items.todo.task-cycle)` (``) - `(neorg.qol.todo-items.todo.task-cycle-reverse)` (no default keybind) With your cursor on a line that contains an item with a TODO attribute, press any of the above keys to toggle the state of that particular item. Parent items of the same type and children items of the same type are update accordingly. --]] local neorg = require("neorg.core") local log, modules = neorg.log, neorg.modules --[[@as neorg.modules]] local module = modules.create("core.qol.todo_items") module.setup = function() return { success = true, requires = { "core.integrations.treesitter" } } end module.load = function() for _, task in ipairs({ "done", "undone", "pending", "on-hold", "cancelled", "important", "recurring", "ambiguous", "cycle", "cycle-reverse", }) do vim.keymap.set( "", string.format("(neorg.qol.todo-items.todo.task-%s)", task), module.public["task-" .. task] ) end end module.config.public = { -- The default order of TODO item cycling when cycling via -- ``. -- -- Defaults to the following order: `undone`, `done`, `pending`. order = { { "undone", " " }, { "done", "x" }, { "pending", "-" }, }, -- The default order of TODO item cycling when the item -- has nested children with TODO items. -- -- When cycling through TODO items with children it's not -- always sensible to follow the same schema as the `order` table. -- -- Defaults to the following order: `undone`, `done`. order_with_children = { { "undone", " " }, { "done", "x" }, }, -- When set to `true`, will automatically convert parent -- items to TODOs whenever a child item's TODO state is updated. -- -- For instance, given the following example: -- ```norg -- - Text -- -- ( ) Child text -- ``` -- -- When this option is `true` and the child's state is updated to e.g. -- `(x)` via the `td` keybind, the new output becomes: -- ```norg -- - (x) Text -- -- (x) Child text -- ``` create_todo_parents = false, -- Automatically update the parent todo state when a child node is updated. -- -- eg: -- ```norg -- - ( ) parent -- -- ( ) child -- ``` -- Marking `-- ( ) child` as done (with a keybind) will result in: -- ```norg -- - (-) parent -- -- (x) child -- ``` update_todo_parents = true, -- When `true`, will automatically create a TODO extension for an item -- if it does not exist and an operation is performed on that item. -- -- Given the following example: -- ```norg -- - Test Item -- ``` -- With this option set to true, performing an operation (like pressing `` -- or what have you) will convert the non-todo item into one: -- ```norg -- - ( ) Test Item -- ``` create_todo_items = true, } ---@alias TodoItemType "undone" ---|"pending" ---|"done" ---|"cancelled" ---|"recurring" ---|"on_hold" ---|"urgent" ---|"uncertain" module.private = { names = { ["x"] = "done", [" "] = "undone", ["-"] = "pending", ["="] = "on_hold", ["_"] = "cancelled", ["!"] = "important", ["+"] = "recurring", ["?"] = "ambiguous", }, fire_update_event = function(char, line) local ev = modules.create_event(module, module.events.defined["todo-changed"].type, { char = char, line = line }) if ev then modules.broadcast_event(ev) end end, --- Updates the parent todo item for the current todo item if it exists ---@param recursion_level number the index of the parent to change. The higher the number the more the code will traverse up the syntax tree. update_parent = function(buf, line, recursion_level) -- Force a reparse (this is required because otherwise some cached nodes will be incorrect) vim.treesitter.get_parser(buf, "norg"):parse() -- If present grab the item that is under the cursor local item_at_cursor = module.private.get_todo_item_from_cursor(buf, line) if not item_at_cursor then return end -- If we set a recursion level then go through and traverse up the syntax tree `recursion_level` times for _ = 0, recursion_level do item_at_cursor = item_at_cursor:parent() ---@diagnostic disable-line -- TODO: type error workaround end -- If the final item does not exist or the target item is not a detached modifier -- (i.e. it does not have a "prefix" node) then it is not a node worth updating. if not item_at_cursor or not item_at_cursor:named_child(0) or not item_at_cursor:named_child(0):type():match("prefix") then return end local counts = { undone = 0, pending = 0, done = 0, cancelled = 0, recurring = 0, on_hold = 0, urgent = 0, uncertain = 0, } local counter = 0 -- Go through all the children of the current todo item node and count the amount of "done" children for node in item_at_cursor:iter_children() do if node:named_child(1) and node:named_child(1):type() == "detached_modifier_extension" then for status in node:named_child(1):iter_children() do if status:type():match("^todo_item_") then local type = status:type():match("^todo_item_(.+)$") counts[type] = counts[type] + 1 if type == "cancelled" then break end counter = counter + 1 end end end end -- [[ -- Compare the counter to the amount of done items. -- If we have even one pending item then set the resulting char to `*` -- If the counter is the same as the done item count then that means all items are complete and we should display a done item in the parent. -- If the done item count is 0 then no task has been completed and we should set an undone item as the parent. -- If all other checks fail and the done item count is less than the total number of todo items then set a pending item. -- ]] if counter == 0 then return end local resulting_char = "" if counts.uncertain > 0 and counts.done + counts.uncertain == counter then resulting_char = "=" elseif counts.on_hold > 0 and counts.done + counts.on_hold + counts.uncertain == counter then resulting_char = "=" elseif counts.pending > 0 then resulting_char = "-" elseif counter == counts.done then resulting_char = "x" elseif counts.done == 0 then resulting_char = " " elseif counts.done < counter then resulting_char = "-" end local first_status_extension = module.private.find_first_status_extension(item_at_cursor:named_child(1)) -- TODO(vhyrro): -- Implement a toggleable behaviour where Neorg can automatically convert this: -- * (@ Mon 5th Feb) Test -- ** ( ) Test -- To this: -- * (x|@ Mon 5th Feb) Test -- ** (x) Test if not first_status_extension then if not module.config.public.create_todo_parents then return end local row, _, _, column = item_at_cursor:named_child(0):range() vim.api.nvim_buf_set_text(buf, row, column, row, column, { "(" .. resulting_char .. ") " }) module.private.fire_update_event(resulting_char, row) module.private.update_parent(buf, line, recursion_level + 1) return end local range = module.required["core.integrations.treesitter"].get_node_range(first_status_extension) -- Replace the line where the todo item is situated vim.api.nvim_buf_set_text( buf, range.row_start, range.column_start, range.row_end, range.column_end, { resulting_char } ) module.private.fire_update_event(resulting_char, range.row_start) module.private.update_parent(buf, line, recursion_level + 1) end, --- Find the first occurence of a status extension within a detached -- modifier extension node. ---@param detached_modifier_extension_node userdata #A valid node of type `detached_modifier_extension` find_first_status_extension = function(detached_modifier_extension_node) if not detached_modifier_extension_node then return end for status in detached_modifier_extension_node:iter_children() do ---@diagnostic disable-line -- TODO: type error workaround if vim.startswith(status:type(), "todo_item_") then return status end end end, --- Tries to locate a todo_item node under the cursor ---@return userdata? #The node if it was located, else nil get_todo_item_from_cursor = function(buf, line) local node_at_cursor = module.required["core.integrations.treesitter"].get_first_node_on_line(buf, line) if not node_at_cursor then return end -- This is done because sometimes the first node can be -- e.g `generic_list`, which only contains the top level list items and -- not their data. It doesn't cost us much to do this operation for other -- nodes anyway. if node_at_cursor:named_child(0) then node_at_cursor = node_at_cursor:named_child(0) end while true do if not node_at_cursor then log.trace("Could not find TODO item under cursor, aborting...") return end local first_named_child = node_at_cursor:named_child(0) if first_named_child and first_named_child:type():match("prefix") then break else node_at_cursor = node_at_cursor:parent() end end return node_at_cursor end, --- Returns the type of a todo item (either "done", "pending" or "undone") ---@param todo_node userdata #The todo node to extract the data from ---@return TodoItemType? #A todo item type as a string, else nil get_todo_item_type = function(todo_node) if not todo_node or not todo_node:named_child(1) then ---@diagnostic disable-line -- TODO: type error workaround return end local todo_type = module.private.find_first_status_extension(todo_node:named_child(1)) ---@diagnostic disable-line -- TODO: type error workaround return todo_type and todo_type:type():sub(string.len("todo_item_") + 1) or nil end, --- Converts the current node and all its children to a certain type ---@param buf number the current buffer number ---@param node userdata the node to modify ---@param todo_item_type TodoItemType #The todo item type as a string ---@param char string the character to place within the square brackets of the todo item (one of "x", "*" or " ") make_all = function(buf, node, todo_item_type, char) if not node then return end local type = node:type():match("^(.+)%d+$") ---@diagnostic disable-line -- TODO: type error workaround -- If the type of the current todo item differs from the one we want to change to then -- We do this because we don't want to be unnecessarily modifying a line that doesn't need changing if module.private.get_todo_item_type(node) == todo_item_type then return end local first_status_extension = module.private.find_first_status_extension(node:named_child(1)) ---@diagnostic disable-line -- TODO: type error workaround local parent_line if not first_status_extension then if not module.config.public.create_todo_items then return end local row, _, _, column = node:named_child(0):range() ---@diagnostic disable-line -- TODO: type error workaround vim.api.nvim_buf_set_text(buf, row, column, row, column, { "(" .. char .. ") " }) parent_line = row else local range = module.required["core.integrations.treesitter"].get_node_range(first_status_extension) parent_line = range.row_start module.private.fire_update_event(char, range.row_start) vim.api.nvim_buf_set_text( buf, range.row_start, range.column_start, range.row_end, range.column_end, { char } ) end if module.config.public.update_todo_parents then module.private.update_parent(buf, parent_line, 0) end for child in node:iter_children() do ---@diagnostic disable-line -- TODO: type error workaround if type == child:type():match("^(.+)%d+$") then module.private.make_all(buf, child, todo_item_type, char) end end end, task_cycle = function(buf, linenr, types, alternative_types) local todo_item_at_cursor = module.private.get_todo_item_from_cursor(buf, linenr - 1) if not todo_item_at_cursor then return end local todo_item_type = module.private.get_todo_item_type(todo_item_at_cursor) --- Gets the next item of a flat list based on the first item ---@param type_list table[] #A list of { "type", "char" } items ---@param item_type string #The `type` field from the `type_list` array ---@return number? #An index into the next item of `type_list` local function get_index(type_list, item_type) for i, element in ipairs(type_list) do if element[1] == item_type then if i >= #type_list then return 1 else return i + 1 end end end end if not todo_item_type then if not module.config.public.create_todo_items then return end module.private.make_all(buf, todo_item_at_cursor, types[1][1], types[1][2]) module.private.update_parent(buf, linenr - 1, 0) return end local index = get_index(types, todo_item_type) local next = types[index] or types[1] for child in todo_item_at_cursor:iter_children() do if module.private.get_todo_item_type(child) then next = alternative_types[get_index(alternative_types, todo_item_type)] or alternative_types[1] break end end module.private.make_all(buf, todo_item_at_cursor, next[1], next[2]) module.private.update_parent(buf, linenr - 1, 0) end, } ---Set the todo item in the given buffer at the given line ---@param buffer number 0 for current ---@param line number 1 based line number, 0 for current ---@param character string local function task_set_at(buffer, line, character) local name = module.private.names[character] if buffer == 0 then buffer = vim.api.nvim_get_current_buf() end if line == 0 then line = vim.api.nvim_win_get_cursor(0)[1] end local todo_item_at_cursor = module.private.get_todo_item_from_cursor(buffer, line - 1) if not todo_item_at_cursor then return end module.private.make_all(buffer, todo_item_at_cursor, name, character) end local function task_set(character) return neorg.utils.wrap_dotrepeat(function() task_set_at(0, 0, character) end) end ---@class core.qol.todo_items module.public = { ["set_at"] = task_set_at, ["task-done"] = task_set("x"), ["task-undone"] = task_set(" "), ["task-pending"] = task_set("-"), ["task-on-hold"] = task_set("="), ["task-cancelled"] = task_set("_"), ["task-important"] = task_set("!"), ["task-recurring"] = task_set("+"), ["task-ambiguous"] = task_set("?"), ["task-cycle"] = function() local buffer = vim.api.nvim_get_current_buf() local cursor = vim.api.nvim_win_get_cursor(0) module.private.task_cycle( buffer, cursor[1], module.config.public.order, module.config.public.order_with_children ) end, ["task-cycle-reverse"] = function() local buffer = vim.api.nvim_get_current_buf() local cursor = vim.api.nvim_win_get_cursor(0) module.private.task_cycle( buffer, cursor[1], vim.fn.reverse(module.config.public.order), vim.fn.reverse(module.config.public.order_with_children) ) end, } module.events.defined = { ["todo-changed"] = modules.define_event(module, "todo-changed"), } return module ================================================ FILE: lua/neorg/modules/core/queries/native/module.lua ================================================ --[[ file: Queries-Module title: Queries Made Easy summary: TS wrapper in order to fetch nodes using a custom table. internal: true --- The `core.queries.native` module provides useful Treesitter wrappers to query information from Norg documents. --]] local neorg = require("neorg.core") local lib, log, modules = neorg.lib, neorg.log, neorg.modules ---@class core.queries.native.tree_node ---@field query string[] ---@field subtree core.queries.native.tree_node[]|nil ---@field recursive boolean|nil local module = modules.create("core.queries.native") module.setup = function() return { success = true, requires = { "core.integrations.treesitter" }, } end module.examples = { ["Get the content of all todo_item1 in a norg file"] = function() local buf = 1 -- The buffer to query informations --- @type core.queries.native.tree_node[] local tree = { { query = { "first", "document_content" }, subtree = { { query = { "all", "generic_list" }, recursive = true, subtree = { { query = { "all", "todo_item1" }, }, }, }, }, }, } -- Get a list of { node, buf } local nodes = module.required["core.queries.native"].query_nodes_from_buf(tree, buf) local extracted_nodes = module.required["core.queries.native"].extract_nodes(nodes) -- Free the text in memory after reading nodes module.required["core.queries.native"].delete_content(buf) print(nodes, extracted_nodes) end, } ---@class core.queries.native module.public = { --- Recursively generates results from a `parent` node, following a `tree` table --- @see First implementation in: https://github.com/danymat/neogen/blob/main/lua/neogen/utilities/nodes.lua ---@param parent userdata ---@param tree core.queries.native.tree_node ---@param results table|nil ---@return table query_from_tree = function(parent, tree, bufnr, results) local res = results or {} for _, subtree in pairs(tree) do local matched, how_to_fix = module.private.matching_nodes(parent, subtree, bufnr) if type(matched) == "string" then log.error( "Oh no! There's been an error in the query. It seems that we've received some malformed input at one of the subtrees present in parent node of type", parent:type() ---@diagnostic disable-line -- TODO: type error workaround ) log.error("Here's the error message:", matched) if how_to_fix then log.warn("To fix the issue:", vim.trim(how_to_fix)) end return ---@diagnostic disable-line -- TODO: type error workaround end -- We extract matching nodes that doesn't have subtree if not subtree.subtree then for _, v in pairs(matched) do table.insert(res, { v, bufnr }) end else for _, node in pairs(matched) do local nodes = module.public.query_from_tree(node, subtree.subtree, bufnr, res) if not nodes then return {} end res = vim.tbl_extend("force", res, nodes) end end end return res end, --- Use a `tree` to query all required nodes from a `bufnr`. Returns a list of nodes of type { node, bufnr } ---@param tree core.queries.native.tree_node ---@param bufnr number ---@return table query_nodes_from_buf = function(tree, bufnr) local temp_buf = module.public.get_temp_buf(bufnr) local root_node = module.required["core.integrations.treesitter"].get_document_root(temp_buf) if not root_node then return {} end local res = module.public.query_from_tree(root_node, tree, bufnr) return res end, --- Extract content from `nodes` of type { node, bufnr } ---@param nodes table ---@param opts table? --- - opts.all_lines (bool) if true, will return all lines instead of the first one ---@return table extract_nodes = function(nodes, opts) opts = opts or {} local res = {} for _, node in ipairs(nodes) do local temp_buf = module.public.get_temp_buf(node[2]) local extracted = vim.split(vim.treesitter.get_node_text(node[1], temp_buf), "\n") if opts.all_lines then table.insert(res, extracted) else table.insert(res, extracted[1]) end end return res end, --- Find the parent `node` that match `node_type`. Returns a node of type { node, bufnr }. --- If `opts.multiple`, returns a table of parent nodes that mached `node_type` --- `node` must be of type { node, bufnr } ---@param node table ---@param node_type string ---@param opts table --- - opts.multiple (bool): if true, will return all recursive parent nodes that match `node_type` ---@return table find_parent_node = function(node, node_type, opts) vim.validate({ node = { node, "table" }, node_type = { node_type, "string" }, opts = { opts, "table", true }, }) opts = opts or {} local res = {} local parent = node[1]:parent() while parent do if parent:type() == node_type then table.insert(res, { parent, node[2] }) if not opts.multiple then return res[1] end end parent = parent:parent() end return res end, --- Creates an unlisted temp buffer reading from the original bufnr. --- This does prevent triggering norg autocommands ---@param buf number #The bufnr to get text from ---@param opts table? #Custom options --- - opts.no_force_read boolean? #If true, will not read original buffer if it fails to open ---@return number #The temporary bufnr get_temp_buf = function(buf, opts) opts = opts or {} -- If we don't have any previous private data, get the file text if not module.private.data.temp_bufs[buf] then -- Get the file name from bufnr local uri = vim.uri_from_bufnr(buf) local fname = vim.uri_to_fname(uri) -- Open and read all lines in the file local f, err = io.open(fname, "r") local lines if not f then log.warn("Can't read file " .. fname) if opts.no_force_read then log.error(err) return ---@diagnostic disable-line -- TODO: type error workaround end lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) else lines = f:read("*a") or "" lines = vim.split(lines, "\n") if lines[#lines] == "" then --vim.split automatically adds an empty line because the file stops with a newline table.remove(lines) end f:close() end -- Stores the lines in a temp buffer local temp_buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(temp_buf, 0, -1, false, lines) vim.api.nvim_buf_attach(temp_buf, false, { on_lines = function() module.private.data.temp_bufs[buf].changed = true end, }) module.private.data.temp_bufs[buf] = { buf = temp_buf, changed = false } end return module.private.data.temp_bufs[buf].buf end, apply_temp_changes = function(buf) local temp_buf = module.private.data.temp_bufs[buf] if temp_buf and temp_buf.changed then -- Write the lines to original file local lines = vim.api.nvim_buf_get_lines(temp_buf.buf, 0, -1, false) local uri = vim.uri_from_bufnr(buf) local fname = vim.uri_to_fname(uri) lib.when(vim.fn.bufloaded(buf) == 1, function() vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) vim.api.nvim_buf_call(buf, lib.wrap(vim.cmd, "write!")) ---@diagnostic disable-line -- TODO: type error workaround end, lib.wrap(vim.fn.writefile, lines, fname)) -- We reset the state as false because we are consistent with the original file temp_buf.changed = false end end, --- Deletes the content from data. --- If no buffer is provided, will delete every buffer datas --- @overload fun() ---@param buf number #The content relative to the provided buffer delete_content = function(buf) lib.when(buf, function() ---@diagnostic disable-line -- TODO: type error workaround module.private.data.temp_bufs[buf] = nil end, function() module.private.data.temp_bufs = {} end) end, } module.private = { data = { -- Must be a table of keys like buffer = string_content temp_bufs = {}, }, --- Returns a list of child nodes (from `parent`) that matches a `tree` ---@param parent userdata ---@param tree core.queries.native.tree_node ---@return table matching_nodes = function(parent, tree, bufnr) local res = {} local where = tree.where ---@diagnostic disable-line -- TODO: type error workaround local matched_query, how_to_fix = module.private.matching_query(parent, tree.query, { recursive = tree.recursive }) if type(matched_query) == "string" then return matched_query, how_to_fix ---@diagnostic disable-line -- TODO: type error workaround end if not where then return matched_query else for _, matched in pairs(matched_query) do local matched_where = module.private.predicate_where(matched, where, { bufnr = bufnr }) if matched_where then table.insert(res, matched) end end end return res end, --- Get a list of child nodes (from `parent`) that match the provided `query` --- @see First implementation in: https://github.com/danymat/neogen/blob/main/lua/neogen/utilities/nodes.lua ---@param parent userdata ---@param query table ---@param opts table --- - opts.recursive (bool): if true will recursively find the matching query ---@return table | any, any ---@diagnostic disable-line -- TODO: type error workaround matching_query = function(parent, query, opts) vim.validate({ parent = { parent, "userdata" }, query = { query, "table" }, opts = { opts, "table", true }, }) opts = opts or {} local res = {} if not query then return "No 'queries' value present in the query object!", [[ Be sure to supply a query parameter, one that looks as such: { query = { "all", "heading1" }, -- You can use any node type here } ]] end if vim.fn.len(query) < 2 then return "Not enough queries supplied! Expected at least 2 but got " .. tostring(#query), ([[ Be sure to supply a second parameter, that being the type of node you would like to operate on. You should change your line to something like: { query = { "%s", "heading1" } } Instead. ]]):format(query[1] or "all") end if not vim.tbl_contains({ "all", "first", "match" }, query[1]) then return "Syntax error: " .. query[1] .. " is not a valid node operation.", ([[ Use a supported node operation. Neorg currently supports "all", "first" and "match". With that in mind, you can do something like this (for example): { query = { "all", "%s" } } ]]):format(query[2]) end ------------------------- for node in parent:iter_children() do ---@diagnostic disable-line -- TODO: type error workaround if node:type() == query[2] then -- query : { "first", "node_name"} first child node that match node_name if query[1] == "first" then table.insert(res, node) break -- query : { "match", "node_name", "test" } all node_name nodes that match "test" content -- elseif query[1] == "match" then -- TODO Match node content -- query : { "all", "node_name" } all child nodes that match node_name elseif query[1] == "all" then table.insert(res, node) end end if opts.recursive then local found = module.private.matching_query(node, query, { recursive = true }) vim.list_extend(res, found) end end return res end, --- Checks if `parent` node matches a `where` query and returns a predicate accordingly ---@param parent userdata ---@param where table ---@param opts table --- - opts.bufnr (number): used in where[1] == "child_content" (in order to get the node's content) ---@return boolean predicate_where = function(parent, where, opts) opts = opts or {} if not where or #where == 0 then return true end -- Where statements requesting children nodes from parent node for node in parent:iter_children() do ---@diagnostic disable-line -- TODO: type error workaround if where[1] == "child_exists" then if node:type() == where[2] then return true end end if where[1] == "child_content" then local temp_buf = module.public.get_temp_buf(opts.bufnr) if node:type() == where[2] and vim.split(vim.treesitter.query.get_node_text(node, temp_buf), "\n")[1] == where[3] ---@diagnostic disable-line -- TODO: type error workaround then return true end end end return false end, } return module ================================================ FILE: lua/neorg/modules/core/scanner/module.lua ================================================ --[[ file: Scanner-Module title: Scanner module for Neorg summary: This module is an implementation of a scanner that can be used anywhere TS can't be used. internal: true --- This module is an implementation of a scanner that can be used anywhere TS can't be used. It is not currently used anywhere, and is small enough to be self-documenting. --]] local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.scanner") ---@class core.scanner module.public = { initialize_new = function(self, source) self:end_session() self.source = source end, end_session = function(self) local tokens = self.tokens self.position = 0 self.buffer = "" self.source = "" self.tokens = {} return tokens end, position = 0, buffer = "", source = "", tokens = {}, current = function(self) if self.position == 0 then return nil end return self.source:sub(self.position, self.position) end, lookahead = function(self, count) count = count or 1 if self.position + count > self.source:len() then return nil end return self.source:sub(self.position + count, self.position + count) end, lookbehind = function(self, count) count = count or 1 if self.position - count < 0 then return nil end return self.source:sub(self.position - count, self.position - count) end, backtrack = function(self, amount) self.position = self.position - amount end, advance = function(self) self.buffer = self.buffer .. self.source:sub(self.position, self.position) self.position = self.position + 1 end, skip = function(self) self.position = self.position + 1 end, mark_end = function(self) if self.buffer:len() ~= 0 then table.insert(self.tokens, self.buffer) self.buffer = "" end end, halt = function(self, mark_end, continue_till_end) if mark_end then self:mark_end() end if continue_till_end then self.buffer = self.source:sub(self.position + 1) self:mark_end() end self.position = self.source:len() + 1 end, } return module ================================================ FILE: lua/neorg/modules/core/storage/module.lua ================================================ --[[ File: Storage Title: Store persistent data and query it easily with `core.storage` Summary: Deals with storing persistent data across Neorg sessions. Internal: true --- --]] local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.storage") module.setup = function() return { requires = { "core.autocommands", }, } end module.config.public = { -- Full path to store data (saved in mpack data format) path = vim.fn.stdpath("data") .. "/neorg.mpack", } module.private = { data = {}, } ---@class core.storage module.public = { --- Grabs the data present on disk and overwrites it with the data present in memory sync = function() local file = io.open(module.config.public.path, "r") if not file then return end local content = file:read("*a") io.close(file) module.private.data = vim.mpack.decode and vim.mpack.decode(content) or vim.mpack.unpack(content) end, --- Stores a key-value pair in the storage ---@param key string #The key to index in the storage ---@param data any #The data to store at the specific key store = function(key, data) module.private.data[key] = data end, --- Removes a key from storage ---@param key string #The name of the key to remove remove = function(key) module.private.data[key] = nil end, --- Retrieves a key from the storage ---@param key string #The name of the key to index ---@return any|table #The data present at the key, or an empty table retrieve = function(key) return module.private.data[key] or {} end, --- Flushes the contents in memory to the location specified in the `path` configuration option. flush = function() local file = io.open(module.config.public.path, "w") if not file then return end file:write(vim.mpack.encode and vim.mpack.encode(module.private.data) or vim.mpack.pack(module.private.data)) io.close(file) end, } module.on_event = function(event) -- Synchronize the data in memory with the data on disk after we leave Neovim if event.type == "core.autocommands.events.vimleavepre" then module.public.flush() end end module.load = function() module.required["core.autocommands"].enable_autocommand("VimLeavePre") module.public.sync() end module.events.subscribed = { ["core.autocommands"] = { vimleavepre = true, }, } return module ================================================ FILE: lua/neorg/modules/core/summary/module.lua ================================================ --[[ file: Summary title: Write notes, not boilerplate. description: The summary module creates links and annotations to all files in a given workspace. summary: Creates links to all files in any workspace. --- The `core.summary` module exposes a single command - `:Neorg generate-workspace-summary`. When executed with the cursor hovering over a heading, `core.summary` will generate, you guessed it, a summary of the entire workspace, with links to each respective entry in that workspace. If arguments are provided then a partial summary is generated containing only categories that you have provided. E.g. `:Neorg generate-workspace-summary work todos` would only generate a summary of the categories `work` and `todos`. The way the summary is generated relies on the `strategy` configuration option, which by default consults the document metadata (see also [`core.esupports.metagen`](@core.esupports.metagen)) or the first heading title as a fallback to build up a tree of categories, titles and descriptions. --]] local neorg = require("neorg.core") local lib, modules, utils = neorg.lib, neorg.modules, neorg.utils local module = modules.create("core.summary") module.setup = function() return { sucess = true, requires = { "core.integrations.treesitter" }, } end module.load = function() modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ ["generate-workspace-summary"] = { min_args = 0, condition = "norg", name = "summary.summarize", }, }) end) local ts = module.required["core.integrations.treesitter"] -- declare query on load so that it's parsed once, on first use local heading_query local get_first_heading_title = function(bufnr) local document_root = ts.get_document_root(bufnr) if not document_root then return end if not heading_query then -- allow second level headings, just in case local heading_query_string = [[ [ (heading1 title: (paragraph_segment) @next-segment ) (heading2 title: (paragraph_segment) @next-segment ) ] ]] heading_query = utils.ts_parse_query("norg", heading_query_string) end -- search up to 20 lines (a doc could potentially have metadata without metadata.title) local _, heading = heading_query:iter_captures(document_root, bufnr)() if not heading then return nil end local start_line, _ = heading:start() local lines = vim.api.nvim_buf_get_lines(bufnr, start_line, start_line + 1, false) if #lines > 0 then local title = lines[1]:gsub("^%s*%*+%s*", "") -- strip out '*' prefix (handle '* title', ' **title', etc) if title ~= "" then -- exclude an empty heading like `*` (although the query should have excluded) return title end end end -- Return true if catagories_path is or is a subcategory of an entry in included_categories local is_included_category = function(included_categories, category_path) local found_match = false for _, included in ipairs(included_categories) do local included_path = vim.split(included, ".", { plain = true }) for i, path in ipairs(included_path) do if path == category_path[i] and i == #included_path then found_match = true break elseif path ~= category_path[i] then break end end end return found_match end -- Insert a categorized record for the given file into the categories table local insert_categorized = function(categories, category_path, norgname, metadata) local leaf_categories = categories for i, path in ipairs(category_path) do local titled_path = lib.title(path) if i == #category_path then -- There are no more sub catergories so insert the record table.insert(leaf_categories[titled_path], { title = tostring(metadata.title), norgname = norgname, description = metadata.description, }) break end local sub_categories = vim.defaulttable() if leaf_categories[titled_path] then -- This category already been added so find it's sub_categories table for _, item in ipairs(leaf_categories[titled_path]) do if item.sub_categories then leaf_categories = item.sub_categories goto continue end end end -- This is a new sub category table.insert(leaf_categories[titled_path], { title = titled_path, sub_categories = sub_categories, }) leaf_categories = sub_categories ::continue:: end end module.config.public.strategy = lib.match(module.config.public.strategy)({ default = function() return function(files, ws_root, heading_level, include_categories) local categories = vim.defaulttable() if vim.tbl_isempty(include_categories) then include_categories = nil end utils.read_files(files, function(bufnr, filename) local metadata = ts.get_document_metadata(bufnr) if not metadata then metadata = {} end local norgname = filename:match("(.+)%.norg$") -- strip extension for link destinations if not norgname then norgname = filename end norgname = string.sub(norgname, ws_root:len() + 1) -- normalise categories into a list. Could be vim.NIL, a number, a string or a list ... if not metadata.categories or metadata.categories == vim.NIL then metadata.categories = { "Uncategorised" } elseif not vim.islist(metadata.categories) then ---@diagnostic disable-line metadata.categories = { tostring(metadata.categories) } end if not metadata.title then metadata.title = get_first_heading_title(bufnr) if not metadata.title then metadata.title = vim.fs.basename(norgname) end end if metadata.description == vim.NIL then metadata.description = nil end for _, category in ipairs(metadata.categories) do local category_path = vim.split(category, ".", { plain = true }) if include_categories then if is_included_category(include_categories, category_path) then insert_categorized(categories, category_path, norgname, metadata) end else insert_categorized(categories, category_path, norgname, metadata) end end end) local result = {} local starting_prefix = string.rep("*", heading_level) local function add_category(category, data, level) local result_temp = {} local sub_cats_temp = {} local new_prefix = starting_prefix .. string.rep("*", level) table.insert(result_temp, new_prefix .. " " .. category) for _, datapoint in ipairs(data) do if datapoint.sub_categories then for sub_category, sub_data in vim.spairs(datapoint.sub_categories) do table.insert(sub_cats_temp, add_category(sub_category, sub_data, level + 1)) end else table.insert( result_temp, table.concat({ string.rep(" ", level + 1), " - {:$", datapoint.norgname, ":}[", lib.title(datapoint.title), "]", }) .. ( datapoint.description and (table.concat({ " - ", datapoint.description })) or "" ) ) end end for _, sub_cat in pairs(sub_cats_temp) do for _, row in pairs(sub_cat) do table.insert(result_temp, row) end end return result_temp end for category, data in vim.spairs(categories) do local temp = add_category(category, data, 0) for _, row in pairs(temp) do table.insert(result, row) end end return result end end, headings = function() return function() end end, by_path = function() return function(files, ws_root, heading_level, include_categories) local categories = vim.defaulttable() if vim.tbl_isempty(include_categories) then include_categories = nil end utils.read_files(files, function(bufnr, filename) local metadata = ts.get_document_metadata(bufnr) or {} local path_tokens = lib.tokenize_path(filename) local category = path_tokens[#path_tokens - 1] or "Uncategorised" local norgname = filename:match("(.+)%.norg$") or filename -- strip extension for link destinations norgname = string.sub(norgname, ws_root:len() + 1) if not metadata.title then metadata.title = get_first_heading_title(bufnr) or vim.fs.basename(norgname) end if metadata.description == vim.NIL then metadata.description = nil end if not include_categories or vim.tbl_contains(include_categories, category:lower()) then table.insert(categories[lib.title(category)], { title = tostring(metadata.title), norgname = norgname, description = metadata.description, }) end end) local result = {} local prefix = string.rep("*", heading_level) for category, data in vim.spairs(categories) do table.insert(result, prefix .. " " .. category) for _, datapoint in ipairs(data) do table.insert( result, table.concat({ string.rep(" ", heading_level), " - {:$", datapoint.norgname, ":}[", lib.title(datapoint.title), "]", }) .. (datapoint.description and (table.concat({ " - ", datapoint.description })) or "") ) end end return result end end, }) or module.config.public.strategy end module.config.public = { -- The strategy to use to generate a summary. -- -- Possible options are: -- - "default" - read the metadata to categorize and annotate files. Files -- without metadata will use the top level heading as the title. If no headings are present, the filename will be used. -- - "by_path" - Similar to "default" but uses the capitalized name of the folder containing a *.norg file as category. -- - A custom function with the signature: -- `fun(files: PathlibPath[], ws_root: PathlibPath, heading_level: number?, include_categories: string[]?): string[]?`. -- Returning a list of lines that make up the summary strategy = "default", } ---@class core.summary module.public = { ---@param buf integer? the buffer to insert the summary to ---@param cursor_pos integer[]? a tuple of row, col of the cursor positon (see nvim_win_get_cursor()) ---@param include_categories string[]? table of strings (ignores case) for categories that you wish to include in the summary. -- if excluded then all categories are written into the summary. generate_workspace_summary = function(buf, cursor_pos, include_categories) local ts = module.required["core.integrations.treesitter"] local buffer = buf or 0 local cursor_position = cursor_pos or vim.api.nvim_win_get_cursor(0) local node_at_cursor = ts.get_first_node_on_line(buffer, cursor_position[1] - 1) if not node_at_cursor or not node_at_cursor:type():match("^heading%d$") then utils.notify( "No heading under cursor! Please move your cursor under the heading you'd like to generate the summary under." ) return end -- heading level of 'node_at_cursor' (summary headings should be one level deeper) local level = tonumber(string.sub(node_at_cursor:type(), -1)) local dirman = modules.get_module("core.dirman") if not dirman then utils.notify("`core.dirman` is not loaded! It is required to generate summaries") return end local ws_root = dirman.get_current_workspace()[2] local generated = module.config.public.strategy( dirman.get_norg_files(dirman.get_current_workspace()[1]) or {}, ws_root, level + 1, vim.tbl_map(string.lower, include_categories or {}) ) if not generated or vim.tbl_isempty(generated) then utils.notify( "No summary to generate! Either change the `strategy` option or ensure you have some indexable files in your workspace." ) return end vim.api.nvim_buf_set_lines(buffer, cursor_position[1], cursor_position[1], true, generated) end, } module.events.subscribed = { ["core.neorgcmd"] = { ["summary.summarize"] = true, }, } module.on_event = function(event) if event.type == "core.neorgcmd.events.summary.summarize" then -- Remove `data` key, and take only the numerical keys from event.content -- these numerical keys are the category args passed to the command local include_categories = { unpack(event.content) } module.public.generate_workspace_summary(event.buffer, event.cursor_position, include_categories) end end return module ================================================ FILE: lua/neorg/modules/core/syntax/module.lua ================================================ --[[ file: Syntax title: Where Treesitter can't Reach description: When a language not supported by Treesitter is found a fallback is made to use vim regex highlighting. summary: Handles interaction for syntax files for code blocks. internal: true --- The `core.syntax` module highlights any `@code` region where there is no treesitter parser present to highlight the region. This module very closely resembles the [`concealer`](@core.concealer), but some parts have been adapted to fit the correct use case. ##### Author's note: This module will appear as spaghetti code at first glance. This is intentional. If one needs to edit this module, it is best to talk to me at `katawful` on GitHub. Any edit is assumed to break this module. --]] local neorg = require("neorg.core") local modules, utils = neorg.modules, neorg.utils local module = modules.create("core.syntax") ---@type core.integrations.treesitter local ts local function schedule(func) vim.schedule(function() if module.private.disable_deferred_updates or ( (module.private.debounce_counters[vim.api.nvim_win_get_cursor(0)[1] + 1] or 0) >= module.config.public.performance.max_debounce ) then return end func() end) end module.setup = function() return { success = true, requires = { "core.autocommands", "core.integrations.treesitter", }, } end module.private = { largest_change_start = -1, largest_change_end = -1, last_change = { active = false, line = 0, }, -- we need to track the buffers in use last_buffer = "", disable_deferred_updates = false, debounce_counters = {}, code_block_table = { --[[ table is setup like so { buf_name_1 = {loaded_regex = {regex_name = {type = "type", range = {start_row1 = end_row1}}}} buf_name_2 = {loaded_regex = {regex_name = {type = "type", range = {start_row1 = end_row1}}}} } --]] }, available_languages = {}, } ---@class core.syntax module.public = { -- fills module.private.loaded_code_blocks with the list of active code blocks in the buffer -- stores globally apparently check_code_block_type = function(buf, reload, from, to) -- parse the current buffer, and clear out the buffer's loaded code blocks if needed local current_buf = vim.api.nvim_buf_get_name(buf) -- load nil table with empty values if module.private.code_block_table[current_buf] == nil then module.private.code_block_table[current_buf] = { loaded_regex = {} } end -- recreate table for buffer on buffer change -- reason for existence: --[[ user deletes a bunch of code blocks from file, and said code blocks were the only regex blocks of that language. on a full buffer refresh like reentering the buffer, this will get cleared to recreate what languages are loaded. then another function will handle unloading syntax files on next load --]] for key in pairs(module.private.code_block_table) do if current_buf == key and reload == true then for k, _ in pairs(module.private.code_block_table[current_buf].loaded_regex) do module.public.remove_syntax( string.format("textGroup%s", string.upper(k)), string.format("textSnip%s", string.upper(k)) ) module.private.code_block_table[current_buf].loaded_regex[k] = nil end end end -- If the tree is valid then attempt to perform the query local root = ts.get_document_root(buf) if root then -- get the language node used by the code block local code_lang = utils.ts_parse_query( "norg", [[( (ranged_verbatim_tag (tag_name) @_tagname (tag_parameters) @language) (#any-of? @_tagname "code" "embed") )]] ) -- check for each code block capture in the root with a language parameter -- to build a table of all the languages for a given buffer local compare_table = {} -- a table to compare to what was loaded for id, node in code_lang:iter_captures(root, buf, from or 0, to or -1) do if id == 2 then -- id 2 here refers to the "language" tag -- find the end node of a block so we can grab the row local end_node = node:next_named_sibling():next_sibling() -- get the start and ends of the current capture local start_row = node:range() + 1 local end_row -- don't try to parse a nil value if end_node == nil then end_row = start_row + 1 else end_row = end_node:range() + 1 end local regex_lang = vim.treesitter.get_node_text(node, buf) -- make sure that the language is actually valid local type_func = function() return module.private.available_languages[regex_lang].type end local ok, type = pcall(type_func) if not ok then type = "null" -- null type will never get parsed like treesitter languages end -- add language to table -- if type is empty it means this language has never been found if module.private.code_block_table[current_buf].loaded_regex[regex_lang] == nil then module.private.code_block_table[current_buf].loaded_regex[regex_lang] = { type = type, range = {}, cluster = "", } end -- else just do what we need to do module.private.code_block_table[current_buf].loaded_regex[regex_lang].range[start_row] = end_row table.insert(compare_table, regex_lang) end end -- compare loaded languages to see if the file actually has the code blocks if from == nil then for lang in pairs(module.private.code_block_table[current_buf].loaded_regex) do local found_lang = false for _, matched in pairs(compare_table) do if matched == lang then found_lang = true break end end -- if no lang was matched, means we didn't find a language in our parse -- remove the syntax include and region if found_lang == false then -- delete loaded lang from the table module.private.code_block_table[current_buf].loaded_regex[lang] = nil module.public.remove_syntax( string.format("textGroup%s", string.upper(lang)), string.format("textSnip%s", string.upper(lang)) ) end end end end end, -- load syntax files for regex code blocks trigger_highlight_regex_code_block = function(buf, remove, ignore_buf, from, to) -- scheduling this function seems to break parsing properly -- schedule(function() local current_buf = vim.api.nvim_buf_get_name(buf) -- only parse from the loaded_code_blocks module, not from the file directly if module.private.code_block_table[current_buf] == nil then return end local lang_table = module.private.code_block_table[current_buf].loaded_regex for lang_name, curr_table in pairs(lang_table) do if curr_table.type == "syntax" then -- NOTE: the regex fallback code was originally mostly adapted from Vimwiki -- In its current form it has been intensely expanded upon local group = string.format("textGroup%s", string.upper(lang_name)) local snip = string.format("textSnip%s", string.upper(lang_name)) local start_marker = string.format("@code %s", lang_name) local end_marker = "@end" local has_syntax = string.format("syntax list @%s", group) -- sync groups when needed if ignore_buf == false and vim.api.nvim_buf_get_name(buf) == module.private.last_buffer then module.public.sync_regex_code_blocks(buf, lang_name, from, to) end -- try removing syntax before doing anything -- fixes hi link groups from not loading on certain updates if remove == true then module.public.remove_syntax(group, snip) end --- @type boolean, string|{ output: string } local ok, result = pcall(vim.api.nvim_exec2, has_syntax, { output = true }) result = result.output or result local count = select(2, result:gsub("\n", "\n")) -- get length of result from syn list local empty_result = 0 -- look to see if the textGroup is actually empty -- clusters don't delete when they're clear for line in result:gmatch("([^\n]*)\n?") do empty_result = string.match(line, "textGroup%w+%s+cluster=NONE") if empty_result == nil then empty_result = 0 else empty_result = #empty_result break end end -- see if the syntax files even exist before we try to call them -- if syn list was an error, or if it was an empty result if ok == false or ( ok == true and ((string.sub(result, 1, 1) == ("N" or "V") and count == 0) or (empty_result > 0)) ) then -- absorb all syntax stuff -- potentially needs to be expanded upon as bad values come in local is_keyword = vim.bo[buf].iskeyword local current_syntax = "" local foldmethod = vim.o.foldmethod local foldexpr = vim.o.foldexpr local foldtext = vim.o.foldtext local foldnestmax = vim.o.foldnestmax local foldcolumn = vim.o.foldcolumn local foldenable = vim.o.foldenable local foldminlines = vim.o.foldminlines if vim.b.current_syntax ~= "" or vim.b.current_syntax ~= nil then current_syntax = lang_name vim.b.current_syntax = nil ---@diagnostic disable-line end -- include the cluster that will put inside the region -- source using the available languages for syntax, table in pairs(module.private.available_languages) do if table.type == "syntax" then if lang_name == syntax then if empty_result == 0 then -- get the file name for the syntax file --- @type string|string[] local file = vim.api.nvim_get_runtime_file(string.format("syntax/%s.vim", syntax), false) if file == nil then file = vim.api.nvim_get_runtime_file( string.format("after/syntax/%s.vim", syntax), false ) end file = file[1] local command = string.format("syntax include @%s %s", group, file) vim.cmd(command) -- make sure that group has things when needed local regex = group .. "%s+cluster=(.+)" --- @type boolean, string|{ output: string } local _, found_cluster = pcall( vim.api.nvim_exec2, string.format("syntax list @%s", group), { output = true } ) found_cluster = found_cluster.output or found_cluster local actual_cluster for match in found_cluster:gmatch(regex) do actual_cluster = match end if actual_cluster ~= nil then module.private.code_block_table[current_buf].loaded_regex[lang_name].cluster = actual_cluster end elseif module.private.code_block_table[current_buf].loaded_regex[lang_name].cluster ~= nil then local command = string.format( "silent! syntax cluster %s add=%s", group, module.private.code_block_table[current_buf].loaded_regex[lang_name].cluster ) vim.cmd(command) end end end end -- reset some values after including vim.bo[buf].iskeyword = is_keyword vim.b.current_syntax = current_syntax or "" ---@diagnostic disable-line has_syntax = string.format("syntax list %s", snip) --- @type boolean, string|{ output: string } _, result = pcall(vim.api.nvim_exec2, has_syntax, { output = true }) result = result.output or result count = select(2, result:gsub("\n", "\n")) -- get length of result from syn list --[[ if we see "-" it means there potentially is already a region for this lang we must have only 1 line, more lines means there is a region already see :h syn-list for the format --]] if count == 0 or (string.sub(result, 1, 1) == "-" and count == 0) then -- set highlight groups local regex_fallback_hl = string.format( [[ syntax region %s \ matchgroup=Snip \ start="%s" end="%s" \ contains=@%s \ keepend ]], snip, start_marker, end_marker, group ) vim.cmd(string.format("%s", regex_fallback_hl)) -- sync everything module.public.sync_regex_code_blocks(buf, lang_name, from, to) end vim.o.foldmethod = foldmethod vim.o.foldexpr = foldexpr vim.o.foldtext = foldtext vim.o.foldnestmax = foldnestmax vim.o.foldcolumn = foldcolumn vim.o.foldenable = foldenable vim.o.foldminlines = foldminlines end vim.b.current_syntax = "" ---@diagnostic disable-line module.private.last_buffer = vim.api.nvim_buf_get_name(buf) end end -- end) end, -- remove loaded syntax include and snip region remove_syntax = function(group, snip) -- these clears are silent. errors do not matter -- errors are assumed to come from the functions that call this local group_remove = string.format("silent! syntax clear @%s", group) vim.cmd(group_remove) local snip_remove = string.format("silent! syntax clear %s", snip) vim.cmd(snip_remove) end, -- sync regex code blocks sync_regex_code_blocks = function(buf, regex, from, to) local current_buf = vim.api.nvim_buf_get_name(buf) -- only parse from the loaded_code_blocks module, not from the file directly if module.private.code_block_table[current_buf] == nil then return end local lang_table = module.private.code_block_table[current_buf].loaded_regex for lang_name, curr_table in pairs(lang_table) do -- if we got passed a regex, then we need to only parse the right one if regex ~= nil then if regex ~= lang_name then goto continue end end if curr_table.type == "syntax" then -- sync from code block -- for incremental syncing if from ~= nil then local found_lang = false for start_row, end_row in pairs(curr_table.range) do -- see if the text changes we made included a regex code block if start_row <= from and end_row >= to then found_lang = true end end -- didn't find match from this range of the current language, skip parsing if found_lang == false then goto continue end end local snip = string.format("textSnip%s", string.upper(lang_name)) local start_marker = string.format("@code %s", lang_name) -- local end_marker = "@end" local regex_fallback_hl = string.format( [[ syntax sync match %s \ grouphere %s \ "%s" ]], snip, snip, start_marker ) vim.cmd(string.format("silent! %s", regex_fallback_hl)) -- NOTE: this is kept as a just in case -- sync back from end block -- regex_fallback_hl = string.format( -- [[ -- syntax sync match %s -- \ groupthere %s -- \ "%s" -- ]], -- snip, -- snip, -- end_marker -- ) -- TODO check groupthere, a slower process -- vim.cmd(string.format("silent! %s", regex_fallback_hl)) -- vim.cmd("syntax sync maxlines=100") end ::continue:: end end, } module.config.public = { -- Performance options for highlighting. -- -- These options exhibit the same behaviour as the [`concealer`](@core.concealer)'s. performance = { -- How many lines each "chunk" of a file should take up. -- -- When the size of the buffer is greater than this value, -- the buffer is then broken up into equal chunks and operations -- are done individually on those chunks. increment = 1250, -- How long the syntax module should wait before starting to conceal -- the buffer. timeout = 0, -- How long the syntax module should wait before starting to conceal -- a new chunk. interval = 500, -- The maximum amount of recalculations that take place at a single time. -- More operations than this count will be dropped. -- -- Especially useful when e.g. holding down `x` in a buffer, forcing -- hundreds of recalculations at a time. max_debounce = 5, }, } module.load = function() -- Enabled the required autocommands -- This is generally any potential redraw event module.required["core.autocommands"].enable_autocommand("BufEnter") module.required["core.autocommands"].enable_autocommand("ColorScheme") module.required["core.autocommands"].enable_autocommand("TextChanged") -- module.required["core.autocommands"].enable_autocommand("TextChangedI") -- module.required["core.autocommands"].enable_autocommand("InsertEnter") module.required["core.autocommands"].enable_autocommand("InsertLeave") module.required["core.autocommands"].enable_autocommand("VimLeavePre") -- Load available regex languages -- get the available regex files for the current session module.private.available_languages = utils.get_language_list(false) ts = module.required["core.integrations.treesitter"] --[[@as core.integrations.treesitter]] end module.on_event = function(event) module.private.debounce_counters[event.cursor_position[1] + 1] = module.private.debounce_counters[event.cursor_position[1] + 1] or 0 local function should_debounce() return module.private.debounce_counters[event.cursor_position[1] + 1] >= module.config.public.performance.max_debounce end if event.type == "core.autocommands.events.bufenter" and event.content.norg then local buf = event.buffer local line_count = vim.api.nvim_buf_line_count(buf) if line_count < module.config.public.performance.increment then module.public.check_code_block_type(buf, false) module.public.trigger_highlight_regex_code_block(buf, false, false) else -- This bit of code gets triggered if the line count of the file is bigger than one increment level -- provided by the user. -- In this case, the syntax trigger enters a block mode and splits up the file into chunks. It then goes through each -- chunk at a set interval and applies the syntax that way to reduce load and improve performance. -- This points to the current block the user's cursor is in local block_current = math.floor(event.cursor_position[1] / module.config.public.performance.increment) local function trigger_syntax_for_block(block) local line_begin = block == 0 and 0 or block * module.config.public.performance.increment - 1 local line_end = math.min( block * module.config.public.performance.increment + module.config.public.performance.increment - 1, line_count ) module.public.check_code_block_type(buf, false, line_begin, line_end) module.public.trigger_highlight_regex_code_block(buf, false, false, line_begin, line_end) end trigger_syntax_for_block(block_current) local block_bottom, block_top = block_current - 1, block_current + 1 local timer = vim.loop.new_timer() timer:start( module.config.public.performance.timeout, module.config.public.performance.interval, vim.schedule_wrap(function() local block_bottom_valid = block_bottom == 0 or (block_bottom * module.config.public.performance.increment - 1 >= 0) local block_top_valid = block_top * module.config.public.performance.increment - 1 < line_count if not vim.api.nvim_buf_is_loaded(buf) or (not block_bottom_valid and not block_top_valid) then timer:stop() return end if block_bottom_valid then trigger_syntax_for_block(block_bottom) block_bottom = block_bottom - 1 end if block_top_valid then trigger_syntax_for_block(block_top) block_top = block_top + 1 end end) ) end vim.api.nvim_buf_attach(buf, false, { on_lines = function(_, cur_buf, _, start, _end) if buf ~= cur_buf then return true end if should_debounce() then return end module.private.last_change.active = true local mode = vim.api.nvim_get_mode().mode if mode ~= "i" then module.private.debounce_counters[event.cursor_position[1] + 1] = module.private.debounce_counters[event.cursor_position[1] + 1] + 1 schedule(function() local new_line_count = vim.api.nvim_buf_line_count(buf) -- Sometimes occurs with one-line undos if start == _end then _end = _end + 1 end if new_line_count > line_count then _end = _end + (new_line_count - line_count - 1) end line_count = new_line_count vim.schedule(function() module.private.debounce_counters[event.cursor_position[1] + 1] = module.private.debounce_counters[event.cursor_position[1] + 1] - 1 end) end) else if module.private.largest_change_start == -1 then module.private.largest_change_start = start end if module.private.largest_change_end == -1 then module.private.largest_change_end = _end end module.private.largest_change_start = start < module.private.largest_change_start and start or module.private.largest_change_start module.private.largest_change_end = _end > module.private.largest_change_end and _end or module.private.largest_change_end end end, }) elseif event.type == "core.autocommands.events.insertleave" then if should_debounce() then return end schedule(function() if not module.private.last_change.active or module.private.largest_change_end == -1 then module.public.check_code_block_type( event.buffer, false -- module.private.last_change.line, -- module.private.last_change.line + 1 ) module.public.trigger_highlight_regex_code_block( event.buffer, false, true, module.private.last_change.line, module.private.last_change.line + 1 ) else module.public.check_code_block_type( event.buffer, false, module.private.last_change.line, module.private.last_change.line + 1 ) module.public.trigger_highlight_regex_code_block( event.buffer, false, true, module.private.largest_change_start, module.private.largest_change_end ) end module.private.largest_change_start, module.private.largest_change_end = -1, -1 end) elseif event.type == "core.autocommands.events.vimleavepre" then module.private.disable_deferred_updates = true -- this autocmd is used to fix hi link syntax languages -- TEMP(vhyrro): Temporarily removed for testing - executes code twice when it should not. -- elseif event.type == "core.autocommands.events.textchanged" then -- module.private.trigger_highlight_regex_code_block(event.buffer, false, true) -- elseif event.type == "core.autocommands.events.textchangedi" then -- module.private.trigger_highlight_regex_code_block(event.buffer, false, true) elseif event.type == "core.autocommands.events.colorscheme" then module.public.trigger_highlight_regex_code_block(event.buffer, true, false) end end module.events.subscribed = { ["core.autocommands"] = { bufenter = true, colorscheme = true, insertleave = true, vimleavepre = true, }, } return module ================================================ FILE: lua/neorg/modules/core/tangle/module.lua ================================================ --[[ file: Tangling title: From Code Blocks to Files description: The `core.tangle` module exports code blocks within a `.norg` file straight to a file of your choice. summary: An Advanced Code Block Exporter. --- The goal of this module is to allow users to spit out the contents of code blocks into many different files. This is the primary component required for a literate configuration in Neorg, where the configuration is annotated and described in a `.norg` document, and the actual code itself is thrown out into a file that can then be normally consumed by e.g. an application. The `tangle` module currently provides a single command: - `:Neorg tangle current-file` - performs all possible tangling operations on the current file ### Usage Tutorial By default, *zero* code blocks are tangled. You must provide where you'd like to tangle each code block manually (global configuration will be discussed later). To do so, add a `#tangle ` tag above the code block you'd wish to export, where is relative to the current file. For example: ```norg #tangle init.lua @code lua print("Hello World!") @end ``` The above snippet will *only* tangle that single code block to the desired output file: `init.lua`. > [!WARNING] > Due to a bug in the norg treesitter parser, `#tangle ./init.lua` or `#tangle folder/init.lua` will not work > As a result, we recommend specifying files destinations in metadata #### Global Tangling for Single Files Apart from tangling a single or a set of code blocks, you can declare a global output file in the document's metadata: ```norg @document.meta tangle: ./init.lua @end ``` This will tangle all `lua` code blocks to `init.lua`, *unless* the code block has an explicit `#tangle` tag associated with it, in which case the `#tangle` tag takes precedence. #### Global Tangling for Multiple Files Apart from a single filepath, you can provide many in an array: ```norg @document.meta tangle: [ ./init.lua ./output.hs ] @end ``` The above snippet tells the Neorg tangling engine to tangle all `lua` code blocks to `./init.lua` and all `haskell` code blocks to `./output.hs`. As always if any of the code blocks have a `#tangle` tag then that takes precedence. If you want to be more verbose, or you're tangling to a file without an extension (perhaps you're writing a shell script that has a shebang) you can also do this: ```norg @document.meta tangle: { lua: ./init.lua python: ./output } @end ``` #### Ignoring Code Blocks Sometimes when tangling you may want to omit some code blocks. For this you may use the `#tangle.none` tag: ```norg #tangle.none @code lua print("I won't be tangled!") @end ``` #### Global Tangling with Extra Options But wait, it doesn't stop there! You can supply a string to `tangle`, an array to `tangle`, but also an object! It looks like this: ```norg @document.meta tangle: { languages: { lua: ./output.lua haskell: my-haskell-file } delimiter: heading scope: all } @end ``` The `language` option determines which filetype should go into which file. It's a simple language-filepath mapping, but it's especially useful when the output file's language type cannot be inferred from the name or shebang. It is also possible to use the name `_` as a catchall to direct output for all files not otherwise listed. The `delimiter` option determines how to delimit code blocks that export to the same file. The following variations are allowed: * `heading` -- Try to determine the filetype of the code block and insert any headings from the original document as a comment in the tangled output. If filetype detection fails, `newline` will be used instead. * `file-content` -- Try to determine the filetype of the codeblock and insert the Neorg file content as a delimiter. If filetype detection fails, `none` will be used instead. * `newline` -- Use an extra newline between tangled blocks. * `none` -- Do not add any delimiter. This implies that the code blocks are inserted into the tangle target as-is. The `scope` option is discussed below. #### Tangling Scopes What you've seen so far is the tangler operating in `all` mode. This means it captures all code blocks of a certain type unless that code block is tagged with `#tangle.none`. There are two other types: `tagged` and `main`. ##### The `tagged` Scope When in this mode, the tangler will only tangle code blocks that have been `tagged` with a `#tangle` tag. Note that you don't have to always provide a filetype, and that: ```norg #tangle @code lua @end ``` Will use the global output file for that language as defined in the metadata. I.e., if I do: ```norg @document.meta tangle: { languages: { lua: ./output.lua } scope: tagged } @end @code lua print("Hello") @end #tangle @code lua print("Sup") @end #tangle other-file.lua @code lua print("Ayo") @end ``` The first code block will not be touched, the second code block will be tangled to `./output.lua` and the third code block will be tangled to `other-file.lua`. You can probably see that this system can get expressive pretty quick. ##### The `main` scope This mode is the opposite of the `tagged` one in that it will only tangle code blocks to files that are defined in the document metadata. I.e. in this case: ```norg @document.meta tangle: { languages: { lua: ./output.lua } scope: main } @end @code lua print("Hello") @end #tangle @code lua print("Sup") @end #tangle other-file.lua @code lua print("Ayo") @end ``` The first code block will be tangled to `./output.lua`, the second code block will also be tangled to `./output.lua` and the third code block will be ignored. --]] local neorg = require("neorg.core") local lib, modules, utils, log = neorg.lib, neorg.modules, neorg.utils, neorg.log local module = modules.create("core.tangle") local Path = require("pathlib") ---@type core.dirman.utils local dirman_utils module.setup = function() return { requires = { "core.integrations.treesitter", "core.dirman.utils", "core.neorgcmd", }, } end module.load = function() dirman_utils = module.required["core.dirman.utils"] modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ tangle = { args = 1, condition = "norg", subcommands = { ["current-file"] = { args = 0, name = "core.tangle.current-file", }, -- directory = { -- max_args = 1, -- name = "core.tangle.directory", -- } }, }, }) end) if module.config.public.tangle_on_write then local augroup = vim.api.nvim_create_augroup("norg_auto_tangle", { clear = true }) vim.api.nvim_create_autocmd("BufWritePost", { desc = "Tangle the current file on write", pattern = "*.norg", group = augroup, command = "Neorg tangle current-file", }) end end local function get_comment_string(language) local cur_buf = vim.api.nvim_get_current_buf() local tmp_buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_set_current_buf(tmp_buf) vim.bo.filetype = language local commentstring = vim.bo.commentstring vim.api.nvim_set_current_buf(cur_buf) vim.api.nvim_buf_delete(tmp_buf, { force = true }) return commentstring end ---@class core.tangle module.public = { tangle = function(buffer) ---@type core.integrations.treesitter local treesitter = module.required["core.integrations.treesitter"] local parsed_document_metadata = treesitter.get_document_metadata(buffer) or {} local tangle_settings = parsed_document_metadata.tangle or {} local options = { languages = tangle_settings.languages or tangle_settings, scope = tangle_settings.scope or "all", -- "all" | "tagged" | "main" delimiter = tangle_settings.delimiter or "newline", -- "newline" | "heading" | "file-content" | "none" } ---@diagnostic disable-next-line if vim.islist(options.languages) then options.filenames_only = options.languages options.languages = {} elseif type(options.languages) == "string" then options.languages = { _ = options.languages } end local document_root = treesitter.get_document_root(buffer) if not document_root then return end local filename_to_languages = {} local tangles = { -- filename = { block_content } } local query_str = lib.match(options.scope)({ _ = [[ (ranged_verbatim_tag name: (tag_name) @_name (#eq? @_name "code") (tag_parameters . (tag_param) @_language)) @tag ]], tagged = [[ (ranged_verbatim_tag [(strong_carryover_set (strong_carryover name: (tag_name) @_strong_carryover_tag_name (#eq? @_strong_carryover_tag_name "tangle"))) (weak_carryover_set (weak_carryover name: (tag_name) @_weak_carryover_tag_name (#eq? @_weak_carryover_tag_name "tangle")))] name: (tag_name) @_name (#eq? @_name "code") (tag_parameters . (tag_param) @_language)) @tag ]], }) local query = utils.ts_parse_query("norg", query_str) local previous_headings = {} local commentstrings = {} local file_content_line_start = {} local buf_name = vim.api.nvim_buf_get_name(buffer) for id, node in query:iter_captures(document_root, buffer, 0, -1) do local capture = query.captures[id] if capture == "tag" then local ok, parsed_tag = pcall(treesitter.get_tag_info, node, true) if not ok then if module.config.public.indent_errors == "print" then print(parsed_tag) else log.error(parsed_tag) end goto skip_tag end if parsed_tag then local declared_filetype = parsed_tag.parameters[1] local block_content = parsed_tag.content if parsed_tag.parameters[1] == "norg" then for i, line in ipairs(block_content) do -- remove escape char local new_line, _ = line:gsub("\\(.?)", "%1") block_content[i] = new_line or "" end end local file_to_tangle_to for _, attribute in ipairs(parsed_tag.attributes) do if attribute.name == "tangle.none" then goto skip_tag elseif attribute.name == "tangle" and attribute.parameters[1] then if options.scope == "main" then goto skip_tag end file_to_tangle_to = table.concat(attribute.parameters) end end -- determine tangle file target if not file_to_tangle_to then if declared_filetype and options.languages[declared_filetype] then file_to_tangle_to = options.languages[declared_filetype] else if options.filenames_only then for _, filename in ipairs(options.filenames_only) do if declared_filetype == vim.filetype.match({ filename = filename, contents = block_content }) ---@diagnostic disable-line -- TODO: type error workaround then file_to_tangle_to = filename break end end end if not file_to_tangle_to then file_to_tangle_to = options.languages["_"] end if declared_filetype then options.languages[declared_filetype] = file_to_tangle_to end end end if not file_to_tangle_to then goto skip_tag end local path_lib_path = Path.new(file_to_tangle_to) if path_lib_path:is_relative() then local buf_path = Path.new(buf_name) file_to_tangle_to = tostring(dirman_utils.expand_pathlib(file_to_tangle_to, true, buf_path):resolve()) end local delimiter_content if options.delimiter == "heading" or options.delimiter == "file-content" then local language if filename_to_languages[file_to_tangle_to] then language = filename_to_languages[file_to_tangle_to] else language = vim.filetype.match({ filename = file_to_tangle_to, contents = block_content }) ---@diagnostic disable-line -- TODO: type error workaround if not language and declared_filetype then language = vim.filetype.match({ filename = "___." .. declared_filetype, contents = block_content, }) end filename_to_languages[file_to_tangle_to] = language end -- Get commentstring from vim scratch buffer if language and not commentstrings[language] then commentstrings[language] = get_comment_string(language) end -- TODO(vhyrro): Maybe issue warnings to the user when the target -- commentstring is not found by Neovim? -- if not language or commentstrings[language] == "" then -- No action -- end if options.delimiter == "heading" then -- get current heading local heading_string local heading = treesitter.find_parent(node, "heading%d+") if heading and heading:named_child(1) then local srow, scol, erow, ecol = heading:named_child(1):range() heading_string = vim.api.nvim_buf_get_text(0, srow, scol, erow, ecol, {})[1] end -- don't reuse the same header more than once if heading_string and language and previous_headings[language] ~= heading then previous_headings[language] = heading if tangles[file_to_tangle_to] then delimiter_content = { "", commentstrings[language]:format(heading_string), "" } else delimiter_content = { commentstrings[language]:format(heading_string), "" } end elseif tangles[file_to_tangle_to] then delimiter_content = { "" } end elseif options.delimiter == "file-content" then if not file_content_line_start[file_to_tangle_to] then file_content_line_start[file_to_tangle_to] = 0 end local start = file_content_line_start[file_to_tangle_to] local srow, _, erow, _ = node:range() delimiter_content = vim.api.nvim_buf_get_lines(buffer, start, srow, true) file_content_line_start[file_to_tangle_to] = erow + 1 for idx, line in ipairs(delimiter_content) do if line ~= "" then delimiter_content[idx] = commentstrings[language]:format(line) end end end elseif options.delimiter == "newline" then if tangles[file_to_tangle_to] then delimiter_content = { "" } end end if not tangles[file_to_tangle_to] then tangles[file_to_tangle_to] = {} end if delimiter_content then vim.list_extend(tangles[file_to_tangle_to], delimiter_content) end vim.list_extend(tangles[file_to_tangle_to], block_content) end end ::skip_tag:: end if options.delimiter == "file-content" then for filename, start in pairs(file_content_line_start) do local language = filename_to_languages[filename] local delimiter_content = vim.api.nvim_buf_get_lines(buffer, start, -1, true) for idx, line in ipairs(delimiter_content) do if line ~= "" then delimiter_content[idx] = commentstrings[language]:format(line) end end vim.list_extend(tangles[filename], delimiter_content) end end return tangles end, } module.config.public = { -- Notify when there is nothing to tangle (INFO) or when the content is empty (WARN). report_on_empty = true, -- Tangle all code blocks in the current norg file on file write. tangle_on_write = false, -- When text in a code block is less indented than the block itself, Neorg will not tangle that -- block to a file. Instead it can either print or vim.notify error. By default, vim.notify is -- loud and is more likely to create a press enter message. -- - "notify" - Throw a normal looking error -- - "print" - print the error indent_errors = "notify", } module.on_event = function(event) if event.type == "core.neorgcmd.events.core.tangle.current-file" then local tangles = module.public.tangle(event.buffer) if not tangles or vim.tbl_isempty(tangles) then if module.config.public.report_on_empty then utils.notify("Nothing to tangle!", vim.log.levels.INFO) end return end local file_count = vim.tbl_count(tangles) local tangled_count = 0 for file_str, content in pairs(tangles) do local file = dirman_utils.expand_pathlib(file_str, true) if not file then goto continue end if not file:parent():exists() then file:parent():mkdir(Path.permission("rwxr-xr-x"), true) end file_str = file:tostring() vim.uv.fs_open(file_str, "w", 438, function(werr, fd) assert( not werr and fd, lib.lazy_string_concat("Failed to open file '", file_str, "' for tangling: ", werr) ) local write_content = table.concat(content, "\n") if module.config.public.report_on_empty and write_content:len() == 0 then vim.schedule(function() utils.notify(string.format("Tangled content for %s is empty.", file_str), vim.log.levels.WARN) end) end vim.uv.fs_write(fd, write_content, 0, function(werr2) assert( not werr2, lib.lazy_string_concat("Failed to write to '", file_str, "' for tangling: ", werr2) ) tangled_count = tangled_count + 1 file_count = file_count - 1 if file_count == 0 then vim.schedule( lib.wrap( utils.notify, string.format( "Successfully tangled %d file%s!", tangled_count, tangled_count == 1 and "" or "s" ) ) ) end end) vim.uv.fs_close(fd, function(err) assert(not err, lib.lazy_string_concat("Failed to close file '", file_str, "' for tangling: ", err)) end) end) ::continue:: end end end module.events.subscribed = { ["core.neorgcmd"] = { ["core.tangle.current-file"] = true, ["core.tangle.directory"] = true, }, } return module ================================================ FILE: lua/neorg/modules/core/tempus/module.lua ================================================ --[[ file: Tempus title: Hassle-Free Dates summary: Parses and handles dates in Neorg. internal: true --- `core.tempus` is an internal module specifically designed to handle complex dates. It exposes two functions: `parse_date(string) -> date|string` and `to_lua_date(date) -> osdate`. ## Keybinds This module exposes the following keybinds (see [`core.keybinds`](@core.keybinds) for instructions on mapping them): - `neorg.tempus.insert-date` - Insert date at cursor position (called from normal mode) - `neorg.tempus.insert-date.insert-mode` - Insert date at cursor position (called from insert mode) --]] local neorg = require("neorg.core") local lib, modules, utils = neorg.lib, neorg.modules, neorg.utils local module = modules.create("core.tempus") -- NOTE: Maybe encapsulate whole date parser in a single PEG grammar? local _, time_regex = pcall(vim.re.compile, [[{%d%d?} ":" {%d%d} ("." {%d%d?})?]]) local timezone_list = { "ACDT", "ACST", "ACT", "ACWST", "ADT", "AEDT", "AEST", "AET", "AFT", "AKDT", "AKST", "ALMT", "AMST", "AMT", "ANAT", "AQTT", "ART", "AST", "AWST", "AZOST", "AZOT", "AZT", "BNT", "BIOT", "BIT", "BOT", "BRST", "BRT", "BST", "BTT", "CAT", "CCT", "CDT", "CEST", "CET", "CHADT", "CHAST", "CHOT", "CHOST", "CHST", "CHUT", "CIST", "CKT", "CLST", "CLT", "COST", "COT", "CST", "CT", "CVT", "CWST", "CXT", "DAVT", "DDUT", "DFT", "EASST", "EAST", "EAT", "ECT", "EDT", "EEST", "EET", "EGST", "EGT", "EST", "ET", "FET", "FJT", "FKST", "FKT", "FNT", "GALT", "GAMT", "GET", "GFT", "GILT", "GIT", "GMT", "GST", "GYT", "HDT", "HAEC", "HST", "HKT", "HMT", "HOVST", "HOVT", "ICT", "IDLW", "IDT", "IOT", "IRDT", "IRKT", "IRST", "IST", "JST", "KALT", "KGT", "KOST", "KRAT", "KST", "LHST", "LINT", "MAGT", "MART", "MAWT", "MDT", "MET", "MEST", "MHT", "MIST", "MIT", "MMT", "MSK", "MST", "MUT", "MVT", "MYT", "NCT", "NDT", "NFT", "NOVT", "NPT", "NST", "NT", "NUT", "NZDT", "NZST", "OMST", "ORAT", "PDT", "PET", "PETT", "PGT", "PHOT", "PHT", "PHST", "PKT", "PMDT", "PMST", "PONT", "PST", "PWT", "PYST", "PYT", "RET", "ROTT", "SAKT", "SAMT", "SAST", "SBT", "SCT", "SDT", "SGT", "SLST", "SRET", "SRT", "SST", "SYOT", "TAHT", "THA", "TFT", "TJT", "TKT", "TLT", "TMT", "TRT", "TOT", "TVT", "ULAST", "ULAT", "UTC", "UYST", "UYT", "UZT", "VET", "VLAT", "VOLT", "VOST", "VUT", "WAKT", "WAST", "WAT", "WEST", "WET", "WIB", "WIT", "WITA", "WGST", "WGT", "WST", "YAKT", "YEKT", } ---@alias Date {weekday: {name: string, number: number}?, day: number?, month: {name: string, number: number}?, year: number?, timezone: string?, time: {hour: number, minute: number, second: number?}?} ---@class core.tempus module.public = { --- Converts a parsed date with `parse_date` to a lua date. ---@param parsed_date Date #The date to convert ---@return osdate #A Lua date to_lua_date = function(parsed_date) local now = os.date("*t") --[[@as osdate]] local parsed = os.time(vim.tbl_deep_extend("force", now, { day = parsed_date.day, month = parsed_date.month and parsed_date.month.number or nil, year = parsed_date.year, hour = parsed_date.time and parsed_date.time.hour, min = parsed_date.time and parsed_date.time.minute, sec = parsed_date.time and parsed_date.time.second, }) --[[@as osdateparam]]) return os.date("*t", parsed) --[[@as osdate]] end, --- Converts a lua `osdate` to a Neorg date. ---@param osdate osdate #The date to convert ---@param include_time boolean? #Whether to include the time (hh::mm.ss) in the output. ---@return Date #The converted date to_date = function(osdate, include_time) -- TODO: Extract into a function to get weekdays (have to hot recalculate every time because the user may change locale local weekdays = {} for i = 1, 7 do table.insert(weekdays, os.date("%A", os.time({ year = 2000, month = 5, day = i })):lower()) ---@diagnostic disable-line -- TODO: type error workaround end local months = {} for i = 1, 12 do table.insert(months, os.date("%B", os.time({ year = 2000, month = i, day = 1 })):lower()) ---@diagnostic disable-line -- TODO: type error workaround end -- os.date("*t") returns wday with Sunday as 1, needs to be -- converted to Monday as 1 local converted_weekday = lib.number_wrap(osdate.wday - 1, 1, 7) return module.private.tostringable_date({ weekday = osdate.wday and { number = converted_weekday, name = lib.title(weekdays[converted_weekday]), } or nil, day = osdate.day, month = osdate.month and { number = osdate.month, name = lib.title(months[osdate.month]), } or nil, year = osdate.year, time = osdate.hour and setmetatable({ hour = osdate.hour, minute = osdate.min or 0, second = osdate.sec or 0, }, { __tostring = function() if not include_time then return "" end return tostring(osdate.hour) .. ":" .. tostring(string.format("%02d", osdate.min)) .. (osdate.sec ~= 0 and ("." .. tostring(osdate.sec)) or "") end, }) or nil, }) end, --- Parses a date and returns a table representing the date ---@param input string #The input which should follow the date specification found in the Norg spec. ---@return Date|string #The data extracted from the input or an error message parse_date = function(input) local weekdays = {} for i = 1, 7 do table.insert(weekdays, os.date("%A", os.time({ year = 2000, month = 5, day = i })):lower()) ---@diagnostic disable-line -- TODO: type error workaround end local months = {} for i = 1, 12 do table.insert(months, os.date("%B", os.time({ year = 2000, month = i, day = 1 })):lower()) ---@diagnostic disable-line -- TODO: type error workaround end local output = {} for word in vim.gsplit(input, "%s+") do if word:len() == 0 then goto continue end if word:match("^-?%d%d%d%d+$") then output.year = tonumber(word) elseif word:match("^%d+%w*$") then output.day = tonumber(word:match("%d+")) elseif vim.list_contains(timezone_list, word:upper()) then output.timezone = word:upper() else do local hour, minute, second = vim.re.match(word, time_regex) if hour and minute then output.time = setmetatable({ hour = tonumber(hour), minute = tonumber(minute), second = second and tonumber(second) or nil, }, { __tostring = function() return word end, }) goto continue end end do local valid_months = {} -- Check for month abbreviation for i, month in ipairs(months) do if vim.startswith(month, word:lower()) then valid_months[month] = i end end local count = vim.tbl_count(valid_months) if count > 1 then return "Ambiguous month name! Possible interpretations: " .. table.concat(vim.tbl_keys(valid_months), ",") elseif count == 1 then local valid_month_name, valid_month_number = unpack(valid_months[1]) output.month = { name = lib.title(valid_month_name), number = valid_month_number, } goto continue end end do word = word:match("^([^,]+),?$") local valid_weekdays = {} -- Check for weekday abbreviation for i, weekday in ipairs(weekdays) do if vim.startswith(weekday, word:lower()) then valid_weekdays[weekday] = i end end local count = vim.tbl_count(valid_weekdays) if count > 1 then return "Ambiguous weekday name! Possible interpretations: " .. table.concat(vim.tbl_keys(valid_weekdays), ",") elseif count == 1 then local valid_weekday_name, valid_weekday_number = unpack(valid_weekdays[1]) output.weekday = { name = lib.title(valid_weekday_name), number = valid_weekday_number, } goto continue end end return "Unidentified string: `" .. word .. "` - make sure your locale and language are set correctly if you are using a language other than English!" end ::continue:: end return module.private.tostringable_date(output) end, insert_date = function(insert_mode) local function callback(input) if input == "" or not input then return end local output if type(input) == "table" then output = tostring(module.public.to_date(input)) else output = module.public.parse_date(input) if type(output) == "string" then utils.notify(output, vim.log.levels.ERROR) vim.ui.input({ prompt = "Date: ", default = input, }, callback) return end output = tostring(output) end vim.api.nvim_put({ "{@ " .. output .. "}" }, "c", false, true) if insert_mode then vim.cmd.startinsert() end end if modules.is_module_loaded("core.ui.calendar") then vim.cmd.stopinsert() modules.get_module("core.ui.calendar").select_date({ callback = vim.schedule_wrap(callback) }) else vim.ui.input({ prompt = "Date: ", }, callback) end end, } module.private = { tostringable_date = function(date_table) return setmetatable(date_table, { __tostring = function() local function d(str) return str and (tostring(str) .. " ") or "" end return vim.trim( d(date_table.weekday and date_table.weekday.name) .. d(date_table.day) .. d(date_table.month and date_table.month.name) .. d(date_table.year and string.format("%04d", date_table.year)) .. d(date_table.time and tostring(date_table.time)) .. d(date_table.timezone) ) end, }) end, } module.load = function() vim.keymap.set("", "(neorg.tempus.insert-date)", lib.wrap(module.public.insert_date, false)) vim.keymap.set("i", "(neorg.tempus.insert-date.insert-mode)", lib.wrap(module.public.insert_date, true)) end return module ================================================ FILE: lua/neorg/modules/core/text-objects/module.lua ================================================ --[[ file: Norg-Text-Objects title: Navigation, Selection, and Swapping summary: A Neorg module for moving and selecting elements of the document. --- - Easily move items up and down in the document - Provides text objects for headings, tags, and lists ## Usage Users can create keybinds for some or all of the different events this module exposes. Those are: - `core.text-objects.item_up` - Moves the current "item" up - `core.text-objects.item_down` - same but down - `core.text-objects.textobject.heading.outer` - `core.text-objects.textobject.heading.inner` - `core.text-objects.textobject.tag.inner` - `core.text-objects.textobject.tag.outer` - `core.text-objects.textobject.list.outer` - around the entire list _Movable "items" include headings, and list items (ordered/unordered/todo)_ ### Example Example keybinds that would go in your Neorg configuration: ```lua vim.keymap.set("n", "", "(neorg.text-objects.item-up)", {}) vim.keymap.set("n", "", "(neorg.text-objects.item-down)", {}) vim.keymap.set({ "o", "x" }, "iH", "(neorg.text-objects.textobject.heading.inner)", {}) vim.keymap.set({ "o", "x" }, "aH", "(neorg.text-objects.textobject.heading.outer)", {}) ``` --]] local neorg = require("neorg.core") local log, modules, lib = neorg.log, neorg.modules, neorg.lib local ts local module = modules.create("core.text-objects") module.setup = function() return { success = true, requires = { "core.integrations.treesitter" }, } end module.load = function() ts = module.required["core.integrations.treesitter"] vim.keymap.set("", "(neorg.text-objects.item-up)", module.public.move_up) vim.keymap.set("", "(neorg.text-objects.item-down)", module.public.move_down) vim.keymap.set( "", "(neorg.text-objects.textobject.heading.outer)", lib.wrap(module.public.highlight_node, "heading.outer") ) vim.keymap.set( "", "(neorg.text-objects.textobject.heading.inner)", lib.wrap(module.public.highlight_node, "heading.inner") ) vim.keymap.set( "", "(neorg.text-objects.textobject.tag.inner)", lib.wrap(module.public.highlight_node, "tag.inner") ) vim.keymap.set( "", "(neorg.text-objects.textobject.tag.outer)", lib.wrap(module.public.highlight_node, "tag.outer") ) vim.keymap.set( "", "(neorg.text-objects.textobject.list.outer)", lib.wrap(module.public.highlight_node, "lits.outer") ) end module.config.public = { moveables = { headings = { "heading%d", "heading%d", }, todo_items = { "todo_item%d", { "todo_item%d", "unordered_list%d", }, }, unordered_list_elements = { "unordered_list%d", { "todo_item%d", "unordered_list%d", }, }, }, } module.private = { get_element_from_cursor = function(node_pattern) local node_at_cursor = vim.treesitter.get_node() if not node_at_cursor or not node_at_cursor:parent() or not node_at_cursor:parent():type():match(node_pattern) then log.trace(string.format("Could not find element of pattern '%s' under the cursor", node_pattern)) return end return node_at_cursor:parent() end, move_item_down = function(pattern, expected_sibling_name, buffer) local element = module.private.get_element_from_cursor(pattern) if not element then return end local next_element = element:next_named_sibling() if type(expected_sibling_name) == "string" then if next_element and next_element:type():match(expected_sibling_name) then ts.swap_nodes(element, next_element, buffer, true) end else for _, expected in ipairs(expected_sibling_name) do if next_element and next_element:type():match(expected) then ts.swap_nodes(element, next_element, buffer, true) return end end end end, move_item_up = function(pattern, expected_sibling_name, buffer) local element = module.private.get_element_from_cursor(pattern) if not element then return end local prev_element = element:prev_named_sibling() if type(expected_sibling_name) == "string" then if prev_element and prev_element:type():match(expected_sibling_name) then ts.swap_nodes(element, prev_element, buffer, true) end else for _, expected in ipairs(expected_sibling_name) do if prev_element and prev_element:type():match(expected) then ts.swap_nodes(element, prev_element, buffer, true) return end end end end, } ---@class core.text-objects module.public = { move_up = function() local config = module.config.public.moveables local buffer = vim.api.nvim_get_current_buf() for _, data in pairs(config) do module.private.move_item_up(data[1], data[2], buffer) end end, move_down = function() local config = module.config.public.moveables local buffer = vim.api.nvim_get_current_buf() for _, data in pairs(config) do module.private.move_item_down(data[1], data[2], buffer) end end, highlight_node = function(name) local textobj_lookup = module.config.private.textobjects[name] if textobj_lookup then return textobj_lookup(vim.treesitter.get_node()) end end, } local function find(node, expected_type) while not node:type():match(expected_type) do if not node:parent() or node:type() == "document" then return end node = node:parent() end return node end local function find_content(node, expected_type, content_field) local heading = find(node, expected_type) if not heading then return end local content = heading:field(content_field or "content") return #content > 0 and content end local function highlight_node(node) if not node then return end local range = module.required["core.integrations.treesitter"].get_node_range(node) if range.column_end == 0 then range.row_end = range.row_end - 1 range.column_end = vim.api.nvim_buf_get_lines(0, range.row_end, range.row_end + 1, true)[1]:len() end if range.column_start == vim.api.nvim_buf_get_lines(0, range.row_start, range.row_start + 1, true)[1]:len() then range.row_start = range.row_start + 1 range.column_start = 0 end -- This method of selection is from ts_utils, it avoids a bug with the nvim_buf_set_mark -- approach local selection_mode = "v" local mode = vim.api.nvim_get_mode() if mode.mode ~= selection_mode then vim.cmd.normal({ selection_mode, bang = true }) end vim.api.nvim_win_set_cursor(0, { range.row_start + 1, range.column_start }) vim.cmd.normal({ bang = true, args = { "o" } }) vim.api.nvim_win_set_cursor(0, { range.row_end + 1, range.column_end }) end module.config.private = { textobjects = { ["heading.outer"] = function(node) return highlight_node(find(node, "^heading%d+$")) end, ["heading.inner"] = function(node) return highlight_node(find_content(node, "^heading%d+$")) end, ["tag.outer"] = function(node) return highlight_node(find(node, "ranged_tag$")) end, ["tag.inner"] = function(node) -- TODO: Fix Treesitter, this is currently buggy return highlight_node(find_content(node, "ranged_tag$")) end, ["list.outer"] = function(node) return highlight_node(find(node, "generic_list")) end, }, } return module ================================================ FILE: lua/neorg/modules/core/todo-introspector/module.lua ================================================ --[[ file: Todo-Introspector title: See Your Progress at a Glance description: The introspector module displays progress for nested tasks. summary: Module for displaying progress of completed subtasks in the virtual line. --- When an item with a TODO status has children with their own TODOs this module enables virtual text in the top level item and displays the progress of the subtasks. By default it displays in the format of `[completed/total] (progress%)`. --]] local neorg = require("neorg") local modules = neorg.modules local module = modules.create("core.todo-introspector") module.private = { namespace = vim.api.nvim_create_namespace("neorg/todo-introspector"), --- List of active buffers buffers = {}, } ---@class core.todo-introspector module.config.public = { -- Highlight group to display introspector in. -- -- Defaults to "Normal". highlight_group = "Normal", -- Which status types to count towards the totol. -- -- Defaults to the following: `done`, `pending`, `undone`, `urgent`. counted_statuses = { "done", "pending", "undone", "urgent", }, -- Which status should count towards the completed count (should be a subset of counted_statuses). -- -- Defaults to the following: `done`. completed_statuses = { "done", }, -- Callback to format introspector. Takes in two parameters: -- * `completed`: number of completed tasks -- * `total`: number of total counted tasks -- -- Should return a string with the format you want to display the introspector in. -- -- Defaults to "[completed/total] (progress%)" format = function(completed, total) -- stylua: ignore start return string.format( "[%d/%d] (%d%%)", completed, total, (total ~= 0 and math.floor((completed / total) * 100) or 0) ) -- stylua: ignore end end, } module.setup = function() return { requires = { "core.integrations.treesitter" }, } end module.load = function() vim.api.nvim_create_autocmd("Filetype", { pattern = "norg", desc = "Attaches the TODO introspector to any Norg buffer.", callback = function(ev) local buf = ev.buf if module.private.buffers[buf] then return end module.private.buffers[buf] = true module.public.attach_introspector(buf) end, }) end --- Attaches the introspector to a given Norg buffer. --- Errors if the target buffer is not a Norg buffer. ---@param buffer number #The buffer ID to attach to. function module.public.attach_introspector(buffer) if not vim.api.nvim_buf_is_valid(buffer) or vim.bo[buffer].filetype ~= "norg" then error(string.format("Could not attach to buffer %d, buffer is not a norg file!", buffer)) end module.required["core.integrations.treesitter"].execute_query( [[ (_ state: (detached_modifier_extension)) @item ]], function(query, id, node) if query.captures[id] == "item" then module.public.perform_introspection(buffer, node) end end, buffer ) vim.api.nvim_buf_attach(buffer, false, { on_lines = vim.schedule_wrap(function(_, buf, _, first) if not vim.api.nvim_buf_is_valid(buf) then return end -- If we delete the last line of a file `first` will point to a nonexistent line -- For this reason we fall back to the line count (accounting for 0-based indexing) -- whenever a change to the document is made. first = math.min(first, vim.api.nvim_buf_line_count(buf) - 1) ---@type TSNode? local node = module.required["core.integrations.treesitter"].get_first_node_on_line(buf, first) if not node then return end vim.api.nvim_buf_clear_namespace(buffer, module.private.namespace, first + 1, first + 1) local function introspect(start_node) local parent = start_node while parent do local child = parent:named_child(1) if child and child:type() == "detached_modifier_extension" then module.public.perform_introspection(buffer, parent) -- NOTE: do not break here as we want the introspection to propagate all the way up the syntax tree end parent = parent:parent() end end introspect(node) local node_above = module.required["core.integrations.treesitter"].get_first_node_on_line(buf, first - 1) if node_above then local todo_status = node_above:named_child(1) if todo_status and todo_status:type() == "detached_modifier_extension" then introspect(node_above) end end end), on_detach = function() vim.api.nvim_buf_clear_namespace(buffer, module.private.namespace, 0, -1) module.private.buffers[buffer] = nil end, }) end --- Aggregates TODO item counts from children. ---@param node TSNode ---@return number completed Total number of completed tasks ---@return number total Total number of counted tasks function module.public.calculate_items(node) local counts = {} for _, status in ipairs(module.config.public.counted_statuses) do counts[status] = 0 end local total = 0 -- Go through all the children of the current todo item node and count the amount of "done" children for child in node:iter_children() do if child:named_child(1) and child:named_child(1):type() == "detached_modifier_extension" then for status in child:named_child(1):iter_children() do if status:type():match("^todo_item_") then local type = status:type():match("^todo_item_(.+)$") if not counts[type] then break end counts[type] = counts[type] + 1 total = total + 1 end end end end local completed = 0 for _, status in ipairs(module.config.public.completed_statuses) do if counts[status] then completed = completed + counts[status] end end return completed, total end --- Displays the amount of done items in the form of an extmark. ---@param buffer number ---@param node TSNode function module.public.perform_introspection(buffer, node) local completed, total = module.public.calculate_items(node) local line, col = node:start() vim.api.nvim_buf_clear_namespace(buffer, module.private.namespace, line, line + 1) if total == 0 then return end vim.api.nvim_buf_set_extmark(buffer, module.private.namespace, line, col, { virt_text = { { module.config.public.format(completed, total), module.config.public.highlight_group, }, }, invalidate = true, hl_mode = "combine", }) end return module ================================================ FILE: lua/neorg/modules/core/ui/calendar/module.lua ================================================ --[[ file: Calendar title: Frictionless Dates description: The calendar module provides a range of functionality for different date-related tasks. summary: Opens an interactive calendar for date-related tasks. --- The calendar is most often invoked with the intent of selecting a date, but may also be launched in standalone mode, select date range mode and others. To view keybinds and help, press `?` in the calendar view. --]] local neorg = require("neorg.core") local modules = neorg.modules local module = modules.create("core.ui.calendar") module.setup = function() return { requires = { "core.ui", "core.ui.calendar.views.monthly", }, } end module.private = { modes = {}, views = {}, get_mode = function(name, callback) if module.private.modes[name] ~= nil then local cur_mode = module.private.modes[name](callback) cur_mode.name = name return cur_mode end print("Error: mode not set or not available") end, get_view = function(name) if module.private.views[name] ~= nil then return module.private.views[name] end print("Error: view not set or not available") end, extract_ui_info = function(buffer, window) local width = vim.api.nvim_win_get_width(window) local height = vim.api.nvim_win_get_height(window) local half_width = math.floor(width / 2) local half_height = math.floor(height / 2) return { window = window, buffer = buffer, width = width, height = height, half_width = half_width, half_height = half_height, } end, open_window = function(options) local MIN_HEIGHT = 14 local buffer, window = module.required["core.ui"].create_split( "calendar-" .. tostring(os.clock()):gsub("%.", "-"), {}, options.height or MIN_HEIGHT + (options.padding or 0) ) vim.api.nvim_create_autocmd({ "WinClosed", "BufDelete" }, { buffer = buffer, callback = function() pcall(vim.api.nvim_win_close, window, true) pcall(vim.api.nvim_buf_delete, buffer, { force = true }) end, }) return buffer, window end, } ---@class core.ui.calendar module.public = { add_mode = function(name, factory) module.private.modes[name] = factory end, add_view = function(name, details) module.private.views[name] = details end, create_calendar = function(buffer, window, options) local callback_and_close = function(result) if options.callback ~= nil then options.callback(result) end pcall(vim.api.nvim_win_close, window, true) pcall(vim.api.nvim_buf_delete, buffer, { force = true }) end local mode = module.private.get_mode(options.mode, callback_and_close) if mode == nil then return end local ui_info = module.private.extract_ui_info(buffer, window) local view = module.private.get_view(options.view or "monthly") view.setup(ui_info, mode, options.date or os.date("*t"), options) end, open = function(options) local buffer, window = module.private.open_window(options) options.mode = "standalone" return module.public.create_calendar(buffer, window, options) end, select_date = function(options) local buffer, window = module.private.open_window(options) options.mode = "select_date" return module.public.create_calendar(buffer, window, options) end, select_date_range = function(options) local buffer, window = module.private.open_window(options) options.mode = "select_range" return module.public.create_calendar(buffer, window, options) end, } module.load = function() -- Add default calendar modes module.public.add_mode("standalone", function(_) return {} end) module.public.add_mode("select_date", function(callback) return { on_select = function(_, date) if callback then callback(date) end return false end, } end) module.public.add_mode("select_range", function(callback) return { range_start = nil, range_end = nil, on_select = function(self, date) if not self.range_start then self.range_start = date return true else if os.time(date) <= os.time(self.range_start) then print("Error: you should choose a date that is after the starting day.") return false end self.range_end = date callback({ self.range_start, self.range_end }) return false end end, get_day_highlight = function(self, date, default_highlight) if self.range_start ~= nil then if os.time(date) < os.time(self.range_start) then return "@comment" end end return default_highlight end, } end) end return module ================================================ FILE: lua/neorg/modules/core/ui/calendar/views/monthly/module.lua ================================================ local neorg = require("neorg.core") local lib, log, modules, utils = neorg.lib, neorg.log, neorg.modules, neorg.utils local module = modules.create("core.ui.calendar.views.monthly") local function reformat_time(date) return os.date("*t", os.time(date)) end module.setup = function() return { requires = { "core.ui.calendar", "core.tempus", }, } end module.private = { namespaces = { logical = vim.api.nvim_create_namespace("neorg/calendar/logical"), decorational = vim.api.nvim_create_namespace("neorg/calendar/decorational"), }, set_extmark = function(ui_info, namespace, row, col, length, virt_text, alignment, extra) if alignment then local text_length = 0 for _, tuple in ipairs(virt_text) do text_length = text_length + tuple[1]:len() end if alignment == "center" then col = col + (ui_info.half_width - math.floor(text_length / 2)) elseif alignment == "right" then col = col + (ui_info.width - text_length) end end local default_extra = { virt_text = virt_text, virt_text_pos = "overlay", } if length then default_extra.end_col = col + length end return vim.api.nvim_buf_set_extmark( ui_info.buffer, namespace, row, col, vim.tbl_deep_extend("force", default_extra, extra or {}) ) end, set_decorational_extmark = function(ui_info, row, col, length, virt_text, alignment, extra) return module.private.set_extmark( ui_info, module.private.namespaces.decorational, row, col, length, virt_text, alignment, extra ) end, set_logical_extmark = function(ui_info, row, col, virt_text, alignment, extra) return module.private.set_extmark( ui_info, module.private.namespaces.logical, row, col, nil, virt_text, alignment, extra ) end, new_view_instance = function() return { current_mode = {}, extmarks = { decorational = { calendar_text = nil, help_and_custom_input = nil, current_view = nil, month_headings = {}, weekday_displays = {}, }, logical = { year = nil, months = { -- [3] = { [31] = } }, }, }, -- TODO: implemant distance like in render_weekday_banner render_month_banner = function(self, ui_info, date, weekday_banner_extmark_id) local month_name = os.date( "%B", os.time({ year = date.year, month = date.month, day = date.day, }) ) ---@cast month_name string local month_length = vim.api.nvim_strwidth(month_name) local weekday_banner_id = vim.api.nvim_buf_get_extmark_by_id( ui_info.buffer, module.private.namespaces.decorational, weekday_banner_extmark_id, { details = true, } ) self.extmarks.decorational.month_headings[weekday_banner_extmark_id] = module.private.set_decorational_extmark( ui_info, 4, weekday_banner_id[2] + math.ceil((weekday_banner_id[3].end_col - weekday_banner_id[2]) / 2) - math.floor(month_length / 2), month_length, { { month_name, "@text.underline" } }, nil, { id = self.extmarks.decorational.month_headings[weekday_banner_extmark_id], } ) end, render_weekday_banner = function(self, ui_info, offset, distance) offset = offset or 0 distance = distance or 4 -- Render the days of the week -- To effectively do this, we grab all the weekdays from a constant time. -- This makes the weekdays retrieved locale dependent (which is what we want). local weekdays = {} local weekdays_string_length = 0 for i = 1, 7 do local weekday = os.date("%a", os.time({ year = 2000, month = 5, day = i })) ---@cast weekday string local truncated = utils.truncate_by_cell(weekday, 2) local truncated_length = vim.api.nvim_strwidth(truncated) weekdays[#weekdays + 1] = { truncated, "@text.title" } weekdays[#weekdays + 1] = { (" "):rep(4 - truncated_length) } weekdays_string_length = truncated_length -- remember last day's length end weekdays[#weekdays] = nil -- delete last padding weekdays_string_length = weekdays_string_length + 4 * 6 -- This serves as the index of this week banner extmark inside the extmark table local absolute_offset = offset + (offset < 0 and (-offset * 100) or 0) local extmark_position = 0 -- Calculate offset position only for the previous and following months if offset ~= 0 then extmark_position = (weekdays_string_length * math.abs(offset)) + (distance * math.abs(offset)) end -- For previous months, revert the offset if offset < 0 then extmark_position = -extmark_position end local weekday_banner_id = module.private.set_decorational_extmark( ui_info, 6, extmark_position, weekdays_string_length, weekdays, "center", { id = self.extmarks.decorational.weekday_displays[absolute_offset], } ) self.extmarks.decorational.weekday_displays[absolute_offset] = weekday_banner_id return weekday_banner_id end, render_month = function(self, ui_info, target_date, weekday_banner_extmark_id) --> Month rendering routine -- We render the first month at the very center of the screen. Each -- month takes up a static amount of characters. -- Render the top text of the month (June, August etc.) -- Render the numbers for weekdays local days_of_month = { -- [day of month] = , } local current_date = os.date("*t") local month, year = target_date.month, target_date.year local days_in_current_month = module.private.get_month_length(month, year) for i = 1, days_in_current_month do days_of_month[i] = tonumber(os.date( "%u", os.time({ year = year, month = month, day = i, }) )) end local beginning_of_weekday_extmark = vim.api.nvim_buf_get_extmark_by_id( ui_info.buffer, module.private.namespaces.decorational, weekday_banner_extmark_id, {} ) local render_column = days_of_month[1] - 1 local render_row = 1 self.extmarks.logical.months[month] = self.extmarks.logical.months[month] or {} for day_of_month, day_of_week in ipairs(days_of_month) do local is_current_day = current_date.year == year and current_date.month == month and day_of_month == current_date.day local start_row = beginning_of_weekday_extmark[1] + render_row local start_col = beginning_of_weekday_extmark[2] + (4 * render_column) if is_current_day then -- TODO: Make this configurable. The user might want the cursor to start -- on a specific date in a specific month. -- Just look up the extmark and place the cursor there. vim.api.nvim_win_set_cursor(ui_info.window, { start_row + 1, start_col }) end local day_highlight = is_current_day and "@text.todo" or nil if self.current_mode.get_day_highlight then day_highlight = self.current_mode:get_day_highlight({ year = year, month = month, day = day_of_month, }, day_highlight) end self.extmarks.logical.months[month][day_of_month] = vim.api.nvim_buf_set_extmark( ui_info.buffer, module.private.namespaces.logical, start_row, start_col, { virt_text = { { (day_of_month < 10 and "0" or "") .. tostring(day_of_month), day_highlight, }, }, virt_text_pos = "overlay", id = self.extmarks.logical.months[month][day_of_month], } ) if day_of_week == 7 then render_column = 0 render_row = render_row + 1 else render_column = render_column + 1 end end end, render_month_array = function(self, ui_info, date, options) -- Render the first weekday banner in the middle local weekday_banner = self:render_weekday_banner(ui_info, 0, options.distance) self:render_month_banner(ui_info, date, weekday_banner) self:render_month(ui_info, date, weekday_banner) local months_to_render = module.private.rendered_months_in_width(ui_info.width, options.distance) months_to_render = math.floor(months_to_render / 2) for i = 1, months_to_render do weekday_banner = self:render_weekday_banner(ui_info, i, options.distance) local positive_target_date = reformat_time({ year = date.year, month = date.month + i, day = 1, }) self:render_month_banner(ui_info, positive_target_date, weekday_banner) self:render_month(ui_info, positive_target_date, weekday_banner) weekday_banner = self:render_weekday_banner(ui_info, i * -1, options.distance) local negative_target_date = reformat_time({ year = date.year, month = date.month - i, day = 1, }) self:render_month_banner(ui_info, negative_target_date, weekday_banner) self:render_month(ui_info, negative_target_date, weekday_banner) end end, render_year_tag = function(self, ui_info, year) -- Display the current year (i.e. `< 2022 >`) local extra = nil if self.extmarks.logical.year ~= nil then extra = { id = self.extmarks.logical.year, } end local extmark = module.private.set_logical_extmark( ui_info, 2, 0, { { "< ", "Whitespace" }, { tostring(year), "@number" }, { " >", "Whitespace" } }, "center", extra ) if self.extmarks.logical.year == nil then self.extmarks.logical.year = extmark end end, render_decorative_text = function(self, ui_info, view) --> Decorational section -- CALENDAR text: self.extmarks.decorational = vim.tbl_deep_extend("force", self.extmarks.decorational, { calendar_text = module.private.set_decorational_extmark(ui_info, 0, 0, 0, { { "CALENDAR", "@text.strong" }, }, "center"), -- Help text at the bottom left of the screen help_and_custom_input = module.private.set_decorational_extmark( ui_info, ui_info.height - 1, 0, 0, { { "?", "@character" }, { " - " }, { "help", "@text.strong" }, { " " }, { "i", "@character" }, { " - " }, { "custom input", "@text.strong" }, } ), -- The current view (bottom right of the screen) current_view = module.private.set_decorational_extmark( ui_info, ui_info.height - 1, 0, 0, { { "[", "Whitespace" }, { view, "@label" }, { "]", "Whitespace" } }, "right" ), }) end, select_current_day = function(self, ui_info, date) local extmark_id = self.extmarks.logical.months[date.month][date.day] local position = vim.api.nvim_buf_get_extmark_by_id( ui_info.buffer, module.private.namespaces.logical, extmark_id, {} ) vim.api.nvim_win_set_cursor(ui_info.window, { position[1] + 1, position[2] }) end, render_view = function(self, ui_info, date, previous_date, options) local is_first_render = (previous_date == nil) if is_first_render then vim.api.nvim_buf_clear_namespace(ui_info.buffer, module.private.namespaces.decorational, 0, -1) vim.api.nvim_buf_clear_namespace(ui_info.buffer, module.private.namespaces.logical, 0, -1) vim.api.nvim_set_option_value("modifiable", true, { buf = ui_info.buffer }) module.private.fill_buffer(ui_info) self:render_decorative_text(ui_info, module.public.view_name:upper()) self:render_year_tag(ui_info, date.year) self:render_month_array(ui_info, date, options) self:select_current_day(ui_info, date) vim.api.nvim_set_option_value("modifiable", false, { buf = ui_info.buffer }) vim.api.nvim_set_option_value("winfixbuf", true, { win = ui_info.window }) return end local year_changed = (date.year ~= previous_date.year) local month_changed = (date.month ~= previous_date.month) local day_changed = (date.day ~= previous_date.day) if year_changed then self:render_year_tag(ui_info, date.year) end if year_changed or month_changed then self:render_month_array(ui_info, date, options) self:clear_extmarks(ui_info, date, options) end if year_changed or month_changed or day_changed then self:select_current_day(ui_info, date) end end, clear_extmarks = function(self, ui_info, current_date, options) local cur_month = current_date.month local rendered_months_offset = math.floor(module.private.rendered_months_in_width(ui_info.width, options.distance) / 2) -- Mimics ternary operator to be concise local month_min = cur_month - rendered_months_offset month_min = month_min <= 0 and (12 + month_min) or month_min local month_max = cur_month + rendered_months_offset month_max = month_max > 12 and (month_max - 12) or month_max local clear_extmarks_for_month = function(month) for _, extmark_id in ipairs(self.extmarks.logical.months[month]) do vim.api.nvim_buf_del_extmark(ui_info.buffer, module.private.namespaces.logical, extmark_id) end self.extmarks.logical.months[month] = nil end for month, _ in pairs(self.extmarks.logical.months) do -- Check if the month is outside the current view range -- considering the month wrapping after 12 if month_min < month_max then if month_min > month or month > month_max then clear_extmarks_for_month(month) end elseif month_min > month_max then if month_max < month and month < month_min then clear_extmarks_for_month(month) end elseif month_min == month_max then if month ~= cur_month then clear_extmarks_for_month(month) end end end end, } end, fill_buffer = function(ui_info) -- There are many steps to render a calendar. -- The first step is to fill the entire buffer with spaces. This lets -- us place extmarks at any position in the document. Won't be used for -- the meaty stuff, but will come in handy for rendering decorational -- elements. local fill = {} local filler = string.rep(" ", ui_info.width) for i = 1, ui_info.height do fill[i] = filler end vim.api.nvim_buf_set_lines(ui_info.buffer, 0, -1, true, fill) end, --- get the number of days in the month, months are wrapped (ie, month 13 <==> month 1) get_month_length = function(month, year) return ({ 31, (module.private.is_leap_year(year)) and 29 or 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, })[lib.number_wrap(month, 1, 12)] end, is_leap_year = function(year) if year % 4 ~= 0 then return false end -- Years disible by 100 are leap years only if also divisible by 400 if year % 100 == 0 and year % 400 ~= 0 then return false end return true end, rendered_months_in_width = function(width, distance) local rendered_month_width = 26 local months = math.floor(width / (rendered_month_width + distance)) -- Do not show more than one year if months > 12 then months = 12 end if months % 2 == 0 then return months - 1 end return months end, display_help = function(lines) local width, height = 44, 32 local buffer = vim.api.nvim_create_buf(false, true) local window = vim.api.nvim_open_win(buffer, true, { style = "minimal", border = "rounded", title = " Calendar ", title_pos = "center", row = (vim.o.lines / 2) - height / 2, col = (vim.o.columns / 2) - width / 2, width = width, height = height, relative = "editor", noautocmd = true, }) vim.api.nvim_set_option_value("winfixbuf", true, { win = window }) local function quit() vim.api.nvim_win_close(window, true) pcall(vim.api.nvim_buf_delete, buffer, { force = true }) end vim.keymap.set("n", "q", quit, { buffer = buffer }) vim.api.nvim_create_autocmd({ "BufLeave", "WinLeave" }, { buffer = buffer, callback = quit, }) local namespace = vim.api.nvim_create_namespace("neorg/calendar-help") vim.api.nvim_set_option_value("modifiable", false, { buf = buffer }) vim.api.nvim_buf_set_extmark(buffer, namespace, 0, 0, { virt_lines = lines, }) end, } ---@class core.ui.calendar.views.monthly module.public = { view_name = "monthly", setup = function(ui_info, mode, date, options) options.distance = options.distance or 4 local view = module.private.new_view_instance() view.current_mode = mode view:render_view(ui_info, date, nil, options) do vim.keymap.set("n", "q", function() vim.api.nvim_buf_delete(ui_info.buffer, { force = true }) end, { buffer = ui_info.buffer }) -- TODO: Make cursor wrapping behaviour configurable vim.keymap.set("n", "l", function() local new_date = reformat_time({ year = date.year, month = date.month, day = date.day + 1 * vim.v.count1, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "h", function() local new_date = reformat_time({ year = date.year, month = date.month, day = date.day - 1 * vim.v.count1, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "j", function() local new_date = reformat_time({ year = date.year, month = date.month, day = date.day + 7 * vim.v.count1, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "k", function() local new_date = reformat_time({ year = date.year, month = date.month, day = date.day - 7 * vim.v.count1, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "", function() local should_redraw = false if view.current_mode.on_select ~= nil then should_redraw = view.current_mode:on_select(date) end if should_redraw then view:render_view(ui_info, date, nil, options) end end, { buffer = ui_info.buffer }) vim.keymap.set("n", "L", function() local new_date = reformat_time({ year = date.year, month = date.month + vim.v.count1, day = date.day, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "H", function() local new_date = reformat_time({ year = date.year, month = date.month - vim.v.count1, day = date.day, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "m", function() local new_date = reformat_time({ year = date.year, month = date.month + vim.v.count1, day = 1, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "M", function() if date.day > 1 then date.month = date.month + 1 end local new_date = reformat_time({ year = date.year, month = date.month - vim.v.count1, day = 1, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "y", function() local new_date = reformat_time({ year = date.year + vim.v.count1, month = date.month, day = date.day, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "Y", function() local new_date = reformat_time({ year = date.year - vim.v.count1, month = date.month, day = date.day, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "$", function() local new_day = date.day - (lib.number_wrap(date.wday - 1, 1, 7) - 1) + 6 local new_date = reformat_time({ year = date.year, month = date.month, day = new_day, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) local start_of_week = function() local new_day = date.day - (lib.number_wrap(date.wday - 1, 1, 7) - 1) local new_date = reformat_time({ year = date.year, month = date.month, day = new_day, }) view:render_view(ui_info, new_date, date, options) date = new_date end vim.keymap.set("n", "0", start_of_week, { buffer = ui_info.buffer }) vim.keymap.set("n", "_", start_of_week, { buffer = ui_info.buffer }) vim.keymap.set("n", "t", function() local new_date = os.date("*t") view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "e", function() local end_of_current_month = module.private.get_month_length(date.month, date.year) if end_of_current_month > date.day then date.month = date.month - 1 end local new_date = reformat_time({ year = date.year, month = date.month + vim.v.count1, day = module.private.get_month_length(date.month + vim.v.count1, date.year), }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "E", function() local new_date = reformat_time({ year = date.year, month = date.month - vim.v.count1, day = module.private.get_month_length(date.month - vim.v.count1, date.year), }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "w", function() local new_date = reformat_time({ year = date.year, month = date.month, day = date.day + 7 * vim.v.count1, }) new_date.day = new_date.day - (lib.number_wrap(new_date.wday - 1, 1, 7) - 1) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "W", function() local new_date = reformat_time({ year = date.year, month = date.month, day = date.day - 7 * vim.v.count1, }) new_date.day = new_date.day - (lib.number_wrap(new_date.wday - 1, 1, 7) - 1) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) local months = {} for i = 1, 12 do table.insert( months, (os.date("%B", os.time({ year = 2000, month = i, day = 1 })) --[[@as string]]):lower() ) end -- store the last `;` repeatable search local last_semi_jump = nil -- flag to set when we're using `;` so it doesn't cycle local skip_next = false vim.keymap.set("n", ";", function() if last_semi_jump then vim.api.nvim_feedkeys(last_semi_jump, "m", false) end end, { buffer = ui_info.buffer }) vim.keymap.set("n", ",", function() if last_semi_jump then local action = string.sub(last_semi_jump, 1, 1) local subject = string.sub(last_semi_jump, 2) local new_keys if string.upper(action) == action then new_keys = action:lower() .. subject else new_keys = action:upper() .. subject end vim.api.nvim_feedkeys(new_keys, "m", false) skip_next = true end end, { buffer = ui_info.buffer }) vim.keymap.set("n", "f", function() local char = vim.fn.getcharstr() for i = date.month + 1, date.month + 12 do local m = lib.number_wrap(i, 1, 12) if months[m]:match("^" .. char) then if not skip_next then last_semi_jump = "f" .. char else skip_next = false end local new_date = reformat_time({ year = date.year, month = m, day = date.day, }) view:render_view(ui_info, new_date, date, options) date = new_date break end end end, { buffer = ui_info.buffer }) vim.keymap.set("n", "F", function() local char = vim.fn.getcharstr() for i = date.month + 11, date.month, -1 do local m = lib.number_wrap(i, 1, 12) if months[m]:match("^" .. char) then if not skip_next then last_semi_jump = "F" .. char else skip_next = false end local new_date = reformat_time({ year = date.year, month = m, day = date.day, }) view:render_view(ui_info, new_date, date, options) date = new_date break end end end, { buffer = ui_info.buffer }) vim.keymap.set("n", "g", function() local day = math.min(vim.v.count1, module.private.get_month_length(date.month, date.year)) local new_date = reformat_time({ year = date.year, month = date.month, day = day, }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer, nowait = true }) vim.keymap.set("n", "G", function() local new_date = reformat_time({ year = date.year, month = date.month, day = module.private.get_month_length(date.month, date.year), }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer }) vim.keymap.set("n", "d", function() local n = vim.v.count1 local weekday = math.min(n, 7) local new_date = reformat_time({ year = date.year, month = date.month, day = date.day + (weekday - lib.number_wrap(date.wday - 1, 1, 7)), }) view:render_view(ui_info, new_date, date, options) date = new_date end, { buffer = ui_info.buffer, nowait = true }) vim.keymap.set( "n", "?", lib.wrap(module.private.display_help, { { { "q", "@namespace" }, { " - " }, { "close this window", "@text.strong" }, }, {}, { { "", "@namespace" }, { " - " }, { "select date", "@text.strong" }, }, {}, { { "--- Basic Movement ---", "@text.title" }, }, {}, { { "l/h", "@namespace" }, { " - " }, { "next/previous day", "@text.strong" }, }, { { "j/k", "@namespace" }, { " - " }, { "next/previous week", "@text.strong" }, }, { { "w/W", "@namespace" }, { " - " }, { "start of next/this or previous week", "@text.strong" }, }, { { "t", "@namespace" }, { " - " }, { "today", "@text.strong" }, }, { { "d", "@namespace" }, { "n" }, { " - " }, { "weekday ", "@text.strong" }, { "n" }, { " (1 = monday)", "@text.strong" }, }, {}, { { "--- Moving Between Months ---", "@text.title" }, }, {}, { { "L/H", "@namespace" }, { " - " }, { "next/previous month (same day)", "@text.strong" }, }, { { "m/M", "@namespace" }, { " - " }, { "1st of next/this or previous month", "@text.strong" }, }, { { "f", "@namespace" }, { "x" }, { "/F", "@namespace" }, { "x" }, { " - " }, { "next/previous month starting with ", "@text.strong" }, { "x" }, }, {}, { { "--- Moving Between Years ---", "@text.title" }, }, {}, { { "y/Y", "@namespace" }, { " - " }, { "next/previous year (same day)", "@text.strong" }, }, { { "gy", "@namespace" }, { " - " }, { "start of the current year", "@text.strong" }, }, { { "c/C", "@namespace" }, { " - " }, { "next/this or previous century", "@text.strong" }, }, { { "g/G", "@namespace" }, { " - " }, { "start/end of month", "@text.strong" }, }, { { " " }, { "g takes you to day of the month", "@text.strong" }, }, {}, { { "--- Additional Info ---", "@text.title" }, }, {}, { { "All movements accept counts" }, }, { { "f/F and g/G work with `;` and `,`" }, }, }), { buffer = ui_info.buffer } ) vim.keymap.set("n", "i", function() local buffer = vim.api.nvim_create_buf(false, true) vim.api.nvim_open_win(buffer, true, { style = "minimal", border = "single", title = "Date (`?` for help)", row = vim.api.nvim_win_get_height(0), col = 0, width = vim.o.columns, height = 1, relative = "win", win = vim.api.nvim_get_current_win(), noautocmd = true, }) vim.cmd.startinsert() local function quit() vim.cmd.stopinsert() vim.api.nvim_buf_delete(buffer, { force = true }) end vim.keymap.set("n", "", quit, { buffer = buffer }) vim.keymap.set("i", "", quit, { buffer = buffer }) vim.keymap.set( "n", "?", lib.wrap(module.private.display_help, { { { "q", "@namespace" }, { " - " }, { "close this window", "@text.strong" }, }, { { "", "@namespace" }, { " - " }, { "confirm date", "@text.strong" }, }, {}, { { "--- Quitting ---", "@text.title" }, }, {}, { { " (insert mode)", "@namespace" }, { " - " }, { "quit", "@text.strong" }, }, { { "", "@namespace" }, { " - " }, { "quit", "@text.strong" }, }, {}, { { "--- Date Syntax ---", "@text.title" }, }, {}, { { "Order " }, { "does not matter", "@text.strong" }, { " with dates." }, }, {}, { { "Some things depend on locale." }, }, {}, { { "Months and weekdays may be written" }, }, { { "with a shorthand." }, }, {}, { { "Years must contain 4 digits at" }, }, { { "all times. Prefix with zeroes" }, }, { { "where necessary." }, }, {}, { { "Hour syntax: `00:00.00` (hour, min, sec)" }, }, {}, { { "--- Examples ---", "@text.title" }, }, {}, { { "Tuesday May 5th 2023 19:00.23", "@neorg.markup.verbatim" }, }, { { "10 Feb CEST 0600", "@neorg.markup.verbatim" }, { " (", "@comment" }, { "0600", "@text.emphasis" }, { " is the year)", "@comment" }, }, { { "9:00.4 2nd March Wed", "@neorg.markup.verbatim" }, }, }), { buffer = buffer } ) vim.keymap.set({ "n", "i" }, "", function() local line = vim.api.nvim_buf_get_lines(buffer, 0, -1, true)[1] local parsed_date = module.required["core.tempus"].parse_date(line) if type(parsed_date) == "string" then log.error("[ERROR]:", parsed_date) return end quit() local lua_date = module.required["core.tempus"].to_lua_date(parsed_date) local should_redraw = false if view.current_mode.on_select ~= nil then should_redraw = view.current_mode:on_select(lua_date) end if should_redraw then view:render_view(ui_info, lua_date, nil, options) end end, { buffer = buffer }) end, { buffer = ui_info.buffer }) end end, } module.load = function() module.required["core.ui.calendar"].add_view(module.public.view_name, module.public) end return module ================================================ FILE: lua/neorg/modules/core/ui/module.lua ================================================ --[[ File: Core-UI Title: Module for managing and displaying UIs to the user. Summary: A set of public functions to help developers create and manage UI (selection popups, prompts...) in their modules. Internal: true --- --]] local neorg = require("neorg.core") local log, modules = neorg.log, neorg.modules local module = modules.create("core.ui", { "selection_popup", "text_popup", }) module.setup = function() for _, imported in pairs(module.imported) do module.public = vim.tbl_extend("force", module.public, imported.public) end return {} end module.private = { namespace = vim.api.nvim_create_namespace("core.ui"), } ---@class core.ui: core.ui.selection_popup, core.ui.text_popup module.public = { --- Returns a table in the form of { width, height } containing the width and height of the current window ---@param half boolean #If true returns a position that could be considered the center of the window get_window_size = function(half) return half and { math.floor(vim.fn.winwidth(0) / 2), math.floor(vim.fn.winheight(0) / 2), } or { vim.fn.winwidth(0), vim.fn.winheight(0) } end, --- Returns a modified version of floating window options. ---@param modifiers table #This option set has two values - center_x and center_y. -- If they either of them is set to true then the window gets centered on that axis. ---@param config table #A table containing regular Neovim options for a floating window apply_custom_options = function(modifiers, config) -- Default modifier options local user_options = { center_x = false, center_y = false, } -- Override the default options with the user provided options user_options = vim.tbl_extend("force", user_options, modifiers or {}) -- Assign some default values to certain config options in case they're not specified config = vim.tbl_deep_extend("keep", config, { relative = "win", row = 0, col = 0, width = 100, height = 100, }) -- Get the current window's dimensions except halved local halved_window_size = module.public.get_window_size(true) -- If we want to center along the x axis then return a configuration that does so if user_options.center_x then config.row = config.row + halved_window_size[2] - math.floor(config.height / 2) end -- If we want to center along the y axis then return a configuration that does so if user_options.center_y then config.col = config.col + halved_window_size[1] - math.floor(config.width / 2) end return config end, --- Applies a set of options to a buffer ---@param buf number the buffer number to apply the options to ---@param option_list table a table of option = value pairs apply_buffer_options = function(buf, option_list) for option_name, value in pairs(option_list or {}) do vim.api.nvim_set_option_value(option_name, value, { buf = buf }) end end, ---Creates a new horizontal split at the bottom of the screen ---@param name string the name of the buffer contained within the split (will have neorg:// prepended to it) ---@param config table? a table of