It all started with the fact that the tagbar stopped working for me . The plugin crashed with an error, supposedly the current version of my Exuberant Ctags is not Exuberant at all. Having rummaged a bit in the source, I realized that the last external command ended with an error, and v: shell_error gave -1, which means, judging by the documentation of vim, that "the command could not be executed". I did not dig further and installed fzf . Fzf , like ctrlp , allows for a fuzzy search in files, tags, buffers, ..., but unlike the latter, it works much faster, but not without drawbacks. The application works directly with the terminal and every time overwrites the history of the entered commands. It also means that we cannot display the search results in the buffer ( neovim , judging by some screencasts, it can), for example, to the right of the main buffer when we are looking for the right tag. Unlike sublime, fzf does not give more weight to the file name, which is why I often received in the top not the results that I expected to see. Everything else, the lack of complete freedom in setting up the color scheme, which in general is not too important for the average user, but not for me, with my increased attention to detail. By freedom, I mean, at a minimum, the distinction between color for ordinary (normal) text and the query string.
All this prompted me to write my own plugin, the appearance of which resembles the standard directory viewer - netrw . I will describe the problems I have encountered and the ways to solve them, assuming that this experience may be useful to someone.
First of all, I would like to have a small tour for those who are taking the first steps in the vim script. Variables have prefixes, some of which you have already seen and written yourself. Usually, the plug-in is configured using global variables with the prefix g:. When writing your plugin, it is appropriate to use the s: prefix, which makes variables available only within the script. To refer to the function argument, use the prefix a:. Variables without a prefix are local to the function in which they were declared. The full list of prefixes can be viewed with the command : help internal-variables .
To manage the buffer, there are two very simple functions: getline and setline . With their help, you can insert search results into the buffer or get the value of the query. I will not dwell on the description of each function, since it is often clear from the name that it does. Almost any keyword from this article can be searched in the documentation, therefore : help getline or : help setline , and for a complete picture I advise you to look at : help function-list with a list of all functions grouped by sections.
Developments
Vim provides many events from the box, however, when writing your own plugin, you may need to create your own events. Fortunately, this is done very simply.
// CustomEvent autocmd User CustomEvent call ... // , "No matching autocommands", if(exists("#User#CustomEvent")) doautocmd User CustomEvent endif
Autoload
I set all the functions in my plugin finder prefix # . This is vim’s built-in autoload mechanism, which searches for a file with the same name in runtimepath . The finder # call function should be located in the runtimepath / finder.vim file, the finder # files # index function should be located in the runtimepath / finder / files.vim file. Then you need to add the plugin to the runtimepath .
set runtimepath+=path/to/plugin
But it is better to use plugin manager for these purposes, for example, vim-plug .
Composite teams
Often there is a situation where the team needs to be combined from different pieces or just insert the value of a variable. For these purposes, in vim there is an execute command, which is often convenient to use with the printf function.
execute printf('syntax match finderPrompt /^\%%%il.\{%i\}/', b:queryLine, len(b:prompt))
So, all we need is a query string and a search result. The input function is responsible for user input in vim, but as far as I know, it does not allow placing the input line at the top, which is quite important when it comes to searching by tags, since it is more convenient to display tags in the order in which they are presented in the file . Moreover, over time, I decided to make a similar hat, which shows netrw . The input string needed to be implemented in the buffer, and here the first difficulties appear.
Request
To get the value of the request, we need to know the line containing the input field and the offset relative to the prompt, and also to set an event handler for the TextChangedI event. Since for anyone who has previously programmed, there should be nothing complicated at this stage, I will omit the code; I will add only that the handler needs to be hung up with the <buffer> attribute.
autocmd TextChangedI <buffer> call ...
Prompt
Since the tooltip is on the same line as the user input, you need to somehow fix it. For these purposes, it would be possible to clear the value of the backspace option, which is responsible for the behavior of keys such as <BS> and <Del>. In particular, I was interested only in eol and start . Eol permits deletion of the end of line character and, accordingly, merging of lines; start also permits deletion of only the text that was entered after the start of insert mode. It was quite convenient and simple: I insert the "Files>" hint, for example, then I start typing the text and when deleting the text, the hint remained in place. True, I did not take into account one moment - quite a lot of logic is needed for such a plug-in to work and getting into normal mode was common practice. Any mapping could easily start a new "session" and the text that was entered earlier ceased to be deleted. I just needed to press <Esc>, for example:
inoremap <Cj> <Esc>:call ...
I had to create a mapping for <BS> and delete the text manually.
inoremap <buffer><BS> <Esc>:call finder#backspace()<CR>
There was some strange flicker of the cursor, which eventually became terribly annoying. A lot of time passed before I realized that I was guilty of the transition to the command-line mode, which we usually initiate by pressing :. At this very moment, the cursor that is above the text disappears. The flicker effect is the stronger, the "heavier" the called function. There were attempts to hang the handler on the TextChangedI event, which checked the current position of the cursor, and if the cursor was dangerously close to the tooltip, then it was necessary just to bind <BS> to do nothing. Unfortunately, sometimes 1 character was deleted. After some time, a solution was found - the attribute <expr> .
map {lhs} {rhs}
Where {rhs} is a valid expression (: help expression-syntax), the result of which is inserted into the buffer. Special keys such as <BS> or <Ch> must be framed with double quotes and escaped with a \ ((: help expr-quote) character).
inoremap <expr><buffer><BS> finder#canGoLeft() ? "\<BS>" : "" inoremap <expr><buffer><Ch> finder#canGoLeft() ? "\<BS>" : "" inoremap <expr><buffer><Del> col(".") == col("$") ? "" : "\<Del>" inoremap <expr><buffer><Left> finder#canGoLeft() ? "\<Left>" : ""
Output
In order to exit the buffer, you can bind <Esc>. The unpleasant point is that some key combinations start with the same sequence of characters as <Esc>. If you enter the input mode and press <Cv>, then any of the arrows, you can see ^ [ as a prefix. For example, for the left arrow, the terminal sends ^ [OD vim'u. Therefore, when any arrow or <S-Tab> is pressed, vim will perform the action assigned to <Esc>, then try to interpret the rest of the characters: for the left arrow, it will insert the empty line at the top (O) and the capital literal "D" on the same line. The esckeys option indicates whether new characters should be expected if the sequence starts with <Esc>, that is, ^ [ , in input mode. It would seem that it is necessary, but it works only if we do not change the behavior of <Esc>.
There could be your joke, dear IDE user.
Perhaps I missed something, but for good reason on various resources it is advised not to change the behavior of this key. If <S-Tab> is not so important, then the arrows would be a good idea to bind to the choice of the next / previous entry. Therefore, instead of <Esc>, we use the InsertLeave event. This entails new problems. How to call a function without leaving the input mode? Reading the documentation, I came across one interesting point - the <Ctrl-c> combination that exits insert mode without triggering the InsertLeave event, but, which is rather strange, if <Cc> is present in the mapping, it does not work and InsertLeave still pops up. Plying across the expanses of the Internet, a solution was found, the general form of which is:
inoremap <BS> <Cr>=expr<CR>
From the documentation it follows that this is an expression register . This is exactly what I needed, except that the result of the expression was inserted into the buffer. Actually, the whole plugin is built on this, since all gestures occur in insert mode. In order not to return an empty string in each function (if this is not done, the function will return 0), I decided to use an intermediary who calls the desired function.
function! finder#call(fn, ...) call call(a:fn, a:000) return "" endfunction
The namespace a: is responsible for accessing the arguments of the function, and the variable a: 000 contains a list of optional parameters. Since it is now possible to write the application logic without leaving the input mode, it would be possible to use the backspace option. However, as I later learned, resetting the value of this option outraged delimitMate , because of which he could not function properly, and I decided to abandon these attempts.
Backend
Just nothing and we already have a pack of useless pixels. It's time to add some life to our buffer. Since vim script can hardly be called a fast language or a language in which it is pleasant to write something complicated, I decided to write a backend on D. Because the fuzzy search is for me too lazy to realize it is not needed, it will be a search taking into account the exact entry, and I decided that I would compare the source string with the user's query character-wise, finding that it would be much faster than using regular expressions. Considering that I actually had 4 modes: ^ query, query, query $, ^ query $, the code looked a bit unattractive. Seeing what I wrote, there was a desire to delete everything and search for regulars. After a while, I realized that what was written could be done using standard Unix tools and decided to return to using grep , thoughts about which I had from the very beginning, but which I discarded due to the presence of "complex" logic. The difficulty was that I had to search by file name, sort by the length of the file path and output not the source line, but its index. It is worth noting that Unix grep was 4 times faster than std.regex , which is in D.
To get the file name, you can use the basename program, but, unfortunately, it does not read the standard input stream and works only directly with parameters. You can also use sed 's!. * / !!' which will cut everything to the last. Vim's built-in function fnamemodify is also suitable .
I decided to do the sorting by means of vim, since it is simpler in terms of implementation and creating my own extensions. The sort function is responsible for sorting, for which you will need to write a comparator .
Flicker cursor
In general, this is a pretty hateful thing. Flicker of the cursor can be seen by setting the incsearch option. Just try to look for something in the buffer and watch the cursor while typing. If everything is clear with the change in the behavior of <BS>, then writing <expr> everywhere, as it turned out, is impossible. This flag prohibits changing any lines other than the one on which the cursor is located. Therefore, for the rest of the logic, the above-mentioned expression register is used , which, like, removes the cursor from the current position to the time the expression is executed. Since the search for several thousand files takes some time, the effect of the cursor flashing when typing each character appears. I must say that non-blocking vim arrived in time, and specifically the function timer_start . When the buffer began to render asynchronously, the problem was gone. Not the best solution, I must say, but I did not find anything more suitable. This is the only reason why the plugin requires vim 8th version.
The third time the problem overtook when I had to do a preview. The cursor blinked at the moment when the cursor position was changing in one of the buffers and the screen was being drawn. I am afraid there is no distortion in the spirit: "to highlight the syntax of the character under the cursor" is not enough, and I decided to leave such machinations until better times.
Notice traces
Since we are constantly changing the contents of the buffer, the tabline will tell us that the buffer has been changed, and the work in the input mode will be accompanied by the corresponding inscription at the bottom left. I don’t know about you, but I like minimal design, and I would like to remove such things. Also, it would be nice to hide the ruler and statusline . To prevent vim from tracking changes in the buffer, you can use the buftype option.
setlocal buftype=nofile
With the status bar, ruler and caption -- INSERT --
bit more complicated, because the options that are responsible for displaying them are global, which means we need to restore the previous value when exiting the buffer. For this, it is convenient to listen to the OptionSet event.
set noshowmode set laststatus=0 set rulerformat=%0(%) redraw
Instead of a rulerformat, one could use a noruler , but the latter requires redrawing the screen with pre-cleaning (redraw!), Which causes an unpleasant effect to the eye.
Here I would like to summarize the most important points about the syntax that played an important role in the work of the plugin.
Element | Example | Description |
---|---|---|
\ c | \ c. * | Ignore case when searching. |
. {-} | . {-} p | "Not greedy" analog . * |
\ zs , \ ze | . {-} \ zsfoo \ ze. * | Start and end of entry, respectively. |
\ @ = , \ @ <= | \ (hidden \) \ @ <= text | The so-called zero-width atoms - cut out the previous atom from the entry. |
\% l | \% 1l | Search on a specific line. |
\ & | p1 \ & p2 \ & .. | Conjunction operator. |
Basename
Since we are working with the file name, we need to specify a region that limits the highlighting of the entry.
\(^\|\%(\/\)\@<=\)[^\/]\+$
config / foobar.php
foobar.php
Now you need to highlight the desired characters. For these purposes, you can use the conjunction operator \ & (:: help branch).
\(^\|\%(\/\)\@<=\)[^\/]\+$\&.\{-}f
config / f oobar.php
f oobar.php
Tip : since this is a regular pattern (: help pattern), you can test everything in a separate buffer by pressing / .
Comments
In fzf there is a useful visual feature - the presence of text that does not affect the search results, that is, comments. At first, I wanted to use some kind of invisible Unicode character to mark the beginning of a comment (the space, for obvious reasons, does not fit), but later I came across a useful feature for the syntax group — conceal . In short, conceal hides any text, leaving it in the buffer. Two options are responsible for the behavior of conceal : conceallevel and concealcursor . At a certain setting, the text may not be hidden, so I advise you to read them. In my plugin, the lines are as follows:
text#finderendline...
where ... is an optional comment, and #finderendline is hidden. An example of hidden text:
syntax match hidden /pattern/ conceal
Scroll
The work of the plugin in the input mode gives a lot of problems, one of which is scrolling. Since the cursor is needed at the place where the request was entered, we cannot move it to highlight the desired line. In order to navigate through the search results, you can use the syntax by creating the appropriate group. Ordinary atom \% l fits perfectly. For example, \ ^% 2l. * $ Selects the second line.
My screen contains 63 lines of text, and since there can be many more entries, the question is how to get to the 64th and subsequent lines. Since the header and the query string should always be in the visible part of the screen, when approaching the end of the screen, we will cut (put into a temporary array) the first (second, third, ...) entry until we reach the end. When moving up, everything is exactly the opposite.
With the availability of this article, vim seems to be hinting - it was necessary to use input , however, when everything is already behind, I am glad that I took a non-standard way and got such valuable experience. That's all, information on installing, using and creating your own extensions can be found in the repository .
Having received a certain base after writing the plugin, I wanted to simplify my life a little.
Exit insert mode
Those who read my previous article know that I previously used sublime to edit text and code. There are significant differences between sublime and vim in how they handle keyboard shortcuts. If a sublime, when entering a combination, inserts text without delay, then vim first waits for a certain time, and only after inserts the desired character, if the combination is "broken". From the very beginning of using vim-mode in general and vim'a in particular, I used df to exit insert mode. It became so habitual that any attempts at retraining to jj , for example, did not succeed. Each time, typing d and a character other than f , I observed a nasty jerk. I decided to repeat the behavior from sublime.
let g:lastInsertedChar = '' function! LeaveInsertMode() let reltime = reltime() let timePressed = reltime[0] * 1000 + reltime[1] / 1000 if(g:lastInsertedChar == 'd' && v:char == 'f' && timePressed - g:lastTimePressed < 500) let v:char = '' call feedkeys("\<Esc>x") endif let g:lastInsertedChar = v:char let g:lastTimePressed = timePressed endfunction autocmd InsertCharPre * call LeaveInsertMode()
Maybe this is not the best code, but it performs its task. The point is this: after pressing d , we have half a second to press f . If the latter is true, f is not printed, and d is removed from the buffer. After which the editor goes into normal mode.
Read-only files
The remaining minor addition will be a ban on editing certain files.
function! PHPVendorFiles() let path = expand("%:p") if(stridx(path, "/vendor/") != -1) setlocal nomodifiable endif endfunction autocmd Filetype php call PHPVendorFiles()
This code prohibits editing a .php file if it is located in the vendor directory.
List of changes to my environment since the publication of the first article.
Source: https://habr.com/ru/post/316752/
All Articles