Closed jtmarmon closed 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.
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
@casimir I will put those in the wiki if you don't mind.
Just for the record, by pressing @ on a selection tabs will be converted to the number of spaces specified by the tabstop
option.
@doppioandante go ahead, IIRC I snatched these on IRC
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.
@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).
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.
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.
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.
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 if
s 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.
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.
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.
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.
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.
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.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.
There's so much frustration and misreading going on I'm going to let other take care of this now ;)
@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.
@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).
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.
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.
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.
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.
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.
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.
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>
}
}
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:
\Z
should be replaced with \z
The -no-hooks
flag was removed, are you sure this snippet will run in the development version of Kakoune?
@lenormf oops, you're right. This snippet version I pasted is for the stable version, 2018-04-13
. Updated my comment.
Tested with current dev version. It is enough to just remove -no-hooks
flag as it is now the default.
It seems that there's needed a more complex logic. I've tried to workaround the problem with two situations:
<a-;>
from insert mode will clear it fully, which should instead leave the contents and then delete indentwidth
amount of spaces.indentwidth
, but can be less, so I tried to make this command delete not more than indentwidth
amount of spaces (but failed):# 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.
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
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.
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.
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.
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>
}
Consider placing the code in the wiki, and back-linking the page in here to make it more visible.
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:
noexpandtab
- use tab
for everything.\t
character, and > will use \t
character when indenting. Aligning cursors with & uses \t
character.expandtab
- use space
for everything.%opt{tabstop}
amount of spaces, and > will indent with spaces.smarttab
- indent with tab
, align with space
.\t
character if your cursor is inside indentation area, e.g. before any non-whitespace character, and insert spaces if cursor is after any non-whitespace character. Aligning cursors with & uses space
.The 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.
@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 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!
@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.
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.
@andreyorst Because it answers the use case to convert tabs to spaces for indenting.
┌──────────────────────────────────────┐
│ What ┊ Input ┊ Output │
├──────────────────────────────────────┤
│ ┊ void main() { ┊ void main() { │
│ ↹ ┊ ▌ ┊ ··▌ │
│ ┊ } ┊ } │
╰──────────────────────────────────────╯
┌───────────────────────────────────────┐
│ What ┊ Input ┊ Output │
├───────────────────────────────────────┤
│ ┊ void main() { ┊ void main() { │
│ ↹ ┊ ··printf('▌') ┊ ··printf('→▌') │
│ ┊ } ┊ } │
╰───────────────────────────────────────╯
┌──────────────────────────────────────┐
│ What ┊ Input ┊ Output │
├──────────────────────────────────────┤
│ ┊ void main() { ┊ void main() { │
│ ⌫ ┊ ··🅿rintf('→') ┊ 🅿rintf('→') │
│ ┊ } ┊ } │
╰──────────────────────────────────────╯
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
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.
@andreyorst Thank you! You have my star. :)
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 tokakrc
: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.