mawww / kakoune

mawww's experiment for a better code editor
http://kakoune.org
The Unlicense
9.94k stars 716 forks source link

equivalent of vim's expandtab #1038

Closed jtmarmon closed 7 years ago

jtmarmon commented 7 years ago

I'd like to be able to indent indentwidth when I hit the tab option. right now my successful workaround is to add the following to kakrc: map global insert <tab> <space><space>

However, considering this seems like a relatively popular feature in tabless codebases, it may be worth considering as an officially supported setting.

leemeichin commented 7 years ago

This might be indirect, but there is built-in editorconfig support, which allows you to configure indentation and some other settings on a per-file/per-project basis. An existing PR completes the support, as far as I can tell.

This wiki page explains how to enable it automatically.

Presuming you use this, you just need to set up the hook and make an editorconfig in your home directory like this:

root = true

[*]
indent_style=space
indent_size=2

The benefit is that this config will hold true for any other editor or IDE you use that supports editorconfig natively or provides a plugin for it.

casimir commented 7 years ago

Personally, I have this in my kakrc:

map global insert <tab> '<a-;><gt>'
map global insert <backtab> '<a-;><lt>'

It does the same but considers %opt{indentwidth}, inserting the right number of <space>'s or a <tab> in insert mode. What @leemachin proposed is a good complement too.

PS: for whoever is wondering (I did) it's not a problem even if you use <tab> for completion canditate selection, kakoune is smart enough to do both

doppioandante commented 7 years ago

@casimir I will put those in the wiki if you don't mind.

doppioandante commented 7 years ago

Just for the record, by pressing @ on a selection tabs will be converted to the number of spaces specified by the tabstop option.

casimir commented 7 years ago

@doppioandante go ahead, IIRC I snatched these on IRC

mawww commented 7 years ago

The canonical way to do that would be to do hook window InsertKey \t %{ exec -draft h@ } that is, add a hook that after inserting a tab in insert mode will use the @ command to convert it to spaces.

lilyball commented 7 years ago

@mawww That hook doesn't work for me. It doesn't ever seem to fire. In fact, I can't seem to get an InsertKey hook to fire on tab, space, return, backspace, forward delete, home, end, etc. I don't know why none of the whitespace keys do it, and as for the others, I would have assumed the difference between InsertKey and InsertChar is the former gets the keys that don't insert characters (such as delete).

I tested using hook window InsertKey .? %{ echo "key: '%val{hook_param}'" } (the regex .? is because I can't figure out how to provide an empty regex, so I wanted something that would match anything, including the empty string).

lilyball commented 7 years ago

In any case, I think Kakoune could benefit from having an expandtab option like Vim, rather than requiring everyone who wants this (very common) functionality to have to read the wiki and add their own hook.

lilyball commented 7 years ago

There's also a related bit of functionality, which is that, with the options set up correctly, Vim will also backspace over a tab's worth of spaces at a time. This way your editing behavior feels exactly like you're using tabs, except you're not (and you have more flexibility, because you can choose to delete individual spaces instead of being required to delete the entire tab). Kakoune really needs some way to replicate this as well.

lilyball commented 7 years ago

Oh geeze, I just figured out what's wrong with the hook. The hook regex implicitly matches the whole line instead of just a substring (which is what I had assumed before as that's how regexes normally work), and the filtering text for special keys is their encoded form. So to match a tab you have to use <tab> instead of \t. However, InsertChar does get \t. I suspect that InsertChar is the better hook to use anyway, because that means the character was inserted rather than just a key was pressed.

lenormf commented 7 years ago

We don't need to have a default option for every functionality that users need, particularly when it's already implementable with a simple hook. That way the documentation remains fairly short, and the code doesn't become bloated with ifs everywhere.

Also most users prefer the default behaviour, as in practice you don't need to insert spaces that often (the alignment primitives do that for you already), and inserting a tabulation character becomes cumbersome.

lilyball commented 7 years ago

No we don't need a default option for every functionality, but indentation with spaces is extremely common and Kakoune not having a good story for dealing with this is I think a pretty big limitation. And while the hook can turn tab characters into spaces, it's a) annoying and non-obvious for something that a very large number of people will want, and b) it doesn't handle deleting spaces, which gets really annoying if it won't delete a tab's worth at a time. Literally every other editor I use right now handles both cases correctly.

lenormf commented 7 years ago

Like I said, not a big percentage of users of the editor require this feature, in my experience it's mostly vim users who have trouble adapting to the new paradigm who ask about it. They usually check the wiki and copy/paste the one line hook it takes to implement the feature, just like they would set the hypothetical option to true in their configuration file if it existed.

lilyball commented 7 years ago

It's not just Vim users. Like I said, every single editor I use today (besides Kakoune) handles this correctly, including Xcode, and even some online editors at various sites. I even downloaded Atom to see how it behaves, and not only does it support this, it behaves this way by default.

lenormf commented 7 years ago

Your argument is that other editors do it that way, so it should be done that way too in Kakoune even though only a minority of users really ask for it, and it takes as much effort to implement it with a hook as it would to enable an hypothetical builtin option. I'm not convinced.

If it was really the most convenient/obvious way to implement this feature, I'm sure the author of the project would have been smart enough to do just that, but they didn't and it's been working just fine for everybody.

lilyball commented 7 years ago
  1. A minority of users? Literally every coder I work with, or have worked with for many many years, uses soft tabs instead of hard tabs. The distribution of users who use soft tabs vs hard tabs probably differs based on language and industry, but soft tabs is really common. Just because people who want soft tabs in Kakoune have had to deal with sub-par workarounds doesn't mean it's not a legitimate issue.
  2. As I've repeatedly stated, the hook is not sufficient. It provides only half of the desired behavior. And if the hook were sufficient, I'd still ask for Kakoune to ship the hook as part of its rc files, using a script-defined option to control its behavior and WinSetOption/BufSetOption to watch for changes to it (to enable/disable the hook). But since the hook isn't sufficient, and the rest of the desired behavior cannot be implemented in a hook (AFAICT), then Kakoune should just grow native support for this functionality.
  3. If your logic is "the author didn't do it, therefore it's not worth doing", then I guess all development can stop now because the editor is perfect as-is.
casimir commented 7 years ago

If it's really that common but also not that wanted it could be wrap in an official kak file. Like in base/tabs.kak with a function like expand tab-enable that set the hook for the buffer.

lenormf commented 7 years ago

There's so much frustration and misreading going on I'm going to let other take care of this now ;)

leemeichin commented 7 years ago

@casimir I suppose this also makes it infinitely simpler for someone (even @kballard) to submit a PR with the kak script (maybe I'm wrong but it shouldn't be impossible with a bit of shell magic, I'll see if I can get anywhere with it myself), since the barrier to contributing this to the editor core itself is quite high and, from what I understand from @lenormf's perspective, at odds with the goal of keeping it as small as is reasonably possible.

If it turns out to be difficult to implement but still doable using just a kak script, then it's further evidence to consider ways to make the scripting side a bit easier, which is a hell of a lot more flexible and valuable than setting the precedent for extending functionality with built-in options.

casimir commented 7 years ago

@leemachin this is the way to go, kakoune's core is design to be really primitive. Most of the smartiness is implemented by kak scripts. This feature would be easy to implement as a script, the tricky part is to define a nice and correct behaviour (cf this thread).

mawww commented 7 years ago

I really prefer that the expand tab functionality be exposed through a hook, as its simpler implementation wise, and makes user discover hooks soon in their Kakoune journey. They'll probably just copy the hook line for a start (and it should definitely by easy to find in the wiki), but that exposes them to this way of doing things, using built-in features to implement more complicated logic.

I have regularly refused to merge small features that could be implemented in a single line hook for this reason, I understand vim users will look for an option, but its only due to familiarity, it does not make more sense for this behaviour to be controlled through an option than through a hook.

Regarding the erase side, I suspect thats pretty simple to do, something like exec %opt{tabwidth}H<a-@><a-;>;d might do the trick.

lilyball commented 7 years ago

In order to erase, you have to know that the user is trying to delete a space anyway. And presumably by the time the hook fires, the character has already been erased, so I'm not sure how you can even tell what character it was that the user just erased.

leemeichin commented 7 years ago

You have the position of the cursor as a variable when executing a hook, which could be used to find out the characters that appear from the start of the line up to that point. If there happens to be whitespace immediately before the cursor and it's a multiple of the tabwidth you can erase the full width each time. If not, normal deletion happens until that condition becomes true again.

What is uncertain is if the hook is fired before the insertion, or after it. Because with the former case, you do know what the character is.

lilyball commented 7 years ago

If the hook is fired before the deletion, then after whatever I do in the hook, the deletion will go through as well. So I'd have to do something strange like detect the whitespace, delete it, then re-insert some character with the sole intention of letting the deletion then delete it. If it's fired after, then I'm SOL.

It also seems likely to be very difficult to make this work properly in a multiple-selection world. I suspect the code to do this would actually be a hell of a lot simpler (and faster) if implemented in the C++ core than trying to do it in shell script invoked from a hook.

mawww commented 7 years ago

you can use a map for it, instead of a hook, with a try block to check if the previous char is a space, and a catch block falling back on regular char deletion.

If we can express that resonably with Kakoune editing language, I'd rather have that logic out of the C++ core, I try to avoid options controling behaviour, as that breaks scripting using Kakoune normal mode.

mawww commented 7 years ago

I'll happily add/tweak features so that we can reasonably implement <backspace> removing whole indents, but I wont be adding an expandtab feature in the core, mainly because we cannot afford having options control key behaviours if we want to mantain scriptability through normal mode commands.

It should be possible to implement all these features in a .kak file.

lisael commented 7 years ago

I've implemented without shell calls with:

One more edit: now it works in most case, that's enough for my day-to-day usage :

# license : WTFPL  - (aka I don't know how to make an addition to the wiki)
map global insert <backspace> '<a-;>:insert-bs<ret>'

hook global InsertChar \t %{
    exec -draft h@
}

def -hidden insert-bs %{
    try %{
        # delete indentwidth spaces before cursor
        exec -draft -no-hooks h %opt{indentwidth}H <a-k>\A<space>+\Z<ret> d
    } catch %{
        exec <backspace>
    }
}
MarSoft commented 6 years ago

Updated version of the insert-bs command for kakoune 2018-04-13:

def -hidden insert-bs %{
    try %{
        # delete indentwidth spaces before cursor
        exec -draft -no-hooks h %opt{indentwidth}HL <a-k>\A<space>+\z<ret> d
    } catch %{
        exec <backspace>
    }
}

Differences:

  1. Since new regex backend, \Z should be replaced with \z
  2. I don't know why, but original command deleted one extra space. Adding L fixes that for me.
lenormf commented 6 years ago

The -no-hooks flag was removed, are you sure this snippet will run in the development version of Kakoune?

MarSoft commented 6 years ago

@lenormf oops, you're right. This snippet version I pasted is for the stable version, 2018-04-13. Updated my comment.

MarSoft commented 6 years ago

Tested with current dev version. It is enough to just remove -no-hooks flag as it is now the default.

andreyorst commented 6 years ago

It seems that there's needed a more complex logic. I've tried to workaround the problem with two situations:

# inserting 'z' before executing <a-;> is used to prevent spaces only line from cleaning.
# stupid workaround, 'hdh' will later delete this 'z'
map global insert <backspace> 'z<a-;>:insert-bs<ret>'

hook global InsertChar \t %{
    exec -draft h@
}

define-command -hidden insert-bs %{
    try %{
        # delete indentwidth spaces before cursor
        exec -draft hdh %opt{indentwidth}HL <a-\;> S.<ret> <a-k><space><ret> d
    } catch %{
        exec <backspace>
    }
}

The problem is if I try to delete single space from the beginning of the line, 4HL will also select previous line. And if word is less than indentwidth this exec will delete spaces before word.

Neither the hook presented before, nor mine are not working exactly how it should.


Given this text, where is cursor, and is space:

text⋅⋅⋅⋅█

Pressing backspace should do:

text█

With tab which is smaller then indentwidth:

te⋅⋅█

Backspace should do:

te█

and hook proposed in https://github.com/mawww/kakoune/issues/1038#issuecomment-408714355 will do:

te⋅█

With 8 spaces and indentwidth = 4:

⋅⋅⋅⋅⋅⋅⋅⋅█

Backspace should do:

⋅⋅⋅⋅█

and hook proposed in https://github.com/mawww/kakoune/issues/1038#issuecomment-408714355 will remove line completely.

And the wiki page https://github.com/mawww/kakoune/wiki/Indentation-and-Tabulation#indentation-and-tab-handling--through-hooks, that suggest those two hooks will work only at the beginning of the line. Vim's expand tab disables tab character insertion completely. However I would like to have an option for opposite behavior - to insert tabs only at the beginning of the line, and use spaces after any text, to do the indentation with tabs, and alignment with spaces

Screwtapello commented 6 years ago

I'm currently using the Indentation and tab handling example from the wiki to emulate Vim's 'smarttab' feature, and I'm quite happy with it. I don't know if it handles tabs after the first non-whitespace character of a line, but if I needed to align things vertically I'd use the & key rather than manually hitting space myself.

andreyorst commented 6 years ago

I don't know if it handles tabs after the first non-whitespace character of a line

It doesn't. I'm using this combination instead:

hook global InsertChar \t %{
    exec -draft h@
}
hook global InsertDelete ' ' %{ try %{
  execute-keys -draft 'h<a-h><a-k>\A\h+\z<ret>i<space><esc><lt>'
}}

but if I needed to align things vertically I'd use the & key rather than manually hitting space myself.

It's not always faster, sometimes hitting tab once inserts needed amount of spaces already.

And the problem is of deletion indentwidth or less spaces ((0:indentwidth]) after first non-whitespace character, not inserting.

andreyorst commented 6 years ago

Also, shouldn't editor config affect Tab key? .editconfig definitely affects changing indentation with > but Tab still inserts \t at the beginning of the line. I think that if user specifies that he wants to be able to get rid of tabs completely it shouldn't involve programming hooks, as it is done in nearly every editor.

I've made these two functions, that I can call per filetype, since .editorconfig doesn't affect Tab:

define-command noexpandtab %{
    hook -group noexpandtab global NormalKey <gt> %{
        execute-keys -draft "xs^\h+<ret><a-@>"
    }
    remove-hooks global expandtab
}

define-command expandtab %{
    hook -group expandtab global InsertChar \t %{
        execute-keys -draft h@
    }
    hook -group expandtab global InsertDelete ' ' %{ try %{
        execute-keys -draft 'h<a-h><a-k>\A\h+\z<ret>i<space><esc><lt>'
    }}
    remove-hooks global noexpandtab
}

However the deletion part still needs fixing. It can't simply be done by shifting line to the left, because if we wan't delete indentwidth amount of characters in the middle of the line we'll fail.

dendik commented 5 years ago

I support @kballard in that some kind of softtab behavior should be shipped with the editor at some point in the future. It is an important detail of "solid tool" vs. "odd toy" feeling for newcomers.

Here is another implementation that aims to be universal-ish.

Whenever tab/backspace is hit:

This way it honors aligntab and indentwidth (but not tabstop... actually, why are there two different options?) and editorconfig.

Possibly undesired behavior: if the cursor is in the first indentwidth characters <backspace> causes the line to dedent anyway.

# source: https://github.com/mawww/kakoune/issues/1038#issuecomment-437524674

map global insert <tab> '<a-;>:insert-tab<ret>'
map global insert <backspace> '<a-;>:insert-backspace<ret>'

define-command -hidden insert-tab %{
    try %{
        raise-if-not-starts-with "^\h*.\z"
        exec -draft <esc><gt>
    } catch %{
        exec <tab>
    }
}

define-command -hidden insert-backspace %{
    try %{
        raise-if-not-starts-with "^\h+.\z"
        exec -draft <esc><lt>
    } catch %{
        exec <backspace>
    }
}

define-command -hidden -params 1 raise-if-not-starts-with %{
    exec -draft <a-h> <a-k> %arg{1} <ret>
}
lenormf commented 5 years ago

Consider placing the code in the wiki, and back-linking the page in here to make it more visible.

andreyorst commented 5 years ago

Ok. so I've fixed all main issues with deindenting and calling hooks several times. Thanks @dendik for providing proper deindenting function that I've revamped to a hook. Here's the result:

define-command noexpandtab %{
    remove-hooks global noexpandtab
    hook -group noexpandtab global NormalKey <gt> %{ try %{
        execute-keys -draft "<a-x>s^\h+<ret><a-@>"
    }}
    set-option global aligntab true
    remove-hooks global expandtab
    remove-hooks global smarttab
}

define-command expandtab %{
    remove-hooks global expandtab
    hook -group expandtab global InsertChar '\t' %{ execute-keys -draft h@ }
    hook -group expandtab global InsertKey <backspace> %{ try %{
        execute-keys -draft <a-h><a-k> "^\h+.\z" <ret>I<space><esc><lt>
    }}
    set-option global aligntab false
    remove-hooks global noexpandtab
    remove-hooks global smarttab
}

define-command smarttab %{
    remove-hooks global smarttab
    hook -group smarttab global InsertKey <tab> %{ try %{
        execute-keys -draft <a-h><a-k> "^\h*.\z" <ret>
    } catch %{
        execute-keys -draft h@
    }}
    hook -group smarttab global NormalKey <gt> %{ try %{
        execute-keys -draft "<a-x>s^\h+<ret><a-@>"
    }}
    set-option global aligntab false
    remove-hooks global expandtab
    remove-hooks global noexpandtab
}

This code adds these three commands to toggle different policy when using Tab and > keys:

The demo: demo (the deindenting with Backspace isn't shown but it works as you expect it to)

These behaviors are pretty common in most editors nowadays, I think that they should be shipped with the editor by default. @mawww do you think that this is too complex and will bring lots of overhead if it will be added as a script that is shipped with the editor by default? I've added this to the Wiki for now, but I think that this should be good default commands since they are the extension of the fingers for people who use different styles of indentation and alignment. I can make a PR if you'll decide to add this.

dpc commented 5 years ago

@andreyorst Any chance you could just publish this as a git repo? I might be the only one, but now that there's plug.kak I actually don't care at all what is shipped with standard kakoune. It's easier to just copy&paste a plug "x/y" %{ ... } from someone, than having to keep up with what is bundled with kak by default. :D

alexherbo2 commented 5 years ago

https://github.com/alexherbo2/space-indent.kak

dpc commented 5 years ago

@alexherbo2 Glorious!

plug "alexherbo2/space-indent.kak" %{
    hook global WinSetOption filetype=rust %{
        space-indent-enable
    }
}

I think it's working right, but for reference, and just in case, I'm posting it here. Thank you!

andreyorst commented 5 years ago

https://github.com/alexherbo2/space-indent.kak

@alexherbo2 this is totally different implementation, which isn't providing option to switch styles. Why you've posted it here if @dpc asked for different thing?

@dpc I don't think that such a basic thing should be considered as a plugin. It feels like builtin feature, and like @dendik said:

It is important detail of "solid tool" vs. "odd toy" feeling for newcomers.

So in my opinion this should be shipped with kakoune instead of a plugin. I appreciate that you are saying about plug.kak, but in my opinion when you're adding basic functions to the editor by installation of plugins, it shows only that the editor isn't providing solid experience by default. If @mawww will decline the PR I'll probably make a git repo for it, but again, this is too simple to be a plugin.

dpc commented 5 years ago

So in my opinion this should be shipped with kakoune instead of a plugin.

If the end result is the same, I don't really care.

Personally, I am a "small stdlib" person. I like the way eg. Rust and Node ecosystems work. Small/none stdlib (though I don't like JS, but that's another story...).

If I was running the show, I would make kak contain only what is "absolutely necessary", ship with only one script: plug.kak (and maybe some common interoperability primitives), and ask everyone to implement everything as a plugin and come back to me only when they really can't implement what they want well.

Now, is tab to space conversion "absolutely necessary"? If your snippet works well, then I guess you've proved that it isn't. :smile:

I'm aware that there are reasons to have "fat stdlib": not trusting too many 3rd parties, no internet access, good initial features etc. For this reason, maybe, at some point, I would pick the most established 3rd party plugins, and start shipping them by default. But that's only after the dust has settled, things cooled down etc.

Anyway, just my opinion.

@andreyorst So it looks like your version does some stuff better. If you've put it out as a plugin, I would be happy to use. Or I might get to it myself at some point, though I am inexperienced, so I wont be able to help people with fixing it etc.

alexherbo2 commented 5 years ago

@andreyorst Because it answers the use case to convert tabs to spaces for indenting.

Examples

Indent with spaces

┌──────────────────────────────────────┐
│ What ┊     Input     ┊    Output     │
├──────────────────────────────────────┤
│      ┊ void main() { ┊ void main() { │
│  ↹   ┊ ▌             ┊ ··▌           │
│      ┊ }             ┊ }             │
╰──────────────────────────────────────╯

Keep tabs in other scenarios

┌───────────────────────────────────────┐
│ What ┊     Input     ┊     Output     │
├───────────────────────────────────────┤
│      ┊ void main() { ┊ void main() {  │
│  ↹   ┊ ··printf('▌') ┊ ··printf('→▌') │
│      ┊ }             ┊ }              │
╰───────────────────────────────────────╯

Deindent

┌──────────────────────────────────────┐
│ What ┊     Input     ┊    Output     │
├──────────────────────────────────────┤
│      ┊ void main() { ┊ void main() { │
│  ⌫   ┊ ··🅿rintf('→') ┊ 🅿rintf('→')   │
│      ┊ }             ┊ }             │
╰──────────────────────────────────────╯
andreyorst commented 5 years ago

I think it answers the use case to convert tabs to spaces used for indenting.

Single use case. I'll wrap my solution to a plugin for now. I see that it isn't really wanted by default

andreyorst commented 5 years ago

So it looks like your version does some stuff better. If you've put it out as a plugin, I would be happy to use. Or I might get to it myself at some point, though I am inexperienced, so I wont be able to help people with fixing it etc.

https://github.com/andreyorst/smarttab.kak

dpc commented 5 years ago

@andreyorst Thank you! You have my star. :)