Skip to content

Commit 1c0edd3

Browse files
committed
feat: Add files auto import! By configuring a filesAutoImport setting you can now setup robust auto imports for .svg, .styles or any other extension!
1 parent 0e061d3 commit 1c0edd3

File tree

7 files changed

+195
-5
lines changed

7 files changed

+195
-5
lines changed

src/configurationType.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,30 @@ export type Configuration = {
684684
* @default false
685685
*/
686686
declareMissingPropertyQuickfixOtherFiles: boolean
687+
/**
688+
* @default {}
689+
*/
690+
filesAutoImport: {
691+
[ext: string]: {
692+
/**
693+
* Override import path (default is "$path")
694+
*/
695+
importPath?: string
696+
/**
697+
* Start phrase that will trigger search for available files import
698+
*/
699+
prefix: string
700+
/**
701+
* @default camel
702+
*/
703+
nameCasing?: 'camel' | 'pascal' | 'constant' | 'snake'
704+
/**
705+
* @default $name
706+
*/
707+
nameTransform?: string
708+
iconPost?: string
709+
}
710+
}
687711
}
688712

689713
// scrapped using search editor. config: caseInsensitive, context lines: 0, regex: const fix\w+ = "[^ ]+"

typescript/src/completionEntryDetails.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,29 @@ export default function completionEntryDetails(
2222
const sourceFile = program?.getSourceFile(fileName)
2323
if (!program || !sourceFile) return
2424

25-
const { documentationOverride, documentationAppend, detailPrepend } = prevCompletionsMap[entryName] ?? {}
25+
const { documentationOverride, documentationAppend, detailPrepend, textChanges } = prevCompletionsMap[entryName] ?? {}
2626
if (documentationOverride) {
27-
return {
27+
const prior: ts.CompletionEntryDetails = {
2828
name: entryName,
2929
kind: ts.ScriptElementKind.alias,
3030
kindModifiers: '',
3131
displayParts: typeof documentationOverride === 'string' ? [{ kind: 'text', text: documentationOverride }] : documentationOverride,
3232
}
33+
if (textChanges) {
34+
prior.codeActions = [
35+
// ...(prior.codeActions ?? []),
36+
{
37+
description: 'Includes Text Changes',
38+
changes: [
39+
{
40+
fileName,
41+
textChanges,
42+
},
43+
],
44+
},
45+
]
46+
}
47+
return prior
3348
}
3449
let prior = languageService.getCompletionEntryDetails(
3550
fileName,
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { camelCase, pascalCase, snakeCase, constantCase } from 'change-case'
2+
import { Configuration } from '../types'
3+
import { nodeModules } from '../utils'
4+
import { sharedCompletionContext } from './sharedContext'
5+
6+
export default () => {
7+
const { c, prior, languageService, languageServiceHost, node, sourceFile, prevCompletionsMap } = sharedCompletionContext
8+
// todo better web support?
9+
if (!node || !languageServiceHost.readDirectory || !nodeModules?.path) return
10+
const filesAutoImport = c('filesAutoImport')
11+
const included: Array<{ ext: string; item: Configuration['filesAutoImport'][string] }> = []
12+
const currentText = node.getText()
13+
for (const [ext, item] of Object.entries(filesAutoImport)) {
14+
if (currentText.startsWith(item.prefix)) included.push({ ext, item })
15+
}
16+
// if (!included.length) return
17+
const root = languageServiceHost.getCurrentDirectory()
18+
// const fileRelative = nodeModules.path.relative(root, sourceFile.fileName)
19+
const collected = [] as string[]
20+
const MAX_ITERATIONS = 200
21+
let iter = 0
22+
const collectFiles = (dir: string) => {
23+
iter++
24+
if (iter > MAX_ITERATIONS) {
25+
console.error('[essentials plugin filesAutoImport] Max iterations reached')
26+
return
27+
}
28+
const files = nodeModules!.fs.readdirSync(dir, { withFileTypes: true })
29+
for (const file of files) {
30+
if (file.isDirectory()) {
31+
if (
32+
file.name === 'node_modules' ||
33+
file.name.startsWith('.') ||
34+
file.name.startsWith('out') ||
35+
file.name.startsWith('build') ||
36+
file.name.startsWith('dist')
37+
)
38+
continue
39+
collectFiles(nodeModules!.path.join(dir, file.name))
40+
} else if (file.isFile()) {
41+
// const ext = nodeModules!.path.extname(file.name)
42+
// if (included.some(i => i.ext === ext)) files.push(nodeModules!.path.join(dir, file.name))
43+
collected.push(nodeModules!.path.relative(root, nodeModules!.path.join(dir, file.name)))
44+
}
45+
}
46+
}
47+
collectFiles(root)
48+
49+
const lastImport = sourceFile.statements.filter(ts.isImportDeclaration).at(-1)
50+
51+
// const directory = languageServiceHost.readDirectory(root, undefined, undefined, undefined, 1)
52+
const completions: Array<{
53+
name: string
54+
insertText: string
55+
addImport: string
56+
detail: string
57+
description: string
58+
sort: number
59+
}> = []
60+
for (const { ext, item } of included) {
61+
const files = collected.filter(f => f.endsWith(ext))
62+
for (const file of files) {
63+
const fullPath = nodeModules.path.join(root, file)
64+
const relativeToFile = nodeModules.path.relative(nodeModules.path.dirname(sourceFile.fileName), fullPath).replaceAll('\\', '/')
65+
const lastModified = nodeModules.fs.statSync(fullPath).mtime
66+
const lastModifiedFormatted = timeDifference(Date.now(), lastModified.getTime())
67+
const importPath = (item.importPath ?? '$path').replaceAll('$path', relativeToFile)
68+
const casingFn = {
69+
camel: camelCase,
70+
pascal: pascalCase,
71+
snake: snakeCase,
72+
constant: constantCase,
73+
}
74+
const name =
75+
item.prefix + casingFn[item.nameCasing ?? 'camel']((item.nameTransform ?? '$name').replaceAll('$name', nodeModules.path.basename(file, ext)))
76+
if (prior.entries.some(e => e.name === name)) continue
77+
completions.push({
78+
name,
79+
insertText: name,
80+
sort: Date.now() - lastModified.getTime(),
81+
detail: `${item.iconPost?.replaceAll('$path', relativeToFile) ?? '📄'} ${lastModifiedFormatted}`,
82+
description: importPath,
83+
addImport: `import ${name} from '${importPath}'`,
84+
})
85+
}
86+
}
87+
88+
const prependImport = lastImport ? '\n' : ''
89+
const entries = completions.map(({ name, insertText, detail, sort, addImport, description }): ts.CompletionEntry => {
90+
prevCompletionsMap[name] = {
91+
textChanges: [
92+
{
93+
newText: `${prependImport}${addImport}`,
94+
span: {
95+
start: lastImport?.end ?? 0,
96+
length: 0,
97+
},
98+
},
99+
],
100+
documentationOverride: description,
101+
}
102+
return {
103+
kind: ts.ScriptElementKind.variableElement,
104+
name,
105+
insertText,
106+
sortText: `${sort}`,
107+
labelDetails: {
108+
description: detail,
109+
},
110+
// description,
111+
}
112+
})
113+
return entries
114+
}
115+
116+
function timeDifference(current, previous) {
117+
const msPerMinute = 60 * 1000
118+
const msPerHour = msPerMinute * 60
119+
const msPerDay = msPerHour * 24
120+
const msPerMonth = msPerDay * 30
121+
const msPerYear = msPerDay * 365
122+
123+
const elapsed = current - previous
124+
125+
if (elapsed < msPerMinute) {
126+
return `${Math.round(elapsed / 1000)} sec ago`
127+
}
128+
129+
if (elapsed < msPerHour) {
130+
return `${Math.round(elapsed / msPerMinute)} min ago`
131+
}
132+
133+
if (elapsed < msPerDay) {
134+
return `${Math.round(elapsed / msPerHour)} h ago`
135+
}
136+
137+
if (elapsed < msPerMonth) {
138+
return `${Math.round(elapsed / msPerDay)} days ago`
139+
}
140+
141+
if (elapsed < msPerYear) {
142+
return `${Math.round(elapsed / msPerMonth)} months ago`
143+
}
144+
145+
return `${Math.round(elapsed / msPerYear)} years ago`
146+
}

typescript/src/completions/sharedContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ export const sharedCompletionContext = {} as unknown as Readonly<{
1616
preferences: ts.UserPreferences
1717
fullText: string
1818
typeChecker: ts.TypeChecker
19-
// languageServiceHost: ts.LanguageServiceHost
19+
languageServiceHost: ts.LanguageServiceHost
2020
}>

typescript/src/completionsAtPosition.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import localityBonus from './completions/localityBonus'
3232
import functionCompletions from './completions/functionCompletions'
3333
import staticHintSuggestions from './completions/staticHintSuggestions'
3434
import typecastCompletions from './completions/typecastCompletions'
35+
import filesAutoImport from './completions/filesAutoImport'
3536

3637
export type PrevCompletionMap = Record<
3738
string,
@@ -42,7 +43,7 @@ export type PrevCompletionMap = Record<
4243
detailPrepend?: string
4344
documentationAppend?: string
4445
range?: [number, number]
45-
// textChanges?: ts.TextChange[]
46+
textChanges?: ts.TextChange[]
4647
}
4748
>
4849
export type PrevCompletionsAdditionalData = {
@@ -63,6 +64,7 @@ export const getCompletionsAtPosition = (
6364
options: ts.GetCompletionsAtPositionOptions | undefined,
6465
c: GetConfig,
6566
languageService: ts.LanguageService,
67+
languageServiceHost: ts.LanguageServiceHost,
6668
scriptSnapshot: ts.IScriptSnapshot,
6769
formatOptions: ts.FormatCodeSettings | undefined,
6870
additionalData: { scriptKind: ts.ScriptKind; compilerOptions: ts.CompilerOptions },
@@ -148,6 +150,7 @@ export const getCompletionsAtPosition = (
148150
prior: prior!,
149151
fullText: sourceFile.getFullText(),
150152
typeChecker: program.getTypeChecker(),
153+
languageServiceHost,
151154
} satisfies typeof sharedCompletionContext)
152155

153156
if (node && !hasSuggestions && ensurePrior() && prior) {
@@ -376,6 +379,7 @@ export const getCompletionsAtPosition = (
376379
}
377380

378381
if (!prior.isMemberCompletion) {
382+
prior.entries = [...prior.entries, ...(filesAutoImport() ?? [])]
379383
prior.entries = markOrRemoveGlobalCompletions(prior.entries, position, languageService, c) ?? prior.entries
380384
}
381385
if (exactNode) {

typescript/src/decorateProxy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const decorateLanguageService = (
8787
if (!scriptSnapshot) return
8888
const compilerOptions = languageServiceHost.getCompilationSettings()
8989
try {
90-
const result = getCompletionsAtPosition(fileName, position, options, c, languageService, scriptSnapshot, formatOptions, {
90+
const result = getCompletionsAtPosition(fileName, position, options, c, languageService, languageServiceHost, scriptSnapshot, formatOptions, {
9191
scriptKind,
9292
compilerOptions,
9393
})

typescript/test/testing.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const getCompletionsAtPosition = (pos: number, { fileName = entrypoint, s
4545
},
4646
defaultConfigFunc,
4747
languageService,
48+
languageServiceHost,
4849
languageServiceHost.getScriptSnapshot(entrypoint)!,
4950
{
5051
convertTabsToSpaces: false,

0 commit comments

Comments
 (0)