Import statement completions by andrewbranch · Pull Request #43149 · microsoft/TypeScript (original) (raw)
Screen capture demo
Enables auto-import-style completions on import statements themselves, e.g.
can offer a completion to
import { useState| } from "react";
(The |
indicates the position of the cursor both before and after the completion.)
This PR is large because, although the feature appears very similar to existing auto imports, it requires us to resolve module specifiers immediately instead of in a subsequent completion details request. Being able to do that with any reasonable amount of code reuse required quite a bit of untangling. The implementation is explained in some detail in code review comments, but note that a large amount of the PR diff is just functions being shuffled around.
Note also this feature is opt-in via a user preference (includeCompletionsForImportStatements
) because it requires corresponding editor changes. So, you won’t be able to test this yourself without building my VS Code PR as well.
Design
- When these completions are offered, they are generally the same as the auto-imports you’d get elsewhere in the file:
- Same list of modules and exports are considered, but we require the first character of what you’ve typed so far to match the first character of the export name, instead of just the “contains characters in order” check. This significantly reduces the number of matches we see after you type a single character, which lets us return results way faster.
- Quote preference is respected and semicolon usage is inferred
- Inference of preferred import style is same as normal auto-import rules (i.e., when
esModuleInterop
introduces ambiguity we try to give you what you want)
- Does not work on
const foo = require("mod")
; only works on imports (thoughimport foo = require("mod")
is supported in TS) - Does not work on imports where there’s already a module specifier, for I think obvious reasons
- Does not work on imports where there’s already another identifier in the declaration, like
import foo, { bar|
orimport { foo, bar|
. This could be relaxed a bit later, but it was complicated and seemed fairly low value. - Inserts a snippet, so your cursor snaps to the end of the identifier you were completing, then you can tab to the end of the statement.
- Due to client limitations (
replacementRange
must be a single line), does not work on imports that already span multiple lines, e.g.
Performance
Following measurements assume these dependencies
"dependencies": { "@angular/core": "^10.0.7", "@types/eslint": "^7.2.5", "@types/lodash": "^4.14.165" "@angular/material": "^11.2.5", "@reduxjs/toolkit": "^1.5.0", "@types/node": "^14.14.17", "@types/puppeteer": "^5.4.2", "@types/react": "^17.0.3", "@types/serverless": "^1.78.17", "aggregate-error": "^3.1.0", "antd": "^4.5.1", "aws-sdk": "*", "eslint": "^7.14.0", "lodash": "4.17.15", "mobx": "^5.15.4", "moment": "2.24.0", "puppeteer": "^6.0.0", "react": "16.12.0", "typescript": "^4.3.0-dev.20210322", }
Import statement completions
Stress-tested with aws-sdk
installed, which has just a ridiculous number of exports. First draft took several seconds without caches, which was too much, so I decided to require the first character of the identifier to match the first character of the export name before continuing the fuzzy match. So whereas regular auto imports will offer useState
when you have just typed state
, import statement completions require you to start with u
.
import a|
: 865 ms (3604 entries)import b|
: 456 ms(1279 entries) (some cache benefit from previous request)import a|
: 625 ms (max cache benefit)
After npm rm aws-sdk
:
import a|
: 116 ms (197 entries)import b|
: 44 ms (105 entries)import a|
: 70 ms
Normal auto imports
The first draft of this PR incurred about a 15% performance penalty on all auto-imports; that has now been reduced to zero or has improved performance in some scenarios. The auto import cache has been split into two pieces that are independently more durable and more reusable than they were combined.
Previously, the process went something like this:
- For each module in the program:
- Can we come up with a module specifier from the importing file to that module without crossing someone else’s node_modules?
- If that module specifier is a bare specifier (e.g.
"lodash"
), do the package.json files visible to the importing file list that package? - If the answer to both of those are yes, proceed with adding info about that module's exports to a big array, deduplicating re-exports along the way.
- Save all that info in a cache.
- Iterate the big map of info and pull out the exports whose names match the partial identifier typed so far.
The main problem with this process is that steps (2) and (3) are pretty expensive, and that work might go to waste after the filter in step (6) is applied. The other problem is that they are dependent on the location of the importing file and the contents of any package.jsons visible to that file, which makes us have to invalidate the cache a lot. Too many inputs combined into a single cache means that when we invalidate the cache, a lot of work that isn’t actually invalid has to be thrown away. Now, we cache “what are the exports and re-exports of every module” and “is file A importable from file B, and by what module specifier” separately. So the process becomes more like:
- For each module in the program:
- Add info about that module’s exports to a big map (not throwing away info about re-exports)
- Cache that big map
- Iterate the big map of info and pull out the exports whose names match the partial identifier typed so far.
- For each name match, see if the module is importable by the importing file (both by module specifier validity and package.json filtering)
- Add that module specifier / importability info to a cache.
This means for subsequent auto imports, we have better chances of getting cache hits, and we do less expensive work up front, even with the caches are totally empty.
Triggering auto import completions in a project with
dev.20210322 (ms) | This PR (ms) | |
---|---|---|
completionInfo (cold) | 259 | 264 |
completionEntryDetails | 102 | 74 |
completionInfo (cached) | 68 | 63 |
completionEntryDetails | 44 | 35 |
To-do
- Do we need a separate user preference for enabling snippet-formatted completions? Yes, done.
- Do some profiling to make sure normal completions / auto imports are not slower
- Check performance when there are huge numbers of possible modules to import from (e.g. with
aws-sdk
installed) and probably implement some limits/mitigations there. Increased strictness of filter to make list smaller. - Get approval on corresponding VS Code PR
- Finish and merge corresponding TypeScript-VS PR (need help from @uniqueiniquity on this one 🙏)
Fixes #31658