1

I apologise if this was asked before, but I couldn't find anything similar.

I am trying to write a simple command to jump to a test file, using the current file name. I have managed to achieve this using:

command Spec :find %:r.spec.js

If my current buffer name is foo.js this will try to jump to foo.spec.js.

The problem is some projects use the *.test.js extension therefore I would like to be able to look for both and to jump to either available. Something like:

command Spec :find %:r.(spec|test).js

However, this does not work. I have tried escaping the atom but vim then just treats it as a literal. Is there any way to achieve this without any plugin? I should mention that using bash commands are also not an option since I use Windows quite a lot as well.

Ideally, if multiple files are found, I would get a wildmenu, but I'd be happy with opening the first found too.

2

In vim globs (like those used in find), alternation between two words can be done using the {a,b} syntax. This will work interactively, presenting a wildmenu, if you press tab.

:find %:r.{spec,test}.js<TAB>

Unfortunately, it won't work from a "Spec" command wrapper, since :find does not tolerate multiple matches. Here is one solution using glob() and inputlist(). Typing :Spec will present a choice if there are multiple matches.

 function! Spec(bang)
    let files = glob('%:r.{spec,test}.js', 1, 1)
    if empty(files)
        echohl Error | echo 'nothing found' | echohl None
        return
    endif
    if len(files) == 1
        execute 'edit' . a:bang files[0]
        return
    endif

    let choice = inputlist(map(copy(files), 'v:key+1 . ": " . v:val'))
    if choice > 0
        execute 'edit' . a:bang files[choice-1]
    endif
endfunction

command! -bang Spec :call Spec('<bang>')
| improve this answer | |
  • This doesn't seem to work for me... I don't think :find uses this kind of regex. It seems to accept basic wildcards (e.g. ? or [...]) but it complains if that expands to more than one filename... – filbranden Apr 19 at 3:32
  • Sadly, this doesn't work for me in gVim Windows E345: Cannot find file "foo.(spec|test).js in path :( It looks like escaping the special characters treats them as part of the string literal – Andrei Nemes Apr 19 at 4:02
  • @filbranden, there is some weirdness I can't explain, check out :echo glob(expand('%:r') . '*\(spec\|test\).js') vs :echo glob(expand('%:r') . '\(spec\|test\).js'). @AndreiNemes fundamentally the problem is :find won't handle multiple filenames. If you type :find %:r.\(spec\|test\).js<tab> it will work as expected, but not as part of :Spec command – Mass Apr 19 at 13:28
  • This works! I had to change the glob to let files = glob('./**/*' . expand('%:r') . '.{spec.js,test.js}', 1, 1) though. Thanks a bunch! – Andrei Nemes Apr 20 at 15:24
  • 1
    By the way, you forgot to return on the len === 1 case :) – Andrei Nemes Apr 20 at 15:31
1

One possible simple approach is to use a :try block to catch an error while trying the *.spec.js filename and then fallback to the *.test.js filename if that fails.

Perhaps something as simple as:

command! Spec try | find %:t:r.spec.js | catch /^Vim(find):E345:/ | find %:t:r.test.js | endtry

(Note I used %:t:r to use just the basename of the file, so that :find gets a filename only and will use 'path' to locate it. That's not the case if the original file was in a subdirectory and you used %:r, in which case it would only look for the *.spec.js file in the same directory as the original file.)

One small issue with this solution is that, when neither file exists, it will only complain about the latter *.test.js, which might give the user the wrong impression that that's the preferred filename...

You can fix that by using a second :try block and issuing a more appropriate error message in that case.

The command gets longer already, so I'll start breaking lines for clarity:

command! Spec
  \   try
  \ |   find %:t:r.spec.js
  \ | catch /^Vim(find):E345:/
  \ |   try
  \ |     find %:t:r.test.js
  \ |   catch /^Vim(find):E345:/
  \ |     echoerr "E345: Can't find file \"".expand("%:t:r").".spec.js\" or \"".expand("%:t:r").".test.js\" in path"
  \ |   endtry
  \ | endtry

At this point it already makes sense to break this out into a function, and have the Spec command call the function...

If you use a function, you might want to consider using findfile() to locate an existing file, possibly use a list and a loop to handle possible extensions in a more flexible way, perhaps consider creating or offering to create the file if it doesn't exist...

But then, you're already halfway through writing your own plug-in for this feature, so you might consider adopting one. The vim-projectionist plug-in by Tim Pope is a great plug-in for jumping between related files in a project and it's quite flexible so you can configure it to match the paths in your project.

| improve this answer | |
  • Branching by exception looks itchy. – dedowsdi Apr 19 at 23:00
  • 1
    I went with the other suggestion, thanks for your input! – Andrei Nemes Apr 20 at 15:25
  • 1
    @AndreiNemes Glad you found a workable solution! – filbranden Apr 20 at 15:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.