epwalsh / obsidian.nvim

Obsidian 🤝 Neovim
Apache License 2.0
4.3k stars 195 forks source link

Smart "enter" to gf_passthrough or toggle_checkbox depending on the context #329

Open sotte opened 10 months ago

sotte commented 10 months ago

🚀 The feature, motivation and pitch

Often there is an obvious action to perform depending on the context (textobject the cursor is on). Why not offer a smart mapping, maybe enter, to do the right thing based on the context:

if cursor_on_link:
  gf_passthrough()
elif cursor_on_checkbox_line:
  toggle_checkbox()

(I'm using a similar mapping in orgmode and I love it.)

Alternatives

Additional context

No response

epwalsh commented 10 months ago

Sounds like a good feature. I'm happy to accept a PR with this as a default mapping.

sotte commented 10 months ago

My dumb pragmatic implementation of this. Just a mapping via lazyvim really.

return {
  {    
  "epwalsh/obsidian.nvim",
   ...
  keys = {
      {
        "<cr>",
        function()
          local util = require("obsidian.util")
          if util.cursor_on_markdown_link(nil, nil, true) then
            vim.cmd("ObsidianFollowLink")
          else
            util.toggle_checkbox()
          end
        end,
        desc = "Smart action: follow/toggle",
        ft = "markdown",
      },

It's slightly annoying that one action is a command and one a lua function that actually does something.

I'll play around with this the next few days and might create a MR. But if someone else comes firs, I'm not offended.

sotte commented 10 months ago

I'm also playing with adding "show notes with tag if cursor is on tag". Not robust yet, but quite nice.

sotte commented 9 months ago

I wanted to add ObsidianLink to SmartAction, i.e. trigger ObsidianLink when I'm in visual mode. This is the code that IMO should do the trick:

ObsidianSmartAction = function(opts)
  -- opts comes from the user command
  -- https://neovim.io/doc/user/lua-guide.html#lua-guide-commands-create
  -- print(vim.inspect(opts))

  local mode = vim.api.nvim_get_mode().mode
  if mode == "v" then
    local client = require("obsidian").get_client()
    client:command("ObsidianLink", opts)
    return
  end
  local obs_util = require("obsidian.util")
  if obs_util.cursor_on_markdown_link(nil, nil, true) then
    vim.cmd("ObsidianFollowLink")
  else
    vim.cmd("SmartToggleTask")
  end
end

I have the problem, that the visual selection is not consistently passed to the function. Sometimes I get the previous selection. Sometimes I get the right one. Sometimes I get no selection at all and the subsequent error message: "ObsidianLink must be called with visual selection". I'm a bit lost how to best call ObsidianLink.

Do you have any suggestions?

sotte commented 9 months ago

Well, registering the mapping with :SmartAction<cr> instead of <cmd>SmartAction<cr> does the trick. But it seems more of a workaround than a proper solution.

Some relevant resources:

sotte commented 9 months ago

Just a quick update on what I use in normal mode now:

ObsidianSmartAction = function()
  local obs_util = require("obsidian.util")

  -- follow link if possible
  if obs_util.cursor_on_markdown_link(nil, nil, true) then
    vim.cmd("ObsidianFollowLink")
    return
  end

  -- show tags if possible
  local current_wrod = vim.fn.expand("<cWORD>")
  if current_wrod:match("^#") then
    vim.cmd("ObsidianTags")
    return
  end

  -- toggle task if possible
  -- custom implementation that cycles through  [ ] [~] [>] [x]
  vim.cmd("ToggleTask")
end
CaeChao commented 8 months ago

Hi @epwalsh I just followed @sotte 's idea and did a custom implementation for smart action here

sotte commented 8 months ago

We could merge this, or a version of this, into obsidian.nvim or create an entry in the cookbook https://github.com/epwalsh/obsidian.nvim/discussions/categories/cookbook.

GitMurf commented 3 months ago

I am loving the smart action! Nice work!

One question I have is whether we can add to the "Toggle Checkbox" action to add to the cycle removing the checkbox all together which then leave just a regular list item.

The cycle would be something like this:

  1. - (regular list item with no checkbox)
  2. - [ ] (open checkbox)
  3. - [x] (completed checkbox)
  4. - [n] (any other custom statuses like ? / - etc.)

Thoughts on this?

GitMurf commented 3 months ago

One question I have is whether we can add to the "Toggle Checkbox" action to add to the cycle removing the checkbox all together which then leave just a regular list item.

Here is my super rough and quick implementation of this on my side overriding the mapping for smart action (included below is just my list toggle override function... I also have the follow link and tag recognition checked for in the actual smart action function keymap):

      local my_list_toggle = function()
        local line_num = unpack(vim.api.nvim_win_get_cursor(0))
        local line = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1]

        local checkbox_pattern = '^%s*- %[.] '
        local checkboxes_cycle = { ' ', 'x' }
        if not string.match(line, checkbox_pattern) then
          -- not a current checkbox
          local unordered_list_pattern = '^(%s*)- (.*)$'
          if string.match(line, unordered_list_pattern) then
            -- this is a regular list item, so make it a checkbox
            line = string.gsub(line, unordered_list_pattern, '%1- [ ] %2')
          else
            -- not a list item at all so make it a list item (NOT a checkbox)
            line = string.gsub(line, '^(%s*)', '%1- ')
          end
        else
          -- already a checkbox so just toggle it to the next item
          local cur_cb_pattern = '^%s*- %[(.)%] '
          local cur_cb_char = string.match(line, cur_cb_pattern)
          if cur_cb_char == nil then
            vim.notify('Error: could not find current checkbox state')
            return
          end
          local replace_cb_pattern = '^(%s*)- %[.%] '
          -- find the current character and replace it with the next one in the checkboxes list
          local found_match = false
          for i, check_char in ipairs(checkboxes_cycle) do
            if cur_cb_char == check_char then
              found_match = true
              local next_cb = ''
              if i == #checkboxes_cycle then
                -- last item in cycle so change to a regular list item
                next_cb = ''
              else
                next_cb = '[' .. checkboxes_cycle[i + 1] .. '] '
              end
              line = string.gsub(line, replace_cb_pattern, '%1- ' .. next_cb)
              break
            end
          end
          if not found_match then
            vim.notify('Error: could not find current checkbox state in cycle list')
            return
          end
        end

        vim.schedule(function()
          vim.api.nvim_buf_set_lines(0, line_num - 1, line_num, true, { line })
        end)
      end