common-intellisense
Version:
261 lines (241 loc) • 8.49 kB
text/typescript
import fsp from 'node:fs/promises'
import { findUp } from 'find-up'
import { createLog, getActiveText, getCurrentFileUrl, setConfiguration, watchFiles } from '@vscode-use/utils'
import type * as vscode from 'vscode'
import { UINames as configUINames } from './constants'
import { formatUIName, getAlias, getIsShowSlots, getSelectedUIs, getUiDeps } from './ui-utils'
import type { ComponentsConfig, Directives, PropsConfig, SubCompletionItem } from './ui/utils'
import { cacheFetch, fetchFromCommonIntellisense, fetchFromLocalUris, fetchFromRemoteNpmUrls, fetchFromRemoteUrls, getLocalCache, localCacheUri } from './fetch'
export interface OptionsComponents {
prefix: string[]
data: (() => vscode.CompletionItem[])[]
directivesMap: Record<string, Directives | undefined>
}
export type Uis = [string, string][]
export const logger = createLog('common-intellisense')
const UI: Record<string, () => any> = {}
const UINames: string[] = []
let optionsComponents: OptionsComponents = { prefix: [], data: [], directivesMap: {} }
let UiCompletions: PropsConfig | null = null
const cacheMap = new Map<string, ComponentsConfig | PropsConfig>()
const pkgUIConfigMap = new Map<string, { propsConfig: PropsConfig, componentsConfig: ComponentsConfig }>()
export const eventCallbacks = new Map<string, SubCompletionItem[]>()
export const completionsCallbacks = new Map<string, SubCompletionItem[]>()
let currentPkgUiNames: null | string[] = null
// const filters = ['js', 'ts', 'jsx', 'tsx', 'vue', 'svelte']
export const urlCache = new Map<string, Uis>()
let stop: any = null
let preUis: Uis | null = null
export function findUI(extensionContext: vscode.ExtensionContext, detectSlots: any) {
UINames.length = 0
optionsComponents = { prefix: [], data: [], directivesMap: {} }
UiCompletions = null
eventCallbacks.clear()
completionsCallbacks.clear()
currentPkgUiNames = null
cacheMap.clear()
pkgUIConfigMap.clear()
urlCache.clear()
const selectedUIs = getSelectedUIs()
const alias = getAlias()
const cwd = getCurrentFileUrl()
if (!cwd || cwd === 'exthhost')
return
if (urlCache.has(cwd)) {
const uis = urlCache.get(cwd)
getOthers()
if (uis && uis.length)
updateCompletions(uis)
return
}
const OnChange = () => findUI(extensionContext, detectSlots)
findPkgUI(cwd, OnChange).then(async (res) => {
if (!res)
return
const { uis } = res
urlCache.set(cwd, uis)
getOthers()
if (!uis || !uis.length)
return
return updateCompletions(uis).then(() => {
logger.info(`findUI: ${uis.map(ui => ui.join('@')).join(' | ')}`)
}).catch((error) => {
logger.info(`updateCompletions获取失败${error?.message || error}`)
})
}).catch((error) => {
logger.info(`findPkgUI获取失败${error?.message || error}`)
})
async function updateCompletions(uis: Uis) {
if (!preUis) {
preUis = uis
}
else if (UiCompletions && (preUis.join('') === uis.join(''))) {
return
}
else {
preUis = uis
}
// 读取本地缓存
await getLocalCache
// 获取远程的 UI 库
const uisName: string[] = []
const originUisName: string[] = []
for await (let [uiName, version] of uis) {
let _version = version.match(/[^~]?(\d+)./)![1]
if (uiName in alias) {
const v = alias[uiName]
const m = v.match(/([^1-9^]+)\^?(\d)/)!
_version = m[2]
originUisName.push(`${uiName}${_version}`)
uiName = m[1]
}
else {
originUisName.push(`${uiName}${_version}`)
}
const formatName = `${formatUIName(uiName)}${_version}`
uisName.push(formatName)
}
if (selectedUIs && selectedUIs.length && !selectedUIs.includes('auto')) {
UINames.push(...selectedUIs.filter(item => uisName.includes(item)))
if (!UINames.length)
setConfiguration('common-intellisense.ui', [])
}
if (!UINames.length)
UINames.push(...uisName)
currentPkgUiNames = uisName
optionsComponents = { prefix: [], data: [], directivesMap: {} }
await Promise.all(UINames.map(async (name: string) => {
let componentsNames
const key = `${name}Components`
if (cacheMap.has(key)) {
componentsNames = cacheMap.get(key)
}
else {
try {
Object.assign(UI, await fetchFromCommonIntellisense(name.replace(/([A-Z])/g, '-$1').toLowerCase()))
componentsNames = UI[key]?.()
cacheMap.set(key, componentsNames)
}
catch (error) {
logger.error(`fetch fetchFromCommonIntellisense [${name}] error: ${String(error)}`)
}
}
if (componentsNames) {
for (const componentsName of componentsNames) {
const { prefix, data, directives, lib } = componentsName
if (!optionsComponents.prefix.includes(prefix))
optionsComponents.prefix.push(prefix)
optionsComponents.data.push(data)
const libWithVersion = originUisName.find(item => item.startsWith(lib))!
optionsComponents.directivesMap[libWithVersion] = directives
}
}
let completion
if (cacheMap.has(name)) {
completion = cacheMap.get(name)
}
else {
completion = UI[name]?.()
cacheMap.set(name, completion)
}
if (!UiCompletions)
UiCompletions = {}
Object.assign(UiCompletions, completion)
}))
try {
fsp.writeFile(localCacheUri, JSON.stringify(Array.from(cacheFetch.entries())))
}
catch (error) {
logger.error(`写入${localCacheUri} 失败: ${String(error)}`)
}
if (getIsShowSlots()) {
const activeText = getActiveText()
if (activeText)
detectSlots(UiCompletions, getUiDeps(activeText), optionsComponents.prefix)
}
}
}
// TODO: 找到依赖包的package.json里面带有本插件相关的配置文件并且读取
export async function findPkgUI(cwd?: string, onChange?: () => void) {
const alias = getAlias()
if (!cwd)
return
const pkg = await findUp('package.json', { cwd })
if (!pkg)
return
if (stop)
stop()
if (onChange)
stop = watchFiles(pkg, { onChange })
const p = JSON.parse(await fsp.readFile(pkg, 'utf-8'))
const { dependencies = {}, devDependencies = {}, peerDependencies = {} } = p
const result: Uis = []
const aliasUiNames = Object.keys(alias)
const deps = { ...dependencies, ...peerDependencies, ...devDependencies }
for (const key in deps) {
if (configUINames.includes(key) || aliasUiNames.includes(key))
result.push([key, deps[key]])
}
return { pkg, uis: result }
}
export function deactivateUICache() {
UINames.length = 0
optionsComponents = { prefix: [], data: [], directivesMap: {} }
UiCompletions = null
cacheMap.clear()
pkgUIConfigMap.clear()
urlCache.clear()
eventCallbacks.clear()
completionsCallbacks.clear()
Object.entries(UI).forEach(([key]) => {
delete UI[key]
})
}
export function getCurrentPkgUiNames() {
return currentPkgUiNames
}
export function getOptionsComponents() {
return optionsComponents
}
export function getUiCompletions() {
return UiCompletions
}
export function getCacheMap() {
return cacheMap
}
async function getOthers() {
try {
const others = Object.assign({}, ...await Promise.all([fetchFromLocalUris(), fetchFromRemoteUrls(), fetchFromRemoteNpmUrls()]))
if (Object.keys(others).length) {
for (const key in others) {
if (key.endsWith('Components')) {
const componentsNames = others[key]?.()
if (!componentsNames)
continue
for (const componentsName of componentsNames) {
const { prefix, data, directives } = componentsName
if (!optionsComponents.prefix.includes(prefix))
optionsComponents.prefix.push(prefix)
optionsComponents.data.push(data)
const libWithVersion = key.slice(0, -'Components'.length)
optionsComponents.directivesMap[libWithVersion] = directives
cacheMap.set(key, componentsName)
}
}
else {
const completion = others[key]?.()
if (!completion)
continue
if (!UiCompletions)
UiCompletions = {}
cacheMap.set(key, completion)
Object.assign(UiCompletions, completion)
}
}
}
Object.assign(UI, others)
}
catch (error) {
logger.error(`fetch error: ${String(error)}`)
}
}