为eclipse写插件是一件美妙的事情,就像在一件伟大的艺术作品上进行二次创作一样,这种感觉实在是好极了。通过本文我希望能够帮助一些对eclipse感兴趣的人,特别是初学者来体会这种感觉,所以我决定写这些东西。在coding的领域里,我有这样一种观念:The simpler it is ,the more useful it is.特别是在你不熟悉的领域里,这条法则尤其适用,所以我决定用简单的方式来展示eclipse的奇妙之处,当然另一方面也是为自己的水平不足找个冠冕堂皇的借口:)
那么就让我从editor——这样一个很多应用程序尤其是文本相关的程序的最基本的部分开始,这也是编写很多插件基本的一个部分。我们来一起学习怎样在eclipse里面建立属于自己的editor。我们的editor是针对namespace2package.mappings文件的,你可能觉得陌生,它是在网格开发中的一种文件,不用担心,它只不过是一种name=value的文件而已,就像java中的property文件一样。大概是这个样子:
Ok,这个文件死定了,以后就跟它耗上了。我希望得到什么样的效果呢?我想给它加上语法色彩,这样就很容易区分name和value了,就像这样:
以后我会称这个editor为MappingEditor,只取个名字而已。
在David Gallardo的Eclipse in Action一书中他给出了一个编写log4j插件的完整的例子,其中就包括一个editor的部分,在Eclipse自带的plug-in的示例工程中也包括一个XML editor的完整例程。前者的工程过于庞大了,包含了编写插件的很多部分,然而在关键部分,他没有给出完整和清晰的概念和脉络,当你第一次看的时候还是会感觉很晕。后者把注意力集中的一个editor上面,代码还算清晰,可惜的是没有足够的注释来帮助你理解概念,所以你不得不去查api帮助,然后一遍一遍的更新自己的概念,然后得到正确的结论。当然学习编写editor的另一个资源是eclipse自带的帮助,HelpàPlatform Plug-in Developer GuideàProgrammer's GuideàEditors,它介绍了足够多的概念,其中有些让人感觉十分陌生,而且也没有能够run的代码,所以你肯定觉得不过瘾。所以在这里我想给出一个折中的办法,我解释一些重要的核心的概念,尽量用简单的方法,然后给出能够run 的代码,也相当一个框架,让你能够在上面修修改改,满足我们都曾经有过的那种心理:)但是别指望我对我说的话负责,我只是说出自己对它们的理解,每个人都可能有他自己的理解方式,所以如果你觉得我说错了,十分欢迎跟我一起探讨。
MappingEditor的功能虽然简单,但是它涵盖了编写editor的几乎所有的概念和主要步骤,我会尽量指出哪些是你所要关注的问题,而哪些是eclipse为你打下的基础,我会告诉你需要编写什么部分来构建属于你自己的editor,恩,我会尽力的。
在开始之前你要有所准备,编写插件并没有想象的那么简单,你可能看过一些关于eclipse的文章,上面大都描绘了一幅唯美壮阔的景象,但是实际上当你亲手做的时候就不那么美妙了,这是一个普遍的道理。你只要光看看在editor这部分eclipse有多少基础类就能明白了。抽象的接口和一些接口的实现,一定有近百个了,如果没有一些帮助真的很难理出一些头绪来。
但是不用太害怕,Editor插件的编写其实已经存在一个frameworks了,eclipse已经为我们做了这些,只要你理解了这个frameworks,那么你要完成的事情只是一小部分了,当然这是相对eclipse做过的那些来说的。理解一个frameworks最好的方法就是理解的它设计的概念,在这里我也想强调和突出概念的解释,希望我能够做到。
现在发现我真的很罗嗦。
在eclipse里面,一个editor就是一个体现MVC模式的综合体,它包括一个可视化的部分(View),一种要处理的数据,即editor的输入(Model),还有控制(Control)部分,你可以认为控制部分就是editor本身,但是实际上是有几个助手类完成的。由于我们处理的内容从根本上说是文本,所以还要用到eclipse里面的另外一个框架,就是Text Frameworks,eclipse一直都是用这个框架来处理文本内容的。记住它们是用来处理文本的就够了,从我们的角度看只不过是一些实用类,所以没有必要了解太深刻,因为我了解的也不深刻:)
现在让我们从总体上看看editor的结构
这个是我们能够很容易理解的在现实中的结构,不能再简单的MVC,画上面一个图简直就是在浪费时间。现在把它映射到eclipse里面,看看都是些什么。图框中的每个名称都对应eclipse里面的一个类,并且它们就是我们要继承的类。
总体的结构就是这样了,绿色的部分是下面我们需要扩展的部分。
一个editor有许多功能,最基本的如剪切,复制,粘贴,这些基本的功能eclipse当然不会忍心让你去费神,它们已经由TextEditor实现了,所以重点不在这里。我希望实现语法色彩(Syntax coloring),它是跟显示相关的内容,所以你可以想到,应该跟上面的SourceViewerConfiguration类相关。在以后我可能会加入内容帮助(Content assist)的部分,在此埋下一个伏笔:)
下面开始来解释:
TextEditor是一个核心类,顺着它的类层次结构看上去,会发现它有很多Fields,一个FileDocumnetProvider,一个SourceView,一个SourceViewerConfiguration,正是通过设置这些 Fields,我们的editor才区别于其他的editor,从而实现自己独特的功能。用比较新一点的观点来看,这种方式类似与IoC,控制反转;另一种用模式的称呼为Templete模式。我们继承TextEditor实现MappingEditor,在其中完成我们的设置。
注:为了减少篇幅,只给出核心代码,省略了package,import的部分,完整的代码和eclipse工程文件可以在文末衔接出下载。
MappingEditor.java
public class MappingEditor extends TextEditor {
private ColorManager colorManager;
public MappingEditor() {
super();
colorManager = new ColorManager();
setSourceViewerConfiguration(
new MappingConfiguration(colorManager));
setDocumentProvider(new MappingDocumentProvider());
}
public void dispose() {
colorManager.dispose();
super.dispose();
}
}
ColorManager是我们自己实现的用来管理颜色资源的类,这是因为在SWT里面颜色等这些系统资源属于比较重量级的资源,统一管理是比较好的解决办法。代码很简单,入目即明。
ColorManager.java
public class ColorManager {
protected Map fColorTable = new HashMap(10);
public void dispose() {
Iterator e = fColorTable.values().iterator();
while (e.hasNext())
((Color) e.next()).dispose();
}
public Color getColor(RGB rgb) {
Color color = (Color) fColorTable.get(rgb);
if (color == null) {
color = new Color(Display.getCurrent(), rgb);
fColorTable.put(rgb, color);
}
return color;
}
}
此外还要用到关于颜色的几个常数,放到IColorConstans接口中
public interface IColorConstants {
RGB NAME = new RGB(0x33, 0x33, 0xFF);
RGB VALUE = new RGB(0xFF, 0x33, 0x33);
}
MappingDocumentProvider继承自FileDocumnetProvider,用来将File类型转变为Editor框架能够处理的统一个的格式Document,我们需要实现它完成这种转换。
MappingDocumentProvider.java
public class MappingDocumentProvider extends FileDocumentProvider {
protected IDocument createDocument(Object element) throws CoreException {
IDocument document = super.createDocument(element);
if (document != null) {
IDocumentPartitioner partitioner =
new DefaultPartitioner(
new MappingPartitionScanner(),
new String[] {
MappingPartitionScanner.XML_TAG,
MappingPartitionScanner.XML_COMMENT });
partitioner.connect(document);
document.setDocumentPartitioner(partitioner);
}
return document;
}
}
MappingPartitionScanner类继承自RuleBasedPartitionScanner
这里出现了另外一个概念Partitions,一个Partition就是文档中的一部分,这样我们就可以处理文档的不同的部分了,你可以预感到这应该跟语法色彩有关了。记住这些部分是相互不重叠,没有那个部分同时属于两个Partition。在完成File向Document的转换时应该实现Partition的划分,在这里必然要有一个工具来将IDocument(I表示接口)划分为不同的Partitions,它就是IDocumentPartitioner。我们需要实现这样一个IDocumentPartitioner,在这里就是MappingPartitionScanner。
那么下一个问题是我们按照什么原则来划分呢?eclipse提供了类似自然语言的方式,通过规则,一个规则就是一个IRule,我们告诉partitioner我们要使用的rule,它就会安装规则将文档划分为不同的部分。站在很高的角度上看这些问题,一切都是那么简单和合理。到了IRule这个级别就不存在什么框架可以使用了,eclipse提供了几个简单但是常用的规则,但是这对于我们复杂的需求来说是远远不够的,我们需要编写自己的Rule,无论是通过继承已有的rule还是实现定义的IRule接口,而这就需要一些技巧和智慧了。在这里,MappingEditor将文档分为两个部分,一个部分代表Value的内容,命名为MAPPING_VALUE,不需要将Name的内容单独划分为一个部分,因为除了Value,剩下的默认的部分就是Name了,用IDocument.DEFAULT_CONTENT_TYPE表示,后面回看到。
让我们来看看MappingPartitionScanner类的实现:
public class MappingPartitionScanner extends RuleBasedPartitionScanner {
public final static String MAPPING_VALUE = "__mapping_value";
public MappingPartitionScanner() {
IToken tag = new Token(MAPPING_VALUE);
IPredicateRule[] rules = new IPredicateRule[1];
rules[0] = new SingleLineRule("=", null, tag, (char) 0, true);
setPredicateRules(rules);
}
}
可以看到我们使用了一条eclispe提供的简单的Rule——SingleLineRule(),该Rule针对文本的每一行,代码中的Rule就是选取一行中“=”开头直至行尾的部分。
现在我们已经将文档划分为了不同的部分,这为语法色彩的实现打下了基础不是吗?
Eclispe提供了另外一个强有力的框架来处理文本,就是Text frameworks。Text frameworks 使用了一种叫做破坏者(damage),修复者(repaire),调节者(reconciling)的模式来实现语法色彩,当我们修改文档的一部分的时候,需要计算哪些部分需要重新显示,这就是破坏。破坏会返回一个区域给修复者来修复,以提供正确的显示。调节者将二者贯穿起来,调节者会监视文档的改变,然后通知破坏者,破坏者计算出改变的部分,然后在告诉与它相应的修复者来修复。
破坏者、修复者和调节者的设置都在SourceViewerConfiguration进行设置,像前面描述的那样,它用来控制显示部分。
MappingConfiguration.java
public class MappingConfiguration extends SourceViewerConfiguration {
private ColorManager colorManager;
private RuleBasedScanner nameScanner;
private RuleBasedScanner valueScanner;
public MappingConfiguration(ColorManager colorManager) {
this.colorManager = colorManager;
}
public String[] getConfiguredContentTypes(ISourceViewer sourceViewer) {
return new String[] {
IDocument.DEFAULT_CONTENT_TYPE,
MappingPartitionScanner.MAPPING_VALUE };
}
protected RuleBasedScanner getNameScanner(){
if (nameScanner == null) {
nameScanner = new RuleBasedScanner();
nameScanner.setDefaultReturnToken(
new Token(
new TextAttribute(
colorManager.getColor(IColorConstants.NAME))));
}
return nameScanner;
}
protected RuleBasedScanner getValueScanner(){
if (valueScanner == null) {
valueScanner = new RuleBasedScanner();
valueScanner.setDefaultReturnToken(
new Token(
new TextAttribute(
colorManager.getColor(IColorConstants.VALUE))));
}
return valueScanner;
}
public IPresentationReconciler getPresentationReconciler(ISourceViewer sourceViewer) {
PresentationReconciler reconciler = new PresentationReconciler();
DefaultDamagerRepairer dr =
new DefaultDamagerRepairer(getValueScanner());
reconciler.setDamager(dr, MappingPartitionScanner.MAPPING_VALUE);
reconciler.setRepairer(dr, MappingPartitionScanner.MAPPING_VALUE);
dr = new DefaultDamagerRepairer(getNameScanner());
reconciler.setDamager(dr, IDocument.DEFAULT_CONTENT_TYPE);
reconciler.setRepairer(dr, IDocument.DEFAULT_CONTENT_TYPE);
return reconciler;
}
}
getConfiguredContentTypes()方法告诉editor文档分为哪几个部分。
getPresentationReconciler()方法设置MappingEditor的调节者的内容,它包含了以下要陈述的主要内容。
我们不准备自己实现过于复杂的Damager和Repairer,所以使用eclispe默认提供的类,在这个方法里面,我们给不同的Partitions设置不同的Damager和Repairer。在创建Damager和Repairer的时候,需要传入一个ITokenScanner。一个ITokenScanner用来在一个Partition里面寻找Token,在这里我们使用一个具体的实现RuleBasedScanner,它根据规则来寻找Token,跟RuleBasedPartitionScanner来划分Partitions的道理是一样的。因为在MappingEditor里面name就是name,value就是value,不会在name或者value的Partition里面进行进一步的划分,所以我们不用制定什么规则,而把整个Partition当作一个Token就可以了,这样我们只要告诉TokenScanner默认情况下返回的Token就可以了,通过setDefaultReturnToken(IToken token)来设置。如果要实现关键词加亮,那么就必须自己实现特定的RuleBasedScanner了。剩下的一个问题是色彩在那里,Token类的实现很简单,只是一个包含Object的类而已,我们给Token加入一个设置Token颜色的TextAttribute属性就可以了。那么editor要显示这个Token的时候,Damager/Repairer机制就会根据这个属性将Token展现给我们,一切就是这样。
介绍完了一些概念,下面我会详细的写出实现插件的每一步,只是希望初学者不要错过什么,对于高水平的同志,完全可以忽略这些,而是专注于概念,很可能你会对概念提出自己的更好的看法,那么欢迎跟我联系,我们一起探讨并改进:)
Eclipse的版本更新有些太快了,当然很大程度上对于我们的插件程序的运行是没有什么影响的,当然也有些要注意的地方。我建议使用“super stable”版本,在这里我使用M7。
Let’s begin.
新建一个插件工程(Plug_in Project),输入工程的名称org.tsinghua.plugins.mappingeditor,没有什么特别的意思,单击next,进入Plug-in Project Structure页,没有必要修改任何东西,单击next,进入Plug-in Content,我在Provider Name里面填上自己的ID,其他的使用默认选项就足够了。单击Finish,一个插件工程框架就建好了,该往里面添加东西了。
让我们看看这个插件工程,在eclipse的Package Explore里面就有如下的目录结构,MappingeditorPlugin.java是插件的主类,作为插件的核心,一般会保存一些全局性的东西,由于我们要编写的插件实在太简单了,所以根本没有必要修改它。
下一步为了我们的插件能够运行,需要引入一些包:
右键单击该工程,选最下边的Propertiesàjava build pathà右边的Libraries页àadd external jars,然后将eclipse的plugins目录下面的如下几个包加入工程里面来:
org.eclipse.ui.editors_3.0.0/editors.jar
org.eclipse.jface.text_3.0.0/jfacetext.jar
org.eclipse.text_3.0.0/text.jar
org.eclipse.ui.workbench.texteditor_3.0.0/texteditor.jar
怎么知道要用哪些包呢,这就是一个经验和知识的问题了,没有什么简单的方法,需要在我们编写代码的过程中慢慢了解。
下一步我们要为我们的editor插件工程引入需要的其他插件,如果你了解eclipse的插件体系结构的话就会明白,我们的插件是不能单独工作的,它需要同其他的插件协同工作,特别是作为editor这样一个eclipse扩展点来说,就像开始我说的那样,如果现在我们没有一件伟大的艺术作品摆在面前,我们什么也做不来。
打开plug-in.xml插件描述文件,选择Dependencies,填加
org.eclipse.jface.text
org.eclipse.ui.editors
org.eclipse.ui.workbench.texteditor
三个插件
OK,到目前为止,一些准备工作都做好了,现在可以全心专注于代码的编写了。新建一个org.tsinghua.plugins.mappingeditor.editors包,将前边提到的类代码添加其中。
代码编写完成后,就要进入最后的工作,我们要扩展eclipse的editor扩展,注册我们的editor当我们试图打开一个namespace2package.mappings文件的时候,eclipse平台会调用我们的editor,这样我们就得告诉eclipse一些我们的信息,就是eclipse扩展点的概念。
进入plugin.xml,点击下面的Extensions页,单击add按钮,
选择Generic Wizards à Schema-based Extension,如上图:
选择org.eclipse.ui.editors扩展点,不用修改别的,然后按Finish结束,
然后结果是这样的:
然后我们填加一个editor:
剩下的就是修改这个扩展点的属性了,告诉eclipse我们做了什么
class属性就是我们的Editor类:
org.tsinghua.plugins.mappingeditor.editors.MappingEditor
default:true
表明将这个editor作为打开我们指定文件的默认的编辑器。
filenames:
就是指定MappingEditor要处理的文件类型,这里是全称namespace2package.mappings,我们可以通过设置extensions属性来指定具有特性扩展名的文件。
icon:为MappingEditor指定一个图标
运行插件,菜单Run—Run..,配置一个插件的运行环境如下图:
双击左边的MappingEditor运行配置文件或者选择Run -- Run as -- 5 Run-time Workbench运行插件,在Workbench中打开namespace2package.mappings文件。
让我们来看看最后的效果吧。
Author: oosky@smth
Mail: wzt@mails.tsinghua.edu.cn