diff --git a/inspect-extension/ts-features/ts-go-to-definition/README.md b/inspect-extension/ts-features/ts-go-to-definition/README.md
new file mode 100644
index 0000000..3c72801
--- /dev/null
+++ b/inspect-extension/ts-features/ts-go-to-definition/README.md
@@ -0,0 +1,160 @@
+# Go to Definition 测试用例
+
+本目录包含用于测试"跳转到定义"功能的测试用例。
+
+## 测试场景
+
+### 1. 组件属性跳转到 properties/props 定义
+
+#### Options API 子组件 (`child-component.mpx`)
+
+| 父组件属性 | 预期跳转目标 |
+| ------------- | ------------------------------- |
+| `title` | `properties.title` |
+| `count` | `properties.count` |
+| `show-header` | `properties.showHeader` |
+| `list-data` | `properties.listData` |
+| `config` | `properties.config` |
+| `name` | `properties.name` (简写形式) |
+| `age` | `properties.age` (简写形式) |
+| `visible` | `properties.visible` (简写形式) |
+
+子组件中方法可能定义在不同位置:
+
+| 父组件事件绑定的方法 | 子组件中定义位置 | 预期跳转目标 |
+| --------------------------- | -------------------- | ------------------------------- |
+| `onMethodsHandler` | methods 中 | `methods.onMethodsHandler` |
+| `onTopLevelHandler` | 顶层(和 data 同级) | `onTopLevelHandler()` |
+| `onTopLevelHandlerWithArgs` | 顶层(和 data 同级) | `onTopLevelHandlerWithArgs()` |
+| `onTopLevelArrowHandler` | 顶层(箭头函数) | `onTopLevelArrowHandler: () =>` |
+
+#### Composition API 子组件 - 泛型写法 (`child-component-setup.mpx`)
+
+| 父组件属性 | 预期跳转目标 |
+| ---------- | ------------------------------------ |
+| `message` | `defineProps<{ message: string }>` |
+| `visible` | `defineProps<{ visible: boolean }>` |
+| `items` | `defineProps<{ items: string[] }>` |
+| `optional` | `defineProps<{ optional?: string }>` |
+| `config` | `defineProps<{ config?: {...} }>` |
+
+#### Composition API 子组件 - 对象写法 (`child-component-setup-object.mpx`)
+
+| 父组件属性 | 预期跳转目标 |
+| ---------- | ----------------------------------- |
+| `msg` | `defineProps({ msg: String })` |
+| `count` | `defineProps({ count: Number })` |
+| `enabled` | `defineProps({ enabled: Boolean })` |
+
+#### withDefaults 子组件 (`child-component-with-defaults.mpx`)
+
+| 父组件属性 | 预期跳转目标 |
+| ---------- | ---------------------- |
+| `title` | `Props.title` 类型定义 |
+| `count` | `Props.count` 类型定义 |
+| `theme` | `Props.theme` 类型定义 |
+
+### 2. 事件方法跳转
+
+#### 支持的事件绑定语法
+
+| 语法 | 示例 | 说明 |
+| ------------------- | ----------------------------- | --------------------- |
+| `bindxxx` | `bindtap="handler"` | 绑定事件 |
+| `bind:xxx` | `bind:tap="handler"` | 绑定事件(带冒号) |
+| `catchxxx` | `catchtap="handler"` | 捕获事件,阻止冒泡 |
+| `catch:xxx` | `catch:tap="handler"` | 捕获事件(带冒号) |
+| `capture-bind:xxx` | `capture-bind:tap="handler"` | 捕获阶段绑定 |
+| `capture-catch:xxx` | `capture-catch:tap="handler"` | 捕获阶段捕获 |
+| `mut-bind:xxx` | `mut-bind:tap="handler"` | 互斥事件绑定 (2.8.2+) |
+| 动态绑定 | `bindtap="{{ handlerName }}"` | 方法名是变量 |
+
+#### Options API 父组件 (`parent-component.mpx`)
+
+点击事件处理方法名,应跳转到对应的定义位置:
+
+| 模板中的方法 | 定义位置 | 预期跳转目标 |
+| ---------------------- | ------------ | --------------------------------------------- |
+| `onChildChange` | methods | `methods.onChildChange` |
+| `handleViewTap` | methods | `methods.handleViewTap` |
+| `handleLongPress` | methods | `methods.handleLongPress` |
+| `inputBlur` | methods | `methods.inputBlur` |
+| `handleImageLoad` | methods | `methods.handleImageLoad` |
+| `setupHandler` | setup 返回值 | `setup() { return { setupHandler } }` |
+| `setupHandlerWithArgs` | setup 返回值 | `setup() { return { setupHandlerWithArgs } }` |
+| `dataHandler` | data | `data.dataHandler` |
+| `computedHandler` | computed | `computed.computedHandler` |
+
+#### Composition API 父组件 (`parent-component-setup.mpx`)
+
+点击事件处理方法名,应跳转到 `
+
+
diff --git a/inspect-extension/ts-features/ts-go-to-definition/child-component-setup.mpx b/inspect-extension/ts-features/ts-go-to-definition/child-component-setup.mpx
new file mode 100644
index 0000000..1a9a249
--- /dev/null
+++ b/inspect-extension/ts-features/ts-go-to-definition/child-component-setup.mpx
@@ -0,0 +1,51 @@
+
+ Child Component (Composition API)
+ 内部点击
+
+
+
+
+
diff --git a/inspect-extension/ts-features/ts-go-to-definition/child-component-with-defaults.mpx b/inspect-extension/ts-features/ts-go-to-definition/child-component-with-defaults.mpx
new file mode 100644
index 0000000..0ad7f47
--- /dev/null
+++ b/inspect-extension/ts-features/ts-go-to-definition/child-component-with-defaults.mpx
@@ -0,0 +1,32 @@
+
+ Child Component (withDefaults)
+
+
+
+
+
diff --git a/inspect-extension/ts-features/ts-go-to-definition/child-component.mpx b/inspect-extension/ts-features/ts-go-to-definition/child-component.mpx
new file mode 100644
index 0000000..92d256f
--- /dev/null
+++ b/inspect-extension/ts-features/ts-go-to-definition/child-component.mpx
@@ -0,0 +1,44 @@
+
+ Child Component (Options API)
+
+
+
+
+
diff --git a/inspect-extension/ts-features/ts-go-to-definition/parent-component-setup.mpx b/inspect-extension/ts-features/ts-go-to-definition/parent-component-setup.mpx
new file mode 100644
index 0000000..9b45d60
--- /dev/null
+++ b/inspect-extension/ts-features/ts-go-to-definition/parent-component-setup.mpx
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+ 点击测试
+
+
+
+
+ 带参数
+
+
+
+
+
+
+
+
+
diff --git a/inspect-extension/ts-features/ts-go-to-definition/parent-component.mpx b/inspect-extension/ts-features/ts-go-to-definition/parent-component.mpx
new file mode 100644
index 0000000..fcf1208
--- /dev/null
+++ b/inspect-extension/ts-features/ts-go-to-definition/parent-component.mpx
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspect-extension/ts-features/ts-go-to-definition/parent-page-setup.mpx b/inspect-extension/ts-features/ts-go-to-definition/parent-page-setup.mpx
new file mode 100644
index 0000000..c9fb0e1
--- /dev/null
+++ b/inspect-extension/ts-features/ts-go-to-definition/parent-page-setup.mpx
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+ 点击测试
+
+
+
+
+
+
+
+
+
diff --git a/inspect-extension/ts-features/ts-go-to-definition/parent-page.mpx b/inspect-extension/ts-features/ts-go-to-definition/parent-page.mpx
new file mode 100644
index 0000000..f8e0e2a
--- /dev/null
+++ b/inspect-extension/ts-features/ts-go-to-definition/parent-page.mpx
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+ 点击测试
+
+
+
+
+ 页面顶层方法
+
+
+
+
+ setup 中的方法
+
+
+
+
+ methods 中的方法
+
+
+
+
+
+
+
+
+
diff --git a/packages/typescript-plugin/src/common.ts b/packages/typescript-plugin/src/common.ts
index aa48ac9..73e4f05 100644
--- a/packages/typescript-plugin/src/common.ts
+++ b/packages/typescript-plugin/src/common.ts
@@ -8,7 +8,7 @@ import {
forEachElementNode,
hyphenateTag,
} from '@mpxjs/language-core'
-import { capitalize } from '@mpxjs/language-shared'
+import { camelize, capitalize } from '@mpxjs/language-shared'
import { _getComponentNames } from './requests/getComponentNames'
import { _getElementNames } from './requests/getElementNames'
@@ -243,10 +243,66 @@ function getDefinitionAndBoundSpan(
}
}
+ // 从模板位置提取属性名和组件标签名
+ const templateContent = root.sfc.template.content
+ const templateOffset = position - root.sfc.template.startTagEnd
+ const attrInfo = extractAttributeNameAtPosition(
+ templateContent,
+ templateOffset,
+ )
+ const tagInfo = extractTagNameAtPosition(templateContent, templateOffset)
+
+ // 如果在组件属性上(非事件绑定),尝试跳转到子组件的属性定义
+ if (attrInfo && tagInfo && !attrInfo.isEvent) {
+ // 从 usingComponents 中查找子组件路径
+ const componentPath = findComponentPath(
+ root.sfc,
+ tagInfo.tagName,
+ fileName,
+ )
+
+ if (componentPath) {
+ const propDefinition = tryFindComponentPropDefinitionByPath(
+ ts,
+ language,
+ asScriptId,
+ componentPath,
+ attrInfo.attrName,
+ )
+ if (propDefinition) {
+ console.log(
+ '[Mpx Go to Definition] Found prop definition in child component:',
+ JSON.stringify(propDefinition),
+ )
+ definitions.add(propDefinition)
+ // 跳过原始定义
+ for (const def of result.definitions) {
+ skippedDefinitions.push(def)
+ }
+ }
+ }
+ }
+
for (const definition of result.definitions) {
- if (
- mpxOptions.extensions.some(ext => definition.fileName.endsWith(ext))
- ) {
+ const isMpxFile = mpxOptions.extensions.some(ext =>
+ definition.fileName.endsWith(ext),
+ )
+
+ console.log(
+ '[Mpx Go to Definition] Processing definition:',
+ JSON.stringify({
+ fileName: definition.fileName,
+ textSpan: definition.textSpan,
+ isMpxFile,
+ definitionName: definition.name,
+ definitionKind: definition.kind,
+ containerName: definition.containerName,
+ extensions: mpxOptions.extensions,
+ }),
+ )
+
+ // 跳过已经处理过的组件属性定义
+ if (isMpxFile && attrInfo && tagInfo && !attrInfo.isEvent) {
continue
}
@@ -316,6 +372,537 @@ function getDefinitionAndBoundSpan(
}
}
+/**
+ * 从模板内容中提取当前位置的属性名
+ * 支持: title="xxx", show-header="{{ xxx }}", bindtap="handler"
+ */
+function extractAttributeNameAtPosition(
+ templateContent: string,
+ offset: number,
+): { attrName: string; isEvent: boolean } | undefined {
+ // 向前查找属性名的开始位置
+ let start = offset
+ while (start > 0) {
+ const char = templateContent[start - 1]
+ // 属性名可以包含字母、数字、连字符、冒号
+ if (/[a-zA-Z0-9\-:]/.test(char)) {
+ start--
+ } else {
+ break
+ }
+ }
+
+ // 向后查找属性名的结束位置
+ let end = offset
+ while (end < templateContent.length) {
+ const char = templateContent[end]
+ if (/[a-zA-Z0-9\-:]/.test(char)) {
+ end++
+ } else {
+ break
+ }
+ }
+
+ if (start === end) {
+ return undefined
+ }
+
+ const attrName = templateContent.slice(start, end)
+
+ // 检查是否是事件绑定
+ const isEvent = /^(bind|catch|capture-bind|capture-catch|mut-bind):?/.test(
+ attrName,
+ )
+
+ return { attrName, isEvent }
+}
+
+/**
+ * 从模板内容中提取当前位置所在的组件标签名
+ */
+function extractTagNameAtPosition(
+ templateContent: string,
+ offset: number,
+): { tagName: string } | undefined {
+ // 向前查找最近的 < 符号
+ let tagStart = offset
+ while (tagStart > 0) {
+ if (templateContent[tagStart] === '<') {
+ break
+ }
+ // 如果遇到 > 说明不在标签内
+ if (templateContent[tagStart] === '>') {
+ return undefined
+ }
+ tagStart--
+ }
+
+ if (tagStart === 0 && templateContent[0] !== '<') {
+ return undefined
+ }
+
+ // 跳过 < 和可能的 /
+ let nameStart = tagStart + 1
+ if (templateContent[nameStart] === '/') {
+ nameStart++
+ }
+
+ // 提取标签名
+ let nameEnd = nameStart
+ while (nameEnd < templateContent.length) {
+ const char = templateContent[nameEnd]
+ if (/[a-zA-Z0-9\-_]/.test(char)) {
+ nameEnd++
+ } else {
+ break
+ }
+ }
+
+ if (nameStart === nameEnd) {
+ return undefined
+ }
+
+ const tagName = templateContent.slice(nameStart, nameEnd)
+ return { tagName }
+}
+
+/**
+ * 从 SFC 的 JSON 配置中查找组件路径
+ */
+function findComponentPath(
+ sfc: any,
+ tagName: string,
+ currentFileName: string,
+): string | undefined {
+ // 尝试从 script[type="application/json"] 中获取 usingComponents
+ const jsonBlock = sfc.customBlocks?.find(
+ (block: any) =>
+ block.type === 'script' && block.attrs?.type === 'application/json',
+ )
+
+ if (!jsonBlock?.content) {
+ // 也尝试从 sfc.json 获取
+ if (sfc.json?.content) {
+ try {
+ const jsonContent = JSON.parse(sfc.json.content)
+ const componentPath = jsonContent.usingComponents?.[tagName]
+ if (componentPath) {
+ return resolveComponentPath(componentPath, currentFileName)
+ }
+ } catch (e) {
+ console.log('[Mpx Go to Definition] Failed to parse json block:', e)
+ }
+ }
+ return undefined
+ }
+
+ try {
+ const jsonContent = JSON.parse(jsonBlock.content)
+ const componentPath = jsonContent.usingComponents?.[tagName]
+ if (componentPath) {
+ return resolveComponentPath(componentPath, currentFileName)
+ }
+ } catch (e) {
+ console.log('[Mpx Go to Definition] Failed to parse json block:', e)
+ }
+
+ return undefined
+}
+
+/**
+ * 解析组件相对路径为绝对路径
+ */
+function resolveComponentPath(
+ componentPath: string,
+ currentFileName: string,
+): string {
+ // 如果是相对路径,解析为绝对路径
+ if (componentPath.startsWith('./') || componentPath.startsWith('../')) {
+ const currentDir = currentFileName.substring(
+ 0,
+ currentFileName.lastIndexOf('/'),
+ )
+ // 简单的路径解析
+ const parts = componentPath.split('/')
+ const currentParts = currentDir.split('/')
+
+ for (const part of parts) {
+ if (part === '.') {
+ continue
+ } else if (part === '..') {
+ currentParts.pop()
+ } else {
+ currentParts.push(part)
+ }
+ }
+
+ let resolvedPath = currentParts.join('/')
+ // 添加 .mpx 扩展名(如果没有)
+ if (!resolvedPath.endsWith('.mpx')) {
+ resolvedPath += '.mpx'
+ }
+ return resolvedPath
+ }
+
+ // 如果是绝对路径或包路径,直接返回
+ return componentPath
+}
+
+/**
+ * 通过组件路径查找属性定义
+ */
+function tryFindComponentPropDefinitionByPath(
+ ts: typeof import('typescript'),
+ language: Language,
+ asScriptId: (fileName: string) => T,
+ componentPath: string,
+ attrName: string,
+): ts.DefinitionInfo | undefined {
+ console.log(
+ '[Mpx Go to Definition] Trying to find prop in component:',
+ JSON.stringify({
+ componentPath,
+ attrName,
+ }),
+ )
+
+ // 获取目标组件的虚拟代码
+ const targetScript = language.scripts.get(asScriptId(componentPath))
+ if (!targetScript?.generated?.root) {
+ console.log(
+ '[Mpx Go to Definition] No target script found for path:',
+ componentPath,
+ )
+ return
+ }
+
+ const targetRoot = targetScript.generated.root
+ if (!(targetRoot instanceof MpxVirtualCode)) {
+ console.log('[Mpx Go to Definition] Target root is not MpxVirtualCode')
+ return
+ }
+
+ // 获取原始 .mpx 文件的 script 内容
+ const sfc = targetRoot.sfc
+ const scriptBlock = sfc.script || sfc.scriptSetup
+ if (!scriptBlock) {
+ console.log(
+ '[Mpx Go to Definition] No script block found in target component',
+ )
+ return
+ }
+
+ console.log(
+ '[Mpx Go to Definition] Target script block info:',
+ JSON.stringify({
+ lang: scriptBlock.lang,
+ startTagEnd: scriptBlock.startTagEnd,
+ endTagStart: scriptBlock.endTagStart,
+ contentLength: scriptBlock.content.length,
+ }),
+ )
+
+ // 在原始脚本内容中查找 properties 定义
+ const propDefinitionOffset = findPropertyDefinitionInScript(
+ ts,
+ scriptBlock.ast,
+ attrName,
+ )
+ if (propDefinitionOffset === undefined) {
+ console.log(
+ '[Mpx Go to Definition] No prop definition found in target script',
+ )
+ return
+ }
+
+ // 计算在原始 .mpx 文件中的位置
+ const absoluteOffset = scriptBlock.startTagEnd + propDefinitionOffset.start
+
+ console.log(
+ '[Mpx Go to Definition] Found prop definition:',
+ JSON.stringify({
+ relativeOffset: propDefinitionOffset,
+ absoluteOffset,
+ }),
+ )
+
+ return {
+ fileName: componentPath,
+ textSpan: {
+ start: absoluteOffset,
+ length: propDefinitionOffset.length,
+ },
+ kind: ts.ScriptElementKind.memberVariableElement,
+ name: attrName,
+ containerName: 'properties',
+ containerKind: ts.ScriptElementKind.classElement,
+ }
+}
+
+/**
+ * 在脚本 AST 中查找 properties 定义
+ * 返回相对于脚本内容的偏移量
+ */
+function findPropertyDefinitionInScript(
+ ts: typeof import('typescript'),
+ scriptAst: ts.SourceFile,
+ propName: string,
+): { start: number; length: number } | undefined {
+ let result: { start: number; length: number } | undefined
+
+ // 将 kebab-case 转换为 camelCase
+ const camelizedPropName = camelize(propName)
+ const possibleNames = [propName, camelizedPropName]
+ if (propName !== camelizedPropName) {
+ possibleNames.push(capitalize(camelizedPropName))
+ }
+
+ function visit(node: ts.Node) {
+ if (result) return
+
+ // 查找 createComponent/createPage/Component 调用
+ if (
+ ts.isCallExpression(node) &&
+ ts.isIdentifier(node.expression) &&
+ ['createComponent', 'createPage', 'Component', 'Page'].includes(
+ node.expression.text,
+ )
+ ) {
+ const arg = node.arguments[0]
+ if (arg && ts.isObjectLiteralExpression(arg)) {
+ // 查找 properties 属性
+ for (const prop of arg.properties) {
+ if (
+ ts.isPropertyAssignment(prop) &&
+ ts.isIdentifier(prop.name) &&
+ prop.name.text === 'properties'
+ ) {
+ if (ts.isObjectLiteralExpression(prop.initializer)) {
+ result = findPropInObjectLiteralAst(
+ ts,
+ scriptAst,
+ prop.initializer,
+ possibleNames,
+ )
+ }
+ break
+ }
+ }
+ }
+ }
+
+ // 查找 defineProps 调用
+ if (
+ ts.isCallExpression(node) &&
+ ts.isIdentifier(node.expression) &&
+ node.expression.text === 'defineProps'
+ ) {
+ // 泛型写法: defineProps<{ propName: string }>() 或 defineProps()
+ if (node.typeArguments?.length) {
+ const typeArg = node.typeArguments[0]
+ if (
+ ts.isTypeReferenceNode(typeArg) &&
+ ts.isIdentifier(typeArg.typeName)
+ ) {
+ result = findTypeAliasAndPropAst(
+ ts,
+ scriptAst,
+ typeArg.typeName.text,
+ possibleNames,
+ )
+ } else if (ts.isTypeLiteralNode(typeArg)) {
+ result = findPropInTypeLiteralAst(
+ ts,
+ scriptAst,
+ typeArg,
+ possibleNames,
+ )
+ }
+ }
+ // 对象写法: defineProps({ propName: String })
+ else if (node.arguments.length) {
+ const arg = node.arguments[0]
+ if (ts.isObjectLiteralExpression(arg)) {
+ result = findPropInObjectLiteralAst(ts, scriptAst, arg, possibleNames)
+ }
+ }
+ }
+
+ // 查找 withDefaults(defineProps(), { ... })
+ if (
+ ts.isCallExpression(node) &&
+ ts.isIdentifier(node.expression) &&
+ node.expression.text === 'withDefaults'
+ ) {
+ const definePropsCall = node.arguments[0]
+ if (
+ definePropsCall &&
+ ts.isCallExpression(definePropsCall) &&
+ ts.isIdentifier(definePropsCall.expression) &&
+ definePropsCall.expression.text === 'defineProps'
+ ) {
+ if (definePropsCall.typeArguments?.length) {
+ const typeArg = definePropsCall.typeArguments[0]
+ // 如果是类型引用,需要找到类型定义
+ if (
+ ts.isTypeReferenceNode(typeArg) &&
+ ts.isIdentifier(typeArg.typeName)
+ ) {
+ const typeName = typeArg.typeName.text
+ result = findTypeAliasAndPropAst(
+ ts,
+ scriptAst,
+ typeName,
+ possibleNames,
+ )
+ } else if (ts.isTypeLiteralNode(typeArg)) {
+ result = findPropInTypeLiteralAst(
+ ts,
+ scriptAst,
+ typeArg,
+ possibleNames,
+ )
+ }
+ }
+ }
+ }
+
+ if (!result) {
+ ts.forEachChild(node, visit)
+ }
+ }
+
+ visit(scriptAst)
+ return result
+}
+
+/**
+ * 在对象字面量 AST 中查找属性定义
+ */
+function findPropInObjectLiteralAst(
+ ts: typeof import('typescript'),
+ sourceFile: ts.SourceFile,
+ obj: ts.ObjectLiteralExpression,
+ possibleNames: string[],
+): { start: number; length: number } | undefined {
+ for (const prop of obj.properties) {
+ if (
+ ts.isPropertyAssignment(prop) ||
+ ts.isShorthandPropertyAssignment(prop)
+ ) {
+ const name = prop.name
+ if (ts.isIdentifier(name) && possibleNames.includes(name.text)) {
+ return {
+ start: name.getStart(sourceFile),
+ length: name.getEnd() - name.getStart(sourceFile),
+ }
+ }
+ if (ts.isStringLiteral(name) && possibleNames.includes(name.text)) {
+ return {
+ start: name.getStart(sourceFile),
+ length: name.getEnd() - name.getStart(sourceFile),
+ }
+ }
+ }
+ // 支持方法简写: propName() { ... }
+ if (ts.isMethodDeclaration(prop)) {
+ const name = prop.name
+ if (ts.isIdentifier(name) && possibleNames.includes(name.text)) {
+ return {
+ start: name.getStart(sourceFile),
+ length: name.getEnd() - name.getStart(sourceFile),
+ }
+ }
+ }
+ }
+ return undefined
+}
+
+/**
+ * 在类型字面量 AST 中查找属性定义
+ */
+function findPropInTypeLiteralAst(
+ ts: typeof import('typescript'),
+ sourceFile: ts.SourceFile,
+ typeLiteral: ts.TypeLiteralNode,
+ possibleNames: string[],
+): { start: number; length: number } | undefined {
+ for (const member of typeLiteral.members) {
+ if (ts.isPropertySignature(member)) {
+ const name = member.name
+ if (ts.isIdentifier(name) && possibleNames.includes(name.text)) {
+ return {
+ start: name.getStart(sourceFile),
+ length: name.getEnd() - name.getStart(sourceFile),
+ }
+ }
+ if (ts.isStringLiteral(name) && possibleNames.includes(name.text)) {
+ return {
+ start: name.getStart(sourceFile),
+ length: name.getEnd() - name.getStart(sourceFile),
+ }
+ }
+ }
+ }
+ return undefined
+}
+
+/**
+ * 查找类型别名并在其中查找属性
+ */
+function findTypeAliasAndPropAst(
+ ts: typeof import('typescript'),
+ sourceFile: ts.SourceFile,
+ typeName: string,
+ possibleNames: string[],
+): { start: number; length: number } | undefined {
+ let result: { start: number; length: number } | undefined
+
+ function visit(node: ts.Node) {
+ if (result) return
+
+ if (ts.isTypeAliasDeclaration(node) && node.name.text === typeName) {
+ if (ts.isTypeLiteralNode(node.type)) {
+ result = findPropInTypeLiteralAst(
+ ts,
+ sourceFile,
+ node.type,
+ possibleNames,
+ )
+ }
+ }
+
+ if (ts.isInterfaceDeclaration(node) && node.name.text === typeName) {
+ for (const member of node.members) {
+ if (ts.isPropertySignature(member)) {
+ const name = member.name
+ if (ts.isIdentifier(name) && possibleNames.includes(name.text)) {
+ result = {
+ start: name.getStart(sourceFile),
+ length: name.getEnd() - name.getStart(sourceFile),
+ }
+ return
+ }
+ if (ts.isStringLiteral(name) && possibleNames.includes(name.text)) {
+ result = {
+ start: name.getStart(sourceFile),
+ length: name.getEnd() - name.getStart(sourceFile),
+ }
+ return
+ }
+ }
+ }
+ }
+
+ if (!result) {
+ ts.forEachChild(node, visit)
+ }
+ }
+
+ visit(sourceFile)
+ return result
+}
+
function getQuickInfoAtPosition(
ts: typeof import('typescript'),
languageService: ts.LanguageService,