IDE对于语言来说非常重要,让新手能更快入门,让老手能有更高的开发效率。所以我摸索着开发了Fanx语言的IDE。这里分享一些IDE内部工作原理和经验。
IDE和编译器
IDE为了实现功能,需要对源码进行解析。经过词法分析、语法分析、语义分析。相当于编译器的前端,相对与编译器来说少了优化器和代码生成等阶段。
- 语法分析:把字符串源文件变成单词串,每个单词叫做token。
- 语法分析:由token串构建AST(抽象语法树)
- 语义分析:决定AST每个结点的类型和相关信息,并进行类型检查。
IDE的语法解析和编译器理论上能共享代码。但是大部分语言编译器在开发时都没有考虑IDE的需求。两者功能上还有一定差异的:
- 错误恢复。编译器在遇见错误的时候可以直接退出。但是IDE不行,需要记录错误,并试图恢复后继续解析。在代码残缺不全的情况下也要进行语义分析。
- 多阶段编译。编译器可以把词法分析、语法分析、语义分析一遍全做了。但是IDE考虑到不能阻塞UI线程,需要分开多层。
- 增量编译。编译器可以把整个项目全部编译一遍。IDE中的文本一直在持续修改,需要增量的编译修改的部分。
IDE一般和编译器是独立的软件。当点击工具栏编译或者运行按钮的时候,IDE启动外部编译器进程进行编译。如果编译器和IDE版本不匹配,可能出现IDE显示有错误,但是编译是成功的。
编辑器
我们首先需要一个文本框来编辑代码,但是GUI框架提供的控件(例如TextArea)可能并不满足自定义颜色的要求。我们可以自己画一个控件,这样可以自定义排版和着色。
比较麻烦的地方是需要自己处理键盘事件,处理打字和光标操作等。还有拷贝和粘贴操作要访问系统剪贴板。中文输入法支持可以用一个隐藏的TextField来实现。这些功能开发完成后,我们就自己造了一个TextArea的轮子出来。
语法高亮
我们只需要完成词法分析,就能完成基本的语法高亮了。如果想要更高级的语义语法高亮,则需要等到完成语法分析或者语义分析。
语法分析和语法分析比较慢,在后台线程进行。词法分析比较快,在UI线程中处理。键盘事件中同步进行词法分析,后台线程的语义分析则是基于源码文本的快照。
语义错误
语义分析不只是当前文件的事情,还涉及依赖的库和项目内的其他文件。所以我们需要针对整个项目进行分析。最后把错误信息显出出来。
由于语义分析是整个项目进行的,所以成本比较高。我们在文件被修改的时候进行增量编译,只编译分析当前文件。当然增量编译的前提是项目首次加载时要进行一次全量编译。如果再进一步优化可以只分析文本被修改的部分所在的函数。
跳转到定义
按ctrl键并鼠标点击符号,跳转到符号的定义。我们在构建抽象语法树的时候一直维护结点所在的文本位置。根据点击位置查询抽象语法树得到点击的结点。点击的位置可能为类名,变量名,函数等,根据不同类型跳到不同的文件位置。在语义分析后我们就已经有了符号之间的引用信息。
补全提示
或者叫做自动完成。当用户键入时提示输入建议,并显示对应API文档。我们仍然和跳转定义的实现一样查询当前光标位置的抽象语法树结点,然后根据结点类型分不同情况做提示。例如用户在一个变量后面输入了“.”, 我们把这个变量在语义分析中确定的类型对应的所有方法列出来,作为候选。
需要把所有的第三方库和其他文件的符号索引在一起,供自动完成来查询。有时为了快速查询可以把符号建成倒排索引或者Trie树索引。很多脚本语言IDE实现会把这些索引持久保存,但我们为了简单,不进行持久化,每次启动时在后台线程建立索引。当文件保存时触发索引的更新。
调试
这个依赖与JVM提供的调试协议和接口。运行程序时提供调试模式的参数,则JVM启动后监听指定端口,并暂停程序,等待调试客户端命令。调试客户端在指定端口中连接被调试进程,并发出相关调试命令。他们通过socket来通信,所以远程调试是可能的。
总结
这里说的都是针对单个语言的基本功能。从头开发完整的IDE的工作量非常大,架构的复杂度也很大。