Skip to content

Commit a3a023b

Browse files
committed
feat: Namespace Imports! Add a new setting that allows you to make *auto imports* add namespace imports instead of named imports (also with ability to use global variables instead)
1 parent c749068 commit a3a023b

File tree

8 files changed

+293
-69
lines changed

8 files changed

+293
-69
lines changed

README.MD

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ This also makes plugin work in Volar's takeover mode!
102102

103103
### Web Support
104104

105-
> Note: when you open TS/JS file in the web for the first time you currently need to switch editors to make everything work!
105+
> Note: when you open TS/JS file in the web for the first time you currently need to switch between editors to make plugin work.
106106
107-
Web-only feature: `import` path resolution
107+
There is web-only feature: fix clicking on relative `import` paths.
108108

109109
### `in` Keyword Suggestions
110110

@@ -149,9 +149,9 @@ Appends *space* to almost all keywords e.g. `const `, like WebStorm does.
149149

150150
(*enabled by default*)
151151

152-
Patches `toString()` insert function snippet on number types to remove tabStop.
152+
Patches `toString()` insert function snippet on number types to remove annoying tab stop.
153153

154-
### Enforce Properties Sorting
154+
### Restore Properties Sorting
155155

156156
(*disabled by default*) enable with `tsEssentialPlugins.fixSuggestionsSorting`
157157

@@ -162,11 +162,7 @@ Try to restore [original](https://github.com/microsoft/TypeScript/issues/49012)
162162
We extend completion list with extensions from module augmentation (e.g. `.css` files if you have `declare module '*.css'`).
163163
But for unchecked contexts list of extensions can be extended with `tsEssentialPlugins.additionalIncludeExtensions` setting.
164164

165-
### Switch Exclude Covered Cases
166-
167-
(*enabled by default*)
168-
169-
Exclude already covered strings / enums from suggestions ([TS repo issue](https://github.com/microsoft/TypeScript/issues/13711)).
165+
<!-- ## Type-Driven Completions -->
170166

171167
### Mark Code Actions
172168

@@ -226,7 +222,58 @@ Some settings examples:
226222
```
227223

228224
> Note: changeSorting might not preserve sorting of other existing suggestions which not defined by rules, there is WIP
229-
> Also I'm thinking of making it learn and syncing of most-used imports automatically
225+
> Also I'm thinking of making it learn and sync most-used imports automatically
226+
227+
### Namespace Imports
228+
229+
If you always want some modules to be imported automatically as namespace import, you're lucky as there is `autoImport.changeToNamespaceImport` setting for this.
230+
231+
Example:
232+
233+
You're completing following Node.js code in empty file:
234+
235+
```ts
236+
readFileSync
237+
```
238+
239+
Default completion and code fix will change it to:
240+
241+
```ts
242+
import { readFileSync } from 'fs'
243+
244+
readFileSync
245+
```
246+
247+
But if you setup this setting:
248+
249+
```json
250+
"tsEssentialPlugins.autoImport.changeToNamespaceImport": {
251+
"fs": {},
252+
},
253+
```
254+
255+
Completing the same code or accepting import code fix will result:
256+
257+
```ts
258+
import * as fs from 'fs'
259+
260+
fs.readFileSync
261+
```
262+
263+
There is also a way to specify different name for namespace or use default import instead.
264+
265+
However there are cases where you have some modules injected globally in your application (e.g. global `React` variable), then you can specify *auto imports feature* to use them instead of adding an import:
266+
267+
```json
268+
"tsEssentialPlugins.autoImport.changeToNamespaceImport": {
269+
"react": {
270+
"namespace": "React",
271+
"addImport": false
272+
},
273+
},
274+
```
275+
276+
`useState` -> `React.useState`
230277

231278
## Rename Features
232279

src/configurationType.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,19 +443,21 @@ export type Configuration = {
443443
/**
444444
* Advanced. Use `suggestions.ignoreAutoImports` setting if possible.
445445
*
446-
* Packages to ignore in import all fix.
446+
* Specify packages to ignore in *add all missing imports* fix, to ensure these packages never get imported automatically.
447447
*
448448
* TODO syntaxes /* and module#symbol unsupported (easy)
449449
* @default []
450450
*/
451451
'autoImport.alwaysIgnoreInImportAll': string[]
452452
/**
453+
* Specify here modules should be imported as namespace import. But note that imports gets processed first by `suggestions.ignoreAutoImports` anyway.
454+
*
453455
* @default {}
454456
*/
455457
'autoImport.changeToNamespaceImport': {
456458
[module: string]: {
457459
/**
458-
* @default module (key)
460+
* Defaults to key
459461
*/
460462
namespace?: string
461463
/**
@@ -464,6 +466,7 @@ export type Configuration = {
464466
useDefaultImport?: boolean
465467
/**
466468
* Set to `false` if module is acessible from global variable
469+
* For now not supported in add all missing imports code action
467470
* @default true */
468471
addImport?: boolean
469472
}

typescript/src/adjustAutoImports.ts

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,32 +39,34 @@ const initIgnoreAutoImport = () => {
3939
// })
4040
}
4141

42-
export const getIgnoreAutoImportSetting = (c: GetConfig) => {
43-
return c('suggestions.ignoreAutoImports').map((spec): ParsedIgnoreSetting => {
44-
const hashIndex = spec.indexOf('#')
45-
let module = hashIndex === -1 ? spec : spec.slice(0, hashIndex)
46-
const moduleCompare = module.endsWith('/*') ? 'startsWith' : 'strict'
47-
if (moduleCompare === 'startsWith') {
48-
module = module.slice(0, -'/*'.length)
49-
}
50-
if (hashIndex === -1) {
51-
return {
52-
module,
53-
symbols: [],
54-
isAnySymbol: true,
55-
moduleCompare,
56-
}
57-
}
58-
const symbolsString = spec.slice(hashIndex + 1)
59-
// * (glob asterisk) is reserved for future ussage
60-
const isAnySymbol = symbolsString === '*'
42+
export function parseIgnoreSpec(spec: string): ParsedIgnoreSetting {
43+
const hashIndex = spec.indexOf('#')
44+
let module = hashIndex === -1 ? spec : spec.slice(0, hashIndex)
45+
const moduleCompare = module.endsWith('/*') ? 'startsWith' : 'strict'
46+
if (moduleCompare === 'startsWith') {
47+
module = module.slice(0, -'/*'.length)
48+
}
49+
if (hashIndex === -1) {
6150
return {
6251
module,
63-
symbols: isAnySymbol ? [] : symbolsString.split(','),
64-
isAnySymbol,
52+
symbols: [],
53+
isAnySymbol: true,
6554
moduleCompare,
6655
}
67-
})
56+
}
57+
const symbolsString = spec.slice(hashIndex + 1)
58+
// * (glob asterisk) is reserved for future ussage
59+
const isAnySymbol = symbolsString === '*'
60+
return {
61+
module,
62+
symbols: isAnySymbol ? [] : symbolsString.split(','),
63+
isAnySymbol,
64+
moduleCompare,
65+
}
66+
}
67+
68+
export const getIgnoreAutoImportSetting = (c: GetConfig) => {
69+
return c('suggestions.ignoreAutoImports').map(spec => parseIgnoreSpec(spec))
6870
}
6971

7072
export const isAutoImportEntryShouldBeIgnored = (ignoreAutoImportsSetting: ParsedIgnoreSetting[], targetModule: string, symbol: string) => {
@@ -78,6 +80,17 @@ export const isAutoImportEntryShouldBeIgnored = (ignoreAutoImportsSetting: Parse
7880
return false
7981
}
8082

83+
export const findIndexOfAutoImportSpec = (ignoreAutoImportsSetting: ParsedIgnoreSetting[], targetModule: string, symbol: string) => {
84+
for (const [i, { module, moduleCompare, isAnySymbol, symbols }] of ignoreAutoImportsSetting.entries()) {
85+
const isIgnoreModule = moduleCompare === 'startsWith' ? targetModule.startsWith(module) : targetModule === module
86+
if (!isIgnoreModule) continue
87+
if (isAnySymbol) return i
88+
if (!symbols.includes(symbol)) continue
89+
return i
90+
}
91+
return
92+
}
93+
8194
export const shouldChangeSortingOfAutoImport = (symbolName: string, c: GetConfig) => {
8295
const arr = c('autoImport.changeSorting')[symbolName]
8396
return arr && arr.length > 0

typescript/src/codeFixes.ts

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import _ from 'lodash'
22
import addMissingProperties from './codeFixes/addMissingProperties'
33
import { changeSortingOfAutoImport, getIgnoreAutoImportSetting, isAutoImportEntryShouldBeIgnored } from './adjustAutoImports'
44
import { GetConfig } from './types'
5-
import { findChildContainingPosition, getIndentFromPos, patchMethod } from './utils'
5+
import { findChildContainingPosition, getCancellationToken, getIndentFromPos, patchMethod } from './utils'
6+
import namespaceAutoImports from './namespaceAutoImports'
67

78
// codeFixes that I managed to put in files
89
const externalCodeFixes = [addMissingProperties]
@@ -22,33 +23,61 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
2223
[Diagnostics.Remove_type_from_import_of_0_from_1, 1, 0],
2324
[Diagnostics.Remove_type_from_import_declaration_from_0, 0],
2425
]
25-
const oldCreateCodeFixAction = tsFull.codefix.createCodeFixAction
26+
const addNamespaceImports = [] as ts.CodeFixAction[]
27+
2628
let prior: readonly ts.CodeFixAction[]
29+
let toUnpatch: (() => void)[] = []
2730
try {
2831
const { importFixName } = tsFull.codefix
2932
const ignoreAutoImportsSetting = getIgnoreAutoImportSetting(c)
3033
const sortFn = changeSortingOfAutoImport(c, (node as ts.Identifier).text)
31-
tsFull.codefix.createCodeFixAction = (fixName, changes, description, fixId, fixAllDescription, command) => {
32-
if (fixName !== importFixName) return oldCreateCodeFixAction(fixName, changes, description, fixId, fixAllDescription, command)
33-
const placeholderIndexesInfo = moduleSymbolDescriptionPlaceholders.find(([diag]) => diag === description[0])
34-
let sorting = '-1'
35-
if (placeholderIndexesInfo) {
36-
const targetModule = description[placeholderIndexesInfo[1] + 1]
37-
const symbolName = placeholderIndexesInfo[2] !== undefined ? description[placeholderIndexesInfo[2] + 1] : (node as ts.Identifier).text
38-
const toIgnore = isAutoImportEntryShouldBeIgnored(ignoreAutoImportsSetting, targetModule, symbolName)
39-
if (toIgnore) {
40-
return {
41-
fixName: 'IGNORE',
42-
changes: [],
43-
description: '',
34+
const unpatch = patchMethod(
35+
tsFull.codefix,
36+
'createCodeFixAction',
37+
oldCreateCodeFixAction => (fixName, changes, description, fixId, fixAllDescription, command) => {
38+
if (fixName !== importFixName) return oldCreateCodeFixAction(fixName, changes, description, fixId, fixAllDescription, command)
39+
const placeholderIndexesInfo = moduleSymbolDescriptionPlaceholders.find(([diag]) => diag === description[0])
40+
let sorting = '-1'
41+
if (placeholderIndexesInfo) {
42+
const targetModule = description[placeholderIndexesInfo[1] + 1]
43+
const symbolName = placeholderIndexesInfo[2] !== undefined ? description[placeholderIndexesInfo[2] + 1] : (node as ts.Identifier).text
44+
45+
const toIgnore = isAutoImportEntryShouldBeIgnored(ignoreAutoImportsSetting, targetModule, symbolName)
46+
47+
const namespaceImportAction =
48+
!toIgnore && namespaceAutoImports(c, sourceFile, targetModule, preferences, formatOptions, start, symbolName)
49+
50+
if (namespaceImportAction) {
51+
const { textChanges, description } = namespaceImportAction
52+
addNamespaceImports.push({
53+
fixName: importFixName,
54+
fixAllDescription: 'Add all missing imports',
55+
fixId: 'fixMissingImport',
56+
description,
57+
changes: [
58+
{
59+
fileName,
60+
textChanges,
61+
},
62+
],
63+
})
64+
}
65+
if (toIgnore /* || namespaceImportAction */) {
66+
return {
67+
fixName: 'IGNORE',
68+
changes: [],
69+
description: '',
70+
}
4471
}
72+
sorting = sortFn(targetModule).toString()
73+
// todo this workaround is weird, sort in another way
4574
}
46-
sorting = sortFn(targetModule).toString()
47-
// todo this workaround is weird, sort in another way
48-
}
49-
return oldCreateCodeFixAction(fixName + sorting, changes, description, fixId, fixAllDescription, command)
50-
}
75+
return oldCreateCodeFixAction(fixName + sorting, changes, description, fixId, fixAllDescription, command)
76+
},
77+
)
78+
toUnpatch.push(unpatch)
5179
prior = languageService.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences)
80+
prior = [...addNamespaceImports, ...prior]
5281
prior = _.sortBy(prior, ({ fixName }) => {
5382
if (fixName.startsWith(importFixName)) {
5483
return +fixName.slice(importFixName.length)
@@ -63,7 +92,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
6392
throw err
6493
})
6594
} finally {
66-
tsFull.codefix.createCodeFixAction = oldCreateCodeFixAction
95+
toUnpatch.forEach(x => x())
6796
}
6897
// todo remove when 5.0 is released after 3 months
6998
// #region fix builtin codefixes/refactorings
@@ -150,12 +179,8 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
150179
languageServiceHost as any /* cancellationToken */,
151180
)
152181
const semanticDiagnostics = languageService.getSemanticDiagnostics(fileName)
153-
const cancellationToken = languageServiceHost.getCompilerHost?.()?.getCancellationToken?.() ?? {
154-
isCancellationRequested: () => false,
155-
throwIfCancellationRequested: () => {},
156-
}
157182
const context: Record<keyof import('typescript-full').CodeFixContextBase, any> = {
158-
cancellationToken,
183+
cancellationToken: getCancellationToken(languageServiceHost),
159184
formatContext: tsFull.formatting.getFormatContext(formatOptions, languageServiceHost),
160185
host: languageServiceHost,
161186
preferences,
@@ -164,6 +189,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
164189
}
165190
const errorCodes = getFixAllErrorCodes()
166191
const ignoreAutoImportsSetting = getIgnoreAutoImportSetting(c)
192+
const additionalTextChanges: ts.TextChange[] = []
167193
for (const diagnostic of semanticDiagnostics) {
168194
if (!errorCodes.includes(diagnostic.code)) continue
169195
const toUnpatch: (() => any)[] = []
@@ -185,6 +211,38 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
185211
}),
186212
({ fix }) => sortFn(fix.moduleSpecifier),
187213
)
214+
const firstFix = fixes[0]
215+
const namespaceImportAction =
216+
!!firstFix &&
217+
namespaceAutoImports(
218+
c,
219+
sourceFile,
220+
firstFix.fix.moduleSpecifier,
221+
preferences,
222+
formatOptions,
223+
diagnostic.start!,
224+
firstFix.symbolName,
225+
)
226+
if (namespaceImportAction) {
227+
fixes = []
228+
if (!namespaceImportAction.namespace) {
229+
additionalTextChanges.push(...namespaceImportAction.textChanges)
230+
} else {
231+
const { namespace, useDefaultImport, textChanges } = namespaceImportAction
232+
additionalTextChanges.push(textChanges[1]!)
233+
fixes.unshift({
234+
...fixes[0]!,
235+
fix: {
236+
kind: ImportFixKind.AddNew,
237+
moduleSpecifier: firstFix.fix.moduleSpecifier,
238+
importKind: useDefaultImport ? tsFull.ImportKind.Default : tsFull.ImportKind.Namespace,
239+
addAsTypeOnly: false,
240+
useRequire: false,
241+
},
242+
symbolName: namespace,
243+
} as FixInfo)
244+
}
245+
}
188246
if (!fixes[0]) throw new Error('No fixes')
189247
return fixes[0]
190248
}) as any,
@@ -209,7 +267,10 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
209267
}
210268
}
211269
}
212-
return tsFull.codefix.createCombinedCodeActions(tsFull.textChanges.ChangeTracker.with(context, importAdder.writeFixes))
270+
return tsFull.codefix.createCombinedCodeActions([
271+
...tsFull.textChanges.ChangeTracker.with(context, importAdder.writeFixes),
272+
{ fileName, textChanges: additionalTextChanges },
273+
])
213274
}
214275
return languageService.getCombinedCodeFix(scope, fixId, formatOptions, preferences)
215276
}

0 commit comments

Comments
 (0)