去年一月份,Angular.js
就停止更新了,但由于年代久远,许多老项目还在使用。就像使用jQuery
网站的数量到今天仍然比Vue
+React
+Angular
的总和还多!有理由相信十余年的维护期,让使用Angular.js
的网站数量不会逊于Angular
,为什么Angular
升级这么难呢?其实从Angular.js
升级到Angular
近似于让Angular.js
换成Vue
,变化实在太大,几乎没办法简单升级过去的。
但这么老的框架,开发生态是不是就很好呢?想多了!搜搜VSCode
里关于Angular.js
的扩展项吧:
几乎全都是一些代码片段、自动补全、模板这样的东西,连个变量跳转都没有啊。
那么接下来就讲讲如何为Angular.js
做一个非常基本的关于$scope
作用域变量的跳转。
初始化
VSCode
插件开发最基本的是要装几个东西,Yeoman
、generator-code
、vsce
,都是npm
里面的都需要全局安装,新建插件项目可以直接yo code
,里面语言我这里就选择Typescript
了。
然后就是具体实现了,做这种代码跳转插件呢主要是要实现vscode.DefinitionProvider
接口,首先新建一个类文件,然后起一个类名叫AngularJsDefineProvider
好了:
import * as vscode from "vscode"
class AngularJsDefineProvider implements vscode.DefinitionProvider {
provideDefinition(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult<vscode.Definition | vscode.DefinitionLink[]> {
...
}
}
这个Provider
其实也就一个provideDefinition()
需要实现,但不是定义了VSCode就会加载这个Provider
的,还需要去注册它,可以回到新建项目时它自带的main.ts
,在里面activate()
里面导入上面的AngularJsDefineProvider
类,然后向vscode
注册:
vscode.languages.registerDefinitionProvider({
scheme: 'file', language: 'javascript'
}, new AngularJsDefineProvider())
这就算是完成初始化了,一般情况下注册东西都要去package.json
里面再写点什么,但这里是语言定义区的注册,所以没有关系,下面看看具体如何实现:
参数解析
先说一说provideDefinition()
这个方法本身,它有2个参数,还有个返回值:
document
顾名思义,是文档对象,是触发跳转定义请求的文档,这个请求当然只可能由一篇文档发出,不可能好几篇文档里面都要求要跳往各自的地方,所以它不是数组。position
则是发出这个定义跳转的具体位置,这个位置他不会是一个点,比喻说你光标停在一个词中间,它会自动拿到整个词的position
。- 然后就是这个返回值
vscode.ProviderResult<vscode.Definition | vscode.DefinitionLink[]>
,这个ProviderResult
就是个说明性的类型,其实内容就是它的泛型本身,也就是vscode.Definition
和vscode.DefinitionLink[]
,而Definition
就是Location | Location[]
,直接由new vscode.Location()
实现的,为什么都有数组呢,这个很好理解,因为除了找定义之外,找引用也是由这个方法发起的,引用它就可能不止一个地方有了,所以要数组来包含多个位置。
然后还需要自己先在类里定义3个变量方便使用:
private readonly document: vscode.TextDocument
private readonly range: vscode.Range
private readonly search: string
为什么定义这些变量呢,首先这里不会只有一个方法,而只要处理这个定义跳转就免不了用document
这个参数,所以要先在类里定义个document
变量当做共享参数,以免以后这个对象传来传去的麻烦。
然后是范围和搜索文本,这个范围是其实就是选中的文本,当然也是很重要的,后面会用它扩大选取搜索更多的内容,而search
就是具体的搜索内容,range
是可以根据情况扩大的,而search
就相当于是根据range
选出来的文本。
下面就可以进行具体的操作了,不过还需要一些前置准备,例如验证条件和搜索方法的实现,来为最终的跳转查找铺平道路。
前置准备
写当然是从provideDefinition()
写起,先验证一下到底有没有position
和range
:
if (position.character <= 0) {
return
}
const range = document.getWordRangeAtPosition(position)
if (!range) {
return
}
如果没有直接return即可。然后给前面定义的3个变量赋值:
this.document = document
this.search = document.getText(range)
this.range = range
然后需要看看,它是不是scope,那无论是定义还是引用前面必然都要有个$scope.`,至于其他地方变量也叫这个`$scope.xx
不需要我们找,那也无所谓了毕竟概率很小。那么还有一个问题,怎么分辨选的那块代码是要找定义还是找引用,一般来说,这里是需要用抽象语法树(AST)来解析的,但我这个其实需求比较简单,直接根据range
搜后面的文本就好了,定义后面肯定有个=
嘛,至于重新赋值也不用太考虑,所以先要写个根据range
搜上下文的方法:
/**
* 在搜索字符串的前后剪裁字符串
*/
public strAt(index): string {
if (this.range.start.character + index < 0) {
return ''
}
if (index < 0) {
const s = new vscode.Range(new vscode.Position(this.range.start.line, this.range.start.character + index),
this.range.start)
return this.document.getText(this.document.validateRange(s))
}
return this.document.getText(new vscode.Range(
this.range.end,
new vscode.Position(this.range.end.line, this.range.end.character + index)))
}
我简单设计了一个strAt()
,根据document.getText
和range
对象剪裁取值,比喻说aa.bb.c
,光标在bb
那range
也就会在bb
,只要调用这个strAt(2)
就会获取向后获取到.c
这段字符串,而只要strAt(-3)
就能向前获取到aa.
这段字符串。
那么下面就可以使用这个strAt()
做验证、定义、引用的判断了:
if (this.strAt(-7) !== "&scope.") {
return
}
if (this.strAt(1) === "=") {
return this.findScopeReference()
}
return this.findScopeDefine()
可以看到有了这个方法真的超简单,那下面就可以专心实现findScopeReference()
和findScopeDefine()
了:
定义与引用查找
到了这一步,其实都不需要用strAt()
来进行上下文查找了,而是直接对整个document
进行全文检索,正常来说其实要检索整个项目所有可查找的document
,但是这里图方便,而且因为Angular.js
模块化其实挺差,写多了都写到一个文件里去了,所以其实也没必要检索整个项目了,如果想检索一些特定文件可以用vscode.workspace.openTextDocument(uri)
来打开具体的文档来进行检索,这里就不演示了,下面看看具体怎么做:
首先是查找定义:
public findScopeDefine(): vscode.Definition {
const thisDocIndex = this.document.getText().indexOf(`scope.${
this.search} = `)
if (thisDocIndex === -1) {
return
}
return new vscode.Location(vscode.Uri.file(this.document.fileName), new vscode.Range(
this.document.positionAt(thisDocIndex + 6),
this.document.positionAt(thisDocIndex + 6 + this.search.length)))
}
定义一般只有一个,直接indexOf
需要找的东西就行了,然后用vscode.Location
包一下就可以完成跳转了。
public findScopeReference(): vscode.Definition[] {
const thisDocArr = [...this.document.getText().matchAll(new RegExp("scope." + this.search, "g"))]
if (!thisDocArr.length) {
return
}
return thisDocArr.map((item) => {
return new vscode.Location(vscode.Uri.file(this.document.fileName), new vscode.Range(
this.document.positionAt(item.index + 6),
this.document.positionAt(item.index + 6 + this.search.length)))
})
}
找引用就需用正则匹配多项了,找到后解构到数组里,再用map
转成vscode.Location
对象。
下面用GIF演示一下:
本文依照作者在2021年的一些开发经验,于2023年7月18日同时写作并发布在lyrieek的稀土掘金社区与阿里云开发者社区。