Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request: move to next form start #53

Open
carlinigraphy opened this issue Feb 27, 2024 · 7 comments
Open

Request: move to next form start #53

carlinigraphy opened this issue Feb 27, 2024 · 7 comments

Comments

@carlinigraphy
Copy link

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
Copy link
Contributor

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

@carlinigraphy
Copy link
Author

carlinigraphy commented Feb 27, 2024

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
Copy link
Owner

julienvincent commented Feb 28, 2024

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
Copy link
Author

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
Copy link
Author

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
Copy link

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

  • How can I identify the correct queries to use i.e. for an element/form/sexp in clojure? I'm not sure where to look.
  • In the docs you linked, goto_next/previous imply that these can't be used to move to nested/parent forms, is there a way around this?

@julienvincent
Copy link
Owner

julienvincent commented Aug 27, 2024

I'm revisiting this issue now and on second thought 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants