From dcc8640c768e97694e54a6f8a7d87cf0e669491b Mon Sep 17 00:00:00 2001 From: NewFuture Date: Mon, 24 Jun 2019 02:18:54 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(completion):=20function=20auto=20compl?= =?UTF-8?q?etion=20=E5=87=BD=E6=95=B0=E6=8F=90=E7=A4=BA=E5=92=8C=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugin/AutoCompletion.ts | 96 ++++++++++++++++++----- src/plugin/PropDefinitionProvider.ts | 8 +- src/plugin/lib/ScriptFile.ts | 109 ++++++++++++++++++++++----- 3 files changed, 175 insertions(+), 38 deletions(-) diff --git a/src/plugin/AutoCompletion.ts b/src/plugin/AutoCompletion.ts index 1587855..a0593a0 100644 --- a/src/plugin/AutoCompletion.ts +++ b/src/plugin/AutoCompletion.ts @@ -22,6 +22,7 @@ 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' @@ -181,27 +182,37 @@ 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 { @@ -304,7 +315,7 @@ export default abstract class AutoCompletion { /** * 闭合标签自动完成 * @param doc - * @param pos + * @param pos */ async createCloseTagCompletionItem(doc: TextDocument, pos: Position): Promise { const text = doc.getText(new Range(new Position(0, 0), pos)) @@ -328,6 +339,57 @@ export default abstract class AutoCompletion { 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/lib/ScriptFile.ts b/src/plugin/lib/ScriptFile.ts index c62cc9d..b858214 100644 --- a/src/plugin/lib/ScriptFile.ts +++ b/src/plugin/lib/ScriptFile.ts @@ -3,9 +3,30 @@ import * as path from 'path' import * as fs from 'fs' import { Location, Uri, Position, Range } 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,71 @@ 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 +} - 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 mtime = fs.statSync(scriptFile).mtimeMs + if (cache && cache.mtime === mtime) { + return cache.data + } + const result = parseScriptFile(scriptFile, type, prop) + if (result && result.length > 0) { + resultCache.set(key, { mtime, data: result }) + } + return result } From 4a86642c32cda6bbbd30566f4504471e498d11f6 Mon Sep 17 00:00:00 2001 From: NewFuture Date: Mon, 24 Jun 2019 02:41:40 +0800 Subject: [PATCH 2/3] style(lint): fix tslint --- src/plugin/AutoCompletion.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugin/AutoCompletion.ts b/src/plugin/AutoCompletion.ts index a0593a0..0255968 100644 --- a/src/plugin/AutoCompletion.ts +++ b/src/plugin/AutoCompletion.ts @@ -315,7 +315,7 @@ export default abstract class AutoCompletion { /** * 闭合标签自动完成 * @param doc - * @param pos + * @param pos */ async createCloseTagCompletionItem(doc: TextDocument, pos: Position): Promise { const text = doc.getText(new Range(new Position(0, 0), pos)) @@ -368,7 +368,7 @@ export default abstract class AutoCompletion { * 排序显示规则 * 1. 正常函数 如 `onTap` * 2. 下划线函数 `_save` - * 3. 生命周期函数 `_onShow` + * 3. 生命周期函数 `onShow` */ if (priotity > 0) { c.detail += '(生命周期函数)' @@ -389,7 +389,7 @@ export default abstract class AutoCompletion { * @param text */ function getMethodKind(text: string) { - return /^\s*[\w_$][\w_$\d]*\s*:/.test(text) ? CompletionItemKind.Property : CompletionItemKind.Method; + return /^\s*[\w_$][\w_$\d]*\s*:/.test(text) ? CompletionItemKind.Property : CompletionItemKind.Method } function autoSuggestCommand() { From 3b2e952c7919ae2065c42d0ab03683f8c22e43b6 Mon Sep 17 00:00:00 2001 From: NewFuture Date: Tue, 25 Jun 2019 22:51:43 +0800 Subject: [PATCH 3/3] fix(scriptfile): fix cache validation --- src/plugin/lib/ScriptFile.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/plugin/lib/ScriptFile.ts b/src/plugin/lib/ScriptFile.ts index b858214..02f951c 100644 --- a/src/plugin/lib/ScriptFile.ts +++ b/src/plugin/lib/ScriptFile.ts @@ -1,7 +1,7 @@ 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, @@ -15,7 +15,7 @@ const wxJsMapCache = new Map() /** * 结果缓存 */ -const resultCache = new Map() +const resultCache = new Map() /** * 保留字段, @@ -126,6 +126,22 @@ function getScriptFile(wxmlFile: string): string | undefined { 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 + } +} + /** * 提取脚本文件中的定义 * @param wxmlFile @@ -138,13 +154,13 @@ export function getProp(wxmlFile: string, type: string, prop: string) { const key = `${scriptFile}?${type}&${prop}` const cache = resultCache.get(key) - const mtime = fs.statSync(scriptFile).mtimeMs - if (cache && cache.mtime === mtime) { + 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, { mtime, data: result }) + resultCache.set(key, { version, data: result }) } return result }