Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `padding_top` option to `Note.insert_text` for configuring blank lines inserted at the top of notes.
- `:Obsidian help` has cmdline completion for wiki pages.
- Obsidian sync client will emit `ObsidianSyncChanged` autocmd event for better status rendering.
- Support LSP code_action:
- Add code_actions with `require"obsidian".code_action.add`.
- Delete code_actions with `require"obsidian".code_action.del`.

### Fixed

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ There's one entry point user command for this plugin: `Obsidian`
- you are in visual mode.
- See [Commands](https://github.com/obsidian-nvim/obsidian.nvim/wiki/Commands) for more info.

> [!Warning]
> Note subcommands related to refactoring, like `rename` and `template`
> And all the visual mode commands, will be moved to code actions in `3.17.0`.

#### Top level commands

- `:Obsidian check` - check for common issues in your vault and plugin setup
Expand Down Expand Up @@ -132,6 +136,11 @@ There's one entry point user command for this plugin: `Obsidian`
- `:Obsidian link_new [TITLE]` - create a new note and link it to an inline visual selection of text
- if title is not given, selected text is used

### LSP code actions

- Use `gra` or `:=vim.lsp.buf.code_action()` to trigger note specific actions.
- See [LSP code actions](https://github.com/obsidian-nvim/obsidian.nvim/wiki/LSP#code-actions) and [Actions](docs/Actions.md) for more info.

## 📝 Requirements

### System requirements
Expand Down
23 changes: 23 additions & 0 deletions docs/Actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
See [LSP code actions](LSP.md#code-actions) for actions exposed via the LSP interface.

| name | mode | description | arguments |
| -------------------- | -------- | -------------------------------------- | -------------------- |
| `follow_link` | `n` | Open the link under the cursor. | `link` |
| `nav_link` | `n` | Navigate to next/previous link. | `direction` |
| `smart_action` | `n` | Context-aware action on the cursor. | |
| `new` | `n` | Create a new note. | `id` |
| `new_from_template` | `n` | Create a new note from a template. | `id`, `template` |
| `unique_note` | `n` | Create a unique note. | `timestamp` |
| `unique_link` | `n` | Create and insert a unique note link. | `timestamp` |
| `add_property` | `n` | Add frontmatter property. | |
| `insert_template` | `n` | Insert a template at cursor. | `name` |
| `rename` | `n` | Rename current note. | `name` |
| `move_note` | `n` | Move current note to another folder. | |
| `merge_note` | `n` | Merge current note into another note. | `dst_note` |
| `start_presentation` | `n` | Start slide presentation. | `buf` |
| `workspace_symbol` | `n` | Search notes, aliases, and headings. | `query` |
| `toggle_checkbox` | `n`, `v` | Toggle (cycle) checkbox state. | `start_lnum`, `end_lnum` |
| `set_checkbox` | `n`, `v` | Set to specific checkbox state. | `state` |
| `link` | `v` | Link selection to an existing note. | |
| `link_new` | `v` | Create a new note and link selection. | `title` |
| `extract_note` | `v` | Move selection to a new note. | `title` |
2 changes: 1 addition & 1 deletion docs/LSP-Progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ Tracking implementation status of [LSP 3.17](https://microsoft.github.io/languag
- [x] Document Symbols (`textDocument/documentSymbol`) - returns markdown headings
- [x] Rename (`textDocument/rename`) - rename notes and update all references across the vault
- [x] Prepare Rename (`textDocument/prepareRename`)
- [x] Code Action (`textDocument/codeAction`)
- [ ] Hover (`textDocument/hover`)
- [ ] Completion Proposals (`textDocument/completion`) - currently handled outside LSP via nvim-cmp/blink.cmp
- [ ] Completion Item Resolve (`completionItem/resolve`)
- [ ] Publish Diagnostics (`textDocument/publishDiagnostics`)
- [ ] Code Action (`textDocument/codeAction`)
- [ ] Code Action Resolve (`codeAction/resolve`)
- [ ] Document Link (`textDocument/documentLink`)
- [ ] Document Link Resolve (`documentLink/resolve`)
Expand Down
30 changes: 30 additions & 0 deletions docs/LSP.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,33 @@ require("obsidian").setup {
},
}
```

## Code Actions

obsidian.nvim exposes a small set of LSP code actions for common note operations. You can trigger them using your normal LSP code action keymap, by default neovim maps `vim.lsp.buf.code_action` to `gra`.

Available actions:

- Normal mode:
- Add file property (`add_property`)
- Insert template at cursor (`insert_template`, requires templates enabled)
- Move current note to another folder (`move_note`)
- Merge current note into another note (`merge_note`)
- Start presentation (`start_presentation`, requires slides enabled)
- Visual mode:
- Link selection as name for an existing note (`link`)
- Link selection as name for a new note (`link_new`)
- Extract selected text to a new note (`extract_note`)

### Code Action API

You can register custom code actions via `require("obsidian").code_action` module.

API:

- `require("obsidian").code_action.add(opts)`, and `opts` field have following fields:
- `name`: command id (snake_case recommended).
- `title`: text shown in the code action picker.
- `fn`: function invoked when the action is executed.
- `cond` (optional): a filter function that gets the current note object, determines whether actions is listed.
- `require("obsidian").code_action.del(name)` removes a previously registered action.
114 changes: 97 additions & 17 deletions lua/obsidian/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,48 @@ M.add_property = function()
note:update_frontmatter(0)
end

---@param template_name string
M.insert_template = function(template_name)
local templates_dir = api.templates_dir()
if not templates_dir then
return log.err "Templates folder is not defined or does not exist"
end
local templates = require "obsidian.templates"

-- We need to get this upfront before the picker hijacks the current window.
local insert_location = api.get_active_window_cursor_location()

local function insert_template(name)
templates.insert_template {
type = "insert_template",
template_name = name,
templates_dir = templates_dir,
location = insert_location,
}
end

if template_name ~= nil then
insert_template(template_name)
return
end

---@type obsidian.PickerEntry[]
local entries = {}
for path in api.dir(tostring(templates_dir)) do
entries[#entries + 1] = {
filename = path,
text = vim.fs.basename(path),
}
end

Obsidian.picker.pick(entries, {
callback = function(entry)
insert_template(entry.filename)
end,
})
end

---@param buf integer|?
M.start_presentation = function(buf)
local note = Note.from_buffer(buf)
require("obsidian.slides").start_presentation(note)
Expand Down Expand Up @@ -649,6 +691,31 @@ M.workspace_symbol = function(query, callback)
end)
end

---Pick a folder under the vault root.
---@param callback fun(directory: string, text: string)
local function pick_folder(callback)
local root = tostring(Obsidian.workspace.root)
local choices = { { filename = root, text = "/" } }

for path, t in vim.fs.dir(root, { depth = math.huge }) do
if t == "directory" then
choices[#choices + 1] = {
filename = vim.fs.joinpath(root, path),
text = path .. "/",
}
end
end

Obsidian.picker.pick(choices, {
callback = function(entry)
callback(entry.filename, entry.text)
end,
format_item = function(v)
return tostring(v.text)
end,
})
end

---@param directory string
---@param text string
local function move_note(directory, text)
Expand All @@ -672,26 +739,39 @@ M.move_note = function()
log.info "Not in an obsidian buffer"
return
end
local root = tostring(Obsidian.workspace.root)
local choices = { { filename = root, text = "/" } }
pick_folder(move_note)
end

for path, t in vim.fs.dir(root, { depth = math.huge }) do
if t == "directory" then
choices[#choices + 1] = {
filename = vim.fs.joinpath(root, path),
text = path .. "/",
}
end
---@param dst_note obsidian.Note
local function merge_note(dst_note)
local current_note = api.current_note()
assert(current_note, "Must be in a note to merge")

local message = ('Are you sure you want to merge "%s" to "%s"? "%s" will be deleted.'):format(
current_note.id,
dst_note.id,
current_note.id
)

if api.confirm(message) == "Yes" then
dst_note:merge(current_note)
dst_note:open { sync = true }
vim.fs.rm(tostring(current_note.path))
end
end

Obsidian.picker.pick(choices, {
callback = function(entry)
move_note(entry.filename, entry.text)
end,
format_item = function(v)
return tostring(v.text)
end,
})
---@param dst_note obsidian.Note?
M.merge_note = function(dst_note)
if dst_note then
merge_note(dst_note)
else
Obsidian.picker.find_notes {
callback = function(path)
local note = Note.from_file(path)
merge_note(note)
end,
}
end
end

return M
2 changes: 1 addition & 1 deletion lua/obsidian/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ M.current_note = function(bufnr, opts)
return Note.from_buffer(bufnr, opts)
end

---@return [number, number, number, number] tuple containing { buf, win, row, col }
---@return [integer, integer, integer, integer] tuple containing { buf, win, row, col }
M.get_active_window_cursor_location = function()
local buf = vim.api.nvim_win_get_buf(0)
local win = vim.api.nvim_get_current_win()
Expand Down
4 changes: 1 addition & 3 deletions lua/obsidian/commands/new_from_template.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
local api = require "obsidian.api"

---@param data obsidian.CommandArgs
return function(data)
local id = table.concat(data.fargs, " ", 1, #data.fargs - 1)
local template = data.fargs[#data.fargs]
api.new_from_template(id, template)
require("obsidian.actions").new_from_template(id, template)
end
9 changes: 4 additions & 5 deletions lua/obsidian/commands/rename.lua
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
---@param data obsidian.CommandArgs
return function(data)
local new_name = vim.trim(data.args)
if #new_name == 0 then
vim.lsp.buf.rename()
else
vim.lsp.buf.rename(new_name)
local new_name
if data.args and string.len(data.args) > 0 then
new_name = vim.trim(data.args)
end
vim.lsp.buf.rename(new_name)
end
42 changes: 3 additions & 39 deletions lua/obsidian/commands/template.lua
Original file line number Diff line number Diff line change
@@ -1,44 +1,8 @@
local templates = require "obsidian.templates"
local log = require "obsidian.log"
local api = require "obsidian.api"

---@param data obsidian.CommandArgs
return function(data)
local templates_dir = api.templates_dir()
if not templates_dir then
return log.err "Templates folder is not defined or does not exist"
end

-- We need to get this upfront before the picker hijacks the current window.
local insert_location = api.get_active_window_cursor_location()

local function insert_template(name)
templates.insert_template {
type = "insert_template",
template_name = name,
templates_dir = templates_dir,
location = insert_location,
}
end

local template_name
if string.len(data.args) > 0 then
local template_name = vim.trim(data.args)
insert_template(template_name)
return
end

local picker = Obsidian.picker
if not picker then
log.err "No picker configured"
return
template_name = vim.trim(data.args)
end

picker.find_files {
prompt_title = "Templates",
dir = templates_dir,
no_default_mappings = true,
callback = function(path)
insert_template(path)
end,
}
require("obsidian.actions").insert_template(template_name)
end
8 changes: 7 additions & 1 deletion lua/obsidian/config/default.lua
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,14 @@ return {
},

---@class obsidian.config.CommentOpts
---@field enabled boolean
---@field enabled? boolean
comment = {
enabled = false,
},

---@class obsidian.config.SlidesOpts
---@field enabled? boolean
slides = {
enabled = true,
},
}
1 change: 1 addition & 0 deletions lua/obsidian/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ local obsidian = {}

obsidian.api = require "obsidian.api"
obsidian.actions = require "obsidian.actions"
obsidian.code_action = require "obsidian.lsp.handlers._code_action"
obsidian.async = require "obsidian.async"
obsidian.Client = require "obsidian.client"
obsidian.commands = require "obsidian.commands"
Expand Down
1 change: 1 addition & 0 deletions lua/obsidian/lsp/handlers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ return {
["textDocument/references"] = require "obsidian.lsp.handlers.references",
["textDocument/definition"] = require "obsidian.lsp.handlers.definition",
["textDocument/documentSymbol"] = require "obsidian.lsp.handlers.document_symbol",
["textDocument/codeAction"] = require "obsidian.lsp.handlers.code_action",
}
Loading
Loading