1

Imagine there is a file src/main.js with import like this:

import { A } from '_components/A';

And _components/A in turn has

export { A } from './B';

I configured includeexpr to expand _ to src so _components/A is transformed to src/components/A. Also since vim doesn't find relative filenames(starting with ./) in path this function tries to make such paths absolute using path to the file loaded in current buffer:

let resolved_path = expand('%:p:.:h') . a:path[1:-1]

Then somewhere in the main.js I want to list all definitions of A by pressing [D over it. Vim doesn't find _components/A in path so it calls function that I defined for includeexpr and successfully gets src/components/A.

The problem occurs when vim encounters reexport of A from src/components/A/B. In this case includeexpr resolves ./B to src/B not to src/components/A/B

Is there a way to get correct filename? What am I doing wrong?

Some of my vim settings:

set path=.,,src
let &l:include = 'from\|require'
let &l:includeexpr = "ResolvePath(v:fname)"

function! ResolvePath(path) abort
  let is_relative = match(a:path, '^\.\/') == 0

  let resolved_path = fnameescape(substitute(a:path, '^\zs[\~_]\ze\a', 'src/', ''))

  if is_relative
    let resolved_path = expand('%:p:.:h') . a:path[1:-1]
  endif

  if isdirectory(resolved_path)
    return get(systemlist('find ' . resolved_path . ' -type f -maxdepth 1 -regex ".*/index.[jt]sx?"'), 0, resolved_path)
  endif

  return resolved_path
endfunction

File structure:

.
└── src
    ├── components
    │   └── A
    │       ├── B
    │       │   └── index.js
    │       └── index.js
    └── main.js
2
  • It would be helpfully if you could provide your configuration of includeexpr and maybe à simplified version of the file structure. Apr 29, 2023 at 5:12
  • 1
    @VivianDeSmedt thank you for the comment. updated the post Apr 29, 2023 at 7:45

1 Answer 1

0

There is a lot of detail missing from your question, like your :help 'path', your :help 'include', your :help 'includeexpr', etc. but also a lot missing in Vim itself. Let's see what we can do.

This is your file structure as I understand it:

.
├── main.js
└── src
    └── components
        ├── A.js
        └── B.js

Out of the box, Vim knows how to find "defines" in a JavaScript file but it doesn't know how to find "includes". That is because, when I updated the default ftplugin for JavaScript a couple years ago, a working &include was not part of the update. My reasoning was that, while it is relatively easy to come up with a working one, writing a working &includeexpr is a lot more involved and I felt that having one without the other didn't make much sense.

The final nail in the coffin is the long standing issue with Vim itself that you can't have both a &define and an &include match on the same line, which is essentially the scenario for:

export { A } from './B';

where you import and export A in one statement. The default &define as I wrote it doesn't support that notation because it simply can't work.

So, without any configuration, Vim is incapable of doing anything with the import line in main.js or the export line in A.js.

Bummer.

Now, here is the &include I've had in my config for many years, which would have been part of that update:

" in after/ftplugin/javascript.vim
setlocal include=^\\s*[^\/]\\+\\(from\\\|require(\\)\\s*['\"]\\ze

If we open main.js in a clean Vim and set the option globally:

$ vim --clean main.js
:set include=^\\s*[^\/]\\+\\(from\\\|require(\\)\\s*['\"]\\ze

we can check with :help :checkpath that the include is found but that the file is not, which is not surprising given the provided path:

:checkpath
--- Included files not found in path ---
_components/A

The include is also found in A.js:

--- Included files in path ---
/Users/romainl/Desktop/test/src/components/./B.js

Now, that we can find includes, we must deal with that funky file name in main.js. :help 'suffixesadd' is correctly set out of the box so Vim will find A.js if we give it A, but the _ alias is weird and non-standard. There are two ways to deal with it.

  1. Add src to your &path and get rid of the _ in the filename:

    :set path+=src
    :set includeexpr=substitute(v:fname,'^_','','')
    :checkpath!
    --- Included files in path ---
    src/components/A.js
    src/components/A.js -->
      src/components/./B.js
    

    This is for the simplest case. In reality you would have to collect all the current aliases in a list and do that for each of them.

  2. Substitute the _ in the filename with the corresponding path.

    :set includeexpr=substitute(v:fname,'^_','src/','')
    :checkpath!
    --- Included files in path ---
    src/components/A.js
    src/components/A.js -->
      src/components/./B.js
    

    Again, this is for the simplest case.

At this point, you should be able to do [D on A in main.js and get:

victory

--- EDIT ---

So, now that we have your exact file structure, the extra index.js adds a bit of complexity because A and B are considered by Vim as legitimate targets and their index.js is never searched.

The fix is to add /index.js (or /index, thanks to &suffixesadd) to the final file name. If you are confident that there will always be an index.js, then it is a simple concatenation or substitution. If you are not (and you shouldn't, IMO), then it will probably require the use of :help glob() to match either A.js or A/index.js given A.

Something like:

glob(substitute('_components/A','^_','src/','')..'/{index.js,}')

which, in this case, should return:

src/components/A/index.js
src/components/A

with the first one being the most likely candidate.

11
  • thank you for your answer. seems like I updated the question while you were writing the answer. I provided some additional information as VivianDeSmedt proposed to do. Apr 29, 2023 at 8:32
  • Yeah, apparently. But see my edit, it doesn't really change things.
    – romainl
    Apr 29, 2023 at 8:49
  • but how are you going to let vim know that the path that should be used for substitution is _components/A/B? When includeexpr gets './B' as v:fname vim doesn't know the context. Apr 29, 2023 at 9:44
  • I made it work but I don't understand what was wrong with my setup since it looks like it does the same thing. I will explore and tell where was the mistake. Thank you! Apr 29, 2023 at 10:34
  • Looks like it is find inside my ResolvePath function not vim doesn't know the context. It tries to find .*/index.[jt]sx? in ./B which is obviously wrong. But unfortunately glob also doesn't know about the context and returns empty list if used with v:fname. So, apparently the real question is "How to get name of the file that is being searched for includes inside includeexpr?" Apr 29, 2023 at 10:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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