[vim] Text object selection for function parameters

295 views
Skip to first unread message

herm...@free.fr

unread,
Aug 24, 2007, 6:08:36 AM8/24/07
to vim...@googlegroups.com
Hello,

While I was categorizing tips on the new tips site, I ran accross a tip [1] that
was doing something I though it was impossible: defining our own text object
selection command.

A few minutes later, I had something for selecting function parameters.

------ %< --------
" Vim7.0 required
function! s:SelectParam(inner)
let pos = getpos('.')
let b = search(s:begin_{a:inner}.'[^(,);]*\%#', 'bcW')
if 0 == b
throw "Not on a parameter"
endif
normal! v
let e = search('\%#[^(,);]\{-}'.s:end_{a:inner}, 'ceW')
if 0 == e
call setpos('.', pos)
throw "Not on a parameter"
endif
endfunction

onoremap i, :<c-u>call <sid>SelectParam(1)<cr>
vnoremap i, :<c-u>call <sid>SelectParam(1)<cr><esc>gv
onoremap a, :<c-u>call <sid>SelectParam(0)<cr>
vnoremap a, :<c-u>call <sid>SelectParam(0)<cr><esc>gv
------ %< --------

However, i'm not really happy with this solution and have a few questions for
you.

a- What "a," should select ? Should it select everything and even the ending
comma? Should it also select the closing brace?
And what about the opening brace and the comma before the parameter?


b- In the same idea, how a <count> on "i," and "a," should work?
Should we use the same kind of approach than the one of "iw" ?
For instance
- first select the inner part
- then select as "a," would have
- then the next "inner" part
- then as two "a," would have
- etc until the closing brace (BTW, should it be selected at the end?)


c- Is there a way to really abort when we are not within a balanced parameter
context.
For instance, if we suppose the following code:
void f(int i, int j
A "di," on the second parameter raises an error, but the parameter is still
deleted.

Is there a way to really abort?


d- Another problem I see is related to parameters having embedded commas or
balanced braces -- or even ignoring comments. Typically when the type of a
formal parameter is a function pointer type, or when the actual parameter is an
expression made of a function call.
Right now I don't see any neat and efficient way to handle this issue. Did any
of you already solved a similar one?

Thanks for any input (ergonomical as technical).

[1] http://vim.wikia.com/wiki/Indent_text_object

--
Luc Hermitte
http://hermitte.free.fr/vim/

A.Politz

unread,
Aug 24, 2007, 3:44:31 PM8/24/07
to v...@vim.org

a),b)
What is s:start and s:end ? I assume they are in '(,'.
I don't understand the difference between 'a' and 'i' mappings.
What are they supposed to select ?
What is the inner part of 'fun(Null,fun2(fun3(a,b,g(NULL))))' ?


c) You have to leave visual, before you throw the exception.

d) It is impossible to do this with regex for every valid argumentlist.

Here is a simple PDA that splits a c argumentlist into a list :

--------%<---------------

func! GetCFargs( fargs )
let fargs = matchstr(a:fargs,'^[^(]*(\zs.*') "skip everything before
first (
let paren_stack = 1 " first paren
let esc_stack = 0 " for \"
let result = []
let i = 0
let states = { 'INSTRING' : 0, 'INCOMMENT' : 1 , 'NORMAL' :2 ,'CHAR' : 4 }
let state = states.NORMAL
let arg = ""
while i < len(fargs)
let c1 = fargs[i]
let c2 = i < len(fargs) ? fargs[i+1] : ''
let arg.= c1
let i+=1
if state == states.INSTRING
if c1 == '\'
let esc_stack+=1
elseif c1 == '"' && esc_stack%2 == 0
let state = states.NORMAL
else
let esc_stack=0
endif
elseif state == states.INCOMMENT
if c1.c2 == '*/'
let state = states.NORMAL
endif
elseif state == states.CHAR
if c1 == "'"
let state = states.NORMAL
endif
else "NORMAL
if c1 == ',' && paren_stack == 1
call add(result,arg[0:-2])
let arg=''
elseif c1 == '('
let paren_stack+=1
elseif c1 == ')'
let paren_stack-=1
elseif c1 == '"'
let state = states.INSTRING
elseif c1.c2 == '/*'
let state = states.INCOMMENT
elseif c1 == "'"
let state = states.CHAR
endif
endif
if paren_stack == 0
call add(result,arg[0:-2])
break
endif
endwhile
if state != states.NORMAL || paren_stack
echo "Error!"
endif
echo result
endfun

map <f6> :call GetCFargs(getline('.'))<cr>

finish

(a,b,c,d)
(a(b,c),b(d,e))
(a(b))
(foo->bar(NULL),12,g(h,s(f,1)))
(/* comment */words->/* ,,,((())\"asd, */text("foos),\,\"tr\ing",/*
comment */0,')'),foo.bar(bar(0,1+2+3,2,bar(8)),42),1*2^25)

--------%<---------------

Interesting idea though.

-ap


A.Politz

unread,
Aug 30, 2007, 7:58:48 PM8/30/07
to vim...@googlegroups.com
herm...@free.fr wrote:

>
>
>d- Another problem I see is related to parameters having embedded commas or
>balanced braces -- or even ignoring comments. Typically when the type of a
>formal parameter is a function pointer type, or when the actual parameter is an
>expression made of a function call.
>Right now I don't see any neat and efficient way to handle this issue. Did any
>of you already solved a similar one?
>
>
>
>Thanks for any input (ergonomical as technical).
>
>[1] http://vim.wikia.com/wiki/Indent_text_object
>
>
>

With searchpos() this becomes easier than I first
thought.

"assert("cursor at or in front of opening paren")
func! FargsPos()
let res = []
let p0 = getpos('.')
"find first paren
if !search('(','Wc')
return []
endif
call add(res,getpos('.'))
"goto closing paren
if searchpair('(','',')','W','Skip()') <= 0
call setpos('.',p0)
return []
endif
let end = getpos('.')
"go back to opening paren
call setpos('.',res[0])
"search for ',' , while not at the closing paren
while searchpair('(',',',')','W','Skip()') > 0 && getpos('.') != end
call add(res,getpos('.'))
endwhile
call add( res , end)
call setpos('.',p0)
return res " = positions of '(' ',' ... ')'
endfun

func! Skip()
return = synIDattr(synID(line('.'), col('.'), 0),'name') =~?
'string\|comment\|character'
endfun


-ap

herm...@free.fr

unread,
Sep 3, 2007, 10:49:28 AM9/3/07
to vim...@googlegroups.com
Hello,

"A.Politz" <pol...@fh-trier.de> wrote:

[snip script]

Your use of searpair() reminded of the second argument I had completly missed.
Applying the same kind of technique than the one I used in my bracketing system,
here is the lastest version of the parameters selection mappings.

It's not yet perfect, but quite functional.

Thanks a lot!

---------- >% ------------
" Notes:
" * "i," can't be used to select several parameters with several uses of
" "i," ; use "a," instead (-> va,a,a,). This is because of single
" letter parameters.
" However, "v2i," works perfectly.
" * Vim7+ only
" * The following should be resistant to &magic, and other mappings
onoremap <silent> i, :<c-u>call <sid>SelectParam(1,0)<cr>
xnoremap <silent> i, :<c-u>call <sid>SelectParam(1,1)<cr><esc>gv
onoremap <silent> a, :<c-u>call <sid>SelectParam(0,0)<cr>
xnoremap <silent> a, :<c-u>call <sid>SelectParam(0,1)<cr><esc>gv

function! s:SelectParam(inner, visual)


let pos = getpos('.')

if a:visual ==1 && s:CurChar("'>") =~ '[(,]'
normal! gvl
else
let b = searchpair('\V(\zs','\V,\zs','\V)','bcW','s:Skip()')


if 0 == b
throw "Not on a parameter"
endif
normal! v

endif
let cnt = v:count <= 0 ? 1 : v:count

while cnt > 0
let cnt -= 1
let e = searchpair('\V(', '\V,','\V)', 'W','s:Skip()')
if 0 == e
exe "normal! \<esc>"


call setpos('.', pos)

throw "Not on a parameter2"
endif
if cnt > 0
normal! l
endif
endwhile
if a:inner == 1
normal! h
endif
endfunction

function! s:CurChar(char)
let c = getline(a:char)[col(a:char)-1]
return c
endfunction

func! s:Skip()
return synIDattr(synID(line('.'), col('.'), 0),'name') =~?
\ 'string\|comment\|character\|doxygen'
endfun
------------- >% ----------------

I'll add it into my C&C++ suite, and I guess I'll upload it soon as an
independant plugin on vim.org.

--
Luc Hermitte

A.Politz

unread,
Sep 3, 2007, 1:15:26 PM9/3/07
to vim...@googlegroups.com

"Your use of searpair() reminded of the second argument I had completly missed."

Which second argument ?


That works already pretty well, nevertheless I made a couple
comments and attached my own version, if you don't mind. I
don't think my version is a 100% thing either. I think
the best approach would be to first define exactly the
behaviour of each command in any state on any part of the
arguments, and model a code which satisfies this behaviour.

What's still missing is :
[cd], - delete, change till end of arg , like 'dw'

-ap

--------------%<---------------------------------------


" Notes:
" * "i," can't be used to select several parameters with several uses of
" "i," ; use "a," instead (-> va,a,a,). This is because of single
" letter parameters.
" However, "v2i," works perfectly.
" * Vim7+ only
" * The following should be resistant to &magic, and other mappings
onoremap <silent> i, :<c-u>call <sid>SelectParam(1,0)<cr>
xnoremap <silent> i, :<c-u>call <sid>SelectParam(1,1)<cr><esc>gv
onoremap <silent> a, :<c-u>call <sid>SelectParam(0,0)<cr>
xnoremap <silent> a, :<c-u>call <sid>SelectParam(0,1)<cr><esc>gv

function! s:SelectParam(inner, visual)
let pos = getpos('.')

"Does not work is [(,] is e.g. inside a string


if a:visual ==1 && s:CurChar("'>") =~ '[(,]'

"Why 'l' ? searchpair() first moves the cursor anyway.
"Besides it does not work at the eol.
normal! gvl
else
"regardless of 'magic' '(', ',' and ')' always match itselves.


let b = searchpair('\V(\zs','\V,\zs','\V)','bcW','s:Skip()')
if 0 == b

"I would prefer to simply return, like 'dw' in an empty buffer.


throw "Not on a parameter"
endif
normal! v
endif
let cnt = v:count <= 0 ? 1 : v:count

while cnt > 0
let cnt -= 1
let e = searchpair('\V(', '\V,','\V)', 'W','s:Skip()')

"Like 999d in a buffer with 10 lines, do what is possible.


if 0 == e
exe "normal! \<esc>"
call setpos('.', pos)
throw "Not on a parameter2"
endif
if cnt > 0

"see 2nd comment


normal! l
endif
endwhile
if a:inner == 1

"better : call search('.','b') , which works also at the start of a line
normal! h
endif
endfunction

function! s:CurChar(char)
let c = getline(a:char)[col(a:char)-1]
return c
endfunction

func! s:Skip()
return synIDattr(synID(line('.'), col('.'), 0),'name') =~?
\ 'string\|comment\|character\|doxygen'
endfun

--------------%<---------------------------------------

--------------%<---------------------------------------


onoremap <silent> i, :<c-u>call <sid>SelectParam(1,0)<cr>
xnoremap <silent> i, :<c-u>call <sid>SelectParam(1,1)<cr><esc>gv
onoremap <silent> a, :<c-u>call <sid>SelectParam(0,0)<cr>
xnoremap <silent> a, :<c-u>call <sid>SelectParam(0,1)<cr><esc>gv

function! s:SelectParam(inner, visual)
if a:visual ==1 && CharAtMark("'>") =~ '[(,]' &&
!SkipAt(line("'>"),col("'>"))
normal! gv
"See the thread 'problem with searchpair()' why this the following
is necessary
elseif searchpair('(',',',')','bcW','Skip()') > 0 ||
searchpair('(',',',')','bW','Skip()') > 0
call search('.')
normal! v
else
return


endif
let cnt = v:count <= 0 ? 1 : v:count

while cnt > 0
let cnt -= 1

if searchpair('(', ',',')', 'W','Skip()') <= 0
break
endif
endwhile
" Don't include the last closing paren
if a:inner == 1 || searchpair('(',',',')','n','Skip()') <= 0
call search('.','b')
endif
endfunction

function! CharAtMark(mark)
let c = getline(a:mark)[col(a:mark)-1]
return c
endfunction

func! Skip()


return synIDattr(synID(line('.'), col('.'), 0),'name') =~?

'special\|string\|comment\|character\|doxygen'
endfun
func! SkipAt(l,c)
return synIDattr(synID(a:l, a:c, 0),'name') =~?
'special\|string\|comment\|character\|doxygen'
endfun

--------------%<---------------------------------------

herm...@free.fr

unread,
Sep 3, 2007, 2:15:53 PM9/3/07
to vim...@googlegroups.com
"A.Politz" <pol...@fh-trier.de> wrote:

> "Your use of searpair() reminded of the second argument I had completly
> missed."
>
> Which second argument ?

The {middle} argument.

> That works already pretty well, nevertheless I made a couple
> comments and attached my own version, if you don't mind.

Please comment and fix as you want. They are welcomed.

> I don't think my version is a 100% thing either. I think
> the best approach would be to first define exactly the
> behaviour of each command in any state on any part of the
> arguments, and model a code which satisfies this behaviour.

So far, most of the small tests I've done seem fine. However, there are still
some odd behaviours I'm not sure if they should be perceived as bugs or
features.

For instance, in
Un(Null,fun2(fun3(a,b,g(NULL))),t, titi, , zzz)
executing "v5a," on the "a" selects from the "a" to the "," preceding the "t".

> What's still missing is :
> [cd], - delete, change till end of arg , like 'dw'

I may be naive, but I think these mappings will be the simple ones.

> --------------%<---------------------------------------


> function! s:SelectParam(inner, visual)
> let pos = getpos('.')
> "Does not work is [(,] is e.g. inside a string

Indeed.

> if a:visual ==1 && s:CurChar("'>") =~ '[(,]'
> "Why 'l' ? searchpair() first moves the cursor anyway.
> "Besides it does not work at the eol.
> normal! gvl

Indeed you're right. I did a few tests with 'l' before thinking of using 'gv'.

> [...]


> if 0 == b
> "I would prefer to simply return, like 'dw' in an empty buffer.
> throw "Not on a parameter"

I'd rather have the mapping insult us than seeing it do strange an unexpected
things. Seeing a "di," misbehave in interactive mode in not a big issue. Seeing
a complex plugin misbehave and not understanding why is much more annoying and
time consuming. As I started working on "i,/a," with the idea of using them in
complex plugins, aborting the mappings was a "natural" choice.

> let e = searchpair('\V(', '\V,','\V)', 'W','s:Skip()')
> "Like 999d in a buffer with 10 lines, do what is possible.

It's the same idea: See what the mapping does on "vi," when the cursor is on
"zzz" when there is no closing bracket.

I perfectly see your point concerning the 999d, but unless we find a parade, I'd
rather not rely on silent aborts.
May be in that case we could test whether the abort position is before the saved
position .... I'll test it later.

> [....]

> call search('.')

Interresing. I've never though about using search() this way. Until now, I've
relying on ':exe "normal! <left/right>"'. This time I used l/h to "simplify" the
code, but completly forgot about eol issue.
I can't remember whether <left>/<right> ignore the EOL, or whether they depend
on a setting.
BTW, what could be the benefits of using search() instead of <right>/<left> ?


Thanks for your improvments.

herm...@free.fr

unread,
Sep 3, 2007, 2:44:40 PM9/3/07
to vim...@googlegroups.com
herm...@free.fr wrote:

> > let e = searchpair('\V(', '\V,','\V)', 'W','s:Skip()')
> > "Like 999d in a buffer with 10 lines, do what is possible.
>
> It's the same idea: See what the mapping does on "vi," when the cursor
> is on "zzz" when there is no closing bracket.
>
> I perfectly see your point concerning the 999d, but unless we find
> a parade, I'd rather not rely on silent aborts.
> May be in that case we could test whether the abort position is
> before the saved position .... I'll test it later.

I've just tested it, and it seems to work well. Here is an excerpt of the
patched function:
----------- %< ------------
function s:SelectParam(inner,visual)
let saved_pos = getpos('.')
.
.
.


while cnt > 0
let cnt -= 1

if 0 == searchpair('(', ',',')', 'W','s:Skip()')
if s:IsBefore(getpos('.'), saved_pos)
" no "vi," when starting from the last parameter
exe "normal! \<esc>"
call setpos('.', saved_pos)
throw (a:visual?'v':'').(a:inner?'i':'a').",: Cursor not on a parameter"
else
echomsg (a:visual?'v':'').(a:inner?'i':'a').",: No more parameters"
" "999di," silently deletes everything till the end
break
endif
endif
endwhile
--------------- >% ----------------


and the new helper function:
--------------- >% ----------------
function! s:IsBefore(lhs_pos, rhs_pos)
if a:lhs_pos[0] != a:rhs_pos[0]
throw "Postions from incompatible buffers can't be ordered"
endif
"1 test lines
"2 test cols
let before
\ = (a:lhs_pos[1] == a:rhs_pos[1])
\ ? (a:lhs_pos[2] < a:rhs_pos[2])
\ : (a:lhs_pos[1] < a:rhs_pos[1])
return before
endfunction

--------------- >% ----------------

A.Politz

unread,
Sep 3, 2007, 3:37:29 PM9/3/07
to vim...@googlegroups.com
herm...@free.fr wrote:

>"A.Politz" <pol...@fh-trier.de> wrote:
>
>
>
>>"Your use of searpair() reminded of the second argument I had completly
>>missed."
>>
>>Which second argument ?
>>
>>
>
>The {middle} argument.
>
>
>
>>That works already pretty well, nevertheless I made a couple
>>comments and attached my own version, if you don't mind.
>>
>>
>
>Please comment and fix as you want. They are welcomed.
>
>
>
>>I don't think my version is a 100% thing either. I think
>>the best approach would be to first define exactly the
>>behaviour of each command in any state on any part of the
>>arguments, and model a code which satisfies this behaviour.
>>
>>
>
>So far, most of the small tests I've done seem fine. However, there are still
>some odd behaviours I'm not sure if they should be perceived as bugs or
>features.
>
>For instance, in
> Un(Null,fun2(fun3(a,b,g(NULL))),t, titi, , zzz)
>executing "v5a," on the "a" selects from the "a" to the "," preceding the "t".
>
>
>

Right, searchpair() continues the search till it finds the
outermost (opening or closing ) parens. It should therefor
abort as soon as it finds a paren.

>>What's still missing is :
>>[cd], - delete, change till end of arg , like 'dw'
>>
>>
>
>I may be naive, but I think these mappings will be the simple ones.
>
>
>
>>--------------%<---------------------------------------
>>function! s:SelectParam(inner, visual)
>> let pos = getpos('.')
>> "Does not work is [(,] is e.g. inside a string
>>
>>
>
>Indeed.
>
>
>
>> if a:visual ==1 && s:CurChar("'>") =~ '[(,]'
>> "Why 'l' ? searchpair() first moves the cursor anyway.
>> "Besides it does not work at the eol.
>> normal! gvl
>>
>>
>
>Indeed you're right. I did a few tests with 'l' before thinking of using 'gv'.
>
>
>
>>[...]
>> if 0 == b
>> "I would prefer to simply return, like 'dw' in an empty buffer.
>> throw "Not on a parameter"
>>
>>
>
>I'd rather have the mapping insult us than seeing it do strange an unexpected
>things. Seeing a "di," misbehave in interactive mode in not a big issue. Seeing
>a complex plugin misbehave and not understanding why is much more annoying and
>time consuming. As I started working on "i,/a," with the idea of using them in
>complex plugins, aborting the mappings was a "natural" choice.
>
>

Firstly : It should never do unexpected things. It would be a bad idea
to write code which is based on a function with undefined or unknown
behaviour.
Assume you are on/in a wellformed argumentlist and based on that
everything is defined/works as expected.
I think you have to make this assumption, consider this declaration of
2 c-functions :

void f1(a,b, ; //oops forgot closing )
int f2 c,d); //oops forgot opening (

With this simple aproach (searchpair()) you have no way of knowing
that this is a syntax error and should not be processed.

I am beginning to think that it would be better to implement a function
which returns concrete positions ( of '(', ',' and ')' s ) which then
could be processed in anyway you like. Similay to my first aproach.

>> let e = searchpair('\V(', '\V,','\V)', 'W','s:Skip()')
>> "Like 999d in a buffer with 10 lines, do what is possible.
>>
>>
>
>It's the same idea: See what the mapping does on "vi," when the cursor is on
>"zzz" when there is no closing bracket.
>
>I perfectly see your point concerning the 999d, but unless we find a parade, I'd
>rather not rely on silent aborts.
>May be in that case we could test whether the abort position is before the saved
>position .... I'll test it later.
>
>
>
>>[....]
>>
>>
>
>
>
>> call search('.')
>>
>>
>
>Interresing. I've never though about using search() this way. Until now, I've
>relying on ':exe "normal! <left/right>"'. This time I used l/h to "simplify" the
>code, but completly forgot about eol issue.
>I can't remember whether <left>/<right> ignore the EOL, or whether they depend
>on a setting.
>BTW, what could be the benefits of using search() instead of <right>/<left> ?
>
>

The benefit is that search('.') moves the cursor to the next char
in the buffer regardless of lines, while <left>/<right>/h/l move
to the next char in the line.

>
>Thanks for your improvments.
>
>
>
-ap

Reply all
Reply to author
Forward
0 new messages