Skip to content

Commit cddf800

Browse files
committed
feat: add importPaths setting
fix #4
1 parent f86ab1f commit cddf800

File tree

6 files changed

+166
-42
lines changed

6 files changed

+166
-42
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ names make it possible to use tab completion so I hope it makes them easier for
2626
- Automatically adds missing imports when a snippet is inserted (as long as it succeeds in parsing the entire file)
2727
- You can configure whether to use controlled or uncontrolled form controls in the extension settings.
2828

29+
# Settings
30+
31+
## Form Control Mode
32+
33+
- controlled - inserts `value` and `onChange` properties
34+
- uncontrolled - inserts `defaultValue` property
35+
36+
## Import Paths
37+
38+
- auto - uses top level if other top level imports are found, second level otherwise
39+
- top level - `import { Button } from '@material-ui/core'`
40+
- second level - `import Button from '@material-ui/core/Button'`
41+
2942
# Snippets
3043

3144
<!-- snippets -->

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@
1717
{
1818
"title": "Material-UI Snippets",
1919
"properties": {
20+
"material-ui-snippets.importPaths": {
21+
"type": "string",
22+
"default": "auto",
23+
"markdownDescription": "Which import paths to use in generated import statements",
24+
"enum": [
25+
"auto",
26+
"top level",
27+
"second level"
28+
],
29+
"enumDescriptions": [
30+
"Uses top level if other top level imports are found, second level otherwise",
31+
"import { Button } from '@material-ui/core'",
32+
"import Button from '@material-ui/core/Button'"
33+
]
34+
},
2035
"material-ui-snippets.formControlMode": {
2136
"type": "string",
2237
"default": "controlled",

src/extension.ts

Lines changed: 94 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface TextSnippet {
88
prefix: string
99
description: string
1010
body: string
11-
imports: Record<string, string>
11+
imports: string[]
1212
}
1313

1414
export function activate(context: vscode.ExtensionContext): void {
@@ -50,22 +50,34 @@ export function activate(context: vscode.ExtensionContext): void {
5050
): vscode.ProviderResult<
5151
vscode.CompletionItem[] | vscode.CompletionList
5252
> {
53-
let insertPosition: vscode.Position = new vscode.Position(0, 0)
5453
let existingImports: Set<string> | null
54+
let insertPosition: vscode.Position = new vscode.Position(0, 0)
55+
let coreInsertPosition: vscode.Position | null = null
56+
let iconsInsertPosition: vscode.Position | null = null
5557
try {
56-
;({ insertPosition, existingImports } = getExistingImports(
57-
document
58-
))
58+
;({
59+
existingImports,
60+
insertPosition,
61+
coreInsertPosition,
62+
iconsInsertPosition,
63+
} = getExistingImports(document))
5964
} catch (error) {
6065
existingImports = null
6166
}
67+
const config = vscode.workspace.getConfiguration(
68+
'material-ui-snippets'
69+
)
70+
let importPaths = config.get('importPaths') || 'auto'
71+
if (importPaths === 'auto') {
72+
importPaths =
73+
coreInsertPosition || iconsInsertPosition
74+
? 'top level'
75+
: 'second level'
76+
}
6277
const result = []
6378
for (const snippet of getSnippets({
6479
language: language as any, // eslint-disable-line @typescript-eslint/no-explicit-any
65-
formControlMode:
66-
vscode.workspace
67-
.getConfiguration('material-ui-snippets')
68-
.get('formControlMode') || 'controlled',
80+
formControlMode: config.get('formControlMode') || 'controlled',
6981
})) {
7082
const { prefix, description, body, imports } = snippet
7183
const snippetCompletion = new vscode.CompletionItem(prefix)
@@ -75,15 +87,79 @@ export function activate(context: vscode.ExtensionContext): void {
7587
)
7688
const finalExistingImports = existingImports
7789
if (finalExistingImports) {
78-
snippetCompletion.additionalTextEdits = [
79-
vscode.TextEdit.insert(
80-
insertPosition,
81-
[...Object.entries(imports)]
82-
.filter(([source]) => !finalExistingImports.has(source))
83-
.map(entry => entry[1])
84-
.join('\n') + '\n'
85-
),
86-
]
90+
const additionalTextEdits: vscode.TextEdit[] = []
91+
if (importPaths === 'second level') {
92+
additionalTextEdits.push(
93+
vscode.TextEdit.insert(
94+
insertPosition,
95+
imports
96+
.filter(comp => !finalExistingImports.has(comp))
97+
.map(comp =>
98+
comp.endsWith('Icon')
99+
? `import ${comp} from '@material-ui/icons/${comp.substring(
100+
0,
101+
comp.length - 4
102+
)}'`
103+
: `import ${comp} from '@material-ui/core/${comp}'`
104+
)
105+
.join('\n') + '\n'
106+
)
107+
)
108+
} else {
109+
const coreImports = imports.filter(
110+
comp =>
111+
!comp.endsWith('Icon') && !finalExistingImports.has(comp)
112+
)
113+
const iconsImports = imports
114+
.filter(
115+
comp =>
116+
comp.endsWith('Icon') && !finalExistingImports.has(comp)
117+
)
118+
.map(
119+
comp => `${comp.substring(0, comp.length - 4)} as ${comp}`
120+
)
121+
122+
if (coreImports.length) {
123+
if (coreInsertPosition) {
124+
additionalTextEdits.push(
125+
vscode.TextEdit.insert(
126+
coreInsertPosition,
127+
', ' + coreImports.join(', ')
128+
)
129+
)
130+
} else {
131+
additionalTextEdits.push(
132+
vscode.TextEdit.insert(
133+
insertPosition,
134+
`import { ${coreImports.join(
135+
', '
136+
)} } from '@material-ui/core'\n`
137+
)
138+
)
139+
}
140+
}
141+
if (iconsImports.length) {
142+
if (iconsInsertPosition) {
143+
additionalTextEdits.push(
144+
vscode.TextEdit.insert(
145+
iconsInsertPosition,
146+
', ' + iconsImports.join(', ')
147+
)
148+
)
149+
} else {
150+
additionalTextEdits.push(
151+
vscode.TextEdit.insert(
152+
insertPosition,
153+
`import { ${iconsImports.join(
154+
', '
155+
)} } from '@material-ui/icons'\n`
156+
)
157+
)
158+
}
159+
}
160+
}
161+
if (additionalTextEdits.length)
162+
snippetCompletion.additionalTextEdits = additionalTextEdits
87163
}
88164
result.push(snippetCompletion)
89165
}

src/getExistingImports.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ import chooseJSCodeshiftParser from 'jscodeshift-choose-parser'
44

55
export default function getExistingImports(
66
document: vscode.TextDocument
7-
): { insertPosition: vscode.Position; existingImports: Set<string> } {
7+
): {
8+
existingImports: Set<string>
9+
insertPosition: vscode.Position
10+
coreInsertPosition: vscode.Position | null
11+
iconsInsertPosition: vscode.Position | null
12+
} {
813
const text = document.getText()
914
const parser = chooseJSCodeshiftParser(document.uri.fsPath)
1015
const j = parser ? jscodeshift.withParser(parser) : jscodeshift
1116

1217
const result: Set<string> = new Set()
1318

1419
let insertLine = 0
20+
let coreInsertPosition: vscode.Position | null = null
21+
let iconsInsertPosition: vscode.Position | null = null
1522

1623
let root
1724
try {
@@ -28,12 +35,42 @@ export default function getExistingImports(
2835
if (!node) return
2936
if (node.loc) insertLine = node.loc.end.line
3037
const source = node.source.value
31-
if (typeof source === 'string' && source.startsWith('@material-ui')) {
32-
result.add(source)
38+
if (typeof source !== 'string') return
39+
if (source === '@material-ui/core') {
40+
for (const specifier of node.specifiers) {
41+
if (specifier.type !== 'ImportSpecifier') continue
42+
const { loc } = specifier
43+
if (loc) {
44+
const { line, column } = loc.end
45+
coreInsertPosition = new vscode.Position(line - 1, column)
46+
}
47+
const { imported, local } = specifier
48+
if (imported && local && imported.name === local.name) {
49+
result.add(local.name)
50+
}
51+
}
52+
} else if (source === '@material-ui/icons') {
53+
for (const specifier of node.specifiers) {
54+
if (specifier.type !== 'ImportSpecifier') continue
55+
const { loc } = specifier
56+
if (loc) {
57+
const { line, column } = loc.end
58+
iconsInsertPosition = new vscode.Position(line - 1, column)
59+
}
60+
const { imported, local } = specifier
61+
if (imported && local && imported.name + 'Icon' === local.name) {
62+
result.add(local.name)
63+
}
64+
}
65+
} else {
66+
const match = /^@material-ui\/(core|icons)\/([^/]+)/.exec(source)
67+
if (match) result.add(match[2] + (match[1] === 'icons' ? 'Icon' : ''))
3368
}
3469
})
3570
return {
36-
insertPosition: new vscode.Position(insertLine, 0),
3771
existingImports: result,
72+
insertPosition: new vscode.Position(insertLine, 0),
73+
coreInsertPosition,
74+
iconsInsertPosition,
3875
}
3976
}

src/getSnippetImports.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,9 @@
1-
export default function getSnippetImports(
2-
body: string
3-
): Record<string, string> {
1+
export default function getSnippetImports(body: string): string[] {
42
const rx = /<([A-Z][A-Za-z]+)/g
53
const components: Set<string> = new Set()
64
let match
75
while ((match = rx.exec(body))) {
86
components.add(match[1])
97
}
10-
const imports: Record<string, string> = {}
11-
;[...components].sort().forEach(component => {
12-
if (/Icon$/.test(component)) {
13-
imports[
14-
`@material-ui/icons/${component.replace(/Icon$/, '')}`
15-
] = `import ${component} from "@material-ui/icons/${component.replace(
16-
/Icon$/,
17-
''
18-
)}";`
19-
} else {
20-
imports[
21-
`@material-ui/core/${component}`
22-
] = `import ${component} from "@material-ui/core/${component}";`
23-
}
24-
})
25-
return imports
8+
return [...components].sort()
269
}

src/snip.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface CompiledSnippet {
1111

1212
export default function snip(
1313
strings: TemplateStringsArray,
14-
...expressions: any[]
14+
...expressions: any[] // eslint-disable-line @typescript-eslint/no-explicit-any
1515
): CompiledSnippet {
1616
const parts = []
1717
for (let i = 0; i < strings.length - 1; i++) {

0 commit comments

Comments
 (0)