Syntax Highlight
developerWorks
https://www.ibm.com/developerworks/cn/opensource/os-cn-ecljtf2/
(暂时没有维护图片链接)
样例代码
级别: 中级
马 若劼 (
[email protected]), 软件工程师, IBM 中国软件开发中心
2008 年 3 月 20 日
Syntax Highlight(语法高亮)是指把文本的不同内容用不同的颜色,字体等渲染,通过这种方式,用户可以快速发现某种内容,可以在短时间内对全文的结构有一个大概的了解。本文探讨如何在 JTF 中实现语法高亮。
Syntax Highlight(语法高亮)是指把文本的不同内容用不同的颜色,字体等渲染,通过这种方式,用户可以快速发现某种内容,可以在短时间内对全文的结构有一个大概的了解。本文探讨如何在 JTF 中实现语法高亮。
前提
语法高亮是把一段文本中的不同内容使用不同的样式来渲染的功能,如下图所示:
图1. Java编辑器的语法高亮
Jav a编辑器的语法高亮
图 1 中的 Java 源代码的不同部分被 Java 编辑器渲染成了不同颜色。比如关键字是偏红的,并且是粗体;变量是蓝色,注释是绿色等等。不同的颜色可以让用户迅速的找到想要的内容,可以说是一个很方便的辅助功能。
在本系列的一部分中,我提到了 ANTLR 以及使用 ANTLR 产生词法和语法分析器。语法高亮是一个很依赖词法分析器的功能,因为词法分析器会把字符流解析成符号流,符号包含了各种属性,比如类型,起始位置和结束位置等等,语法高亮恰恰需要这些信息来渲染文本。我们已经有了一些基础类可以帮助我们管理符号列表,比如 TokenList,它可以根据字符偏移得到对应的符号。这为实现语法高亮提供了良好的基础。
回页首
ITokenScanner
JTF 是通过一个 ITokenScanner 接口来了解符号信息的,我们先来看一看这个接口的定义:
清单 1. ITokenScanner 接口
public interface ITokenScanner {
void setRange(IDocument document, int offset, int length);
IToken nextToken();
int getTokenOffset();
int getTokenLength();
}
可见通过这个接口,JTF 可以遍历所有的符号并且得到符号的偏移和长度信息。所以实现这个接口是一个实质性的工作。但是 JTF 怎么知道如何去渲染不同的文本呢?为了解决这个问题,JTF 也提供了一个 IToken 接口,并且有一个基本的实现类 Token。注意 ANTLR 里面也有 Token 这个类,两者的概念类似,只不过用法不同。JTF 的 Token 可以附带一些自定义的对象,我们可以把文本的属性,比如前景背景字体样式等等,都放到 Token 里面,这样 JTF 就知道该如何渲染文本了。这些文本属性也被 JTF 包装成了一个 TextAttribute 类,我们在构造 Token 之后的传入 TextAttribute 对象就可以了。
既然我们有了 TokenList 这样的基础类,实现 ITokenScanner 就是很简单的事了,我们看看 ExprTokenScanner 的代码:
清单 2. ExprTokenScanner 实现了 ITokenScanner
public class ExprTokenScanner implements ITokenScanner {
/*
* JFace token type definition
*/
private static IToken VARIABLE = new Token(
new TextAttribute(ColorManager.getInstance().getColor(IColorConstants.VARIABLE)));
private static IToken INTEGER = new Token(
new TextAttribute(ColorManager.getInstance().getColor(IColorConstants.INTEGER)));
private static IToken DEFAULT = new Token(
new TextAttribute(ColorManager.getInstance().getColor(IColorConstants.DEFAULT)));
// character offset
private int offset;
// last token returned by nextToken()
private CommonToken lastToken;
// token list
private TokenList tokenList;
public ExprTokenScanner() {
offset = 0;
}
public int getTokenLength() {
return lastToken.getStopIndex() - lastToken.getStartIndex() + 1;
}
public int getTokenOffset() {
return lastToken.getStartIndex();
}
public IToken nextToken() {
if(lastToken == null)
lastToken = tokenList.getToken(offset);
else
lastToken = tokenList.getNextToken(lastToken);
if(lastToken == null)
return Token.UNDEFINED;
switch(lastToken.getType()) {
case IExprTokens.ID:
return VARIABLE;
case IExprTokens.INT:
return INTEGER;
case org.antlr.runtime.Token.EOF:
return Token.EOF;
default:
return DEFAULT;
}
}
public void setRange(IDocument document, int offset, int length) {
lastToken = null;
this.offset = offset;
tokenList = TokenManager.getTokenList(document);
}
}
getTokenLength 和 getTokenOffset 的实现是相当直接的,因为长度和偏移信息都保存在了 ANTLR 的 Token 实现中。nextToken 稍微有点长,因为我们需要把 ANTLR 的 Token 映射到 JTF 的 Token,所有用了一个 swtich 语句来检查了一下 Token 的类型,从而返回相应的 JTF Token。返回的 Token 已经预先定义好了,并加上了文本属性信息。setRange 可能是个不太直观的方法,这个方法有两个目的:第一是将 ITokenScanner 和 IDocument 分离开,这样一个 ITokenScanner 可以为多个 IDocument 服务;第二是提高性能,因为每次都从头把文本渲染一遍是个不太经济的做法,所以 setRange 有 offset 和 length 两个参数来控制需要渲染的范围,提高了渲染速度。
回页首
PresentationReconciler
有了 TokenScanner 之后,剩下的事情就是让 JTF 知道它。上一部分提到了 SourceViewerConfiguration,它是 JTF 和外界交互的枢纽。我已经实现了 ExprConfiguation,但是它还是空的,现在我们就要覆盖父类的 getPresentationReconciler 方法:
清单 3. 覆盖父类的 getPresentationReconciler 方法
public IPresentationReconciler getPresentationReconciler(ISourceViewer sourceViewer)
{
PresentationReconciler reconciler = new PresentationReconciler();
DefaultDamagerRepairer dr = new DefaultDamagerRepairer(getTokenScanner());
reconciler.setDamager(dr, IDocument.DEFAULT_CONTENT_TYPE);
reconciler.setRepairer(dr, IDocument.DEFAULT_CONTENT_TYPE);
return reconciler;
}
PresentationReconciler,顾名思义,是一个负责 Presentation 的类,这里 Presentation 指的就是文本的颜色,字体等等。文本渲染是一个不断进行的过程,因为用户可能在不断的修改文本内容。JTF 必须知道用户输入了什么,然后重新检查一下输入的内容,再刷新。所以 JTF 引入了 Damager 和 Repairer 的概念,Damager 负责计算用户的输入对哪些区域造成了破坏,Repairer 负责重新渲染被破坏的文本区域。Damager 和 Repairer 是按照文本类型来组织的,但是在我们的例子中,只有一个缺省文本类型,所以我们只为缺省类型设置了 Damager 和 Repairer。稍后我们会简单介绍一下文本类型的概念。
提示:ExprConfiguration 已经在 JTFDialog 中和 ExprViewer 联系起来了,具体参见 JTFDialog的createDialogArea 方法。
幸运的是 JTF 已经为我们提供了 PresentationReconciler 以及 Damager,Repairer 的缺省实现,很少有需要扩展这些类的情况。这里我就不详细解释了。
回页首
Content Type
你可以把 Content Type 翻译成文本类型后者内容类型,总之它是用来描述具有某种特征的文字的。比如在 XML 语言中,标签肯定是由大于号和小于号括起来的,这样你就可以给 XML 的标签定义一种文本类型,更细一点,XML 的结束标签是由 "</" 和 ">" 括起来的,所以也可以为结束标签定义一种单独的文本类型。上一小节提到了不同的文本类型可以有不同的 Damager 和 Repairer,实际上 JTF 中很多特性都可以针对某个文本类型来做,因此适当的使用文本类型可以做一些更精细的控制。不过,这也得根据你要编辑的文本来具体考虑,像 XML 这样的非常结构化的语言,划分文本类型是很容易的。但是对于我这个例子中的数学表达式语言来说,就不太方便划分出很多类型来,你非要划分一下,可能会使得简单问题复杂化了。所以,要具体问题具体分析。
另外一个问题是 JTF 如何知道某段文本是什么样的类型呢?这里需要引入一个 Partition(分区)的概念,在 JTF 中,每个文档都有一个与之联系的 Document Partitioner(文档分区器),它会根据你给定的规则来扫描整个文本从而辨别哪块是哪个类型。所以它有一个相同的问题:对于非结构化的语言,划分分区不是特别容易的事情。
提示:关于分区和扫描规则的内容,读者可以参考 IDocumentPartitioner 和 IRule 接口以及它们的实现。这里不做详细的解释了。
所以当你要处理一种文法规则不那么严谨的语言的时候,也可以不定义文本类型,就用缺省的就够了。我的例子就都采用了这样的方式。同时由于 ANTLR 生成的词法分析器非常好用,所以也没有必要那么做。
回页首
结束语
语法高亮的部分到这里就完成了,下图是例子的效果:
图 1. 语法高亮效果图
语法高亮效果图
变量的颜色是咖啡色,数字是蓝色,其它都缺省是黑色。有了语法高亮的帮助,整个文档结构都一目了然了。
回页首
声明
本文仅代表作者的个人观点,不代表 IBM 的立场。