diff --git a/README.md b/README.md index 9bbf3be..d01e55f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Global-note.nvim + It's a simple Neovim plugin that provides a global note in a float window. It could also provide other global, project local, file local notes (if it's required). @@ -16,6 +17,7 @@ vim.keymap.set("n", "n", global_note.toggle_note, { ``` ### Options +
click All options here are default: @@ -33,10 +35,6 @@ All options here are default: -- string or fun(): string title = "Global note", - -- Ex command name. - -- string - command_name = "GlobalNote", - -- A nvim_open_win config to show float window. -- table or fun(): table window_config = function() @@ -79,6 +77,7 @@ All options here are default: ### Additional presets + You can use additional presets to have other global notes, project local notes, file local notes or anything you can come up with. @@ -97,14 +96,12 @@ require("global-note").setup({ projects = { filename = "projects-to-do.md", title = "List of projects", - command_name = "ProjectsNote", -- All not specified options are used from the root. }, food = { filename = "want-to-eat.md", title = "List of food", - command_name = "FoodNote", -- All not specified options are used from the root. }, }, @@ -115,19 +112,164 @@ require("global-note").toggle_note() require("global-note").toggle_note("projects") require("global-note").toggle_note("food") --- Commands to toggle notes (they are generated by command_name field): --- :GlobalNote -- by default --- :ProjectsNote --- :FoodNote +-- Commands to toggle notes (they are generated field name): +-- :GlobalNote -- default global note +-- :GlobalNote projects +-- :GlobalNote food + +-- Command to pick a note: +-- :GlobalNotePick +``` + +### Dynamically managing notes with scopes + +A note can have a **global** or project **local** scope, and can be created dynamically without having to predefine them. + +A local note is a note that is specific to a project. And they are stored as `//.md`. + +#### Create note + +To create a note, use the following function: + +```lua +require('global-note').create_note(opts) +``` + +`opts?`: table, with the following fields: + +- `local_scope?: boolean` if true, create a local note, otherwise, create a global note + +
Examples + +```lua +-- create a global note +require('global-note').create_note() +--or +require('global-note').create_note({local_scope=false}) + +-- create a local note +require('global-note').create_note({local_scope=true}) +``` + +
+ +#### Toggle note + +To open a note, use the following function: + +```lua +require('global-note').toggle_note(preset_name, local_scope) +``` + +`preset_name?: string` if not provided, open the default global note + +`local_scope?: boolean` if true, look for a local note with name ``, otherwise, look for a global note with name `` + +
Examples + +```lua +-- toggle the default global note +require('global-note').toggle_note() + +-- toggle the global note `projects` +require('global-note').toggle_note('projects') +--or +require('global-note').toggle_note('projects', {local_scope=false}) + +-- toggle the local note `projects` +require('global-note').toggle_note('projects', {local_scope=true}) +``` + +
+ +#### Pick note + +To pick a note, use the following function: + +```lua +require('global-note').pick_note(opts) +``` + +`opts?`: table, with the following fields: + +- `local_scope: boolean` if true, pick a local note, otherwise, pick a global note + +
Examples + +```lua +-- pick a global note +require('global-note').pick_note() +--or +require('global-note').pick_note({local_scope=false}) + +-- pick a local note +require('global-note').pick_note({local_scope=true}) +``` + +
+ +#### Commands + +`:GlobalNoteCreate `: create a note, `` is optional, if it is `local`, create a local note, otherwise global note + +
Examples + +```lua +-- create a global note +-- :GlobalNoteCreate +-- or +-- :GlobalNoteCreate global + +-- create a local note +-- :GlobalNoteCreate local +``` + +
+ +`:GlobalNote `: open global note, if `` is not provided, open the default global note, otherwise, open the note with name `` and the given scope + +
Examples + +```lua +-- open the default global note +-- :GlobalNote +-- or +-- :GlobalNote global + +-- open the global note `projects` +-- :GlobalNote projects +-- or +-- :GlobalNote projects global + +-- open the local note `projects` +-- :GlobalNote projects local ``` +
+ +`:GlobalNotePick `: pick a note to open, `` is optional, if it is `local`, select a local note, otherwise global note + +
Examples + +```lua +-- pick a global note +-- :GlobalNotePick +-- or +-- :GlobalNotePick global + +-- pick a local note +-- :GlobalNotePick local +``` + +
+ --- -### Configuration usecases: +### Configuration usecases :warning: **Usecases require some functions from below!** @@ -138,7 +280,6 @@ local global_note = require("global-note") global_note.setup({ additional_presets = { project_local = { - command_name = "ProjectNote", filename = function() return get_project_name() .. ".md" @@ -165,8 +306,6 @@ local global_note = require("global-note") global_note.setup({ additional_presets = { git_branch_local = { - command_name = "GitBranchNote", - directory = function() return vim.fn.stdpath("data") .. "/global-note/" .. get_project_name() end, diff --git a/lua/global-note/init.lua b/lua/global-note/init.lua index 38b3cff..17be0b6 100644 --- a/lua/global-note/init.lua +++ b/lua/global-note/init.lua @@ -1,5 +1,6 @@ local preset = require("global-note.preset") local utils = require("global-note.utils") +local loop = vim.loop local M = { _inited = false, @@ -9,7 +10,6 @@ local M = { ---@diagnostic disable-next-line: param-type-mismatch directory = utils.joinpath(vim.fn.stdpath("data"), "global-note"), title = "Global note", - command_name = "GlobalNote", window_config = function() local window_height = vim.api.nvim_list_uis()[1].height local window_width = vim.api.nvim_list_uis()[1].width @@ -33,7 +33,6 @@ local M = { ---@field filename? string|fun(): string? Filename of the note. ---@field directory? string|fun(): string? Directory to keep notes. ---@field title? string|fun(): string? Floating window title. ----@field command_name? string Ex command name. ---@field window_config? table|fun(): table A nvim_open_win config. ---@field post_open? fun(buffer_id: number, window_id: number) It's called after the window creation. ---@field autosave? boolean Whether to use autosave. @@ -41,48 +40,250 @@ local M = { ---@class GlobalNote_UserConfig: GlobalNote_UserPreset ---@field additional_presets? { [string]: GlobalNote_UserPreset } +local _default_preset = nil +local function get_default_preset() + if _default_preset == nil then + if not M._inited then + M.setup() + end + + local options = vim.deepcopy(M.options) + + options.additional_presets = nil + options.name = "" + + _default_preset = preset.new(options) + end + return _default_preset +end + +local _presets = nil +local function get_presets() + if _presets == nil then + if not M._inited then + M.setup() + end + + _presets = {} + + local disk_notes = {} + + local dir = M.options.directory + + if not loop.fs_stat(dir) then + return _presets + end + + local handle = loop.fs_scandir(dir) + if handle then + while true do + local name, type = loop.fs_scandir_next(handle) + if not name then + break + end + if type == "file" then + local base_name = vim.fn.fnamemodify(name, ":t:r") + local is_global = (base_name .. ".md") == M.options.filename + + disk_notes[base_name] = { + filename = base_name .. ".md", + title = is_global and M.options.title or base_name, + } + end + end + else + print("Failed to open directory: " .. dir) + end + + local additional_presets = + vim.tbl_deep_extend("force", {}, disk_notes, M.options.additional_presets) + + for key, value in pairs(additional_presets) do + local preset_options = + vim.tbl_extend("force", get_default_preset(), value) + preset_options.name = key + _presets[key] = preset.new(preset_options) + end + end + return _presets +end + +local cwd = loop.cwd() +local cwd_basename = vim.fn.fnamemodify(cwd, ":t:r") +local path_separator = package.config:sub(1, 1) +local local_dir = nil + +local _local_presets = nil +local function get_local_presets() + if _local_presets == nil then + if not M._inited then + M.setup() + end + + _local_presets = {} + local disk_notes = {} + + if not loop.fs_stat(local_dir) then + return _local_presets + end + + local handle = loop.fs_scandir(local_dir) + if handle then + while true do + local name, type = loop.fs_scandir_next(handle) + if not name then + break + end + if type == "file" then + local base_name = vim.fn.fnamemodify(name, ":t:r") + + disk_notes[base_name] = { + filename = base_name .. ".md", + title = base_name, + directory = local_dir, + } + end + end + else + print("Failed to open directory: " .. local_dir) + end + + for key, value in pairs(disk_notes) do + local preset_options = + vim.tbl_extend("force", get_default_preset(), value) + preset_options.name = key + _local_presets[key] = preset.new(preset_options) + end + end + return _local_presets +end + ---@param options? GlobalNote_UserConfig M.setup = function(options) local user_options = vim.deepcopy(options or {}) user_options.additional_presets = user_options.additional_presets or {} - -- Setup default preset - local default_preset_options = + M.options = vim.tbl_extend("force", M._default_preset_default_values, user_options) - default_preset_options.additional_presets = nil - default_preset_options.name = "" - M._default_preset = preset.new(default_preset_options) - - -- Setup additional presets - M._presets = {} - for key, value in pairs(user_options.additional_presets) do - local preset_options = vim.tbl_extend("force", M._default_preset, value) - preset_options.command_name = value.command_name - preset_options.name = key - M._presets[key] = preset.new(preset_options) - end + + local_dir = M.options.directory .. path_separator .. cwd_basename + + vim.api.nvim_create_user_command("GlobalNote", function(opts) + local note_name = opts.fargs[1] or "" + local scope = opts.fargs[2] or "global" + local local_scope = scope == "local" + + M.toggle_note(note_name, local_scope) + end, { + nargs = "*", + desc = "Toggle global note", + }) + + vim.api.nvim_create_user_command("GlobalNoteCreate", function(opts) + local is_local = opts.args == "local" + M.create_note({ local_scope = is_local }) + end, { + nargs = "?", + desc = "Create new note", + complete = function(arglead, cmdline, cursorpos) + return { "local", "global" } + end, + }) + + vim.api.nvim_create_user_command("GlobalNotePick", function(opts) + local is_local = opts.args == "local" + M.pick_note({ local_scope = is_local }) + end, { + nargs = "?", + desc = "Create new note", + complete = function(arglead, cmdline, cursorpos) + return { "local", "global" } + end, + }) M._inited = true end ---Opens or closes a note in a floating window. ---@param preset_name? string preset to use. If it's not set, use default preset. -M.toggle_note = function(preset_name) - if not M._inited then - M.setup() - end - - local p = M._default_preset +---@param local_scope? boolean whether to use local scope. +M.toggle_note = function(preset_name, local_scope) + local p = get_default_preset() if preset_name ~= nil and preset_name ~= "" then - p = M._presets[preset_name] + p = (local_scope and get_local_presets() or get_presets())[preset_name] if p == nil then - local template = "The preset with the name %s doesn't eixst" + local template = "The preset with the name %s doesn't exist" local message = string.format(template, preset_name) - error(message) + vim.notify(message, vim.log.levels.WARN) end end p:toggle() end +---Creates a new note. +---@param opts? table # A table with a key 'local_scope' set to true if the note should be local. +M.create_note = function(opts) + local local_scope = opts ~= nil and opts.local_scope + + local name = vim.fn.input("Note name: ") + + if name == "" then + return + end + + local dir = local_scope and local_dir or M.options.directory + + if not loop.fs_stat(dir) then + loop.fs_mkdir(dir, 493) + end + + local destination = dir .. name .. ".md" + + if loop.fs_stat(destination) then + local template = "Another note with the name %s already exists" + local message = string.format(template, name) + vim.notify(message, vim.log.levels.WARN) + return + end + + local preset_options = vim.tbl_extend("force", get_default_preset(), { + filename = name .. ".md", + title = name, + directory = dir, + }) + preset_options.name = name + + if local_scope then + get_local_presets()[name] = preset.new(preset_options) + else + get_presets()[name] = preset.new(preset_options) + end + + M.toggle_note(name, local_scope) +end + +---Picks a note to open. +---@param opts? table # A table with a key 'local_scope' set to true if the note should be local. +M.pick_note = function(opts) + local local_scope = opts ~= nil and opts.local_scope + local presets = local_scope and get_local_presets() or get_presets() + local items = {} + + for key, _ in pairs(presets) do + table.insert(items, key) + end + + vim.ui.select(items, { + prompt = "Pick a note", + format_item = function(item) + return presets[item].title or item + end, + }, function(item) + if item then + M.toggle_note(item, local_scope) + end + end) +end + return M diff --git a/lua/global-note/preset.lua b/lua/global-note/preset.lua index aa5feb7..c621d2c 100644 --- a/lua/global-note/preset.lua +++ b/lua/global-note/preset.lua @@ -5,7 +5,6 @@ local utils = require("global-note.utils") ---@field filename string|fun(): string? Filename of the note. ---@field directory string|fun(): string? Directory to keep notes. ---@field title string|fun(): string? Floating window title. ----@field command_name? string Ex command name. ---@field window_config table|fun(): table A nvim_open_win config. ---@field post_open fun(buffer_id: number, window_id: number) It's called after the window creation. ---@field autosave boolean Whether to use autosave. @@ -37,10 +36,6 @@ local new = function(options) options.title, { "string", "function" }, }, - ["options.command_name"] = { - options.command_name, - { "string", "nil" }, - }, ["options.window_config"] = { options.window_config, { "table", "function" }, @@ -58,20 +53,6 @@ local new = function(options) ---@class GlobalNote_Preset local p = vim.deepcopy(options) - if type(p.command_name) == "string" and p.command_name ~= "" then - local desc = "Toggle note in a floating window" - if p.name ~= "" then - desc = string.format("Toggle %s note in a floating window", p.name) - end - - vim.api.nvim_create_user_command(p.command_name, function() - p:toggle() - end, { - nargs = 0, - desc = desc - }) - end - ---Expands preset fields to a finite values. ---If user can't produce a critical value then nil is returned. ---@return GlobalNote_ExpandedPreset? @@ -118,6 +99,8 @@ local new = function(options) end end + title = title or vim.fn.fnamemodify(filename, ":t") + if title ~= nil then window_config.title = title end diff --git a/lua/global-note/utils.lua b/lua/global-note/utils.lua index e595be6..e9c8a93 100644 --- a/lua/global-note/utils.lua +++ b/lua/global-note/utils.lua @@ -52,7 +52,7 @@ M.joinpath = function(...) return vim.fs.joinpath(...) end - return table.concat({ ... }, '/'):gsub('//+', '/') + return table.concat({ ... }, "/"):gsub("//+", "/") end return M