UNPKG

common-intellisense

Version:
983 lines (915 loc) 30.4 kB
import fsp from 'node:fs/promises' import * as vscode from 'vscode' import { parse } from '@vue/compiler-sfc' import type { SFCTemplateBlock } from '@vue/compiler-sfc' import { parse as tsParser } from '@typescript-eslint/typescript-estree' import { createRange, getActiveText, getActiveTextEditor, getActiveTextEditorLanguageId, getCurrentFileUrl, getLocale, getOffsetFromPosition, getPosition, isInPosition, registerCodeLensProvider } from '@vscode-use/utils' import { traverse } from '@babel/types' import type { VineCompilerHooks, VineDiagnostic, VineFileCtx } from '@vue-vine/compiler' import { compileVineTypeScriptFile, createCompilerCtx, } from '@vue-vine/compiler' import { isVine, isVue, toCamel } from './ui/utils' import type { PropsConfig, PropsConfigItem } from './ui/utils' const { parse: svelteParser } = require('svelte/compiler') // 引入vue-parser只在template中才处理一些逻辑 let isInTemplate = false export function parser(code: string, position: vscode.Position) { const entry = getCurrentFileUrl() if (!entry) return const isVine = entry.endsWith('.vine.ts') if (isVine) { return parserVine(code, position) } else { const suffix = entry.slice(entry.lastIndexOf('.') + 1) if (!suffix) return isInTemplate = false if (suffix === 'vue') { const result = transformVue(code, position) if (!result) return if (!result.refs?.length || !result.template) return result const refsMap = findRefs(result.template, result.refs) return Object.assign(result, { refsMap }) } if (/ts|js|jsx|tsx/.test(suffix)) return parserJSX(code, position) if (suffix === 'svelte') return parserSvelte(code, position) } return true } export function transformVue(code: string, position: vscode.Position, offset = 0) { const { descriptor: { template, script, scriptSetup }, errors, } = parse(code) if (errors.length) return const _script = script || scriptSetup if (!template) { if (_script?.lang === 'tsx') { const r = parserJSX(_script.content, position) r.loc = _script.loc return r } return } if (_script && isInPosition(_script.loc, position, offset)) { const content = _script.content! const refs: (string | [string, string])[] = [] for (const match of content.matchAll(/(const|let|var)\s+([\w$]+)\s*=\s*(ref|useTemplateRef)[^()]*\(([^)]*)\)/g)) { if (match[3] === 'useTemplateRef') { refs.push([match[2], match[4].slice(1, -1)]) } else if (match) { refs.push(match[2]) } } return { type: 'script', refs, template, } } if (!isInPosition(template.loc, position, offset)) return // 在template中 const { ast } = template const r = dfs(ast.children, template, position, offset) if (r) { r.loc = _script?.loc return r } return r } export function transformVine(vineFileCtx: VineFileCtx, position: vscode.Position) { const targetInPositionNode = vineFileCtx.vineCompFns.find(item => item.fnDeclNode.loc ? isInPosition(item.fnDeclNode.loc, position) : false) if (!targetInPositionNode) return const { templateAst, fnDeclNode, templateStringNode } = targetInPositionNode const children = templateAst?.children if (!children) return const parent = fnDeclNode const result = dfs(children, parent, position, templateStringNode?.quasi.quasis[0].start || 0) const refsMap = findRef(children, {}) if (result) return Object.assign(result, refsMap) return { type: 'script', refsMap, } } function dfs(children: any, parent: any, position: vscode.Position, offset = 0) { for (const child of children) { const { loc, tag, props, children } = child if (!isInPosition(loc, position, offset)) continue if (tag) { const isTag = isInPosition({ start: loc.start, end: { line: loc.start.line, column: loc.start.column + tag.length, }, }, position, offset) if (isTag) { return { tag, props, type: 'tag', isInTemplate: true, parent: { tag: parent.tag ? parent.tag : 'template', props: parent.props || [], }, template: parent, } } } if (props && props.length) { for (const prop of props) { if (isInPosition(prop.loc, position, offset)) { if (!isInAttribute(child, position, offset)) return false if ((prop.name === 'bind' || prop.name === 'on') && prop.exp && isInPosition(prop.exp.loc, position)) { return { tag, propName: prop.exp?.content !== undefined, props, type: 'props', isInTemplate: true, isValue: prop.exp?.content !== undefined, parent: { tag: parent.tag ? parent.tag : 'template', props: parent.props || [], }, isDynamic: prop.name === 'bind', isEvent: prop.name === 'on', template: parent, } } else { let propName = prop.name if (prop.arg && isInPosition(prop.arg.loc, position)) propName = prop.arg.content else if (prop.exp && isInPosition(prop.exp.loc, position)) propName = prop.exp.content return { tag, propName, props, type: 'props', isInTemplate: true, isValue: prop.value?.content !== undefined, parent: { tag: parent.tag ? parent.tag : 'template', props: parent.props || [], }, template: parent, } } } } } if (children && children.length) { const result = dfs(children, child, position, offset) as any if (result) return result } if (tag) { if (!isInAttribute(child, position, offset)) return false return { type: 'props', tag, props, isInTemplate: true, parent: { tag: parent.tag ? parent.tag : 'template', props: parent.props || [], }, template: parent, } } if (child.type === 2 || child.content?.type === 2) { return { type: 'text', isInTemplate: true, props, parent: { tag: parent.tag ? parent.tag : 'template', props: parent.props || [], }, template: parent, } } return } } export function getReactRefsMap() { const ast = tsParser(getActiveText()!, { jsx: true, loc: true }) const children = ast.body return findJsxRefs(children) } export function parserJSX(code: string, position: vscode.Position) { try { const ast = tsParser(code, { jsx: true, loc: true }) const children = ast.body const result = jsxDfs(children, null, position) const map = findJsxRefs(children) if (result) return Object.assign(result, map) return { type: 'script', ...map, } } catch (error) { console.error(error) } } function jsxDfs(children: any, parent: any, position: vscode.Position) { for (const child of children) { let { loc, type, openingElement, body: children, argument, declarations, init } = child child.name = openingElement?.name.name if (!loc) loc = convertPositionToLoc(child) if (!isInPosition(loc, position)) continue if (!openingElement && child.attributes) { openingElement = { name: { name: child.name, }, attributes: child.attributes, } } if (openingElement && openingElement.attributes.length) { for (const prop of openingElement.attributes) { if (!prop.loc) prop.loc = convertPositionToLoc(prop) if (isInPosition(prop.loc, position)) { return { tag: openingElement.name.type === 'JSXMemberExpression' ? `${openingElement.name.object.name}.${openingElement.name.property.name}` : openingElement.name.name, propName: typeof prop.name === 'string' ? prop.type === 'EventHandler' ? 'on' : prop.name : prop.name.name, props: openingElement.attributes, propType: prop.type, type: 'props', isInTemplate, isValue: prop.value ? Array.isArray(prop.value) ? prop.value[0]?.raw !== undefined : prop.value.type === 'JSXExpressionContainer' ? prop.value?.expression !== undefined : prop.value?.value !== undefined : false, parent, isEvent: prop.type === 'EventHandler' || (prop.type === 'JSXAttribute' && prop.name.name.startsWith('on')), } } } } if (type === 'JSXElement' || type === 'Element' || (type === 'ReturnStatement' && (argument.type === 'JSXElement' || argument.type === 'JSXFragment'))) isInTemplate = true if (child.children) { children = child.children } else if (type === 'ExportNamedDeclaration') { children = child.declaration } else if (type === 'ObjectExpression') { children = child.properties } else if (type === 'Property' && child.value.type === 'FunctionExpression') { children = child.value.body.body } else if (type === 'ExportDefaultDeclaration') { if (child.declaration.type === 'FunctionDeclaration') children = child.declaration.body.body else children = child.declaration.arguments } else if (type === 'JSXExpressionContainer' || type === 'ChainExpression') { if (child.expression.type === 'CallExpression') { children = child.expression.arguments } else if (child.expression.type === 'ConditionalExpression') { children = [ child.expression.alternate, child.expression.consequent, ].filter(Boolean) } else if (child.expression.type === 'LogicalExpression') { children = [ child.expression.left, child.expression.right, ] } else { children = child.expression } } else if (type === 'TemplateLiteral') { children = child.expressions } else if (type === 'ConditionalExpression') { children = [ child.alternate, child.consequent, ].filter(Boolean) } else if (type === 'ArrowFunctionExpression') { children = child.body } else if (type === 'VariableDeclaration') { children = declarations } else if (type === 'VariableDeclarator') { children = init } else if (type === 'ReturnStatement') { children = argument } else if (type === 'JSXElement') { children = child.children } else if (type === 'ExportNamedDeclaration') { children = child.declaration.body } else if (type === 'CallExpression') { children = child.arguments } if (children && !Array.isArray(children)) children = [children] if (children && children.length) { const p = child.type === 'JSXElement' ? { name: openingElement.name.name, props: openingElement.attributes } : null const result = jsxDfs(children, p, position) as any if (result) return result } if (type === 'JSXElement' || type === 'Element' || type === 'InlineComponent') { const target = openingElement.attributes.find((item: any) => isInPosition(item.loc, position) || item.value === null) if (!openingElement) { openingElement = { name: { name: child.name, }, attributes: child.attributes, } } if (target) { return { type: 'props', tag: openingElement.name.type === 'JSXMemberExpression' ? `${openingElement.name.object.name}.${openingElement.name.property.name}` : openingElement.name.name, props: openingElement.attributes, propName: target.value ? typeof target.name === 'string' ? target.type === 'EventHandler' ? 'on' : target.name : target.name.name : '', propType: target.type, isInTemplate, parent, } } const isTag = isInPosition({ start: loc.start, end: { line: loc.start.line, column: loc.start.column + child.name.length, }, }, position) return { type: isTag ? 'tag' : 'props', tag: openingElement.name.type === 'JSXMemberExpression' ? `${openingElement.name.object.name}.${openingElement.name.property.name}` : openingElement.name.name, props: openingElement.attributes, isInTemplate, parent, } } if (type === 'JSXText' || type === 'Text') { return { isInTemplate, type: 'text', props: openingElement?.attributes, parent, } } return } } function findJsxRefs(childrens: any, map: any = {}, refs: any = []) { for (const child of childrens) { let { type, openingElement, body: children, argument, declarations, init, id, expression } = child if (child.children) { children = child.children } else if (type === 'VariableDeclaration') { children = declarations } else if (type === 'VariableDeclarator') { children = init if (init.callee && init.callee.name === 'useRef') { refs.push(id.name) continue } } else if (type === 'ExpressionStatement') { children = expression.arguments } else if (type === 'ReturnStatement') { children = argument } else if (type === 'JSXElement') { children = child.children } else if (type === 'ExportDefaultDeclaration') { if (child.declaration.type === 'FunctionDeclaration') { children = child.declaration.body.body } } else if (!children) { continue } if (children && !Array.isArray(children)) children = [children] if (openingElement && openingElement.attributes.length) { for (const prop of openingElement.attributes) { if (prop.name.name === 'ref') { const value = prop.value?.expression?.name || prop.value.value map[value] = transformTagName(openingElement.name.name) } } } if (children && children.length) findJsxRefs(children, map, refs) } return { refsMap: map, refs, } } export function findRefs(template: SFCTemplateBlock, refsMap: (string | [string, string])[]) { const { ast } = template return findRef(ast.children, {}, refsMap) } function findRef(children: any, map: any, refsMap: (string | [string, string])[] = []) { for (const child of children) { const { tag, props, children } = child if (props && props.length) { for (const prop of props) { const { name, value } = prop if (!value) continue let { content } = value if ((name !== 'ref') || !content) continue for (const r of refsMap) { if (Array.isArray(r) && r[1] === content) { content = r[0] } } const tagName = transformTagName(tag) map[content] = tagName } } if (children && children.length) findRef(children, map, refsMap) as any } return map } export function parserSvelte(code: string, position: vscode.Position) { const { html } = svelteParser(code) const result = jsxDfs([html], null, position) const map = { refsMap: {}, refs: [], } if (result) return Object.assign(result, map) return { type: 'script', ...map, } } // let stop: any = null // export const alias = getConfiguration('common-intellisense.alias') as Record<string, string> // export async function findPkgUI(cwd?: string) { // if (!cwd) // return // const pkg = await findUp('package.json', { cwd }) // if (!pkg) // return // if (stop) // stop() // stop = watchFiles(pkg, { // onChange() { // urlCache.clear() // findUI() // }, // }) // const p = JSON.parse(await fsp.readFile(pkg, 'utf-8')) // const { dependencies, devDependencies } = p // const result = [] // const aliasUiNames = Object.keys(alias) // if (dependencies) { // for (const key in dependencies) { // if (UINames.includes(key) || aliasUiNames.includes(key)) // result.push([key, dependencies[key]]) // } // } // if (devDependencies) { // for (const key in devDependencies) { // if (UINames.includes(key) || aliasUiNames.includes(key)) // result.push([key, devDependencies[key]]) // } // } // return { pkg, uis: result } // } export function transformTagName(name: string) { return name[0].toUpperCase() + name.replace(/(-\w)/g, (match: string) => match[1].toUpperCase()).slice(1) } export function isInAttribute(child: any, position: any, offset: number) { const len = child.props.length let end = null const start = { column: child.loc.start.column + child.tag.length + 1, line: child.loc.start.line, offset: child.loc.start.offset + child.tag.length + 1, } if (!len) { const childNode = child.children?.[0] if (childNode) { end = { line: childNode.loc.start.line, column: childNode.loc.start.column - 1, offset: childNode.loc.start.offset - 1, } } else { if (child.isSelfClosing) { end = { line: child.loc.end.line, column: child.loc.end.column - 2, offset: child.loc.end.offset - 2, } } else { const startOffset = start.offset const match = child.loc.source.slice(child.tag.length + 1).match('>')! const endOffset = startOffset + match.index const _offset = getOffsetFromPosition(position)! return (startOffset + offset < _offset) && (_offset <= endOffset + offset) } } } else { const offsetX = child.props[len - 1].loc.end.offset - child.loc.start.offset const x = child.loc.source.slice(offsetX).match('>').index! end = { column: child.props[len - 1].loc.end.column + 1 + x, line: child.props[len - 1].loc.end.line, offset: child.props[len - 1].loc.end.offset + 1 + x, } } const _offset = getOffsetFromPosition(position)! const startOffset = start.offset const endOffset = end.offset return (startOffset + offset < _offset) && (_offset <= endOffset + offset) } export function convertPositionToLoc(data: any) { const { start, end } = data const activeTextEditor = getActiveTextEditor()! const document = activeTextEditor.document return { start: convertOffsetToLineColumn(document, start), end: convertOffsetToLineColumn(document, end), } } function convertOffsetToPosition(document: vscode.TextDocument, offset: number) { return document.positionAt(offset) } function convertOffsetToLineColumn(document: vscode.TextDocument, offset: number) { const position = convertOffsetToPosition(document, offset) const lineText = document.lineAt(position.line).text const line = position.line + 1 const column = position.character + 1 const lineOffset = document.offsetAt(position) return { line, column, lineText, lineOffset } } const modules: any = { children: [], offset: 0, } export async function detectSlots(UiCompletions: any, uiDeps: any, prefix: string[]) { const children = (await getTemplateAst(UiCompletions, uiDeps, prefix)).filter(item => item.children.length) if (!children.length) { modules.children = [] modules.offset = 0 return } modules.children = children } export function registerCodeLensProviderFn() { const isZh = getLocale().includes('zh') return registerCodeLensProvider(['vue', 'javascriptreact', 'typescriptreact', 'typescript'], { provideCodeLenses() { const languageId = getActiveTextEditorLanguageId() if (languageId === 'typescript' && !isVine()) return [] const result: vscode.CodeLens[] = [] const children = modules.children children.forEach((child: any) => { const offset = child.offset child.children.forEach((m: any) => { const { child, slots } = m const range = child.loc const filters: string[] = [] for (const c of Array.from(child.children) as any) { if (c.type === 'JSXElement') { if (c.openingElement.name.name !== 'template') continue for (const p of c.openingElement.attributes) { const namespace = p.name.namespace.name if (namespace === 'v-slot') { const slotName = p.name.name.name filters.push(slotName) break } } } else if (c.tag === 'template' && c.props) { for (const p of c.props) { if (p.name === 'slot') { const slotName = p.arg.content filters.push(slotName) break } } } else if (c.codegenNode?.tag === 'template' && c.codegenNode.props) { for (const p of c.codegenNode.props) { if (p.name === 'slot') { const slotName = p.arg.content filters.push(slotName) break } } } } slots.filter((s: any) => !filters.includes(s.name)).forEach((s: any, i: number) => { const { name, description, description_zh } = s // 计算偏移量 let codeLensRange = null if (isVine()) { const fixedStart = getPosition(range.start.offset + offset).position const fixedEnd = getPosition(range.end.offset + offset).position codeLensRange = createRange(fixedStart, fixedEnd) } else { codeLensRange = createRange(range.start.line - 1, range.start.column, range.end.line - 1, range.end.column) } result.push(new vscode.CodeLens(codeLensRange, { title: `${i === 0 ? 'Slots: ' : ''}${name}`, tooltip: isZh ? description_zh : description, command: 'common-intellisense.slots', arguments: [child, name, offset], })) }) }) }) return result }, }) } async function getTemplateAst(UiCompletions: any, uiDeps: any, prefix: string[]): Promise<[{ children: any, offset: number }] | []> { const code = getActiveText()! if (isVue()) { const { descriptor: { template, script, scriptSetup }, } = parse(code) const _script = script || scriptSetup if (!template) { if (_script?.lang === 'tsx') { const children = findAllJsxElements(_script.content) return [{ children: await findUiTag(children, UiCompletions, [], new Set(), uiDeps, prefix), offset: _script.loc.start.offset, }] } return [] } return [{ children: await findUiTag(template.ast.children, UiCompletions, [], new Set(), uiDeps, prefix), offset: 0, }] } else if (isVine()) { const { vineFileCtx } = createVineFileCtx('', code) if (!vineFileCtx.vineCompFns) return [] return await Promise.all(vineFileCtx.vineCompFns.map(async (item) => { const r = { children: await findUiTag(item.templateAst?.children, UiCompletions, [], new Set(), uiDeps, prefix), offset: item.templateStringNode?.quasi.quasis[0].start || 0, } return r })) as any } else if (['javascriptreact', 'typescriptreact'].includes(getActiveTextEditorLanguageId()!)) { const children = findAllJsxElements(code) return [{ children: await findUiTag(children, UiCompletions, [], new Set(), uiDeps, prefix), offset: 0, }] } return [] } const originTag = ['div', 'span', 'ul', 'li', 'ol', 'p', 'main', 'header', 'footer', 'template', 'img', 'aside', 'body', 'a', 'video', 'table', 'th', 'tr', 'td', 'form', 'input', 'label', 'button', 'article', 'section'] async function findUiTag(children: any, UiCompletions: any, result: any[] = [], cacheMap = new Set(), uiDeps: any, prefix: string[]) { for (const child of children) { let tag: string = child.tag if (child.type === 'JSXElement') tag = child.openingElement.name.name if (!tag) continue const nextChildren = child.children if (nextChildren?.length) await findUiTag(nextChildren, UiCompletions, result, cacheMap, uiDeps, prefix) const range = child.range ?? child.loc if (cacheMap.has(range)) continue if (originTag.includes(tag)) continue const tagName = toCamel(`-${tag}`) let target = UiCompletions[tagName] || await findDynamicComponent(tagName, {}, UiCompletions, prefix) const importUiSource = uiDeps[tagName] if (!target) continue if (importUiSource && target.uiName !== importUiSource) { for (const p of prefix.filter(Boolean)) { const realName = p[0].toUpperCase() + p.slice(1) + tagName const newTarget = UiCompletions[realName] if (!newTarget) continue if (newTarget.uiName === importUiSource) { target = newTarget break } } } if (!target || !target.rawSlots?.length) continue cacheMap.add(range) result.push({ child, slots: target.rawSlots, }) } return result } function findAllJsxElements(code: string) { const ast = tsParser(code, { jsx: true, loc: true, range: true }) as any const results: any = [] traverse(ast, (node) => { if (node.type === 'JSXElement') { results.push(node) } else if (node.type === 'ObjectExpression') { const _node: any = node.properties?.find((p: any) => p?.key?.name === 'render') || node.properties?.find((p: any) => p?.key?.name === 'setup') const t = _node?.value if (t) { traverse(t, (nextNode) => { if (nextNode.type === 'JSXElement') { const tag = (nextNode.openingElement.name as any)?.name if (tag && !originTag.includes(tag)) results.push(nextNode) } }) } } }) return results } export function parserVine(code: string, position: vscode.Position) { const { vineFileCtx } = createVineFileCtx('', code) if (!vineFileCtx.vineCompFns.length) return return transformVine(vineFileCtx, position) } export function createVineFileCtx(sourceFileName: string, source: string) { const compilerCtx = createCompilerCtx({ envMode: 'module', vueCompilerOptions: { // 'module' will break Volar virtual code's mapping mode: 'function', // These options below is for resolving conflicts // with original compiler's mode: 'module' cacheHandlers: false, prefixIdentifiers: false, scopeId: null, }, inlineTemplate: false, }) const vineCompileErrs: VineDiagnostic[] = [] const vineCompileWarns: VineDiagnostic[] = [] const compilerHooks: VineCompilerHooks = { onError: err => vineCompileErrs.push(err), onWarn: warn => vineCompileWarns.push(warn), getCompilerCtx: () => compilerCtx, } const vineFileCtx = compileVineTypeScriptFile( source, sourceFileName, { compilerHooks, babelParseOptions: { tokens: true, }, }, ) return { vineFileCtx, vineCompileErrs, vineCompileWarns, } } export function isSamePrefix(label: string, key: string) { let labelName = label.split('=')[0] if (labelName.indexOf(' ')) { // 防止匹配到描述中的= labelName = labelName.split(' ')[0] } return labelName === key } const IMPORT_REG = /import\s+(\S+)\s+from\s+['"]([^"']+.vue)['"]/g export function getImportDeps(text: string) { text = text.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '') const deps: Record<string, string> = {} for (const match of text.matchAll(IMPORT_REG)) { if (!match) continue const from = match[2] if (!/^[./@]/.test(from)) continue deps[match[1]] = from } return deps } export function getAbsoluteUrl(url: string) { return path.resolve(getCurrentFileUrl()!, '..', url) } export async function findDynamicComponent(name: string, deps: Record<string, string>, UiCompletions: PropsConfig, prefix: string[], from?: string) { // const prefix = optionsComponents.prefix let target = findDynamic(name, UiCompletions, prefix, from) if (target) return target let dep if (dep = deps[name]) { // 只往下找一层 const tag = await getTemplateParentElementName(getAbsoluteUrl(dep)) if (!tag) return target = findDynamic(tag, UiCompletions, prefix, from) } return target } function findDynamic(tag: string, UiCompletions: PropsConfig, prefix: string[], from?: string) { let target: PropsConfigItem | null = UiCompletions[tag] if (target && from && target.lib !== from) { target = null } if (!target) { for (const p of prefix) { if (!p) continue const t = UiCompletions[p[0].toUpperCase() + p.slice(1) + tag] if (from && t && t.lib === from) { target = t break } else if (t) { target = t break } } } return target } async function getTemplateParentElementName(url: string) { const code = await fsp.readFile(url, 'utf-8') // 如果有defineProps或者props的忽律,交给v-component-prompter处理 const { descriptor: { template, script, scriptSetup }, } = parse(code) if (script?.content && /^\s*props:\s*\{/.test(script.content)) return if (scriptSetup?.content && /defineProps\(/.test(scriptSetup.content)) return if (!template?.ast?.children?.length) return let result = '' for (const child of template.ast.children) { const node = child as any if (node.tag) { if (result) // 说明template下不是唯一父节点 return result = node.tag } } return result }