您的位置:首页 > 编程语言 > Java开发

代码编辑器语法着色功能实现-Java版

2011-05-15 08:34 706 查看
引言

最近在整理代码时,找到了去年写的一个编辑器语法着色功能的代码。想来刚刚完成的时候,就想着能够公布出来,与大家交流。但直到现在仍寂寞的待在文件夹的角落里,颇有点明珠暗投的意思。自己能理解简单,通过讲解让别人也理解就不那么容易了,还是要找机会发布一下。

语法着色,是现在代码编辑器最基本的功能之一,相信大家也都非常熟悉。定义什么的就不必下了。比起干巴巴的文字,图更吸引大家的眼球。先看一下实现后的效果。

Java语法着色



SQL语法着色:



实现:

1.我先假设自己已经实现了语法着色功能。那我应该怎么调用呢?语法着色应该发生在代码修改的时候,也就是文字的编辑,包括插入和删除,剪切和粘贴也都是插入和删除的一种。那么Java的控件中有没有一个编辑器可以感应到插入和删除呢?恰好有一个:JTextPane。监听器应该是下面这个样子:

textPane.getDocument().addDocumentListener(new DocumentListener() {

public void changedUpdate(DocumentEvent e) {
// 这个方法在文本的样式修改时被调用,在着色时用不到,DO NOTHING
}

public void removeUpdate(DocumentEvent e) {
// 删除一段文字时调用,相当于被删除的文字后的文字向前移动了被删除文字的长度
processPaint(e.getOffset(), -e.getLength());
}

public void insertUpdate(DocumentEvent e) {
// 输入一段文字时调用
processPaint(e.getOffset(), e.getLength());
}

});


2.这样,问题就被变成,在代码发生变化时,如何实现一个方法processPaint()来着色,看下面的代码,在插入删除事件中,将处理塞到事件队列最后,以免出现线程问题。具体操作看一下注释:

要解释的是那个【分段】,用的单词是Token,指的是被着色的一个代码片段

void processPaint(final int startOffset, final int shift) {
SwingUtilities.invokeLater(new Runnable() {

public void run() {
Token firstAffectToken = null;
int index = 0;
// 开始计算对现有的文字分段造成的影响
for (Token t : tokenList) {
// 找到中间是修改起点的文字分段,做为要处理的第一个分段
if (t.getOffset() < startOffset
&& t.getOffset() + t.getLength() > startOffset) {
firstAffectToken = t;
break;
}
// 如果正好从一个分段的开始修改,则将此分段的前一个分段做为第一个分段(因为前一个分段可能与修改后的文字重新
// 组合,变成新的分段
if (t.getOffset() == startOffset && index > 0) {
firstAffectToken = tokenList.get(--index);
break;
}
index++;
}
// 如果第一个分段是空,可能没有分段,则重新创建分段
if (firstAffectToken == null) {
index = 0;
firstAffectToken = new Token(0, 0, DEFAULT);
}
final Token fromToken = firstAffectToken;
final int fromIndex = index;
// 从被影响的第一个分段开始着色,同进还要调整后续未修改分段的起点shift
setSyntaxColor(fromToken, fromIndex, shift);
// 处理完成后,就会生成当前文字的着色分段列表,可以被重用,如生成HTML
}
});
}


3.着色事件和着色方法都有了,那么接下来就是着色规则了,为了可扩展,这部分应该是可以动态设置的,以Java的语法规则为例,比如我们要实现对JavaDoc,单行注释,多行注释,字符串和关键字的着色:

public static final Color JAVADOC_DEFAULT     = new Color(98, 175, 244); // @jve:decl-index=0:

public static final Color SINGLE_LINE_COMMENT = new Color(0, 255, 0);

public static final Color KEYWORD             = new Color(128, 0, 128); // @jve:decl-index=0:

public static final Color STRING              = new Color(0, 0, 255);   // @jve:decl-index=0:
private IRule[] buildJavaRules() {
SyntaxColorStyle defaultStyle = StyleFactory.fromDefault("Tahoma", 14);

SyntaxColorStyle javaDocStyle = defaultStyle.clone();
javaDocStyle.setForeground(JAVADOC_DEFAULT);

SyntaxColorStyle commentStyle = defaultStyle.clone();
commentStyle.setForeground(SINGLE_LINE_COMMENT);

SyntaxColorStyle keywordStyle = defaultStyle.clone();
keywordStyle.setForeground(KEYWORD);
keywordStyle.setBold(true);

SyntaxColorStyle stringStyle = defaultStyle.clone();
stringStyle.setForeground(STRING);

return new IRule[] {
RuleFactory.createMultiLineRule("javadoc", "/**", "*/", javaDocStyle,
"/**/"),
RuleFactory.createMultiLineRule("comment", "/*", "*/", commentStyle),
RuleFactory.createSingleLineRule("string", "/"", "/"", '//',
stringStyle),
RuleFactory.createSingleLineRule("string", "/'", "/'", '//',
stringStyle),
RuleFactory.createSingleLineRule("comment", "//", commentStyle),
RuleFactory.createKeywordRule("keyword", true, keywordStyle,
new String[] { "abstract", "boolean", "break", "byte", "case",
"catch", "char", "class", "continue", "default", "do",
"double", "else", "extends", "false", "final", "finally",
"float", "for", "if", "implements", "import", "instanceof",
"int", "interface", "long", "native", "new", "null", "package",
"private", "protected", "public", "return", "short", "static",
"super", "switch", "synchronized", "this", "throw", "throws",
"transient", "true", "try", "void", "volatile", "while" }) };
}


原理

把原理放到这里来讲,是因为原理之类的总是最难理解的,把难的放到开头,吓跑观众就不美了。

实现的思路就是:用一个游标,从代码修改处开始,逐个字符向后移动,每到一个字符,就拿出所有的着色规则来看从当前字符开始的字符串是否符合此规则,如果符合,那就是找到一个着色分段,游标就会移到规则结束字符之后,再开始下一次判断。如果所有的规则都不符合,就加到默认着色片段中,你说默认着色片段是什么?就是不着色片段。这样不断的循环,直到所有的字符都被扫过,着色分段就全部找到了,再把每个分段一上色,OK,着色成功。

各位观众,最最关键的着色核心方法终于要登场了,什么?你说前面有个着色方法了?那只是一个马甲。

protected void setSyntaxColor(Token startToken, int affectIndex, int shift) {
// 首先认为开始的文字是不需着色的默认样式
Token defaultToken = new Token(startToken.getOffset(), 0, DEFAULT);
// 指定开始扫描的文字偏移量
scanner.setOffset(defaultToken.getOffset());
// 新的着色分段列表
List<Token> newList = new ArrayList<Token>();
// 首先将未受影响的分段(也就是修改发生之前的分段)复制过来
for (int i = 0; i < affectIndex; ++i) {
newList.add(tokenList.get(i));
}
Token newToken = null;
boolean matched = false;
while (scanner.hasMoreChar()) {
matched = false;
for (IRule rule : rules) {
// 从当前文字开始,测试每一个规则,看是否符合规则,如果符合,则生成新的分段,扫描器的当前位置也会向后移动
// 否则规则恢复扫描器当前位置,由下一个扫描器测试,这里,每个规则都要保证扫描器的当前位置是正确的
newToken = rule.evaluate(scanner);
if (newToken != null) {
// 在生成新的着色分段后,先判断之前默认的分段是否有长度
if (defaultToken.getLength() > 0) {
// 判断默认分段是否与修改前的分段相同(也就是在修改后,分段只是平移,没有被破坏)
// 如果是这样,则认为从这个分段向后的所有分段都没有受到影响,可以直接平移使用,无需再扫描,提高效率
if (isExists(newList, affectIndex, defaultToken, shift)) {
return;
}
// 这是个新分段,需要着色
paintToken(defaultToken);
newList.add(defaultToken);
}
// 同上面的默认分段,这里处理的是着色分段
if (isExists(newList, affectIndex, defaultToken, shift)) {
return;
}
paintToken(newToken);
newList.add(newToken);
matched = true;
break;
}
}
// 找到一个新的着色分段,则默认分段被归零,重新计算
if (matched) {
defaultToken = new Token(scanner.getOffset(), 0, DEFAULT);
}
else {
// 所有的规则都不符合,默认分段增加此字符
defaultToken.increaseLength();
// 扫描器跳一个字符
scanner.read();
}
}
// 最后还剩余默认分段,也要加入分段列表
// defaultToken.increaseLength();
if (defaultToken.getLength() > 0) {
paintToken(defaultToken);
newList.add(defaultToken);
}
// 保存新生成的着色分段列表
tokenList = newList;
}


后记

代码的主要部分就说明完毕了。其它的规则的实现,样式的注入,还有通过生成的分段列表生成HTML之类的就不一一讲解了,大家有兴趣看看后附的源代码就好了。

还有一点在意的是:比如Eclipse编辑器的JavaDoc样式中还有可能有内部的样式,如@return就是用粗体实现的,我们的代码中还没有实现。大家有什么好的思路,可以提供一下。

源码:点此下载http://download.csdn.net/source/3280472

参考资料

1.Eclipse源代码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: