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

Behavior of text object when outside of matching pairs #1139

Open
lobre opened this issue Sep 28, 2023 · 4 comments
Open

Behavior of text object when outside of matching pairs #1139

lobre opened this issue Sep 28, 2023 · 4 comments

Comments

@lobre
Copy link

lobre commented Sep 28, 2023

I am trying to analyse the differences between vis and vim when targeting a matching pair from outside of the text object, or when there are both a matching pair enclosed or on the same line further away.

First, let's talk about single-line, without any enclosing pairs above or below. See this simple example. The cursor is represented by .

var▌ my_var int = my_function("my_parameter")

In vim, I can do ci" and I will be able to directly change my_parameter. This also works in vis 🎉. The behavior is the same for simple quotes. However, in vim, this also works for other pairs such as for ()/{}/[]/<>. And this does not seem supported in vis, while being a really handy feature. When I try ci( in the above example, it enters insert mode without moving the cursor.

Now, if we talk about visual mode, even doing vi" or vi' does not produce the desired behavior. Effectively, it will enter visual mode and select everything from the current cursor until the closing " or ' instead of just selecting the inside of the matching " or '. Doing vi( does nothing except going into visual mode.

Now let's take an example with multiline enclosing pairs.

struct {
  my_var = (
    f▌unction("test")
  )
}

So the behavior in vim here when doing ci( or vi( will be to change the inside of the outer parenthesis just above and below. However, it will ignore the new-line characters or spaces that are on the same lines as ( and ).

So doing ci(#my comment will result into:

struct {
  my_var = (
    #my comment
  )
}

However, if the initial example had non-space nor new-line characters on the same line as (, such as:

struct {
  my_var = ( # my small comment
    f▌unction("test")
  )
}

The change would start applying on the same line as ( and the result would result in:

struct {
  my_var = (# my comment
  )
}

Vis behavior is different. Taking the initial example, it will not ignore new-line characters and spaces, so doing ci(#replacement will result in:

struct {
  my_var = (#replacement)
}

When doing ca( or va(, the behavior seems more consistent between vim and vis.

So those are two examples that show differences between vis and vim. First I wanted to know if there were technical or design-related reasons for them. Maybe vis not acting at the line level leads to those?

Otherwise, are those differences existing on purpose, or should they be considered bugs?

Whatever the outcome of this issue, those differences are not documented in the FAQ "Differences from Vi(m)".

@rnpnr
Copy link
Collaborator

rnpnr commented Sep 28, 2023

The first part is likely a bug and patches would be welcome.

The second part is a feature. ci( i.e. change in ( should do just that, it shouldn't care about what is inside in the ().

@lobre
Copy link
Author

lobre commented Sep 29, 2023

The second part is a feature. ci( i.e. change in ( should do just that, it shouldn't care about what is inside in the ().

Your comment is valid at first sight. But thinking more about the problem, I am starting to wonder.

Some people expect the behavior that you state. If I di(, I want all the characters (whitespace, newline, ...) deleted inside the parenthesis. See this issue that complains about vim's behavior:

https://stackoverflow.com/questions/70671890/vim-select-text-between-brackets-including-new-line-character

However, in practice, I think that what vim does could be desired. I am taking the example from the issue here, but feel free to imagine any block in your programming language or in a json file:

import {
  one,
  two,
  three,
  four,
} from "../myfile";

Often, the user will want to replace the content of a block with something else. I cannot generalize, but I feel that in replacements, users will 95% of the time want to keep their indentation level and directly start replacing. Let's say I want this:

import {
  five
} from "../myfile";

In the above block in vim, I simply do ci{five and I can go ahead with the replacement.

With vis, I have to do ci{<Enter><Tab>five<Enter>.

I can otherwise do vi{_cfive, but needing to use visual mode for a simple replacement feels heavy.

By the way, Kakoune works like vis for the above example, as it does not exclude new lines or spaces. But as it has motions first, visual mode is the norm and so it makes sense to implement it this way, and to let the user decide then to do _ to unselect whitespace afterward. It is part of the normal workflow in this editor. Vis is verb-first, so it is different and forcing the user to go to visual mode for a simple change could be cumbersome.

For deletions, the desired behavior might be more like vis. If I want to empty the block there are chances that I want those braces collapsed (import {} from "../myfile").

So in the end, I am not exactly sure that vis should diverge from vim for this behavior.

Note also that vim goes even farther, and this might feel opinionated. If there are empty-indented lines before the closing of the text-object character, there are also excluded. In this example, -- represents two spaces and | is the cursor.

import {
  one,
  |two,
  three,
  four,
--
--
} from "../myfile";

If I execute ci{, I will preserve those two lines and I will end up here:

import {
--|
--
--
} from "../myfile";

I am not sure why this is acting this way.

For reference, here is the location in neovim (should be similar in vim) where all this logic of excluding newline or whitespace is developed.

https://github.com/neovim/neovim/blob/9afbfb4d646cd240e97dbaae109f12bfc853112c/src/nvim/textobject.c#L965

@ninewise
Copy link
Collaborator

ninewise commented Sep 30, 2023 via email

@lobre
Copy link
Author

lobre commented Sep 30, 2023

So rather than have ci( do "change inside surrounding parentheses" you'd make it "change from next ) to matching ("?

Not exactly. I would stick to vim behaviour which I think is more or less "change inside surrounded parenthesis if there are surrounded parenthesis, otherwise change inside next match of parenthesis on the same line".

Vis does not work with lines being the base though. So not sure if it should match on next lines also.

Should we then also make a difference for ci) to be "change from previous ( to matching )" so you could edit the wrapped text before the cursor?

I would personally not move away from vim's implementation if we don't have good reasons to.

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

3 participants