|
| 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 | +} |
0 commit comments