Should VISUAL remove indentation first before insertion?

Asked by dima gorbik

I've found one problem with existing snippets and also those that I am trying to creat. Consider this case: I have few indented lines:

   test
   one
   more

I select them all with `V`, press tab and then use `if` standard snippet. I will get:

if (smith) {
      test
      one
      more
}

So statements inside would be indented twice because I've "copied" those with indentations and then snippet has added it's own indentation once again. So I wonder, if there is an easy way to remove an indentation first before pasting visual selection into the snippet (inside the snippet engine). Should this behavior be the default?
I understand that probably I could make a shortcut for visual mode that would unindent selection first and then trigger UltiSnips function for capturing the selection. Would you suggest doing that?

Thanks,
Dima

Question information

Language:
English Edit question
Status:
Answered
For:
UltiSnips Edit question
Assignee:
No assignee Edit question
Last query:
Last reply:
Revision history for this message
SirVer (sirver) said :
#1

No, UltiSnips should not guess what indent you wanted to have - there are languages where indent matters a lot.

A simple solution for your problem would be to copy with 'v' instead of V (i.e. only select the text, not the whole line). UltiSnips will then not mess with the indent - however when you select whole lines, UltiSnips will assume that you want to have them indented as the TabStop is indented.

There is a bunch of other ways of course: you could use << to dedent first or write python block inside the snippet that removes indents from visual before pasting it in - but your use case should be fully covered with using v instead of V.

Revision history for this message
slanted (ryu-b) said :
#2

I have the same problem with my snippets. 'v' doesn't really do the trick, the alignment gets thrown out of wack as well. I don't suppose you've ever concidered adding an to the 'options' to handle leading indention?

(before)
    for (i=0; i < count; i++)
    {
        ...
    }
(using 'v')
    if ( )
    {
        for (i=0; i < count; i++)
    {
        ...
    }
    }

Revision history for this message
SirVer (sirver) said :
#3

The problem you have is not really visible here at launchpad as they do not use a mono font. Could you visualize tabs and spaces in your comment. It seems that your use case should be properly handled when using V (line select mode) instead of v.

No, I did not consider an option for this so far. I feel there are too many things that people would want to do with indents to make this really viable.

Revision history for this message
dima gorbik (dgorbik) said :
#4

I think if UltiSnips could take the shortest whitespace prefix among all visual selection lines and apply that to all lines of the snippet would be good enough for most cases.

Revision history for this message
SirVer (sirver) said :
#5

I am not convinced - there are languages where whitespace is significant (e.g. python or make). For your own snippets you can use python's textwrap.dedent(text). Use a python block instead of ${VISUAL} that looks something like this:

`!p snip.rv = textwrap.dedent(snip.v.text)`

Revision history for this message
SirVer (sirver) said :
#6

oh, you need to import textwrap before this :)

Revision history for this message
copacetickid (copacetickid) said :
#7

The inline python didn't work for me as is -- all lines after the first were stripped of all indentation. I ended up using this (which hard-codes 2 space tab width):

`!p snip.rv = re.sub(r'\n', '\n ', snip.v.text).strip()`

Revision history for this message
mMontu (mmontu) said :
#8

I see the same problem here. Changing the following lines on plugin/Ultisnips/__init__.py

from

            text = _vim_line_with_eol(sl-1)[sc:]
            for cl in range(sl,el-1):
                text += _vim_line_with_eol(cl)
            text += _vim_line_with_eol(el-1)[:ec+1]

to
            text = re.sub(r'^\s*', '', _vim_line_with_eol(sl-1)[sc:])
            for cl in range(sl,el-1):
                text += re.sub(r'^\s*', '', _vim_line_with_eol(cl))
            text += re.sub(r'^\s*', '', _vim_line_with_eol(el-1)[:ec+1])

seems to solve the problem. I've made some tests with C++ and python and looks good -- but maybe I'm missing some of the downsides pointed by SirVer.

Revision history for this message
SirVer (sirver) said :
#9

Is this also when you select full lines using V ? Also, you should probably use http://docs.python.org/2/library/textwrap.html#textwrap.dedent instead - which does exactly what you want.

Revision history for this message
mMontu (mmontu) said :
#10

It does work when selecting multiple lines with V -- I just noticed it fails for a single line selection.

Thanks for pointing textwrap.dedent -- I didn't used it because including it on the function could incur in overhead (https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Import_Statement_Overhead), and including it global would make it visible to the entire script, what could cause clashes.

Undoing the changes above and replacing the definition of _vim_line_with_eol() from

        def _vim_line_with_eol(ln):
            return _vim.buf[ln] + '\n'

to

        def _vim_line_with_eol(ln):
           if self._mode != 'V':
              return _vim.buf[ln] + '\n'
           else:
              return re.sub(r'^\s*', '', _vim.buf[ln]) + '\n'

makes it work for both single and multiple line selection with V. It doesn't change the behavior for selections with v, which is ok for single line selections.

Revision history for this message
SirVer (sirver) said :
#11

I still have problems understanding what the exact issue is at hand. Would any of you mind opening an issue with a concrete example of what is failing and why?

> I didn't used it because including it on the function could incur in overhead

That is the wrong reason for not using it :). First, running a regular expression (i.e. parsing it and then executing the state machine) is certainly longer than importing a simple python module. And second ultisnips uses this in some places already, so as soon as UltiSnips is loaded in Vim, this module is imported into the internal python process that Vim runs - so it is already imported when you want to use it and it will cost you exactly nothing.

Revision history for this message
mMontu (mmontu) said :
#12

> I still have problems understanding what the exact issue is at hand. Would any of you mind opening an issue with a concrete example of what is failing and why?

I have problems understanding your statement: this issue already provided a concrete example, and explains the problem. Maybe you didn't noticed because your settings doesn't matches the one used to create the example -- IndentConsistencyCop (http://www.vim.org/scripts/script.php?script_id=1690) could help you with that. In order to avoid copying the example and correcting 'tabstop', 'shiftwidth', etc, you can try the following steps:

* :e temp.c
* iif<tab><c-j>test;<cr>one;<cr>more;<esc>
* visually select the second and third statements inside the if with V, press <tab>if<tab>
* go back to normal mode, then oxyz;<esc>

You should get something like this:

if (/* condition */) {
    test;
    if (/* condition */) {
        xyz;
            one;
            more;
    }
}

It should be clear that the lines "one;" and "more;" are incorrectly indented, i.e., the replacement from the Visual placeholder doubled the indentation.

> That is the wrong reason for not using it
grep -r textwrap inside the plugin folder return nothing. Anyway, the documentation link that I've mentioned on my last message seems to contradict what you just said: every call to doit1() is slower due to the import statement. But I agree that parsing the regex could be slower. My knowledge in python is very limited -- maybe it could be something like this:

    _dedent = re.compile(r'^\s*')
    def conserve(self):
           :
           :
        def _vim_line_with_eol(ln):
           if self._mode != 'V' and ln != sl:
              return _vim.buf[ln] + '\n'
           else:
              return self._dedent.sub('', _vim.buf[ln]) + '\n'

Revision history for this message
SirVer (sirver) said :
#13

Your example was helpful. I created https://github.com/SirVer/ultisnips/issues/217

> grep -r textwrap inside the plugin folder return nothing

You are right, it is right now only used in test.py - sorry for that. However the rest of my argument stands essentially the same: you should not optimize prematurely. I doubt that importing will be noticable - given that the first time you expand a snippet, all of UltiSnips is loaded.

I'l look into this issue right now.

Revision history for this message
mMontu (mmontu) said :
#14

SirVer, your commit solved the problem, thanks!

 Your solution is better than the one I proposed as it preserves indentation on visually selected test, as the else block on the example below:

* :e temp.c
* iif<tab><c-j>ife<tab><esc>
* visually select the entire else block with V, press <tab>for<tab>
* go back to normal mode, then joxyz;<esc>

if (/* condition */) {
    if (/* condition */) {
        /* code */
    }
    for (i = 0; i < count; ++i)
    {
        xyz;
        else {
            /* else */
        }
    }
}

Revision history for this message
SirVer (sirver) said :
#15

great that you approve and that it solved your problems. Glad to be of service and thanks for getting back.

Can you help with this problem?

Provide an answer of your own, or ask dima gorbik for more information if necessary.

To post a message you must log in.