diff --git a/.github/workflows/.luarc.json b/.github/workflows/.luarc.json index 1cc27f873..e83431278 100644 --- a/.github/workflows/.luarc.json +++ b/.github/workflows/.luarc.json @@ -1,13 +1,13 @@ { "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", - "runtime.version": "LuaJIT", - "runtime.path": [ + "Lua.runtime.version": "LuaJIT", + "Lua.runtime.path": [ "lua/?.lua", "lua/?/init.lua" ], - "workspace.library": [ - "/github/workspace/deps/neodev.nvim/types/stable" + "Lua.workspace.library": [ + "/home/runner/work/neorg/neorg/deps/neodev.nvim/types/stable" ], - "diagnostics.libraryFiles": "Disable", - "workspace.checkThirdParty": "Disable" + "Lua.diagnostics.libraryFiles": "Disable", + "Lua.workspace.checkThirdParty": "Disable" } diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 93c63d9de..a4132e802 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,9 @@ name: Linter - -on: [push, pull_request_target] +on: + pull_request: ~ + push: + branches: + - '*' jobs: luacheck: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index dfdef60e8..22e18d5b6 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -21,7 +21,7 @@ jobs: path: "deps/neodev.nvim" - name: Type Check Code Base - uses: mrcjkb/lua-typecheck-action@v0.1.2 + uses: mrcjkb/lua-typecheck-action@v0.2.1 with: configpath: .github/workflows/.luarc.json directories: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 3477ff419..117d4687a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,72 @@ # Changelog +## [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) diff --git a/README.md b/README.md index 8876f2cc4..81f546a8d 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ To learn more about the philosophy of the project check the [philosophy](#-philo For neovim beginners who don't want to tinker with the configurations: -1. Install [Meslo Nerd Font](https://github.com/ryanoasis/nerd-fonts/). -2. Set your terminal font to "MesloLGM Nerd Font Mono". +1. Install one of the Nerd fonts, for example Meslo Nerd Font from [Nerd Fonts](https://www.nerdfonts.com/font-downloads). +2. Set your terminal font to the installed Nerd Font. 3. Make sure you have git by running `git --version` 4. Paste the sample init.lua below to `~/.config/nvim/init.lua` 5. Start taking notes by `nvim test.norg` @@ -252,6 +252,7 @@ You can install it through your favorite plugin manager: { "nvim-neorg/neorg", build = ":Neorg sync-parsers", + -- tag = "*", dependencies = { "nvim-lua/plenary.nvim" }, config = function() require("neorg").setup { @@ -492,6 +493,6 @@ Immense thank you to all of the sponsors of my work!
-vsedov   bR3iN   skbolton   dimas-cyriaco   molleweide   theherk   Gwenillia   purepani    +vsedov   bR3iN   skbolton   dimas-cyriaco   molleweide   theherk   Gwenillia   purepani   SNesbakk   mackieem   bmillemathias   refaelsh   
diff --git a/ROADMAP.md b/ROADMAP.md index 946e17da8..b41ea9ed2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -123,10 +123,10 @@ embeddable-anywhere tools. These include: 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. -- [ ] Sqlite - having an sqlite database as a centralized data store will be super - useful when dealing with both local and remote servers for data synchronization - across devices. Such a server would store things like backlinks (for zettelkasten) or notes - (for GTD). +- [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 diff --git a/doc/neorg.norg b/doc/neorg.norg index cd07f3601..0aa10fead 100644 --- a/doc/neorg.norg +++ b/doc/neorg.norg @@ -136,7 +136,7 @@ version: 0.1 ** Horizontal Lines - You can also place horizontal lines using two or more underscores like so: + 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) diff --git a/lua/neorg.lua b/lua/neorg.lua index 36e8ec3fa..7b3cd4037 100644 --- a/lua/neorg.lua +++ b/lua/neorg.lua @@ -1,35 +1,36 @@ ---[[ --- ROOT NEORG FILE --- This file is the beginning of the entire plugin. It's here that everything fires up and starts pumping. ---]] +--- @brief [[ +--- This file marks the beginning of the entire plugin. It's here that everything fires up and starts pumping. +--- @brief ]] --- Require the most important modules local neorg = require("neorg.core") local config, log, modules = neorg.config, neorg.log, neorg.modules ---- This function takes in a user config, parses it, initializes everything and launches neorg if inside a .norg or .org file ----@param cfg table #A table that reflects the structure of config.user_config +--- @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) config.user_config = vim.tbl_deep_extend("force", config.user_config, cfg or {}) - -- Create a new global instance of the neorg logger + -- Create a new global instance of the neorg logger. log.new(config.user_config.logger or log.get_default_config(), true) - -- Make the Neorg filetype detectable through `vim.filetype`. - -- TODO: Make a PR to Neovim to natively support the org and norg - -- filetypes. + -- TODO(vhyrro): Remove this after Neovim 0.10, where `norg` files will be + -- detected automatically. vim.filetype.add({ extension = { norg = "norg", }, }) - -- If the file we have entered has a .norg extension + -- 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 + -- Then boot up the environment. neorg.org_file_entered(false) else - -- Else listen for a BufReadPost event and fire up the Neorg environment + -- Else listen for a BufReadPost event for `.norg` files and fire up the Neorg environment. vim.cmd([[ autocmd BufAdd *.norg ++once :lua require('neorg').org_file_entered(false) command! -nargs=* NeorgStart delcommand NeorgStart | lua require('neorg').org_file_entered(true, ) @@ -45,8 +46,8 @@ function neorg.setup(cfg) 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" +--- @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 {} @@ -115,6 +116,9 @@ function neorg.org_file_entered(manual, arguments) 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 @@ -124,7 +128,7 @@ function neorg.org_file_entered(manual, arguments) end --- Returns whether or not Neorg is loaded ----@return boolean +--- @return boolean function neorg.is_loaded() return config.started end diff --git a/lua/neorg/core/callbacks.lua b/lua/neorg/core/callbacks.lua index 23123d8a7..352a53cf0 100644 --- a/lua/neorg/core/callbacks.lua +++ b/lua/neorg/core/callbacks.lua @@ -1,16 +1,19 @@ ---[[ - Neorg User Callbacks File - User callbacks are ways for the user to directly interact with Neorg and respond on certain events. ---]] +--- @brief [[ +--- Defines user callbacks - ways for the user to directly interact with Neorg and respon 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, content) #The function to call whenever our event gets triggered ----@param content_filter fun(event) #A filtering function to test if a certain event meets our expectations +--- 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 {} @@ -18,8 +21,9 @@ function callbacks.on_event(event_name, callback, 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 table #An event as returned by modules.create_event() +--- 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] diff --git a/lua/neorg/core/config.lua b/lua/neorg/core/config.lua index 3bd781b29..771980917 100644 --- a/lua/neorg/core/config.lua +++ b/lua/neorg/core/config.lua @@ -1,4 +1,37 @@ --- Grab OS info on startup +--- @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" + +--- @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() @@ -19,11 +52,18 @@ local function get_os_info() end return "linux" end + + error("[neorg]: Unable to determine the currently active operating system!") end local os_info = get_os_info() --- Configuration template +--- 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, @@ -39,10 +79,13 @@ local config = { arguments = {}, norg_version = "1.1.1", - version = "6.2.0", + version = "7.0.0", os_info = os_info, pathsep = os_info == "windows" and "\\" or "/", + + hook = nil, + started = false, } return config diff --git a/lua/neorg/core/lib.lua b/lua/neorg/core/lib.lua index 879a12e20..91111c6ae 100644 --- a/lua/neorg/core/lib.lua +++ b/lua/neorg/core/lib.lua @@ -1,14 +1,9 @@ -local lib = { - -- TODO: Are the mod functions used anywhere? - mod = { --- Modifiers for the `map` function - exclude = {}, --- Filtering modifiers that exclude certain elements from a table - }, -} - ---- Returns the item that matches the first item in statements ----@param value any #The value to compare against ----@param compare? function #A custom comparison function ----@return function #A function to invoke with a table of potential matches +local lib = {} + +--- Returns the item that matches the first item in statements. +--- @param value any The value to compare against. +--- @param compare? fun(lhs: any, rhs: any): boolean A custom comparison function. +--- @return fun(statements: table): any # A function to invoke with a table of potential matches. function lib.match(value, compare) -- Returning a function allows for such syntax: -- match(something) { ..matches.. } @@ -74,11 +69,12 @@ function lib.match(value, compare) end end ---- Wrapped around `match()` that performs an action based on a condition ----@param comparison boolean #The comparison to perform ----@param when_true function|any #The value to return when `comparison` is true ----@param when_false function|any #The value to return when `comparison` is false ----@return any #The value that either `when_true` or `when_false` returned +--- Wrapped around `match()` that performs an action based on a condition. +--- @param comparison boolean The comparison to perform. +--- @param when_true function|any The value to return when `comparison` is true. +--- @param when_false function|any The value to return when `comparison` is false. +--- @return any # The value that either `when_true` or `when_false` returned. +--- @see neorg.core.lib.match function lib.when(comparison, when_true, when_false) if type(comparison) ~= "boolean" then comparison = (comparison ~= nil) @@ -90,12 +86,13 @@ function lib.when(comparison, when_true, when_false) }) end ---- Maps a function to every element of a table --- The function can return a value, in which case that specific element will be assigned --- the return value of that function. ----@param tbl table #The table to iterate over ----@param callback function #The callback that should be invoked on every iteration ----@return table #A modified version of the original `tbl`. +--- Maps a function to every element of a table. +--- The function can return a value, in which case that specific element will be assigned +--- the return value of that function. +--- @generic K, V +--- @param tbl table The table to iterate over +--- @param callback fun(key: K, value: V, tbl: table): any? The callback that should be invoked on every iteration +--- @return table # A modified version of the original `tbl`. function lib.map(tbl, callback) local copy = vim.deepcopy(tbl) @@ -111,10 +108,10 @@ function lib.map(tbl, callback) end --- Iterates over all elements of a table and returns the first value returned by the callback. ----@param tbl table #The table to iterate over ----@param callback function #The callback function that should be invoked on each iteration. ---- Can return a value in which case that value will be returned from the `filter()` call. ----@return any|nil #The value returned by `callback`, if any +--- @generic K, V +--- @param tbl table The table to iterate over. +--- @param callback fun(key: K, value: V): V? The callback function that should be invoked on each iteration. Can return a value in which case that value will be returned from the `filter()` call. +--- @return V? # The value returned by `callback`, if any. function lib.filter(tbl, callback) for k, v in pairs(tbl) do local cb = callback(k, v) @@ -125,10 +122,11 @@ function lib.filter(tbl, callback) end end ---- Finds any key in an array ----@param tbl any #An array of values to iterate over ---@diagnostic disable-line -- TODO: type error workaround ----@param element any #The item to find ----@return any|nil #The found value or `nil` if nothing could be found +--- Finds any key in an array. +--- @generic V +--- @param tbl { [number]: V } An array of values to iterate over. +--- @param element V The item to find. +--- @return V? # The found value or `nil` if nothing could be found. function lib.find(tbl, element) return lib.filter(tbl, function(key, value) if value == element then @@ -138,9 +136,9 @@ function lib.find(tbl, element) end --- Inserts a value into a table if it doesn't exist, else returns the existing value. ----@param tbl table #The table to insert into ----@param value number|string #The value to insert ----@return any #The item to return +--- @param tbl table The table to insert into. +--- @param value any The value to insert. +--- @return `value` # The item to return. function lib.insert_or(tbl, value) local item = lib.find(tbl, value) @@ -150,10 +148,10 @@ function lib.insert_or(tbl, value) end)() end ---- Picks a set of values from a table and returns them in an array ----@param tbl table #The table to extract the keys from ----@param values any array[string] #An array of strings, these being the keys you'd like to extract ---@diagnostic disable-line -- TODO: type error workaround ----@return any array[any] #The picked values from the table ---@diagnostic disable-line -- TODO: type error workaround +--- Picks a set of values from a table and returns them in an array. +--- @param tbl table The table to extract the keys from. +--- @param values string[] An array of strings, these being the keys you'd like to extract. +--- @return any # The picked values from the table. function lib.pick(tbl, values) local result = {} @@ -167,9 +165,9 @@ function lib.pick(tbl, values) end --- Tries to extract a variable in all nesting levels of a table. ----@param tbl table #The table to traverse ----@param value any #The value to look for - note that comparison is done through the `==` operator ----@return any|nil #The value if it was found, else nil +--- @param tbl table The table to traverse. +--- @param value any The value to look for - note that comparison is done through the `==` operator. +--- @return any? # The value if it was found, else nil. function lib.extract(tbl, value) local results = {} @@ -186,10 +184,10 @@ function lib.extract(tbl, value) return results end ---- Wraps a conditional "not" function in a vim.tbl callback ----@param cb function #The function to wrap ----@vararg ... #The arguments to pass to the wrapped function ----@return function #The wrapped function in a vim.tbl callback +--- Wraps a conditional "not" function in a callback. +--- @param cb function The function to wrap +--- @param ... any The arguments to pass to the wrapped function +--- @return function # The wrapped function in a callback function lib.wrap_cond_not(cb, ...) local params = { ... } return function(v) @@ -197,10 +195,10 @@ function lib.wrap_cond_not(cb, ...) end end ---- Wraps a conditional function in a vim.tbl callback ----@param cb function #The function to wrap ----@vararg ... #The arguments to pass to the wrapped function ----@return function #The wrapped function in a vim.tbl callback +--- Wraps a conditional function in a callback. +--- @param cb function The function to wrap. +--- @param ... any The arguments to pass to the wrapped function. +--- @return function # The wrapped function in a callback. function lib.wrap_cond(cb, ...) local params = { ... } return function(v) @@ -208,10 +206,11 @@ function lib.wrap_cond(cb, ...) end end ---- Wraps a function in a callback ----@param function_pointer function #The function to wrap ----@vararg ... #The arguments to pass to the wrapped function ----@return function #The wrapped function in a callback +--- Wraps a function in a callback. +--- @generic T: function, A +--- @param function_pointer T The function to wrap. +--- @param ... A The arguments to pass to the wrapped function. +--- @return fun(...: A): T # The wrapped function in a callback. function lib.wrap(function_pointer, ...) local params = { ... } @@ -219,7 +218,7 @@ function lib.wrap(function_pointer, ...) local prev = function_pointer -- luacheck: push ignore - function_pointer = function(...) ---@diagnostic disable-line -- TODO: type error workaround + function_pointer = function() return prev, unpack(params) end -- luacheck: pop @@ -230,10 +229,11 @@ function lib.wrap(function_pointer, ...) end end ---- Repeats an arguments `index` amount of times ----@param value any #The value to repeat ----@param index number #The amount of times to repeat the argument ----@return ... #An expanded vararg with the repeated argument +--- Repeats an arguments `index` amount of times. +--- @generic T +--- @param value T The value to repeat. +--- @param index number The amount of times to repeat the argument. +--- @return T ... # An expanded vararg with the repeated argument. function lib.reparg(value, index) if index == 1 then return value @@ -243,26 +243,27 @@ function lib.reparg(value, index) end --- Lazily concatenates a string to prevent runtime errors where an object may not exist --- Consider the following example: --- --- lib.when(str ~= nil, str .. " extra text", "") --- --- This would fail, simply because the string concatenation will still be evaluated in order --- to be placed inside the variable. You may use: --- --- lib.when(str ~= nil, lib.lazy_string_concat(str, " extra text"), "") --- --- To mitigate this issue directly. ---- @vararg string #An unlimited number of strings ----@return string #The result of all the strings concatenateA. +--- Consider the following example: +--- +--- lib.when(str ~= nil, str .. " extra text", "") +--- +--- This would fail, simply because the string concatenation will still be evaluated in order +--- to be placed inside the variable. You may use: +--- +--- lib.when(str ~= nil, lib.lazy_string_concat(str, " extra text"), "") +--- +--- To mitigate this issue directly. +--- @param ... string An unlimited number of strings. +--- @return string The result of all the strings concatenated. function lib.lazy_string_concat(...) return table.concat({ ... }) end --- Converts an array of values to a table of keys ----@param values string[]|number[] #An array of values to store as keys ----@param default any #The default value to assign to all key pairs ----@return table #The converted table +--- @generic K, V +--- @param values K[] An array of values to store as keys. +--- @param default V The default value to assign to all key pairs. +--- @return { [K]: V } # The converted table. function lib.to_keys(values, default) local ret = {} @@ -274,9 +275,9 @@ function lib.to_keys(values, default) end --- Constructs a new key-pair table by running a callback on all elements of an array. ----@param keys string[] #A string array with the keys to iterate over ----@param cb function #A function that gets invoked with each key and returns a value to be placed in the output table ----@return table #The newly constructed table +--- @param keys string[] A string array with the keys to iterate over. +--- @param cb fun(key: string): any? A function that gets invoked with each key and returns a value to be placed in the output table. +--- @return table # The newly constructed table. function lib.construct(keys, cb) local result = {} @@ -287,10 +288,10 @@ function lib.construct(keys, cb) return result end ---- If `val` is a function, executes it with the desired arguments, else just returns `val` ----@param val any|function #Either a function or any other value ----@vararg any #Potential arguments to give `val` if it is a function ----@return any #The returned evaluation of `val` +--- If `val` is a function, executes it with the desired arguments, else just returns `val`. +--- @param val function|any Either a function or any other value. +--- @param ... any Potential arguments to give `val` if it is a function. +--- @return any # The returned evaluation of `val`. function lib.eval(val, ...) if type(val) == "function" then return val(...) @@ -300,14 +301,18 @@ function lib.eval(val, ...) end --- Extends a list by constructing a new one vs mutating an existing --- list in the case of `vim.list_extend` +--- list in the case of `vim.list_extend`. +--- @param list any[] The list to extend. +--- @param ... any[] A set of lists to expand the current list by. +--- @return any[] # The lists concatenated to each other. function lib.list_extend(list, ...) return list and { unpack(list), unpack(lib.list_extend(...)) } or {} end --- Converts a table with `key = value` pairs to a `{ key, value }` array. ----@param tbl_with_keys table #A table with key-value pairs ----@return any array #An array of `{ key, value }` pairs. ---@diagnostic disable-line -- TODO: type error workaround +--- @generic K, V +--- @param tbl_with_keys table A table with key-value pairs. +--- @return { [number]: { [0]: K, [1]: V } } # An array of `{ key, value }` pairs. function lib.unroll(tbl_with_keys) local res = {} @@ -319,10 +324,11 @@ function lib.unroll(tbl_with_keys) end --- Works just like pcall, except returns only a single value or nil (useful for ternary operations --- which are not possible with a function like `pcall` that returns two values). ----@param func function #The function to invoke in a protected environment ----@vararg any #The parameters to pass to `func` ----@return any|nil #The return value of the executed function or `nil` +--- which are not possible with a function like `pcall` that returns two values). +--- @generic T +--- @param func fun(...: any): T The function to invoke in a protected environment. +--- @param ... any The parameters to pass to `func`. +--- @return T? # The return value of the executed function or `nil`. function lib.inline_pcall(func, ...) local ok, ret = pcall(func, ...) @@ -333,10 +339,10 @@ function lib.inline_pcall(func, ...) -- return nil end ---- Perform a backwards search for a character and return the index of that character ----@param str string #The string to search ----@param char string #The substring to search for ----@return number|nil #The index of the found substring or `nil` if not found +--- Perform a backwards search for a character and return the index of that character. +--- @param str string The string to search. +--- @param char string The substring to search for. +--- @return number? # The index of the found substring or `nil` if not found. function lib.rfind(str, char) local length = str:len() local found_from_back = str:reverse():find(char) @@ -344,9 +350,9 @@ function lib.rfind(str, char) end --- Ensure that a nested set of variables exists. --- Useful when you want to initialise a chain of nested values before writing to them. ----@param tbl table #The table you want to modify ----@vararg string #A list of indices to recursively nest into. +--- Useful when you want to initialise a chain of nested values before writing to them. +--- @param tbl table The table you want to modify. +--- @param ... string A list of indices to recursively nest into. function lib.ensure_nested(tbl, ...) local ref = tbl or {} @@ -357,8 +363,8 @@ function lib.ensure_nested(tbl, ...) end --- Capitalizes the first letter of each word in a given string. ----@param str string #The string to capitalize ----@return string #The capitalized string. +--- @param str string The string to capitalize. +--- @return string # The capitalized string. function lib.title(str) local result = {} @@ -371,10 +377,10 @@ function lib.title(str) end --- Wraps a number so that it fits within a given range. ----@param value number #The number to wrap ----@param min number #The lower bound ----@param max number #The higher bound ----@return number #The wrapped number, guarantees `min <= value <= max`. +--- @param value number The number to wrap. +--- @param min number The lower bound. +--- @param max number The higher bound. +--- @return number # The wrapped number, guarantees `min <= value <= max`. function lib.number_wrap(value, min, max) local range = max - min + 1 local wrapped_value = ((value - min) % range) + min @@ -386,9 +392,21 @@ function lib.number_wrap(value, min, max) return wrapped_value end +--- Split a path into its components. +--- Example: `/my/cool/path/file.txt` --> `{ my, cool, path, file.txt }` +--- @param path string The path to split. +--- @return string[] # The path components. +function lib.tokenize_path(path) + local tokens = {} + for capture in path:gmatch("[^/\\]+") do + table.insert(tokens, capture) + end + return tokens +end + --- Lazily copy a table-like object. ----@param to_copy table|any #The table to copy. If any other type is provided it will be copied immediately. ----@return table #The copied table +--- @param to_copy table|any The table to copy. If any other type is provided it will be copied immediately. +--- @return table|any # The copied table. function lib.lazy_copy(to_copy) if type(to_copy) ~= "table" then return vim.deepcopy(to_copy) @@ -449,26 +467,30 @@ function lib.lazy_copy(to_copy) }) end ---- Wrapper function to add two values --- This function only takes in one argument because the second value --- to add is provided as a parameter in the callback. ----@param amount number #The number to add ----@return function #A callback adding the static value to the dynamic amount +lib.mod = {} + +--- Wrapper function to add two values. +--- This function only takes in one argument because the second value to add is provided as a parameter in the callback. +--- @param amount number The number to add. +--- @return fun(_, value: number): number # A callback adding the static value to the dynamic amount. function lib.mod.add(amount) return function(_, value) return value + amount end end ---- Wrapper function to set a value to another value in a `map` sequence ----@param to any #A static value to set each element of the table to ----@return function #A callback that returns the static value +--- Wrapper function to set a value to another value in a `map` sequence. +--- @generic T +--- @param to T A static value to set each element of the table to. +--- @return fun(): T # A callback that returns the static value function lib.mod.modify(to) return function() return to end end +lib.mod.exclude = {} + function lib.mod.exclude.first(func, alt) return function(i, val) return i == 1 and (alt and alt(i, val) or val) or func(i, val) diff --git a/lua/neorg/core/log.lua b/lua/neorg/core/log.lua index e738da989..9e2aced7e 100644 --- a/lua/neorg/core/log.lua +++ b/lua/neorg/core/log.lua @@ -9,24 +9,36 @@ local lib = require("neorg.core.lib") --- User configuration section +--- @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 float Can limit the number of decimals displayed for floats. + +--- User configuration section +--- @type neorg.log.configuration local default_config = { - -- Name of the plugin. Prepended to log messages plugin = "neorg", - -- Should print the output to neovim while running use_console = true, - -- Should highlighting be used in console (using echohl) highlights = true, - -- Should write to a file use_file = true, - -- Any messages above this level will be logged. level = "warn", - -- Level configuration modes = { { name = "trace", hl = "Comment", level = vim.log.levels.TRACE }, { name = "debug", hl = "Comment", level = vim.log.levels.DEBUG }, @@ -36,7 +48,6 @@ local default_config = { { name = "fatal", hl = "ErrorMsg", level = 5 }, }, - -- Can limit the number of decimals displayed for floats float_precision = 0.01, } @@ -47,8 +58,10 @@ log.get_default_config = function() return default_config end -local unpack = unpack or table.unpack ---@diagnostic disable-line -- TODO: type error workaround +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 diff --git a/lua/neorg/core/modules.lua b/lua/neorg/core/modules.lua index 826258b7b..8716b293e 100644 --- a/lua/neorg/core/modules.lua +++ b/lua/neorg/core/modules.lua @@ -1,61 +1,83 @@ +--- @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 ---[[ --- BASE FILE FOR MODULES --- This file contains the base module implementation ---]] - 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.configuration +--- 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. +--- +--- @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[] } + +--- @class (exact) neorg.module +--- 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. +--- @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? table 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. + 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. +--- 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 = { - - -- Invoked before any initial loading happens setup = function() return { success = true, requires = {}, replaces = nil, replace_merge = false } end, - -- Invoked after the module has been configured load = function() end, - -- Invoked whenever an event that the module has subscribed to triggers - -- callback function with a "event" parameter on_event = function() end, - -- Invoked after all plugins are loaded neorg_post_load = function() end, - -- The name of the module, note that modules beginning with core are neorg's inbuilt modules name = "core.default", - -- The path of the module, can be used in require() statements path = "modules.core.default.module", - -- A convenience table to place all of your private variables that you don't want to expose here. private = {}, - -- Every module can expose any set of information it sees fit through the public field - -- All functions and variables declared in this table will be visible to any other module loaded public = { - -- Current Norg version that this module supports. - -- Your module will use this version if not specified, but you can override it. - -- Overriding it will mean that your module is only compatible with the overriden Norg revision. - -- E.g: setting version = "1.0.0" will mean that your module requires Norg 1.0.0+ to operate version = config.norg_version, }, - -- Configuration for the module config = { - private = { -- Private module config, cannot be changed by other modules or by the user + private = { --[[ config_option = false, @@ -65,7 +87,7 @@ function modules.create(name, imports) --]] }, - public = { -- Public config, can be changed by modules and the user + public = { --[[ config_option = false, @@ -75,13 +97,9 @@ function modules.create(name, imports) --]] }, - -- This table houses all the changes the user made to the public table, - -- useful for when you want to know exactly what the user tinkered with. - -- Shouldn't be commonly used. custom = {}, }, - -- Event data regarding the current module events = { subscribed = { -- The events that the module is subscribed to --[[ @@ -99,8 +117,6 @@ function modules.create(name, imports) }, }, - -- If you ever require a module through the return value of the setup() function, - -- All of the modules' public APIs will become available here required = { --[[ ["core.test"] = { @@ -114,7 +130,6 @@ function modules.create(name, imports) --]] }, - -- Example bits of code that the user can look through examples = { --[[ a_cool_test = function() @@ -123,12 +138,9 @@ function modules.create(name, imports) --]] }, - -- 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. imported = { --[[ - ["my.module.submodule"] = { ... }, + ["my.module.submodule"] = { ... }, --]] }, } @@ -155,8 +167,9 @@ function modules.create(name, imports) 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 ... (varargs) - a list of module names to load. +--- @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) @@ -199,25 +212,17 @@ 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 ---[[ --- NEORG MODULE MANAGER --- This file is responsible for loading, calling and managing modules --- Modules are internal mini-programs that execute on certain events, they build the foundation of Neorg itself. ---]] - ---[[ --- The reason we do not just call this variable modules.loaded_modules.count is because --- someone could make a module called "count" and override the variable, causing bugs. ---]] +--- 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 table #The actual module to load ----@return boolean #Whether the module successfully loaded +--- 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) @@ -228,6 +233,7 @@ function modules.load_module_from_table(module) 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, @@ -258,6 +264,7 @@ function modules.load_module_from_table(module) -- 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 @@ -279,7 +286,7 @@ function modules.load_module_from_table(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[required_module] then + if config.user_config.load[required_module] then log.trace( "Wanted module", required_module, @@ -289,7 +296,7 @@ function modules.load_module_from_table(module) if not modules.load_module(required_module) then log.error( "Unable to load wanted module for", - loaded_module.name, + module.name, "- the module didn't load successfully" ) @@ -410,17 +417,20 @@ function modules.load_module_from_table(module) 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 +--- 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 @@ -477,8 +487,8 @@ function modules.load_module(module_name, cfg) 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 table #A valid table as returned by modules.create() ----@param parent_module string|table #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() +--- @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 @@ -490,17 +500,18 @@ function modules.load_module_as_dependency_from_table(module, parent_module) 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 +--- @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 ----@param module_name string #The name of the module to retrieve +--- Retrieves the public API exposed by the module. +--- @param module_name string The name of the module to retrieve. +--- @return neorg.module.public? 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.") @@ -511,7 +522,8 @@ function modules.get_module(module_name) 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) +--- @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.") @@ -522,14 +534,15 @@ function modules.get_module_config(module_name) end --- Returns true if module with name module_name is loaded, false otherwise ----@param module_name string #The name of an arbitrary module +--- @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 struct | nil (if any error occurs) +--- 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 @@ -550,8 +563,8 @@ function modules.get_module_version(module_name) 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: table) #The callback to execute. +--- @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))) @@ -561,10 +574,65 @@ function modules.await(module_name, callback) callbacks.on_event("core.module_loaded", function(_, module) callback(module.public) end, function(event) - return event.content.name == module_name ---@diagnostic disable-line -- TODO: type error workaround + 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 @@ -576,7 +644,8 @@ end --- 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 +--- @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%.") @@ -590,9 +659,10 @@ function modules.split_event_type(type) return split_event_type end ---- Returns an event template defined in module.events.defined ----@param module table #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') +--- 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 @@ -614,9 +684,10 @@ function modules.get_event_template(module, type) 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 table #A reference to the module invoking the function ----@param name string #A relative path to a valid event template +--- 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 @@ -645,12 +716,13 @@ function modules.define_event(module, name) return new_event end ---- Returns a copy of the event template provided by a module ----@param module table #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 any #The content of the event, can be anything from a string to a table to whatever you please ----@return table #New event -function modules.create_event(module, type, content) +--- 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] @@ -659,7 +731,7 @@ function modules.create_event(module, type, content) if not event_template then log.warn("Unable to create event of type", type, ". Returning nil...") - return ---@diagnostic disable-line -- TODO: type error workaround + return end -- Make a deep copy here - we don't want to override the actual base table! @@ -670,23 +742,28 @@ function modules.create_event(module, type, content) new_event.referrer = module.name -- Override all the important values - new_event.split_type = modules.split_event_type(type) - new_event.filename = vim.fn.expand("%:t") - new_event.filehead = vim.fn.expand("%:p:h") - new_event.cursor_position = vim.api.nvim_win_get_cursor(0) - new_event.line_content = vim.api.nvim_get_current_line() + 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)) + 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 = vim.api.nvim_get_current_buf() - new_event.window = vim.api.nvim_get_current_win() + 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 table #An event, usually created by modules.create_event() ----@param callback function? #A callback to be invoked after all events have been asynchronously broadcast +--- @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 @@ -718,9 +795,9 @@ function modules.broadcast_event(event, 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 table #An event, usually created by modules.create_event() +--- 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 diff --git a/lua/neorg/core/utils.lua b/lua/neorg/core/utils.lua index a07bef144..fbd66096e 100644 --- a/lua/neorg/core/utils.lua +++ b/lua/neorg/core/utils.lua @@ -7,16 +7,17 @@ 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 any # Parsed query +--- @return table # 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 - return vim.treesitter.parse_query(language, query_string) ---@diagnostic disable-line -- TODO: type error workaround + 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 @@ -33,26 +34,31 @@ function utils.get_username() 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 +--- 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 + + -- 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 + + -- Search for available parsers for _, parser in pairs(vim.api.nvim_get_runtime_file("parser/*.so", true)) do - local parser_name = vim.fn.fnamemodify(parser, ":t:r") + 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 @@ -66,7 +72,11 @@ function utils.get_language_list(values) 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" }, @@ -98,11 +108,11 @@ function utils.get_language_shorthands(reverse_lookup) return reverse_lookup and vim.tbl_add_reverse_lookup(langs) or langs 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 +--- 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 @@ -117,16 +127,18 @@ function utils.is_minimum_version(major, minor, patch) 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 +--- @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 ---@diagnostic disable-line -- TODO: type error workaround + return end -- Define variables that split the version up into 3 slices local split_version, versions, ret = - vim.split(version_string, ".", true), { "major", "minor", "patch" }, { major = 0, minor = 0, patch = 0 } ---@diagnostic disable-line -- TODO: type error workaround + 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 @@ -135,7 +147,7 @@ function utils.parse_version_string(version_string) version_string, "failed - too many version numbers provided. Version should follow this layout: .." ) - return ---@diagnostic disable-line -- TODO: type error workaround + return end -- Loop through all the versions and check whether they are valid numbers. If they are, add them to the return table @@ -145,7 +157,7 @@ function utils.parse_version_string(version_string) if not num then log.warn("Invalid version provided, string cannot be converted to integral type.") - return ---@diagnostic disable-line -- TODO: type error workaround + return end ret[ver] = num @@ -155,16 +167,16 @@ function utils.parse_version_string(version_string) return ret end ---- Custom neorg notifications. Wrapper around vim.notify ----@param msg string message to send ----@param log_level integer|nil log level in `vim.log.levels`. +--- 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[] #An array of files to open. ----@param callback fun(buffer: integer, filename: string) #The callback to invoke for each file. +--- @param files string[] 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 local bufnr = vim.uri_to_bufnr(vim.uri_from_fname(file)) @@ -195,7 +207,8 @@ function utils.wrap_dotrepeat(event_handler) utils._neorg_is_dotrepeat = false utils.set_operatorfunc(function() if utils._neorg_is_dotrepeat then - local pos = vim.fn.getpos(".") + local pos = assert(vim.fn.getpos(".")) + event.buffer = pos[1] event.cursor_position = { pos[2], pos[3] } end @@ -206,10 +219,10 @@ function utils.wrap_dotrepeat(event_handler) end end ---- Truncate input str to fit inside the col_limit when displayed. Takes non-ascii chars into account. ----@param str string ----@param col_limit integer #str will be cut so that when displayed, the display length does not exceed limit ----@return string #substring of input str +--- 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) diff --git a/lua/neorg/modules/core/autocommands/module.lua b/lua/neorg/modules/core/autocommands/module.lua index 6cd015b47..5ec9b396c 100644 --- a/lua/neorg/modules/core/autocommands/module.lua +++ b/lua/neorg/modules/core/autocommands/module.lua @@ -44,14 +44,15 @@ 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 -function _neorg_module_autocommand_triggered(name, triggered_from_norg) - local event = modules.create_event(module, name, { norg = triggered_from_norg }) +---@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.autocmd_base = function(name) +module.private.autocmd_base = function(name) return modules.define_event(module, name) end @@ -71,21 +72,18 @@ module.public = { vim.cmd("augroup Neorg") if dont_isolate and vim.fn.exists("#Neorg#" .. autocmd .. "#*") == 0 then - vim.cmd( - "autocmd " - .. autocmd - .. ' * :lua _neorg_module_autocommand_triggered("core.autocommands.events.' - .. autocmd - .. '", false)' - ) + 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.cmd( - "autocmd " - .. autocmd - .. ' *.norg :lua _neorg_module_autocommand_triggered("core.autocommands.events.' - .. autocmd - .. '", true)' - ) + 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 @@ -218,118 +216,118 @@ module.events.subscribed = { -- All the autocommand definitions module.events.defined = { - bufadd = module.autocmd_base("bufadd"), - bufdelete = module.autocmd_base("bufdelete"), - bufenter = module.autocmd_base("bufenter"), - buffilepost = module.autocmd_base("buffilepost"), - buffilepre = module.autocmd_base("buffilepre"), - bufhidden = module.autocmd_base("bufhidden"), - bufleave = module.autocmd_base("bufleave"), - bufmodifiedset = module.autocmd_base("bufmodifiedset"), - bufnew = module.autocmd_base("bufnew"), - bufnewfile = module.autocmd_base("bufnewfile"), - bufreadpost = module.autocmd_base("bufreadpost"), - bufreadcmd = module.autocmd_base("bufreadcmd"), - bufreadpre = module.autocmd_base("bufreadpre"), - bufunload = module.autocmd_base("bufunload"), - bufwinenter = module.autocmd_base("bufwinenter"), - bufwinleave = module.autocmd_base("bufwinleave"), - bufwipeout = module.autocmd_base("bufwipeout"), - bufwrite = module.autocmd_base("bufwrite"), - bufwritecmd = module.autocmd_base("bufwritecmd"), - bufwritepost = module.autocmd_base("bufwritepost"), - chaninfo = module.autocmd_base("chaninfo"), - chanopen = module.autocmd_base("chanopen"), - cmdundefined = module.autocmd_base("cmdundefined"), - cmdlinechanged = module.autocmd_base("cmdlinechanged"), - cmdlineenter = module.autocmd_base("cmdlineenter"), - cmdlineleave = module.autocmd_base("cmdlineleave"), - cmdwinenter = module.autocmd_base("cmdwinenter"), - cmdwinleave = module.autocmd_base("cmdwinleave"), - colorscheme = module.autocmd_base("colorscheme"), - colorschemepre = module.autocmd_base("colorschemepre"), - completechanged = module.autocmd_base("completechanged"), - completedonepre = module.autocmd_base("completedonepre"), - completedone = module.autocmd_base("completedone"), - cursorhold = module.autocmd_base("cursorhold"), - cursorholdi = module.autocmd_base("cursorholdi"), - cursormoved = module.autocmd_base("cursormoved"), - cursormovedi = module.autocmd_base("cursormovedi"), - diffupdated = module.autocmd_base("diffupdated"), - dirchanged = module.autocmd_base("dirchanged"), - fileappendcmd = module.autocmd_base("fileappendcmd"), - fileappendpost = module.autocmd_base("fileappendpost"), - fileappendpre = module.autocmd_base("fileappendpre"), - filechangedro = module.autocmd_base("filechangedro"), - exitpre = module.autocmd_base("exitpre"), - filechangedshell = module.autocmd_base("filechangedshell"), - filechangedshellpost = module.autocmd_base("filechangedshellpost"), - filereadcmd = module.autocmd_base("filereadcmd"), - filereadpost = module.autocmd_base("filereadpost"), - filereadpre = module.autocmd_base("filereadpre"), - filetype = module.autocmd_base("filetype"), - filewritecmd = module.autocmd_base("filewritecmd"), - filewritepost = module.autocmd_base("filewritepost"), - filewritepre = module.autocmd_base("filewritepre"), - filterreadpost = module.autocmd_base("filterreadpost"), - filterreadpre = module.autocmd_base("filterreadpre"), - filterwritepost = module.autocmd_base("filterwritepost"), - filterwritepre = module.autocmd_base("filterwritepre"), - focusgained = module.autocmd_base("focusgained"), - focuslost = module.autocmd_base("focuslost"), - funcundefined = module.autocmd_base("funcundefined"), - uienter = module.autocmd_base("uienter"), - uileave = module.autocmd_base("uileave"), - insertchange = module.autocmd_base("insertchange"), - insertcharpre = module.autocmd_base("insertcharpre"), - textyankpost = module.autocmd_base("textyankpost"), - insertenter = module.autocmd_base("insertenter"), - insertleavepre = module.autocmd_base("insertleavepre"), - insertleave = module.autocmd_base("insertleave"), - menupopup = module.autocmd_base("menupopup"), - optionset = module.autocmd_base("optionset"), - quickfixcmdpre = module.autocmd_base("quickfixcmdpre"), - quickfixcmdpost = module.autocmd_base("quickfixcmdpost"), - quitpre = module.autocmd_base("quitpre"), - remotereply = module.autocmd_base("remotereply"), - sessionloadpost = module.autocmd_base("sessionloadpost"), - shellcmdpost = module.autocmd_base("shellcmdpost"), - signal = module.autocmd_base("signal"), - shellfilterpost = module.autocmd_base("shellfilterpost"), - sourcepre = module.autocmd_base("sourcepre"), - sourcepost = module.autocmd_base("sourcepost"), - sourcecmd = module.autocmd_base("sourcecmd"), - spellfilemissing = module.autocmd_base("spellfilemissing"), - stdinreadpost = module.autocmd_base("stdinreadpost"), - stdinreadpre = module.autocmd_base("stdinreadpre"), - swapexists = module.autocmd_base("swapexists"), - syntax = module.autocmd_base("syntax"), - tabenter = module.autocmd_base("tabenter"), - tableave = module.autocmd_base("tableave"), - tabnew = module.autocmd_base("tabnew"), - tabnewentered = module.autocmd_base("tabnewentered"), - tabclosed = module.autocmd_base("tabclosed"), - termopen = module.autocmd_base("termopen"), - termenter = module.autocmd_base("termenter"), - termleave = module.autocmd_base("termleave"), - termclose = module.autocmd_base("termclose"), - termresponse = module.autocmd_base("termresponse"), - textchanged = module.autocmd_base("textchanged"), - textchangedi = module.autocmd_base("textchangedi"), - textchangedp = module.autocmd_base("textchangedp"), - user = module.autocmd_base("user"), - usergettingbored = module.autocmd_base("usergettingbored"), - vimenter = module.autocmd_base("vimenter"), - vimleave = module.autocmd_base("vimleave"), - vimleavepre = module.autocmd_base("vimleavepre"), - vimresized = module.autocmd_base("vimresized"), - vimresume = module.autocmd_base("vimresume"), - vimsuspend = module.autocmd_base("vimsuspend"), - winclosed = module.autocmd_base("winclosed"), - winenter = module.autocmd_base("winenter"), - winleave = module.autocmd_base("winleave"), - winnew = module.autocmd_base("winnew"), - winscrolled = module.autocmd_base("winscrolled"), + 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 = { diff --git a/lua/neorg/modules/core/concealer/module.lua b/lua/neorg/modules/core/concealer/module.lua index 4dd411472..43bf098b8 100644 --- a/lua/neorg/modules/core/concealer/module.lua +++ b/lua/neorg/modules/core/concealer/module.lua @@ -425,6 +425,19 @@ local function format_ordered_icon(pattern, index) end +local superscript_digits = { + "⁰", + "¹", + "²", + "³", + "⁴", + "⁵", + "⁶", + "⁷", + "⁸", + "⁹", +} + ---@class core.concealer module.public = { foldtext = function() @@ -493,7 +506,7 @@ module.public = { end local text = (" "):rep(len - 1) .. icon - if #text > len and not has_anticonceal then + if vim.fn.strcharlen(text) > len and not has_anticonceal then -- TODO warn neovim version return end @@ -501,7 +514,7 @@ module.public = { 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 #text > len then + if vim.fn.strcharlen(text) > len then assert(has_anticonceal) set_mark(bufid, row_0b, col_0b + len, text:sub(first_unicode_end + 1), highlight, { virt_text_pos = "inline", @@ -510,6 +523,22 @@ module.public = { 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("%d+") then + local t = {} + for i = 1, #link_title do + local d = link_title:sub(i, i):byte() - 0x30 + table.insert(t, superscript_digits[d + 1]) + 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, + multilevel_copied = function(config, bufid, node) if not config.icons then return @@ -550,13 +579,18 @@ module.public = { end end, - fill_width = function(config, bufid, node) + render_horizontal_line = function(config, bufid, node) if not config.icon then return end - local row_start_0b, col_start_0b = node:range() - local line_len = vim.api.nvim_win_get_width(0) - set_mark(bufid, row_start_0b, col_start_0b, config.icon:rep(line_len - col_start_0b), config.highlight) + + 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(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) @@ -611,6 +645,7 @@ module.public = { 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, }) else vim.api.nvim_buf_set_extmark(bufid, module.private.ns_icon, row_0b, len, { @@ -624,6 +659,7 @@ module.public = { }, virt_text_pos = "overlay", virt_text_win_col = len, + spell = config.spell_check, }) end end @@ -801,8 +837,13 @@ module.config.public = { 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 = "⁑ ", @@ -833,7 +874,15 @@ module.config.public = { icon = "─", highlight = "@neorg.delimiters.horizontal_line", nodes = { "horizontal_line" }, - render = module.public.icon_renderers.fill_width, + -- 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, }, }, @@ -874,6 +923,9 @@ module.config.public = { -- 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, @@ -1080,10 +1132,12 @@ local function prettify_range(bufid, row_start_0b, row_end_0bex) remove_prettify_flag_range(bufid, pos_start_0b_0b.x, pos_end_0bin_0bex.x + 1) add_prettify_flag_range(bufid, pos_start_0b_0b.x, pos_end_0bin_0bex.x + 1) - local current_row_0b = vim.api.nvim_win_get_cursor(0)[1] - 1 + 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.conceallevel - local concealcursor = vim.wo.concealcursor + local conceallevel = vim.wo[winid].conceallevel + local concealcursor = vim.wo[winid].concealcursor assert(document_root) @@ -1108,11 +1162,15 @@ local function prettify_range(bufid, row_start_0b, row_end_0bex) node_row_end_0bex ) ) + if has_conceal then - goto continue + if config.render_concealed then + config:render_concealed(bufid, node) + end + else + config:render(bufid, node) end - config:render(bufid, node) ::continue:: end end @@ -1208,6 +1266,8 @@ local function update_cursor(event) end local function handle_init_event(event) + -- TODO: make sure only init once + assert(vim.api.nvim_win_is_valid(event.window)) update_cursor(event) @@ -1251,17 +1311,10 @@ local function handle_init_event(event) mark_all_lines_changed(event.buffer) if module.config.public.folds and vim.api.nvim_win_is_valid(event.window) then - local opts = { - scope = "local", - win = event.window, - } - vim.api.nvim_set_option_value("foldmethod", "expr", opts) - vim.api.nvim_set_option_value("foldexpr", "nvim_treesitter#foldexpr()", opts) - vim.api.nvim_set_option_value( - "foldtext", - "v:lua.require'neorg'.modules.get_module('core.concealer').foldtext()", - opts - ) + local wo = vim.wo[event.window] + wo.foldmethod = "expr" + wo.foldexpr = vim.treesitter.foldexpr and "v:lua.vim.treesitter.foldexpr()" or "nvim_treesitter#foldexpr()" + wo.foldtext = "v:lua.require'neorg'.modules.get_module('core.concealer').foldtext()" local init_open_folds = module.config.public.init_open_folds local function open_folds() @@ -1277,8 +1330,7 @@ local function handle_init_event(event) log.warn('"init_open_folds" must be "auto", "always", or "never"') end - local foldlevel = vim.api.nvim_get_option_value("foldlevel", opts) - if foldlevel == 0 then + if wo.foldlevel == 0 then open_folds() end end @@ -1344,9 +1396,14 @@ 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.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, @@ -1379,7 +1436,8 @@ module.load = function() 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("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") @@ -1412,7 +1470,8 @@ end module.events.subscribed = { ["core.autocommands"] = { - bufnewfile = true, + -- bufnewfile = true, + filetype = true, bufreadpost = true, insertenter = true, insertleave = true, diff --git a/lua/neorg/modules/core/dirman/module.lua b/lua/neorg/modules/core/dirman/module.lua index 6a2352cc4..9b0a01759 100644 --- a/lua/neorg/modules/core/dirman/module.lua +++ b/lua/neorg/modules/core/dirman/module.lua @@ -181,10 +181,12 @@ module.public = { -- Broadcast the workspace_changed event with all the necessary information modules.broadcast_event( - modules.create_event( - module, - "core.dirman.events.workspace_changed", - { old = current_ws, new = new_workspace } + assert( + modules.create_event( + module, + "core.dirman.events.workspace_changed", + { old = current_ws, new = new_workspace } + ) ) ) @@ -204,7 +206,9 @@ module.public = { module.config.public.workspaces[workspace_name] = workspace_path -- Broadcast the workspace_added event with the newly added workspace as the content modules.broadcast_event( - modules.create_event(module, "core.dirman.events.workspace_added", { workspace_name, workspace_path }) + assert( + modules.create_event(module, "core.dirman.events.workspace_added", { workspace_name, workspace_path }) + ) ) -- Sync autocompletions so the user can see the new workspace @@ -333,7 +337,7 @@ module.public = { local bufnr = module.public.get_file_bufnr(fname) modules.broadcast_event( - modules.create_event(module, "core.dirman.events.file_created", { buffer = bufnr, opts = opts }) + assert(modules.create_event(module, "core.dirman.events.file_created", { buffer = bufnr, opts = opts })) ) if not opts.no_open then diff --git a/lua/neorg/modules/core/esupports/hop/module.lua b/lua/neorg/modules/core/esupports/hop/module.lua index 22ffc37d1..279ce3d59 100644 --- a/lua/neorg/modules/core/esupports/hop/module.lua +++ b/lua/neorg/modules/core/esupports/hop/module.lua @@ -49,6 +49,15 @@ module.config.public = { 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 + ---@class core.esupports.hop module.public = { --- Follow link from a specific node @@ -204,8 +213,10 @@ module.public = { return end + local link_not_found_buf = module.required["core.ui"].create_split("link-not-found") + local selection = module.required["core.ui"] - .begin_selection(module.required["core.ui"].create_split("link-not-found")) + .begin_selection(link_not_found_buf) :listener({ "", }, function(self) @@ -455,12 +466,7 @@ module.public = { -- Check whether the node captured node is in bounds. -- There are certain rare cases where incorrect nodes would be parsed. - if - capture_node_range.row_start >= range.row_start - and capture_node_range.row_end <= capture_node_range.row_end - and capture_node_range.column_start >= range.column_start - and capture_node_range.column_end <= range.column_end - then + if range_contains(range, capture_node_range) then local extract_node_text = lib.wrap(module.required["core.integrations.treesitter"].get_node_text, node) parsed_link_information[capture] = parsed_link_information[capture] diff --git a/lua/neorg/modules/core/esupports/metagen/module.lua b/lua/neorg/modules/core/esupports/metagen/module.lua index 1d61af4d8..8ad5fd3fb 100644 --- a/lua/neorg/modules/core/esupports/metagen/module.lua +++ b/lua/neorg/modules/core/esupports/metagen/module.lua @@ -280,16 +280,17 @@ module.public = { for _, tree in pairs(languagetree:children()) do if tree:lang() ~= "norg_meta" or meta_root then - return + goto continue end local meta_tree = tree:parse()[1] if not meta_tree then - return + goto continue end meta_root = meta_tree:root() + ::continue:: end if not meta_root then diff --git a/lua/neorg/modules/core/export/markdown/module.lua b/lua/neorg/modules/core/export/markdown/module.lua index ecfdf528e..9d15d3ba9 100644 --- a/lua/neorg/modules/core/export/markdown/module.lua +++ b/lua/neorg/modules/core/export/markdown/module.lua @@ -76,6 +76,25 @@ local function todo_item_extended(replace_text) 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() @@ -104,6 +123,23 @@ local function handle_heading_newlines() 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() @@ -115,10 +151,11 @@ module.load = function() "definition-lists", "mathematics", "metadata", + "latex", } end - module.config.public.extensions = lib.to_keys(module.config.public.extensions) + module.config.public.extensions = lib.to_keys(module.config.public.extensions, {}) end module.config.public = { @@ -126,7 +163,7 @@ module.config.public = { -- 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` and 'metadata'. + -- `definition-lists`, `mathematics`, `metadata` and `latex`. extensions = {}, -- Data about how to render mathematics. @@ -180,7 +217,7 @@ module.public = { 0, }, tag_indent = 0, - tag_close = "", + tag_close = nil, ranged_tag_indentation_level = 0, is_url = false, } @@ -379,9 +416,11 @@ module.public = { } elseif text == "embed" - and node:next_sibling() - and module.required["core.integrations.treesitter"].get_node_text(node:next_sibling()) - == "markdown" + 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 = { @@ -534,33 +573,19 @@ module.public = { ["strong_carryover"] = "", ["weak_carryover"] = "", - ["key"] = true, - - [":"] = ": ", - - ["["] = function(_, _, state) - return { - state = { - indent = state.indent + 2, - }, - } - end, - ["]"] = function(_, _, state) - return { - state = { - indent = state.indent - 2, - }, - } + ["key"] = function(text, _, state) + return string.rep(" ", state.indent) .. text end, - ["value"] = function(text, node, state) - if node:parent():type() == "array" then - return "\n" .. string.rep(" ", state.indent) .. "- " .. text - end + [":"] = ": ", - return text - end, + ["["] = update_indent(2), + ["]"] = update_indent(-2), + ["{"] = update_indent(2), + ["}"] = update_indent(-2), + ["string"] = handle_metadata_literal, + ["number"] = handle_metadata_literal, ["horizontal_line"] = "___", }, @@ -637,10 +662,8 @@ module.public = { ["heading5"] = handle_heading_newlines(), ["heading6"] = handle_heading_newlines(), - ["pair"] = function(output) - table.insert(output, "\n") - return output - end, + ["object"] = handle_metadata_composite_element("{}"), + ["array"] = handle_metadata_composite_element("[]"), }, cleanup = function() diff --git a/lua/neorg/modules/core/integrations/truezen/module.lua b/lua/neorg/modules/core/integrations/truezen/module.lua index 54703c0f9..184bf261e 100644 --- a/lua/neorg/modules/core/integrations/truezen/module.lua +++ b/lua/neorg/modules/core/integrations/truezen/module.lua @@ -12,7 +12,7 @@ local modules = neorg.modules local module = modules.create("core.integrations.truezen") -module.load = function() +module.setup = function() local success, truezen = pcall(require, "true-zen.main") if not success then diff --git a/lua/neorg/modules/core/integrations/zen_mode/module.lua b/lua/neorg/modules/core/integrations/zen_mode/module.lua index 54fb4793c..2c0d5d772 100644 --- a/lua/neorg/modules/core/integrations/zen_mode/module.lua +++ b/lua/neorg/modules/core/integrations/zen_mode/module.lua @@ -12,7 +12,7 @@ local modules = neorg.modules local module = modules.create("core.integrations.zen_mode") -module.load = function() +module.setup = function() local success, zen_mode = pcall(require, "zen_mode") if not success then diff --git a/lua/neorg/modules/core/keybinds/keybinds.lua b/lua/neorg/modules/core/keybinds/keybinds.lua index 5ac3e01ac..961ab5fbc 100644 --- a/lua/neorg/modules/core/keybinds/keybinds.lua +++ b/lua/neorg/modules/core/keybinds/keybinds.lua @@ -14,26 +14,38 @@ module.config.public = { n = { -- Marks the task under the cursor as "undone" -- ^mark Task as Undone - { leader .. "tu", "core.qol.todo_items.todo.task_undone", opts = { desc = "Mark as Undone" } }, + { + leader .. "tu", + "core.qol.todo_items.todo.task_undone", + opts = { desc = "[neorg] Mark as Undone" }, + }, -- Marks the task under the cursor as "pending" -- ^mark Task as Pending - { leader .. "tp", "core.qol.todo_items.todo.task_pending", opts = { desc = "Mark as Pending" } }, + { + leader .. "tp", + "core.qol.todo_items.todo.task_pending", + opts = { desc = "[neorg] Mark as Pending" }, + }, -- Marks the task under the cursor as "done" -- ^mark Task as Done - { leader .. "td", "core.qol.todo_items.todo.task_done", opts = { desc = "Mark as Done" } }, + { leader .. "td", "core.qol.todo_items.todo.task_done", opts = { desc = "[neorg] Mark as Done" } }, -- Marks the task under the cursor as "on_hold" -- ^mark Task as on Hold - { leader .. "th", "core.qol.todo_items.todo.task_on_hold", opts = { desc = "Mark as On Hold" } }, + { + leader .. "th", + "core.qol.todo_items.todo.task_on_hold", + opts = { desc = "[neorg] Mark as On Hold" }, + }, -- Marks the task under the cursor as "cancelled" -- ^mark Task as Cancelled { leader .. "tc", "core.qol.todo_items.todo.task_cancelled", - opts = { desc = "Mark as Cancelled" }, + opts = { desc = "[neorg] Mark as Cancelled" }, }, -- Marks the task under the cursor as "recurring" @@ -41,7 +53,7 @@ module.config.public = { { leader .. "tr", "core.qol.todo_items.todo.task_recurring", - opts = { desc = "Mark as Recurring" }, + opts = { desc = "[neorg] Mark as Recurring" }, }, -- Marks the task under the cursor as "important" @@ -49,51 +61,63 @@ module.config.public = { { leader .. "ti", "core.qol.todo_items.todo.task_important", - opts = { desc = "Mark as Important" }, + opts = { desc = "[neorg] Mark as Important" }, }, -- Marks the task under the cursor as "ambiguous" -- ^mark Task as ambiguous - { leader .. "ta", "core.qol.todo_items.todo.task_ambiguous", opts = { desc = "Mark as Ambigous" } }, + { + leader .. "ta", + "core.qol.todo_items.todo.task_ambiguous", + opts = { desc = "[neorg] Mark as Ambigous" }, + }, -- Switches the task under the cursor between a select few states - { "", "core.qol.todo_items.todo.task_cycle", opts = { desc = "Cycle Task" } }, + { "", "core.qol.todo_items.todo.task_cycle", opts = { desc = "[neorg] Cycle Task" } }, -- Creates a new .norg file to take notes in -- ^New Note - { leader .. "nn", "core.dirman.new.note", opts = { desc = "Create New Note" } }, + { leader .. "nn", "core.dirman.new.note", opts = { desc = "[neorg] Create New Note" } }, -- Hop to the destination of the link under the cursor - { "", "core.esupports.hop.hop-link", opts = { desc = "Jump to Link" } }, - { "gd", "core.esupports.hop.hop-link", opts = { desc = "Jump to Link" } }, - { "gf", "core.esupports.hop.hop-link", opts = { desc = "Jump to Link" } }, - { "gF", "core.esupports.hop.hop-link", opts = { desc = "Jump to Link" } }, + { "", "core.esupports.hop.hop-link", opts = { desc = "[neorg] Jump to Link" } }, + { "gd", "core.esupports.hop.hop-link", opts = { desc = "[neorg] Jump to Link" } }, + { "gf", "core.esupports.hop.hop-link", opts = { desc = "[neorg] Jump to Link" } }, + { "gF", "core.esupports.hop.hop-link", opts = { desc = "[neorg] Jump to Link" } }, -- Same as ``, except opens the destination in a vertical split { "", "core.esupports.hop.hop-link", "vsplit", - opts = { desc = "Jump to Link (Vertical Split)" }, + opts = { desc = "[neorg] Jump to Link (Vertical Split)" }, }, - { ">.", "core.promo.promote", opts = { desc = "Promote Object (Non-Recursively)" } }, - { "<,", "core.promo.demote", opts = { desc = "Demote Object (Non-Recursively)" } }, + { ">.", "core.promo.promote", opts = { desc = "[neorg] Promote Object (Non-Recursively)" } }, + { "<,", "core.promo.demote", opts = { desc = "[neorg] Demote Object (Non-Recursively)" } }, - { ">>", "core.promo.promote", "nested", opts = { desc = "Promote Object (Recursively)" } }, - { "<<", "core.promo.demote", "nested", opts = { desc = "Demote Object (Recursively)" } }, + { ">>", "core.promo.promote", "nested", opts = { desc = "[neorg] Promote Object (Recursively)" } }, + { "<<", "core.promo.demote", "nested", opts = { desc = "[neorg] Demote Object (Recursively)" } }, - { leader .. "lt", "core.pivot.toggle-list-type", opts = { desc = "Toggle (Un)ordered List" } }, - { leader .. "li", "core.pivot.invert-list-type", opts = { desc = "Invert (Un)ordered List" } }, + { + leader .. "lt", + "core.pivot.toggle-list-type", + opts = { desc = "[neorg] Toggle (Un)ordered List" }, + }, + { + leader .. "li", + "core.pivot.invert-list-type", + opts = { desc = "[neorg] Invert (Un)ordered List" }, + }, - { leader .. "id", "core.tempus.insert-date", opts = { desc = "Insert Date" } }, + { leader .. "id", "core.tempus.insert-date", opts = { desc = "[neorg] Insert Date" } }, }, i = { - { "", "core.promo.promote", opts = { desc = "Promote Object (Recursively)" } }, - { "", "core.promo.demote", opts = { desc = "Demote Object (Recursively)" } }, - { "", "core.itero.next-iteration", "", opts = { desc = "Continue Object" } }, - { "", "core.tempus.insert-date-insert-mode", opts = { desc = "Insert Date" } }, + { "", "core.promo.promote", opts = { desc = "[neorg] Promote Object (Recursively)" } }, + { "", "core.promo.demote", opts = { desc = "[neorg] Demote Object (Recursively)" } }, + { "", "core.itero.next-iteration", "", opts = { desc = "[neorg] Continue Object" } }, + { "", "core.tempus.insert-date-insert-mode", opts = { desc = "[neorg] Insert Date" } }, }, -- TODO: Readd these @@ -110,13 +134,17 @@ module.config.public = { keybinds.map_event_to_mode("traverse-heading", { n = { -- Move to the next heading in the document - { "j", "core.integrations.treesitter.next.heading", opts = { desc = "Move to Next Heading" } }, + { + "j", + "core.integrations.treesitter.next.heading", + opts = { desc = "[neorg] Move to Next Heading" }, + }, -- Move to the previous heading in the document { "k", "core.integrations.treesitter.previous.heading", - opts = { desc = "Move to Previous Heading" }, + opts = { desc = "[neorg] Move to Previous Heading" }, }, }, }, { @@ -128,13 +156,13 @@ module.config.public = { keybinds.map_event_to_mode("traverse-link", { n = { -- Move to the next link in the document - { "j", "core.integrations.treesitter.next.link", opts = { desc = "Move to Next Link" } }, + { "j", "core.integrations.treesitter.next.link", opts = { desc = "[neorg] Move to Next Link" } }, -- Move to the previous link in the document { "k", "core.integrations.treesitter.previous.link", - opts = { desc = "Move to Previous Link" }, + opts = { desc = "[neorg] Move to Previous Link" }, }, }, }, { @@ -145,13 +173,13 @@ module.config.public = { -- Map the below keys on presenter mode keybinds.map_event_to_mode("presenter", { n = { - { "", "core.presenter.next_page", opts = { desc = "Next Page" } }, - { "l", "core.presenter.next_page", opts = { desc = "Next Page" } }, - { "h", "core.presenter.previous_page", opts = { desc = "Previous Page" } }, + { "", "core.presenter.next_page", opts = { desc = "[neorg] Next Page" } }, + { "l", "core.presenter.next_page", opts = { desc = "[neorg] Next Page" } }, + { "h", "core.presenter.previous_page", opts = { desc = "[neorg] Previous Page" } }, -- Keys for closing the current display - { "q", "core.presenter.close", opts = { desc = "Close Presentation" } }, - { "", "core.presenter.close", opts = { desc = "Close Presentation" } }, + { "q", "core.presenter.close", opts = { desc = "[neorg] Close Presentation" } }, + { "", "core.presenter.close", opts = { desc = "[neorg] Close Presentation" } }, }, }, { silent = true, @@ -162,18 +190,18 @@ module.config.public = { -- Apply the below keys to all modes keybinds.map_to_mode("all", { n = { - { leader .. "mn", "Neorg mode norg", opts = { desc = "Enter Norg Mode" } }, + { leader .. "mn", "Neorg mode norg", opts = { desc = "[neorg] Enter Norg Mode" } }, { leader .. "mh", "Neorg mode traverse-heading", - opts = { desc = "Enter Heading Traversal Mode" }, + opts = { desc = "[neorg] Enter Heading Traversal Mode" }, }, { leader .. "ml", "Neorg mode traverse-link", - opts = { desc = "Enter Link Traversal Mode" }, + opts = { desc = "[neorg] Enter Link Traversal Mode" }, }, - { "gO", "Neorg toc split", opts = { desc = "Open a Table of Contents" } }, + { "gO", "Neorg toc split", opts = { desc = "[neorg] Open a Table of Contents" } }, }, }, { silent = true, diff --git a/lua/neorg/modules/core/keybinds/module.lua b/lua/neorg/modules/core/keybinds/module.lua index 2b6ff515f..bd9a0af92 100644 --- a/lua/neorg/modules/core/keybinds/module.lua +++ b/lua/neorg/modules/core/keybinds/module.lua @@ -228,7 +228,13 @@ module.public = { bound_keys[neorg_mode][mode][key] = bound_keys[neorg_mode][mode][key] and nil end, - remap = function(neorg_mode, mode, key, new_rhs) + --- Remaps a key to a specific Neorg mode + ---@param neorg_mode string #The Neorg mode to bind to + ---@param mode string #The Neovim mode to bind to, e.g. `n` or `i` etc. + ---@param key string #The lhs value from `:h vim.keymap.set` + ---@param new_rhs string|function #The rhs value from `:h vim.keymap.set` + ---@param opts table #The table value from `:h vim.keymap.set` + remap = function(neorg_mode, mode, key, new_rhs, opts) if neorg_mode ~= "all" and current_mode ~= neorg_mode then return end @@ -237,12 +243,24 @@ module.public = { bound_keys[neorg_mode][mode] = bound_keys[neorg_mode][mode] or {} bound_keys[neorg_mode][mode][key] = bound_keys[neorg_mode][mode][key] or {} - local opts = bound_keys[neorg_mode][mode][key].opts - - payload.map(neorg_mode, mode, key, new_rhs, opts) + payload.map( + neorg_mode, + mode, + key, + new_rhs, + vim.tbl_deep_extend("force", bound_keys[neorg_mode][mode][key].opts or {}, opts or {}) + ) end, - remap_event = function(neorg_mode, mode, key, new_event) + --- Remaps a key to a specific Neorg keybind. + -- `remap()` binds to any rhs value, whilst `remap_event()` is essentially a wrapper + -- for Neorg keybind `neorg_mode` `expr` + ---@param neorg_mode string #The Neorg mode to bind to + ---@param mode string #The Neovim mode to bind to, e.g. `n` or `i` etc. + ---@param key string #The lhs value from `:h vim.keymap.set` + ---@param new_event string #The Neorg event to bind to (e.g. `core.dirman.new.note`) + ---@param opts table #The table value from `:h vim.keymap.set` + remap_event = function(neorg_mode, mode, key, new_event, opts) if neorg_mode ~= "all" and current_mode ~= neorg_mode then return end @@ -251,14 +269,12 @@ module.public = { bound_keys[neorg_mode][mode] = bound_keys[neorg_mode][mode] or {} bound_keys[neorg_mode][mode][key] = bound_keys[neorg_mode][mode][key] or {} - local opts = bound_keys[neorg_mode][mode][key].opts - payload.map( neorg_mode, mode, key, "Neorg keybind " .. neorg_mode .. " " .. new_event .. "", - opts + vim.tbl_deep_extend("force", bound_keys[neorg_mode][mode][key].opts or {}, opts or {}) ) end, @@ -368,7 +384,7 @@ module.public = { -- Broadcast our event with the desired payload! modules.broadcast_event( - modules.create_event(module, "core.keybinds.events.enable_keybinds", payload), + assert(modules.create_event(module, "core.keybinds.events.enable_keybinds", payload)), function() for neorg_mode, neovim_modes in pairs(bound_keys) do if neorg_mode == "all" or neorg_mode == current_mode then @@ -444,10 +460,12 @@ module.on_event = function(event) -- If it is defined then broadcast the event if module.events.defined[keybind_event_path] then modules.broadcast_event( - modules.create_event( - module, - "core.keybinds.events." .. keybind_event_path, - vim.list_slice(event.content, 3) + assert( + modules.create_event( + module, + "core.keybinds.events." .. keybind_event_path, + vim.list_slice(event.content, 3) + ) ) ) else -- Otherwise throw an error diff --git a/lua/neorg/modules/core/latex/renderer/module.lua b/lua/neorg/modules/core/latex/renderer/module.lua index 14b75c265..18c329736 100644 --- a/lua/neorg/modules/core/latex/renderer/module.lua +++ b/lua/neorg/modules/core/latex/renderer/module.lua @@ -6,6 +6,9 @@ assert(vim.re ~= nil, "Neovim 0.10.0+ is required to run the `core.renderer.late module.setup = function() return { + wants = { + "core.integrations.image", + }, requires = { "core.integrations.treesitter", "core.autocommands", @@ -200,7 +203,7 @@ module.on_event = function(event) return end - return event_handlers[event.type](event) + return event_handlers[event.type]() end module.events.subscribed = { diff --git a/lua/neorg/modules/core/mode/module.lua b/lua/neorg/modules/core/mode/module.lua index 686016ee5..4647ca102 100644 --- a/lua/neorg/modules/core/mode/module.lua +++ b/lua/neorg/modules/core/mode/module.lua @@ -67,10 +67,12 @@ module.public = { -- Broadcast the mode_created event modules.broadcast_event( - modules.create_event( - module, - "core.mode.events.mode_created", - { current = module.config.public.current_mode, new = mode_name } + assert( + modules.create_event( + module, + "core.mode.events.mode_created", + { current = module.config.public.current_mode, new = mode_name } + ) ) ) @@ -105,10 +107,12 @@ module.public = { -- Broadcast the mode_set event to all subscribed modules modules.broadcast_event( - modules.create_event( - module, - "core.mode.events.mode_set", - { current = module.config.public.previous_mode, new = mode_name } + assert( + modules.create_event( + module, + "core.mode.events.mode_set", + { current = module.config.public.previous_mode, new = mode_name } + ) ) ) end, diff --git a/lua/neorg/modules/core/neorgcmd/module.lua b/lua/neorg/modules/core/neorgcmd/module.lua index 34f8d45bc..569ed4130 100644 --- a/lua/neorg/modules/core/neorgcmd/module.lua +++ b/lua/neorg/modules/core/neorgcmd/module.lua @@ -91,7 +91,7 @@ module.examples = { end, } -module.load = function() ---@diagnostic disable-line -- TODO: type error workaround Duplicate field `load` (L26) +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, { @@ -284,10 +284,12 @@ module.private = { end modules.broadcast_event( - modules.create_event( - module, - table.concat({ "core.neorgcmd.events.", ref.name }), - vim.list_slice(args, argument_index + 1) + assert( + modules.create_event( + module, + table.concat({ "core.neorgcmd.events.", ref.name }), + vim.list_slice(args, argument_index + 1) + ) ) ) end, diff --git a/lua/neorg/modules/core/promo/module.lua b/lua/neorg/modules/core/promo/module.lua index 5c7f4ec8c..2bab77dfb 100644 --- a/lua/neorg/modules/core/promo/module.lua +++ b/lua/neorg/modules/core/promo/module.lua @@ -324,10 +324,12 @@ module.on_event = neorg.utils.wrap_dotrepeat(function(event) if modules.loaded_modules["core.concealer"] then modules.broadcast_event( - modules.create_event( - modules.loaded_modules["core.concealer"], - "core.concealer.events.update_region", - { start = start_pos[1] - 1, ["end"] = end_pos[1] + 2 } + assert( + modules.create_event( + modules.loaded_modules["core.concealer"], + "core.concealer.events.update_region", + { start = start_pos[1] - 1, ["end"] = end_pos[1] + 2 } + ) ) ) end @@ -341,10 +343,12 @@ module.on_event = neorg.utils.wrap_dotrepeat(function(event) if modules.loaded_modules["core.concealer"] then modules.broadcast_event( - modules.create_event( - modules.loaded_modules["core.concealer"], - "core.concealer.events.update_region", - { start = start_pos[1] - 1, ["end"] = end_pos[1] + 2 } + assert( + modules.create_event( + modules.loaded_modules["core.concealer"], + "core.concealer.events.update_region", + { start = start_pos[1] - 1, ["end"] = end_pos[1] + 2 } + ) ) ) end diff --git a/lua/neorg/modules/core/qol/toc/module.lua b/lua/neorg/modules/core/qol/toc/module.lua index 94b34b1c9..fc2efb91a 100644 --- a/lua/neorg/modules/core/qol/toc/module.lua +++ b/lua/neorg/modules/core/qol/toc/module.lua @@ -4,7 +4,6 @@ description: The TOC module geneates 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`. @@ -47,8 +46,53 @@ module.config.public = { -- If `true`, will close the Table of Contents after an entry in the table -- is picked. close_after_use = false, + + -- If `true`, the width of the Table of Contents window will automatically + -- fit its longest line + fit_width = true, + + -- If `true`, `cursurline` will be enabled (highlighted) in the ToC window, + -- and the cursor position between ToC and content window will be synchronized. + sync_cursorline = 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 + module.public = { parse_toc_macro = function(buffer) local toc, toc_name = false, nil @@ -103,6 +147,7 @@ module.public = { else prefix = nil end + title = nil elseif capture == "title" then title = node end @@ -136,103 +181,199 @@ module.public = { return qflist_data end, - update_toc = function(namespace, toc_title, original_buffer, original_window, ui_buffer, ui_window) - vim.api.nvim_buf_clear_namespace(original_buffer, namespace, 0, -1) + -- 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) - table.insert(toc_title, "") - vim.api.nvim_buf_set_lines(ui_buffer, 0, -1, true, toc_title) - local offset = vim.api.nvim_buf_line_count(ui_buffer) - - local prefix, title - local extmarks = {} + 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 success = module.required["core.integrations.treesitter"].execute_query( - [[ - (_ - . - (_) @prefix - (detached_modifier_extension - (todo_item_cancelled))? @cancelled - title: (paragraph_segment) @title) - ]], - function(query, id, node) - local capture = query.captures[id] + local start_lines = ui_data.start_lines + assert(start_lines) - if capture == "prefix" then - if node:type():match("_prefix$") then - prefix = node - else - prefix = nil - end - elseif capture == "title" then - title = node - elseif capture == "cancelled" then - prefix = nil - end + 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, - if prefix and title then - local _, column = title:start() - table.insert( - extmarks, - vim.api.nvim_buf_set_extmark(original_buffer, namespace, (prefix:start()), column, {}) - ) + update_toc = function(toc_title, ui_data, norg_buffer) + local ui_buffer = ui_data.buffer + ui_data.norg_buffer = norg_buffer - local prefix_text = - module.required["core.integrations.treesitter"].get_node_text(prefix, original_buffer) - local title_text = - vim.trim(module.required["core.integrations.treesitter"].get_node_text(title, original_buffer)) + vim.bo[ui_buffer].modifiable = true + vim.api.nvim_buf_clear_namespace(norg_buffer, toc_namespace, 0, -1) - 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(toc_title, "") + vim.api.nvim_buf_set_lines(ui_buffer, 0, -1, true, toc_title) - vim.api.nvim_buf_set_lines( - ui_buffer, - -1, - -1, - true, - { table.concat({ "• {", prefix_text, title_text, "}" }) } - ) + local norg_data = {} + data_of_norg_buf[norg_buffer] = norg_data - prefix, title = nil, nil - end - end, - original_buffer - ) + local extmarks = {} + norg_data.extmarks = extmarks - if not success then + local offset = vim.api.nvim_buf_line_count(ui_buffer) + local start_lines = { offset = offset } + ui_data.start_lines = start_lines + + 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 curline = vim.api.nvim_win_get_cursor(ui_window)[1] - local extmark_lookup = extmarks[curline - offset] - - if not extmark_lookup then + local location = get_target_location_under_cursor(ui_data) + if not location then return end - local location = vim.api.nvim_buf_get_extmark_by_id(original_buffer, namespace, extmark_lookup, {}) - - vim.api.nvim_set_current_win(original_window) - vim.api.nvim_set_current_buf(original_buffer) - vim.api.nvim_win_set_cursor(original_window, { location[1] + 1, location[2] }) + local norg_window = vim.fn.bufwinid(norg_buffer) + vim.api.nvim_set_current_win(norg_window) + vim.api.nvim_set_current_buf(norg_buffer) + 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, } +local function get_max_virtcol() + local n_line = vim.fn.line("$") + local result = 1 + for i = 1, n_line do + -- FIXME: for neovim <=0.9.*, virtcol() doesn't accept winid argument + result = math.max(result, vim.fn.virtcol({ i, "$" })) + 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 + +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 + +local function create_ui(tabpage, mode) + 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), { ft = "norg" }, mode) + + 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 + + if module.config.public.sync_cursorline then + ui_wo.cursorline = true + end + + local ui_data = { + buffer = ui_buffer, + tabpage = tabpage, + } + + ui_data_of_tabpage[tabpage] = ui_data + + return ui_data +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) @@ -249,66 +390,122 @@ module.on_event = function(event) return end - -- FIXME(vhyrro): When the buffer already exists then simply refresh the buffer - -- instead of erroring out. - local namespace = vim.api.nvim_create_namespace("neorg/toc") - local buffer, window = - module.required["core.ui"].create_vsplit("toc", { ft = "norg" }, (event.content[1] or "left") == "left") + local tabpage = vim.api.nvim_win_get_tabpage(vim.fn.bufwinid(norg_buffer)) + if ui_data_of_tabpage[tabpage] then + module.public.update_toc(toc_title, ui_data_of_tabpage[tabpage], norg_buffer) + return + end + + local ui_data = ui_data_of_tabpage[tabpage] or create_ui(tabpage, (event.content[1] or "left") == "left") - vim.api.nvim_win_set_option(window, "scrolloff", 999) - vim.api.nvim_win_set_option(window, "conceallevel", 0) - module.public.update_toc(namespace, toc_title, event.buffer, event.window, buffer, window) + module.public.update_toc(toc_title, ui_data_of_tabpage[tabpage], norg_buffer) + + if module.config.public.fit_width then + local max_virtcol_1bex = get_max_virtcol() + local current_winwidth = vim.fn.winwidth(vim.fn.bufwinid(ui_data.buffer)) + local new_winwidth = math.min(current_winwidth, math.max(30, max_virtcol_1bex - 1)) + vim.cmd(("vertical resize %d"):format(new_winwidth + 1)) -- +1 for margin + end local close_buffer_callback = function() - -- Check if buffer exists before deleting it - if vim.api.nvim_buf_is_loaded(buffer) then - vim.api.nvim_buf_delete(buffer, { force = true }) + -- 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.api.nvim_buf_set_keymap(buffer, "n", "q", "", { + vim.api.nvim_buf_set_keymap(ui_data.buffer, "n", "q", "", { callback = close_buffer_callback, }) vim.api.nvim_create_autocmd("WinClosed", { - buffer = buffer, - once = true, - callback = close_buffer_callback, + pattern = "*", + callback = function(ev) + if ev.buf == ui_data.buffer then + close_buffer_callback() + end + end, }) - do - local previous_buffer, previous_window = event.buffer, event.window + 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("BufWritePost", { - pattern = "*.norg", - callback = function() - if not vim.api.nvim_buf_is_valid(buffer) or not vim.api.nvim_buf_is_loaded(buffer) then - return true + 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 - toc_title = vim.split(module.public.parse_toc_macro(previous_buffer) or "Table of Contents", "\n") - module.public.update_toc(namespace, toc_title, previous_buffer, previous_window, buffer, window) + -- 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, }) - vim.api.nvim_create_autocmd("BufEnter", { + -- Sync cursor: content -> ToC + vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { pattern = "*.norg", - callback = function() - if not vim.api.nvim_buf_is_valid(buffer) or not vim.api.nvim_buf_is_loaded(buffer) then - return true + callback = unlisten_if_closed(function(buf, ui) + if buf ~= ui.norg_buffer then + return end - local buf = vim.api.nvim_get_current_buf() - - if buf == buffer or previous_buffer == buf then + if not data_of_norg_buf[buf] then + -- toc not yet created because BufEnter is not yet triggered return end - previous_buffer, previous_window = buf, vim.api.nvim_get_current_win() + module.public.update_cursor(ui) + end), + }) - toc_title = vim.split(module.public.parse_toc_macro(buf) or "Table of Contents", "\n") - module.public.update_toc(namespace, toc_title, buf, previous_window, buffer, window) - 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 end diff --git a/lua/neorg/modules/core/summary/module.lua b/lua/neorg/modules/core/summary/module.lua index 7438f6f06..5c65a2d57 100644 --- a/lua/neorg/modules/core/summary/module.lua +++ b/lua/neorg/modules/core/summary/module.lua @@ -45,16 +45,14 @@ module.load = function() end) local ts = module.required["core.integrations.treesitter"] - module.config.public.strategy = lib.match(module.config.public.strategy)({ - default = function() - -- 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 heading_query then - -- allow second level headings, just in case - local heading_query_string = [[ + -- 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 heading_query then + -- allow second level headings, just in case + local heading_query_string = [[ [ (heading1 title: (paragraph_segment) @next-segment @@ -64,23 +62,25 @@ module.load = function() ) ] ]] - 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 + 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 + 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() @@ -167,7 +167,8 @@ module.load = function() table.insert( result, table.concat({ - " - {:$", + string.rep(" ", heading_level), + " - {:$", datapoint.norgname, ":}[", lib.title(datapoint.title), @@ -184,6 +185,64 @@ module.load = function() 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, string.len(ws_root) + 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 @@ -193,7 +252,8 @@ module.config.public = { -- 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. - ---@type string|fun(files: string[], ws_root: string, heading_level: number?, include_categories: string[]?): string[]? + -- - "by_path" - Similar to "default" but uses the capitalized name of the folder containing a *.norg file as category. + -- ---@type string|fun(files: string[], ws_root: string, heading_level: number?, include_categories: string[]?): string[]? strategy = "default", } diff --git a/lua/neorg/modules/core/syntax/module.lua b/lua/neorg/modules/core/syntax/module.lua index 87e1284fb..ab0d90d00 100644 --- a/lua/neorg/modules/core/syntax/module.lua +++ b/lua/neorg/modules/core/syntax/module.lua @@ -225,7 +225,11 @@ module.public = { module.public.remove_syntax(group, snip) end - local ok, result = pcall(vim.api.nvim_exec, has_syntax, true) ---@diagnostic disable-line -- TODO: type error workaround : `nvim_exec` not found + --- @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 @@ -250,7 +254,7 @@ module.public = { then -- absorb all syntax stuff -- potentially needs to be expanded upon as bad values come in - local is_keyword = vim.api.nvim_buf_get_option(buf, "iskeyword") + local is_keyword = vim.bo[buf].iskeyword local current_syntax = "" local foldmethod = vim.o.foldmethod local foldexpr = vim.o.foldexpr @@ -272,6 +276,7 @@ module.public = { 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 @@ -280,14 +285,23 @@ module.public = { false ) end - file = file[1] ---@diagnostic disable-line -- TODO: type error workaround + + 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=(.+)" - local _, found_cluster = - pcall(vim.api.nvim_exec, string.format("syntax list @%s", group), true) ---@diagnostic disable-line -- TODO: type error workaround : `nvim_exec` not found + --- @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 @@ -311,7 +325,7 @@ module.public = { end -- reset some values after including - vim.api.nvim_buf_set_option(buf, "iskeyword", is_keyword) + vim.bo[buf].iskeyword = is_keyword if current_syntax ~= "" or current_syntax ~= nil then vim.b.current_syntax = current_syntax else @@ -319,7 +333,9 @@ module.public = { end has_syntax = string.format("syntax list %s", snip) - _, result = pcall(vim.api.nvim_exec, has_syntax, true) ---@diagnostic disable-line -- TODO: type error workaround : `nvim_exec` not found + --- @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 --[[ @@ -533,7 +549,7 @@ module.on_event = function(event) local timer = vim.loop.new_timer() - timer:start( ---@diagnostic disable-line -- TODO: type error workaround + timer:start( module.config.public.performance.timeout, module.config.public.performance.interval, vim.schedule_wrap(function() @@ -542,7 +558,7 @@ module.on_event = function(event) 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() ---@diagnostic disable-line -- TODO: type error workaround + timer:stop() return end diff --git a/lua/neorg/modules/core/tangle/module.lua b/lua/neorg/modules/core/tangle/module.lua index 13dbffaa8..20e190692 100644 --- a/lua/neorg/modules/core/tangle/module.lua +++ b/lua/neorg/modules/core/tangle/module.lua @@ -416,12 +416,19 @@ module.public = { end, } +module.config.public = { + -- Notify when there is nothing to tangle (INFO) or when the content is empty (WARN). + report_on_empty = true, +} + 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 - utils.notify("Nothing to tangle!", vim.log.levels.WARN) + if module.config.public.report_on_empty then + utils.notify("Nothing to tangle!", vim.log.levels.INFO) + end return end @@ -429,27 +436,27 @@ module.on_event = function(event) local tangled_count = 0 for file, content in pairs(tangles) do - vim.loop.fs_open( - vim.fn.expand(file), ---@diagnostic disable-line -- TODO: type error workaround - "w", - 438, - function(err, fd) - file_count = file_count - 1 - assert(not err, lib.lazy_string_concat("Failed to open file '", file, "' for tangling: ", err)) - - vim.loop.fs_write( - fd, ---@diagnostic disable-line -- TODO: type error workaround - table.concat(content, "\n"), - 0, - function(werr) - assert( - not werr, - lib.lazy_string_concat("Failed to write to file '", file, "' for tangling: ", werr) - ) - end - ) + -- resolve upward relative path like `../../` + local relative_file, upward_count = string.gsub(file, "%.%.[\\/]", "") + if upward_count > 0 then + local base_dir = vim.fn.expand("%:p" .. string.rep(":h", upward_count + 1)) --[[@as string]] + file = vim.fs.joinpath(base_dir, relative_file) + end + + vim.loop.fs_open(vim.fn.expand(file) --[[@as string]], "w", 438, function(err, fd) + assert(not err and fd, lib.lazy_string_concat("Failed to open file '", file, "' for tangling: ", err)) + + 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), vim.log.levels.WARN) + end) + end + vim.loop.fs_write(fd, write_content, 0, function(werr) + assert(not werr, lib.lazy_string_concat("Failed to write to '", file, "' for tangling: ", werr)) tangled_count = tangled_count + 1 + file_count = file_count - 1 if file_count == 0 then vim.schedule( lib.wrap( @@ -462,8 +469,8 @@ module.on_event = function(event) ) ) end - end - ) + end) + end) end end end diff --git a/lua/neorg/modules/core/upgrade/module.lua b/lua/neorg/modules/core/upgrade/module.lua index 8cb172de7..1627f5ace 100644 --- a/lua/neorg/modules/core/upgrade/module.lua +++ b/lua/neorg/modules/core/upgrade/module.lua @@ -45,6 +45,8 @@ module.load = function() modules.await("core.neorgcmd", function(neorgcmd) neorgcmd.add_commands_from_table({ upgrade = { + min_args = 1, + max_args = 1, subcommands = { ["current-file"] = { name = "core.upgrade.current-file",