julienvincent / nvim-paredit

A Paredit implementation for Neovim, built using Treesitter and written in Lua.
MIT License
160 stars 8 forks source link

Request: move to next form start #53

Open carlinigraphy opened 6 months ago

carlinigraphy commented 6 months ago

Love the plugin. The tree-sitter API is absolutely the correct way to go about s-expr editing. The only feature I find missing is the ability to jump to the sibling form's start/end. (Not the existing functionality of jumping to the parent's start/end.)

I spent the weekend drafting a brief example.

api/motions.lua

-- Normalize to zero indicies.
local function get_cursor()
  local cursor = vim.api.nvim_win_get_cursor(0)
  return {
    row    = cursor[1] - 1,
    column = cursor[2]
  }
end

-- "Fix" index back to one-based.
local function set_cursor(node, where, opts)
  local row, column = node:start()
  vim.api.nvim_win_set_cursor(0, {row+1, column})
end

-- Core functionality here. Depth-first search, returning in order:
--  1. First child (if found)
--  2. Next sibling (if found)
-- Recurse up the tree until hitting the document root.
local function next_node(node)
  if node:named_child_count() > 0 then
    return node:named_child(0)
  end

  repeat
    if node:next_named_sibling() then
      return node:next_named_sibling()
    end
    node = node:parent()
  until not node:parent()
end

-- Jump target must satisfy:
--  1. Ahead of the cursor
--  2. Is a form
--  3. Isn't a comment
local function predicate(node, cursor)
  local lang = langs.get_language_api()
  local node_row, node_col = node:start()

  -- Cursor is on a later line.
  local much_after = (node_row  > cursor.row)

  -- Cursor is on the same line, later column.
  local little_after = (node_row == cursor.row) and
                       (node_col  > cursor.column)

  return lang.node_is_form(node)      and
          (little_after or much_after) and
          not lang.node_is_comment(node)
end

function M.next_paren()
  local cursor = get_cursor()
  local node   = ts.get_node_at_cursor()

  repeat
    node = next_node(node)
  until
    not node
    or predicate(node, cursor)

  if node then
    set_cursor(node)
  end
end

I haven't implemented the details to also jump to the form end, or jump to previous start/end, but I think this should serve as a solid baseline if you're interested.

NoahTheDuke commented 6 months ago

Isn't that paredit.api.move_to_next_element_head? What is the difference here?

carlinigraphy commented 6 months ago

Not quite, no.

As far as I understand it, move_to_next_element_head moves to the next sibling-level element head, but will not descend into sub-forms, or walk the tree back up to find subsequent forms. If in a sub-list, it fails to jump further upon reaching the end of that form.

Example: initial cursor location indicated with |, and jump locations with sequential numbers.

(def|ine (foo bar)
        ;1
  (cond
  ;2
    [(null? bar) '()]
    [else (cons (car bar) (foo (cdr bar)))]))
                                           ;3

(define bar foo) ; won't jump down here

My implementation above:

(def|ine (foo bar)
         ;1
  (cond
  ;2
    [(null? bar) '()]
    ;3,4         ;5
    [else (cons (car bar) (foo (cdr bar)))]))
    ;6    ;7    ;8        ;9   ;10

(define bar foo)
;11
julienvincent commented 6 months ago

Thanks for your interest, glad you are enjoying the plugin!


Something that I am cautious to do is broaden the scope of this plugin too much. Fundamentally I want to keep it focused on lisp specific syntax modifications.

I'm definitely not apposed to adding movement API's to this plugin (we already have done this) if they make sense. But as much as possible if it is something that can be pushed out to other plugins I think I would prefer that.

I say all this because there is already a generalized treesitter textobjects plugin for various operation found here - https://github.com/nvim-treesitter/nvim-treesitter-textobjects. Specifically relevant to this discussion is the section on motions.

Can you take a look at that and see if that is something that might address your use case? One thing that stands out as different after giving it a brief glance is that your implementation automatically decides to move to an inner form but the textobjects plugin would require separate keybindings.

If that's not a good enough solution then maybe it's something we can consider adding. Let me know your thoughts.


Also relevant here is #49 (which I haven't yet replied to either 😅)

carlinigraphy commented 6 months ago

Something that I am cautious to do is broaden the scope of this plugin too much. Fundamentally I want to keep it focused on lisp specific syntax modifications.

I absolutely understand. It's a pet peeve of mine when plugins end up bloated with unnecessary features and functionality. This was a main aspect that drew me to nvim-paredit; everything feels like it belongs.

As there were a few core "motions" already in the API, I figured it may be useful to provide a different method for advancing the cursor. (I think the core of my next_node() function above may actually simplify some of your current node seeking code.)

It's been a little while since I've looked into nvim-treesitter-textobjects, but I'll give it another look. On a cursory glance, it does seem to be more geared to imperative code, rather than s-expression selection/manipulation.

Please feel free to resolve this issue. Thank you for your consideration.

carlinigraphy commented 5 months ago

Turns out I got kinda fixated playing around with this concept. Spent a while iterating on movements, if you're interested in taking a look.

https://github.com/carlinigraphy/scm-edit.nvim/blob/main/lua/scm-edit/motions.lua

I find the _bracket() binary search function to be particularly elegant.

AlexChalk commented 4 months ago

Hi @julienvincent, I haven't done any meaningful treesitter configuration, but I'd be interested to try if it can get this functionality:

julienvincent commented 3 weeks ago

I'm revisiting this issue now and on second though I think I'd be happy to accept PR's that add this kind of functionality.

If someone is interested in working on this I think it would an appropriate addition to nvim-paredit.