diff --git a/src/extension.ts b/src/extension.ts index 34e4792..7e9969f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -63,7 +63,7 @@ export function activate(context: ExtensionContext) { languages.registerDefinitionProvider([pug].concat(wxml), propDefinitionProvider), // 自动补全 - languages.registerCompletionItemProvider(wxml, autoCompletionWxml, '<', ' ', ':', '@', '.', '-', '"', '\'', '\n'), + languages.registerCompletionItemProvider(wxml, autoCompletionWxml, '<', ' ', ':', '@', '.', '-', '"', '\'', '\n', '/'), languages.registerCompletionItemProvider(pug, autoCompletionPug, '\n', ' ', '(', ':', '@', '.', '-', '"', '\''), // trigger 需要是上两者的和 languages.registerCompletionItemProvider(vue, autoCompletionVue, '<', ' ', ':', '@', '.', '-', '(', '"', '\'') diff --git a/src/plugin/AutoCompletion.ts b/src/plugin/AutoCompletion.ts index 835dcba..0255968 100644 --- a/src/plugin/AutoCompletion.ts +++ b/src/plugin/AutoCompletion.ts @@ -15,12 +15,14 @@ import { import * as path from 'path' -import {Config} from './lib/config' -import {getCustomOptions, getTextAtPosition, getRoot, getEOL} from './lib/helper' -import {LanguageConfig} from './lib/language' -import {getTagAtPosition} from './getTagAtPosition/' +import { Config } from './lib/config' +import { getCustomOptions, getTextAtPosition, getRoot, getEOL, getLastChar } from './lib/helper' +import { LanguageConfig } from './lib/language' +import { getTagAtPosition } from './getTagAtPosition/' import * as s from './res/snippets' import { getClass } from './lib/StyleFile' +import { getCloseTag } from './lib/closeTag' +import { getProp } from './lib/ScriptFile' export default abstract class AutoCompletion { abstract id: 'wxml' | 'wxml-pug' @@ -32,7 +34,7 @@ export default abstract class AutoCompletion { return this.isPug ? this.config.pugQuoteStyle : this.config.wxmlQuoteStyle } - constructor(public config: Config) {} + constructor(public config: Config) { } getCustomOptions(doc: TextDocument) { return getCustomOptions(this.config, doc) @@ -42,7 +44,7 @@ export default abstract class AutoCompletion { let c = tag.component let item = new CompletionItem(c.name, CompletionItemKind.Module) - let {attrQuote, isPug} = this + let { attrQuote, isPug } = this let allAttrs = c.attrs || [] let attrs = allAttrs .filter(a => a.required || a.subAttrs) @@ -80,7 +82,7 @@ export default abstract class AutoCompletion { defaultValue = a.enum && a.enum[0].value } - let {attrQuote, isPug} = this + let { attrQuote, isPug } = this if (a.boolean) { item.insertText = new SnippetString(isPug && defaultValue === 'false' ? `${a.name}=false` : a.name) @@ -90,7 +92,7 @@ export default abstract class AutoCompletion { : this.setDefault(1, defaultValue) // 是否有可选值,如果有可选值则触发命令的自动补全 - let values = a.enum ? a.enum : a.subAttrs ? a.subAttrs.map(sa => ({value: sa.equal})) : [] + let values = a.enum ? a.enum : a.subAttrs ? a.subAttrs.map(sa => ({ value: sa.equal })) : [] if (values.length) { value = '\${1}' item.command = autoSuggestCommand() @@ -149,7 +151,7 @@ export default abstract class AutoCompletion { // 添加 Snippet let userSnippets = this.config.snippets - let allSnippets: s.Snippets = (this.isPug ? {...s.PugSnippets, ...userSnippets.pug} : {...s.WxmlSnippets, ...userSnippets.wxml}) + let allSnippets: s.Snippets = (this.isPug ? { ...s.PugSnippets, ...userSnippets.pug } : { ...s.WxmlSnippets, ...userSnippets.wxml }) items.push(...Object.keys(allSnippets) .filter(k => filter(k)) .map(k => { @@ -180,34 +182,44 @@ export default abstract class AutoCompletion { if (!tag) return [] if (tag.isOnTagName) { return this.createComponentSnippetItems(lc, doc, pos, tag.name) - } else if (tag.isOnAttrValue && tag.attrName) { + } + if (tag.isOnAttrValue && tag.attrName) { let attrValue = tag.attrs[tag.attrName] if (tag.attrName === 'class') { let existsClass = (tag.attrs.class || '') as string return this.autoCompleteClassNames(doc, existsClass ? existsClass.trim().split(/\s+/) : []) - } else if (typeof attrValue === 'string' && attrValue.trim() === '') { - let values = await autocompleteTagAttrValue(tag.name, tag.attrName, lc, this.getCustomOptions(doc)) - if (!values.length) return [] - let range = doc.getWordRangeAtPosition(pos, /['"]\s*['"]/) - if (range) { - range = new Range( - new Position(range.start.line, range.start.character + 1), - new Position(range.end.line, range.end.character - 1) - ) + } else if (typeof attrValue === 'string') { + if ((tag.attrName.startsWith('bind') || tag.attrName.startsWith('catch'))) { + // 函数自动补全 + return this.autoCompleteMethods(doc, attrValue.replace(/"|'/, '')) + } else if (attrValue.trim() === '') { + let values = await autocompleteTagAttrValue(tag.name, tag.attrName, lc, this.getCustomOptions(doc)) + if (!values.length) return [] + let range = doc.getWordRangeAtPosition(pos, /['"]\s*['"]/) + if (range) { + range = new Range( + new Position(range.start.line, range.start.character + 1), + new Position(range.end.line, range.end.character - 1) + ) + } + return values.map(v => { + let it = new CompletionItem(v.value, CompletionItemKind.Value) + it.documentation = new MarkdownString(v.markdown) + it.range = range + return it + }) } - return values.map(v => { - let it = new CompletionItem(v.value, CompletionItemKind.Value) - it.documentation = new MarkdownString(v.markdown) - it.range = range - return it - }) + + // } else if ((tag.attrName.startsWith('bind') || tag.attrName.startsWith('catch')) && typeof attrValue === 'string') { + + // return this.autoCompleteMethods(doc, attrValue.replace(/"|'/, '')) } return [] } else { let res = await autocompleteTagAttr(tag.name, tag.attrs, lc, this.getCustomOptions(doc)) let triggers: CompletionItem[] = [] - let {natives, basics} = res + let { natives, basics } = res let noBasics = lc.noBasicAttrsComponents || [] if (noBasics.indexOf(tag.name) < 0) { @@ -299,6 +311,85 @@ export default abstract class AutoCompletion { return items } + + /** + * 闭合标签自动完成 + * @param doc + * @param pos + */ + async createCloseTagCompletionItem(doc: TextDocument, pos: Position): Promise { + const text = doc.getText(new Range(new Position(0, 0), pos)) + if (text.length < 2 || text.substr(text.length - 2) !== '') { + completionItem.range = new Range(pos, nextPos) + completionItem.label = closeTag.substr(0, closeTag.length - 1) + } + return [completionItem] + } + + return [] + } + + /** + * 函数自动提示 + * @param doc + * @param prefix 函数前缀,空则查找所有函数 + */ + autoCompleteMethods(doc: TextDocument, prefix: string): CompletionItem[] { + /** + * 页面周期和组件 生命周期函数, + * 显示时置于最后 + * 列表中顺序决定显示顺序 + */ + const lowPriority = [ + 'onPullDownRefresh', 'onReachBottom', 'onPageScroll', + 'onShow', 'onHide', 'onTabItemTap', 'onLoad', 'onReady', 'onResize', 'onUnload', 'onShareAppMessage', + 'error', 'creaeted', 'attached', 'ready', 'moved', 'detached', 'observer', + ] + const methods = getProp(doc.uri.fsPath, 'method', (prefix || '[\\w_$]') + '[\\w\\d_$]*') + const root = getRoot(doc) + return methods.map(l => { + const c = new CompletionItem(l.name, getMethodKind(l.detail)) + const filePath = root ? path.relative(root, l.loc.uri.fsPath) : path.basename(l.loc.uri.fsPath) + // 低优先级排序滞后 + const priotity = lowPriority.indexOf(l.name) + 1 + c.detail = `${filePath}\n[${l.loc.range.start.line}行,${l.loc.range.start.character}列]` + c.documentation = new MarkdownString('```ts\n' + l.detail + '\n```') + /** + * 排序显示规则 + * 1. 正常函数 如 `onTap` + * 2. 下划线函数 `_save` + * 3. 生命周期函数 `onShow` + */ + if (priotity > 0) { + c.detail += '(生命周期函数)' + c.kind = CompletionItemKind.Field + c.sortText = '}'.repeat(priotity) + } else { + c.sortText = l.name.replace('_', '{') + } + return c + }) + } + +} + +/** + * 是否为属性式函数声明 + * 如 属性式声明 `foo:()=>{}` + * @param text + */ +function getMethodKind(text: string) { + return /^\s*[\w_$][\w_$\d]*\s*:/.test(text) ? CompletionItemKind.Property : CompletionItemKind.Method } function autoSuggestCommand() { diff --git a/src/plugin/PropDefinitionProvider.ts b/src/plugin/PropDefinitionProvider.ts index 018cdc4..8663628 100644 --- a/src/plugin/PropDefinitionProvider.ts +++ b/src/plugin/PropDefinitionProvider.ts @@ -1,5 +1,5 @@ import { Config } from './lib/config' -import {DefinitionProvider, TextDocument, Position, CancellationToken, Location, Uri, Range} from 'vscode' +import { DefinitionProvider, TextDocument, Position, CancellationToken, Location, Uri, Range } from 'vscode' import { getTagAtPosition } from './getTagAtPosition' import { getClass } from './lib/StyleFile' import { getProp } from './lib/ScriptFile' @@ -9,13 +9,13 @@ const reserveWords = [ ] export class PropDefinitionProvider implements DefinitionProvider { - constructor(public config: Config) {} + constructor(public config: Config) { } public async provideDefinition(document: TextDocument, position: Position, token: CancellationToken) { const tag = getTagAtPosition(document, position) const locs: Location[] = [] if (tag) { - const {attrs, attrName, posWord} = tag + const { attrs, attrName, posWord } = tag const rawAttrValue = ((attrs['__' + attrName] || '') as string).replace(/^['"]|['"]$/g, '') // 去除引号 // 不在属性上 @@ -42,7 +42,7 @@ export class PropDefinitionProvider implements DefinitionProvider { } searchScript(type: 'prop' | 'method', word: string, doc: TextDocument) { - return getProp(doc.fileName, type, word) + return getProp(doc.fileName, type, word).map(p => p.loc) } searchStyle(className: string, document: TextDocument, position: Position) { diff --git a/src/plugin/WxmlAutoCompletion.ts b/src/plugin/WxmlAutoCompletion.ts index 167af13..ee6fbee 100644 --- a/src/plugin/WxmlAutoCompletion.ts +++ b/src/plugin/WxmlAutoCompletion.ts @@ -16,6 +16,9 @@ export default class extends AutoCompletion implements CompletionItemProvider { id = 'wxml' as 'wxml' provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): Promise { + if (token.isCancellationRequested) { + return Promise.resolve([]) + } let language = getLanguage(document, position) if (!language) return [] as any @@ -39,6 +42,8 @@ export default class extends AutoCompletion implements CompletionItemProvider { case '-': // v-if case '.': // 变量或事件的修饰符 return this.createSpecialAttributeSnippetItems(language, document, position) + case '/': // 闭合标签 + return this.createCloseTagCompletionItem(document, position) default: if (char >= 'a' && char <= 'z') { // 输入属性时自动提示 diff --git a/src/plugin/lib/ScriptFile.ts b/src/plugin/lib/ScriptFile.ts index c62cc9d..02f951c 100644 --- a/src/plugin/lib/ScriptFile.ts +++ b/src/plugin/lib/ScriptFile.ts @@ -1,11 +1,32 @@ import { getFileContent, match, getPositionFromIndex } from './helper' import * as path from 'path' import * as fs from 'fs' -import { Location, Uri, Position, Range } from 'vscode' +import { Location, Uri, Position, Range, window } from 'vscode' + +interface PropInfo { + loc: Location, + name: string, + detail: string, +} +/** + * js/ts 文件映射缓存 + */ +const wxJsMapCache = new Map() +/** + * 结果缓存 + */ +const resultCache = new Map() + +/** + * 保留字段, + * 用于无限制匹配式函数过滤 + * `if(x){}` 等满足函数正 + */ +const reservedWords = ['if', 'switch', 'catch', 'while', 'for', 'constructor'] function parseScriptFile(file: string, type: string, prop: string) { let content = getFileContent(file) - let locs: Location[] = [] + let locs: PropInfo[] = [] let reg: RegExp | null = null /** @@ -27,21 +48,31 @@ function parseScriptFile(file: string, type: string, prop: string) { * 允许参数跨行 * - 无参数`()` * - 有参数`( e )` + * - 有参类型`( e?:{}= )` * - 参数跨行 * ```ts * ( * e: event * ) * ``` + * ```js + * /\(\s*(?:[\w\d_$]+(?:[,=:?][\s\S]*?)?)?\)/ + * ``` */ - const param = `\\([\\s\\S]*?\\)` - const async = `(async\\s+)?` + const param = `\\(${s}(?:[\\w\\d_$]+(?:[,=:?][\\s\\S]*?)?)?\\)` + const async = `(?:async\\s+)?` + /** + * 返回值正则 + * `:type ` + */ + const returnType = `(?::[\\s\\S]*?)?` /** * 方法定义正则 * - 普通方法`prop(...){` + * - 返回值方法`prop(...): void {` * - 异步方法`async prop(...){` */ - const methodReg = `${async}${prop}${s}${param}${s}\\{` + const methodReg = `${async}(${prop})${s}${param}${s}${returnType}\\{` /** * 属性式函数定义 正则 * - 箭头函数`prop: (...) =>` @@ -49,27 +80,87 @@ function parseScriptFile(file: string, type: string, prop: string) { * - 普通函数声明`prop: function...` * - 异步函数声明`prop: async function...` */ - const propFuncReg = `${prop}${s}:${async}(${param}${s}=>|function\\W)` + const propFuncReg = `(${prop})${s}:${s}${async}(?:${param}${s}${returnType}=>|function\\W)` reg = new RegExp(`^${s}(${methodReg}|${propFuncReg})`, 'gm') } - match(content, reg!).forEach(mat => { - let pos = getPositionFromIndex(content, mat.index + mat[0].indexOf(prop)) - let endPos = new Position(pos.line, pos.character + prop.length) - locs.push(new Location(Uri.file(file), new Range(pos, endPos))) - }) + match(content, reg!) + .filter(mat => { + const property = mat[2] || mat[3] + // 精确匹配或者不是关键字 + return property === prop || reservedWords.indexOf(property) === -1 + }) + .forEach(mat => { + const property = mat[2] || mat[3] || prop + let pos = getPositionFromIndex(content, mat.index + mat[0].indexOf(property)) + let endPos = new Position(pos.line, pos.character + property.length) + locs.push({ + loc: new Location(Uri.file(file), new Range(pos, endPos)), + name: property, + detail: mat[1] || mat[0] + }) + }) return locs } -export function getProp(wxmlFile: string, type: string, prop: string) { - let dir = path.dirname(wxmlFile) - let base = path.basename(wxmlFile, path.extname(wxmlFile)) - let exts = ['js', 'ts'] +/** + * 解析文件映射关系 + * @param wxmlFile + */ +function getScriptFile(wxmlFile: string): string | undefined { + if (wxJsMapCache.has(wxmlFile)) { + return wxJsMapCache.get(wxmlFile) + } + const dir = path.dirname(wxmlFile) + const base = path.basename(wxmlFile, path.extname(wxmlFile)) + + const exts = ['ts', 'js'] // 先ts 再js 防止读取编译后的 for (const ext of exts) { - let file = path.join(dir, base + '.' + ext) - if (fs.existsSync(file)) return parseScriptFile(file, type, prop) + const file = path.join(dir, base + '.' + ext) + if (fs.existsSync(file)) { + wxJsMapCache.set(wxmlFile, file) + return file + } } + return undefined +} + + +/** + * 获取文件版本信息, + * 编辑器 和 文件系统 + * 只能用===判断 + * @param file + */ +function getVersion(file: string): number { + const editor = window.visibleTextEditors.find(e => e.document.fileName === file) + if (editor) { + return editor.document.version + } else { + return fs.statSync(file).mtimeMs + } +} - return [] +/** + * 提取脚本文件中的定义 + * @param wxmlFile + * @param type + * @param prop + */ +export function getProp(wxmlFile: string, type: string, prop: string) { + const scriptFile = getScriptFile(wxmlFile) + if (!scriptFile) return [] + + const key = `${scriptFile}?${type}&${prop}` + const cache = resultCache.get(key) + const version = getVersion(scriptFile) + if (cache && cache.version === version) { + return cache.data + } + const result = parseScriptFile(scriptFile, type, prop) + if (result && result.length > 0) { + resultCache.set(key, { version, data: result }) + } + return result } diff --git a/src/plugin/lib/closeTag.ts b/src/plugin/lib/closeTag.ts new file mode 100644 index 0000000..c719720 --- /dev/null +++ b/src/plugin/lib/closeTag.ts @@ -0,0 +1,46 @@ +/** + * 获取闭合标签 + * from: https://github.com/formulahendry/vscode-auto-close-tag/blob/master/src/extension.ts + * @param text + * @param excludedTags + */ +export function getCloseTag(text: string, excludedTags: string[] = []): string { + // 过滤变量/和值中`<``>` + text = text.replace(/"[^"]*"|'[^']*'|\{\{[^\}]*?\}\}/g, '') + // const regex = /<(\/?[\w\d-]*)(?:\s+[^<>]*?[^\s/<>=]+?)*?\s?>/g + const regex = /<(\/?[\w\d-]+)[^<>]*\s?>/g // 简化正则提高性能 + + let result = null + const stack = [] + // tslint:disable-next-line: no-conditional-assignment + while (result = regex.exec(text)) { + if (!result[1] || result[0].endsWith('/>')) { + // 自闭标签 + continue + } + const isStartTag = result[1].substr(0, 1) !== '/' + const tag = isStartTag ? result[1] : result[1].substr(1) + if (excludedTags.indexOf(tag.toLowerCase()) === -1) { + if (isStartTag) { + stack.push(tag) + } else if (stack.length > 0) { + const lastTag = stack[stack.length - 1] + if (lastTag === tag) { + stack.pop() + } + } + } + } + if (stack.length > 0) { + const closeTag = stack[stack.length - 1] + if (text.substr(text.length - 2) === '' + } + if (text.substr(text.length - 1) === '<') { + return '/' + closeTag + '>' + } + return '' + } else { + return '' + } +}