first commit

This commit is contained in:
Elflare
2025-07-05 16:39:53 +08:00
parent ed12516cd1
commit 7e5ba72b71
6 changed files with 812 additions and 1 deletions

269
README.md
View File

@@ -1 +1,270 @@
# memos.nvim
English | [简体中文](./README.md#memosnvim-简体中文)
A Neovim plugin to interact with [Memos](https://github.com/usememos/memos) right inside the editor. List, create, edit, and delete your memos without leaving Neovim.
## ✨ Features
- **List Memos**: View, search, and paginate through your memos in a floating window.
- **Create & Edit**: Create new memos or edit existing ones in a dedicated buffer with `markdown` filetype support.
- **Delete Memos**: Delete memos directly from the list.
- **Customizable**: Configure API endpoints, keymaps, and more.
## 📦 Installation
Requires [plenary.nvim](https://github.com/nvim-lua/plenary.nvim).
Install with [lazy.nvim](https://github.com/folke/lazy.nvim):
```lua
-- lua/plugins/memos.lua
return {
-- Memos.nvim: A plugin to interact with Memos from within Neovim
{
-- IMPORTANT: Replace with your actual GitHub username/repo
"your-github-username/memos.nvim",
-- Optional, but good for identification in Lazy UI
name = "memos.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
-- Load the plugin on these commands
cmd = { "Memos", "MemosCreate" },
config = function()
require("memos").setup({
-- REQUIRED: Your Memos host URL
host = "https://your-memos-host.com",
-- REQUIRED: Your Memos API token (Open API)
token = "your-super-secret-token",
-- Optional: Customize keymaps
keymaps = {
-- Keymaps for the editing/creating buffer
buffer = {
save = "<leader>ms", -- Save the current memo
},
-- Keymaps for the memo list window
list = {
add_memo = "a", -- Add a new memo
edit_memo = "<CR>", -- Edit selected memo
-- ... other keymaps can be configured here
}
},
})
end,
},
-- Plenary is a required dependency
{
"nvim-lua/plenary.nvim",
lazy = true,
},
}
```
## 🚀 Usage
### Commands
- `:Memos`: Opens a floating window to list and search your memos.
- `:MemosCreate`: Opens a new buffer to create a new memo.
- `:MemosSave`: (Available in the memo buffer) Saves the memo you are currently creating or editing.
### Default Keymaps
#### In the Memo List Window
| Key | Action |
| ------------------ | ------------------------------------ |
| `a` | Add a new memo |
| `d` or `dd` | Delete the selected memo |
| `<CR>` | Edit the selected memo |
| `<Tab>` | Edit the selected memo in a vsplit |
| `s` | Search your memos |
| `r` | Refresh the memo list |
| `.` | Load the next page of memos |
| `q` | Quit the list window |
#### In the Edit/Create Buffer
| Key | Action |
| ------------------ | ------------------------------------ |
| `<leader>ms` | Save the current memo |
## ⚙️ Configuration
You can override the default settings by passing a table to the `setup()` function.
```lua
require("memos").setup({
-- REQUIRED: Your Memos host URL
host = "https://your-memos-host.com",
-- REQUIRED: Your Memos API token (Open API)
token = "your-super-secret-token",
-- Number of memos to fetch per page
pageSize = 50,
-- Set to false or nil to disable a keymap
keymaps = {
-- Keymaps for the memo list window
list = {
add_memo = "a",
delete_memo = "d",
delete_memo_visual = "dd",
edit_memo = "<CR>",
vsplit_edit_memo = "<Tab>",
search_memos = "s",
refresh_list = "r",
next_page = ".",
quit = "q",
},
-- Keymaps for the editing/creating buffer
buffer = {
save = "<leader>ms",
},
},
})
```
---
# memos.nvim (简体中文)
[English](./README.md#memosnvim) | 简体中文
一个 Neovim 插件,让你在编辑器内部直接与 [Memos](https://github.com/usememos/memos) 进行交互。无需离开 Neovim 即可列表、创建、编辑和删除你的 memos。
## ✨ 功能
- **列表 Memos**: 在浮动窗口中查看、搜索和翻页你的 memos。
- **创建与编辑**: 在专用的、支持 `markdown` 文件类型的缓冲区中创建新 memo 或编辑现有 memo。
- **删除 Memos**: 直接从列表中删除 memo。
- **可定制**: 可配置 API 地址、快捷键等。
## 📦 安装
需要 [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) 插件。
使用 [lazy.nvim](https://github.com/folke/lazy.nvim) 安装:
```lua
-- lua/plugins/memos.lua
return {
-- Memos.nvim: 一个在 Neovim 中与 Memos 交互的插件
{
-- 重要: 请将这里替换为你的 GitHub 用户名/仓库名
"your-github-username/memos.nvim",
-- 可选,但在 Lazy 管理界面中易于识别
name = "memos.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
-- 在执行这些命令时加载插件
cmd = { "Memos", "MemosCreate" },
config = function()
require("memos").setup({
-- 必填: 你的 Memos 服务地址
host = "https://your-memos-host.com",
-- 必填: 你的 Memos API 令牌 (Open API)
token = "your-super-secret-token",
-- 可选: 自定义快捷键
keymaps = {
-- 编辑/创建窗口的快捷键
buffer = {
save = "<leader>ms", -- 保存当前 memo
},
-- memo 列表窗口的快捷键
list = {
add_memo = "a", -- 新增 memo
edit_memo = "<CR>", -- 编辑所选 memo
-- ... 其他快捷键也可以在这里配置
}
},
})
end,
},
-- Plenary 是一个必要的依赖
{
"nvim-lua/plenary.nvim",
lazy = true,
},
}
```
## 🚀 使用方法
### 命令
- `:Memos`: 打开一个浮动窗口,列出并搜索你的 memos。
- `:MemosCreate`: 打开一个新的缓冲区来创建 memo。
- `:MemosSave`: (在 memo 编辑缓冲区中可用) 保存你正在创建或编辑的 memo。
### 默认快捷键
#### 在 Memo 列表窗口中
| 按键 | 功能 |
| ------------------ | ---------------------------------- |
| `a` | 新增一个 memo |
| `d``dd` | 删除所选的 memo |
| `<CR>` | 编辑所选的 memo |
| `<Tab>` | 在垂直分屏中编辑所选的 memo |
| `s` | 搜索你的 memos |
| `r` | 刷新 memo 列表 |
| `.` | 加载下一页 memos |
| `q` | 退出列表窗口 |
#### 在编辑/创建缓冲区中
| 按键 | 功能 |
| ------------------ | ---------------------------------- |
| `<leader>ms` | 保存当前 memo |
## ⚙️ 配置
你可以通过向 `setup()` 函数传递一个 table 来覆盖默认设置。
```lua
require("memos").setup({
-- 必填: 你的 Memos 服务地址
host = "https://your-memos-host.com",
-- 必填: 你的 Memos API 令牌 (Open API)
token = "your-super-secret-token",
-- 每页获取的 memo 数量
pageSize = 50,
-- 设置为 false 或 nil 可以禁用某个快捷键
keymaps = {
-- memo 列表窗口的快捷键
list = {
add_memo = "a",
delete_memo = "d",
delete_memo_visual = "dd",
edit_memo = "<CR>",
vsplit_edit_memo = "<Tab>",
search_memos = "s",
refresh_list = "r",
next_page = ".",
quit = "q",
},
-- 编辑/创建窗口的快捷键
buffer = {
save = "<leader>ms",
},
},
})

133
doc/memos.nvim.txt Normal file
View File

@@ -0,0 +1,133 @@
*memos.nvim.txt* For Neovim | English and 简体中文
==============================================================================
CONTENTS *memos.nvim-contents*
1. About....................................|memos-about|
2. Features.................................|memos-features|
3. Installation.............................|memos-installation|
4. Usage....................................|memos-usage|
5. Configuration............................|memos-configuration|
==============================================================================
1. About *memos-about*
A Neovim plugin to interact with Memos (https://github.com/usememos/memos)
right inside the editor. List, create, edit, and delete your memos without
leaving Neovim.
一个 Neovim 插件,让你在编辑器内部直接与 Memos 进行交互。无需离开 Neovim
即可列表、创建、编辑和删除你的 memos。
==============================================================================
2. Features *memos-features*
- *List Memos*: View, search, and paginate through your memos in a floating
window.
- *Create & Edit*: Create new memos or edit existing ones in a dedicated
buffer with `markdown` filetype support.
- *Delete Memos*: Delete memos directly from the list.
- *Lazy Loading*: The plugin is loaded only when you run one of its commands.
- *Customizable*: Configure API endpoints, keymaps, and more.
==============================================================================
3. Installation *memos-installation*
Requires |plenary.nvim|.
Install with |lazy.nvim|:
>lua
-- lua/plugins/memos.lua
return {
{
"your-github-username/memos.nvim",
name = "memos.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
cmd = { "Memos", "MemosCreate" },
config = function()
require("memos").setup({
host = "https://your-memos-host.com",
token = "your-super-secret-token",
})
end,
},
{
"nvim-lua/plenary.nvim",
lazy = true,
},
}
<
==============================================================================
4. Usage *memos-usage*
COMMANDS *memos-commands*
*:Memos*
Opens a floating window to list and search your memos.
*:MemosCreate*
Opens a new buffer to create a new memo.
*:MemosSave*
(Available in the memo buffer) Saves the memo you are currently
creating or editing.
DEFAULT KEYMAPS *memos-keymaps*
*In the Memo List Window:*
| Key | Action |
|--------------------|--------------------------------------|
| `a` | Add a new memo |
| `d` or `dd` | Delete the selected memo |
| `<CR>` | Edit the selected memo |
| `<Tab>` | Edit the selected memo in a vsplit |
| `s` | Search your memos |
| `r` | Refresh the memo list |
| `.` | Load the next page of memos |
| `q` | Quit the list window |
*In the Edit/Create Buffer:*
| Key | Action |
|--------------------|--------------------------------------|
| `<leader>ms` | Save the current memo |
==============================================================================
5. Configuration *memos-configuration*
You can override the default settings by passing a table to the `setup()`
function.
Default configuration:
>lua
require("memos").setup({
-- REQUIRED: Your Memos host URL
host = "https://your-memos-host.com",
-- REQUIRED: Your Memos API token (Open API)
token = "your-super-secret-token",
-- Number of memos to fetch per page
pageSize = 50,
-- Set to false or nil to disable a keymap
keymaps = {
-- Keymaps for the memo list window
list = {
add_memo = "a",
delete_memo = "d",
delete_memo_visual = "dd",
edit_memo = "<CR>",
vsplit_edit_memo = "<Tab>",
search_memos = "s",
refresh_list = "r",
next_page = ".",
quit = "q",
},
-- Keymaps for the editing/creating buffer
buffer = {
save = "<leader>ms",
},
},
})
<
vim:tw=78:ts=8:ft=help:norl:

92
lua/memos/api.lua Normal file
View File

@@ -0,0 +1,92 @@
local Job = require('plenary.job')
local config = require('memos').config
local M = {}
local function run_curl(args, on_exit)
table.insert(args, '-H')
table.insert(args, 'Authorization: Bearer ' .. config.token)
Job:new({ command = 'curl', args = args, on_exit = on_exit }):start()
end
function M.get_current_user(callback)
run_curl({
'-s', '--fail', '-X', 'POST', config.host .. '/api/v1/auth/status',
}, function(job, return_val)
if return_val == 0 then
local result_string = table.concat(job:result(), "")
local data = vim.json.decode(result_string)
callback(data or nil)
else
vim.schedule(function() vim.notify("Failed to get user info.", vim.log.levels.ERROR) end)
callback(nil)
end
end)
end
function M.list_memos(parent, filter, pageSize, pageToken, callback)
local list_url = config.host .. '/api/v1/memos'
local params = {}
table.insert(params, "parent=" .. parent)
table.insert(params, "pageSize=" .. tostring(pageSize))
if pageToken and pageToken ~= "" then table.insert(params, "pageToken=" .. pageToken) end
if filter and filter ~= '' then
table.insert(params, 'filter=content.contains("' .. vim.fn.escape(filter, '\\"') .. '")')
end
list_url = list_url .. "?" .. table.concat(params, "&")
run_curl({
'-s', '--fail', '-X', 'GET', list_url,
}, function(job, return_val)
if return_val == 0 then
local result_string = table.concat(job:result(), "")
local data = vim.json.decode(result_string)
callback({ memos = data.memos or {}, nextPageToken = data.nextPageToken or "" })
else
vim.schedule(function() vim.notify("Failed to fetch memos.", vim.log.levels.ERROR) end)
callback(nil)
end
end)
end
-- 【修改】让 create_memo 的回调函数返回新创建的 memo 对象
function M.create_memo(content, callback)
local create_url = config.host .. '/api/v1/memos'
local json_data = vim.json.encode({ content = content })
run_curl({
'-s', '--fail', '-X', 'POST', create_url,
'-H', 'Content-Type: application/json',
'--data', json_data,
}, function(job, return_val)
if return_val == 0 then
local result_string = table.concat(job:result(), "")
local new_memo = vim.json.decode(result_string)
callback(new_memo)
else
callback(nil)
end
end)
end
function M.update_memo(memo_name, content, callback)
local update_url = config.host .. '/api/v1/' .. memo_name
local json_data = vim.json.encode({ content = content })
run_curl({
'-s', '--fail', '-X', 'PATCH', update_url,
'-H', 'Content-Type: application/json',
'--data', json_data,
}, function(job, return_val)
callback(return_val == 0)
end)
end
function M.delete_memo(memo_name, callback)
local delete_url = config.host .. '/api/v1/' .. memo_name
run_curl({
'-s', '--fail', '-X', 'DELETE', delete_url,
}, function(job, return_val)
callback(return_val == 0)
end)
end
return M

43
lua/memos/init.lua Normal file
View File

@@ -0,0 +1,43 @@
local M = {}
M.config = {
host = nil,
token = nil,
pageSize = 50,
keymaps = {
-- 在列表窗口中的快捷键
list = {
add_memo = "a",
delete_memo = "d",
delete_memo_visual = "dd", -- 和 d 功能一样,为了符合习惯
edit_memo = "<CR>",
vsplit_edit_memo = "<Tab>",
search_memos = "s",
refresh_list = "r",
next_page = ".",
quit = "q",
},
-- 在编辑和创建窗口中的快捷键
buffer = {
save = "<leader>ms",
},
},
}
function M.setup(opts)
-- 使用 "force" 策略,并将默认配置放在前面,用户的配置在后面
M.config = vim.tbl_deep_extend("force", M.config, opts or {})
if not M.config.host or not M.config.token then
vim.notify("Memos: `host` and `token` must be configured.", vim.log.levels.ERROR)
end
end
function M.create_memo()
require("memos.ui").create_memo_in_buffer()
end
function M.show_list()
require("memos.ui").show_memos_list()
end
return M

251
lua/memos/ui.lua Normal file
View File

@@ -0,0 +1,251 @@
local api = require('memos.api')
local config = require('memos').config
local M = {}
-- 存放状态的变量
local memos_cache = {}
local buf_id = nil
local current_page_token = nil
local current_user = nil
local current_filter = nil
-- ===================================================================
-- 首先定义所有会被其他函数调用的 local "辅助" 函数
-- ===================================================================
-- 渲染 Memos 列表到 buffer
local function render_memos(data, append)
vim.schedule(function()
if not buf_id or not vim.api.nvim_buf_is_valid(buf_id) then return end
local new_memos = data.memos or {}
current_page_token = data.nextPageToken or ""
if append then
memos_cache = vim.list_extend(memos_cache, new_memos)
else
memos_cache = new_memos
end
local lines = {}
local k = config.keymaps.list
if #memos_cache == 0 then
local help = string.format("No memos found. Press '%s' to refresh, '%s' to add, or '%s' to quit.",
k.refresh_list, k.add_memo, k.quit)
table.insert(lines, help)
else
for i, memo in ipairs(memos_cache) do
local first_line = memo.content:match("^[^\n]*")
local display_time = memo.displayTime:sub(1, 10)
table.insert(lines, string.format("%d. [%s] %s", i, display_time, first_line))
end
end
if current_page_token ~= "" then
table.insert(lines, "...")
table.insert(lines, string.format("(Press '%s' to load more)", k.next_page))
end
vim.api.nvim_buf_set_option(buf_id, 'modifiable', true)
vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, lines)
vim.api.nvim_buf_set_option(buf_id, 'modifiable', false)
end)
end
-- 设置编辑 buffer 的通用函数
local function setup_buffer_for_editing()
vim.bo.buftype = 'nofile'
vim.bo.bufhidden = 'wipe'
vim.bo.swapfile = false
vim.bo.buflisted = true
vim.bo.filetype = 'markdown'
local save_key_string = ""
if config.keymaps.buffer.save and config.keymaps.buffer.save ~= "" then
save_key_string = string.format(" or %s", config.keymaps.buffer.save)
end
if vim.b.memos_memo_name then
vim.notify("Editing memo. Use :MemosSave" .. save_key_string .. " to save.")
else
vim.notify("📝 New memo. Use :MemosSave" .. save_key_string .. " to create.")
end
vim.api.nvim_buf_create_user_command(0, 'MemosSave', 'lua require("memos.ui").save_or_create_dispatcher()', {})
if config.keymaps.buffer.save and config.keymaps.buffer.save ~= "" then
vim.api.nvim_buf_set_keymap(0, 'n', config.keymaps.buffer.save, '<Cmd>MemosSave<CR>', { noremap = true, silent = true })
end
end
-- 打开一个已存在的 Memo
local function open_memo_for_edit(memo, open_cmd)
vim.cmd(open_cmd)
local first_line = memo.content:match("^[^\n]*")
local buffer_name = "memos/" .. memo.name:gsub("memos/", "") .. "/" .. first_line:gsub("[/\\]", "_"):sub(1, 50) .. ".md"
vim.api.nvim_buf_set_name(0, buffer_name)
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(memo.content, '\n'))
vim.b.memos_memo_name = memo.name
setup_buffer_for_editing()
end
-- ===================================================================
-- 然后定义所有对外暴露的 M.xxx 函数,确保它们能访问到上面的 local 函数
-- ===================================================================
-- 【修正】将此函数移到 render_memos 定义之后,解决报错
function M.refresh_list_silently()
if not current_user or not current_user.name then return end
api.list_memos(current_user.name, current_filter, config.pageSize, nil, function(data)
render_memos(data, false)
end)
end
function M.save_or_create_dispatcher()
local memo_name = vim.b.memos_memo_name
local content = table.concat(vim.api.nvim_buf_get_lines(0, 0, -1, false), '\n')
if content == '' then
vim.notify("Memo is empty, not sending.", vim.log.levels.WARN)
return
end
if memo_name then
api.update_memo(memo_name, content, function(success)
if success then
vim.schedule(function() vim.notify("✅ Memo updated successfully!") end)
M.refresh_list_silently()
end
end)
else
api.create_memo(content, function(new_memo)
if new_memo and new_memo.name then
vim.schedule(function()
vim.notify("✅ Memo created successfully!")
vim.cmd('bdelete!') -- 强制关闭临时的 new_memo buffer
open_memo_for_edit(new_memo, 'enew') -- 用新 memo 的信息打开一个标准的编辑窗口
M.refresh_list_silently()
end)
else
vim.schedule(function() vim.notify("❌ Failed to create memo.", vim.log.levels.ERROR) end)
end
end)
end
end
function M.create_memo_in_buffer()
vim.cmd('enew')
vim.b.memos_memo_name = nil
vim.api.nvim_buf_set_name(0, "memos/new_memo.md")
setup_buffer_for_editing()
end
function M.show_memos_list(filter)
current_filter = filter
local should_create_buf = true
if buf_id and vim.api.nvim_buf_is_valid(buf_id) then
should_create_buf = false
else
buf_id = vim.api.nvim_create_buf(true, true) -- listed = true, scratch = true
vim.api.nvim_buf_set_name(buf_id, " Memos")
vim.bo[buf_id].buftype = 'nofile'
vim.bo[buf_id].swapfile = false
vim.bo[buf_id].filetype = 'memos_list'
vim.bo[buf_id].modifiable = false
end
-- 【修正】确保跳转到列表窗口,并且是在当前窗口打开
local win_id = vim.fn.bufwinid(buf_id)
if win_id ~= -1 then
vim.api.nvim_set_current_win(win_id)
else
vim.api.nvim_set_current_buf(buf_id)
end
vim.schedule(function() vim.notify("Getting user info...") end)
api.get_current_user(function(user)
if user and user.name then
current_user = user
vim.schedule(function() vim.notify("Fetching memos for " .. user.name .. "...") end)
api.list_memos(user.name, current_filter, config.pageSize, nil, function(data)
render_memos(data, false)
end)
else
vim.schedule(function() vim.notify("Could not get user, aborting fetch.", vim.log.levels.ERROR) end)
end
end)
local function set_keymap(key, command)
if key and key ~= "" then
vim.api.nvim_buf_set_keymap(buf_id, 'n', key, command, { noremap = true, silent = true })
end
end
if config.keymaps and config.keymaps.list then
local list_keymaps = config.keymaps.list
set_keymap(list_keymaps.edit_memo, '<Cmd>lua require("memos.ui").edit_selected_memo()<CR>')
set_keymap(list_keymaps.vsplit_edit_memo, '<Cmd>lua require("memos.ui").edit_selected_memo_in_vsplit()<CR>')
set_keymap(list_keymaps.quit, '<Cmd>bdelete!<CR>')
set_keymap(list_keymaps.search_memos, '<Cmd>lua require("memos.ui").search_memos()<CR>')
set_keymap(list_keymaps.refresh_list, '<Cmd>lua require("memos.ui").show_memos_list()<CR>')
set_keymap(list_keymaps.next_page, '<Cmd>lua require("memos.ui").load_next_page()<CR>')
set_keymap(list_keymaps.add_memo, '<Cmd>lua require("memos.ui").create_memo_in_buffer()<CR>')
set_keymap(list_keymaps.delete_memo, '<Cmd>lua require("memos.ui").confirm_delete_memo()<CR>')
set_keymap(list_keymaps.delete_memo_visual, '<Cmd>lua require("memos.ui").confirm_delete_memo()<CR>')
end
end
function M.load_next_page()
if not current_page_token or current_page_token == "" then
vim.notify("No more pages to load.", vim.log.levels.INFO)
return
end
if not current_user or not current_user.name then
vim.notify("User info not available.", vim.log.levels.WARN)
return
end
vim.schedule(function() vim.notify("Loading next page...") end)
api.list_memos(current_user.name, current_filter, config.pageSize, current_page_token, function(data)
render_memos(data, true)
end)
end
function M.edit_selected_memo()
local line_num = vim.api.nvim_win_get_cursor(0)[1]
local selected_memo = memos_cache[line_num]
if selected_memo then
open_memo_for_edit(selected_memo, 'enew')
end
end
function M.edit_selected_memo_in_vsplit()
local line_num = vim.api.nvim_win_get_cursor(0)[1]
local selected_memo = memos_cache[line_num]
if selected_memo then
open_memo_for_edit(selected_memo, 'vsplit | enew')
end
end
function M.confirm_delete_memo()
local line_num = vim.api.nvim_win_get_cursor(0)[1]
local selected_memo = memos_cache[line_num]
if not selected_memo then return end
local choice = vim.fn.confirm("Delete this memo?\n[" .. selected_memo.content:sub(1, 50) .. "...]", "&Yes\n&No", 2)
if choice == 1 then
api.delete_memo(selected_memo.name, function(success)
if success then
vim.schedule(function()
vim.notify("✅ Memo deleted.")
M.show_memos_list(current_filter)
end)
else
vim.schedule(function() vim.notify("❌ Failed to delete memo.", vim.log.levels.ERROR) end)
end
end)
end
end
function M.search_memos()
vim.ui.input({ prompt = "Search Memos: " }, function(input)
M.show_memos_list(input or "")
end)
end
return M

23
plugin/memos.lua Normal file
View File

@@ -0,0 +1,23 @@
-- 创建新 Memo 的命令
vim.api.nvim_create_user_command(
'MemosCreate',
function()
require('memos').create_memo()
end,
{
nargs = 0,
desc = "Create a new Memos entry"
}
)
-- 【新增】查看 Memos 列表的命令
vim.api.nvim_create_user_command(
'Memos',
function()
require('memos').show_list()
end,
{
nargs = 0,
desc = "List and search your Memos"
}
)