Skip to content

Commit

Permalink
Merge pull request #42 from NewFuture/feat-function-autocompletion
Browse files Browse the repository at this point in the history
Feat function autocompletion
  • Loading branch information
qiu8310 authored Jul 1, 2019
2 parents eba2ba0 + 3b2e952 commit 01c77bb
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 38 deletions.
94 changes: 78 additions & 16 deletions src/plugin/AutoCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {
Expand Down
8 changes: 4 additions & 4 deletions src/plugin/PropDefinitionProvider.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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, '') // 去除引号

// 不在属性上
Expand All @@ -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) {
Expand Down
127 changes: 109 additions & 18 deletions src/plugin/lib/ScriptFile.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>()
/**
* 结果缓存
*/
const resultCache = new Map<string, { version: number, data: PropInfo[] }>()

/**
* 保留字段,
* 用于无限制匹配式函数过滤
* `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
/**
Expand All @@ -27,49 +48,119 @@ 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: (...) =>`
* - 异步箭头函数`prop: async (...) =>`
* - 普通函数声明`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
}

0 comments on commit 01c77bb

Please sign in to comment.