创建 HTML 编辑器
内容助理的概念与 JFace 文本查看器(即 org.eclipse.jface.text.source.SourceViewer 类)的特定实现有关。整个 Eclipse 工作台中都使用了这个类的实例来实现各种编辑器。然而,SourceViewers 并不仅限用于 Eclipse 工作台,而是还使用在基于 SWT 和 JFace JAR 建立的任何应用程序中。本文将在 Eclipse 编辑器插件的环境中展示内容助理的实现,并给出关于如何通过“裸”SourceViewers 使用内容助理的技巧。
下面让我们实现一个简单的 HTML 编辑器。内容助理对 HTML 编辑可能非常有用。例如,内容助理能够生成诸如表或链接等典型的 HTML 结构,或者能够将选中的文本区域包装到样式标签中。
为节省时间,我们将使用 New Plug-in Project 向导之一来实现这个编辑器,以生成适当的编辑器插件。由于所生成的这个编辑器是 XML 编辑器,而 HTML 是基于 XML 的标记语言,我们只需进行一些次要的修改,将所生成的编辑器转换为一个 HTML 编辑器。下面就让我们开始吧。
在调用 New 向导之后,选择 Plug-in Development 和 Plug-in Project。在随后的屏幕上,输入项目名称“Sample HTML Editor”。在接下来的屏幕上,定义适当的插件 ID,比如“com.bdaum.SampleHTMLEditor”。下面的屏幕允许您选择适当的代码生成向导。请选择 Plug-in with an editor,如图 1 所示。
图 1. 带编辑器的插件
在下一个屏幕上,修改建议的插件名称(如果想这样做的话)和插件类名称,并指定一个提供者名称。其他内容保留不变。
继续到下一个屏幕,把建议的名称 Editor Class Name 修改为“HTMLEditor”,把 Editor Name 修改为“Sample HTML Editor”,把 File Extension 修改为“html, htm”,如图 2 所示。后一个条目将把新的编辑器与具有 .html 或 .htm 文件扩展名的所有文件关联起来。
图 2. 编辑器选项
单击 Finish 按钮来生成新的编辑器。现在通过 Run > Run as ... > Run-time workbench 启动一个新的工作台。在创建具有 .html 或 .htm 文件扩展名的新文件(或导入这样的文件)之后,再使用新的编辑器来打开它。
添加内容助理
正如您很快将会发现的,这个编辑器没有具备内容助理特性;按 Ctrl + 空格键没有任何作用。SourceViewers 默认情况下没有配备内容助理。我们需要相应地配置这个 HTML 编辑器中使用的 SourceViewer。
HTML 编辑器的 SourceViewer 的配置是通过所生成的类 XMLConfiguration 来表示的,这个类是 SourceViewerConfiguration 的子类(如果您愿意,可以将这个类重命名为 HTMLConfiguration,不过这并不是必需的)。为了向源代码查看器添加一个内容助理,我们需要重写 SourceViewerConfiguration 方法 getContentAssistant()。这最适合通过 Java 编辑器的上下文功能 Source > Override/Implement Methods...来完成,这个功能会为该方法创建一个存根(stub)。现在我们需要实现这个方法,并返回一个 IContentAssistant 类型的适当实例。
内容助理由一个或多个内容处理器组成,我们想要支持的每种内容类型分别有一个内容处理器。源代码查看器处理过的文档可以划分为具有不同内容类型的多个分区。这样的分区将由分区扫描程序确定,事实上,我们在包 com.bdaum.HTMLEditor.editors 中发现了一个类 XMLPartitionScanner。这个类为我们的文档类型 XML_DEFAULT、XML_COMMENT 和 XML_TAG 定义了三种不同的内容类型。此外,文档也可能包含 IDocument.DEFAULT_CONTENT_TYPE 类型的分区。
在新方法 getContentAssistant() 中,我们首先创建了 IContentAssistant 的默认实现的一个新实例,并给它配备了针对 XML_DEFAULT、XML_TAG 和 IDocument.DEFAULT_CONTENT_TYPE 内容类型的完全一样的内容助理处理器。由于不打算在 HTML 注释内提供辅助,因此我们没有为内容类型 XML_COMMENT 创建内容助理处理器。清单 1 显示了该代码。
清单 1. getContentAssistant
public IContentAssistant getContentAssistant(SourceViewer sourceViewer) {
// Create content assistant
ContentAssistant assistant = new ContentAssistant();
// Create content assistant processor
IContentAssistProcessor processor = new HtmlContentAssistProcessor();
// Set this processor for each supported content type
assistant.setContentAssistProcessor(processor, XMLPartitionScanner.XML_TAG);
assistant.setContentAssistProcessor(processor, XMLPartitionScanner.XML_DEFAULT);
assistant.setContentAssistProcessor(processor, IDocument.DEFAULT_CONTENT_TYPE);
// Return the content assistant
return assistant;
}
实现内容助理处理器
类 HtmlContentAssistProcessor 还不存在。现在通过单击 QuickFix 灯泡状图标来创建它。在这个新类中,我们只需完成从接口 IContentAssistProcessor 继承来的预先生成的方法。我们最感兴趣的方法是 computeCompletionProposals()。这个方法返回一个 CompletionProposal 实例数组,我们提供的每个建议分别有一个实例。例如,我们可以提供所有 HTML 标签的集合以供选择。然而,我们希望它更高级一点。当在编辑器中选中一个文本范围时,我们希望提供一个可用于包装这段文本的样式标签集合。否则,我们就提供用于创建新 HTML 结构的标签。图 3 和图 4 显示了我们想要达到的效果。
图 3. structProposal
图 4. styleProposal
因此,首先要从编辑器的 SourceViewer 实例中检索当前选中的内容(参见清单 2)。
清单 2. computeCompletionProposals
public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer,
int documentOffset) {
// Retrieve current document
IDocument doc = viewer.getDocument();
// Retrieve current selection range
Point selectedRange = viewer.getSelectedRange();
然后创建一个 ArrayList 实例,用于收集所生成的 ICompletionProposal 实例,如清单 3 所示。
清单 3. computeCompletionProposals (续)
List propList = new ArrayList();
如果选中了文本范围,则检索选中的文本,并计算出样式标签建议,如清单 4 所示。
清单 4. computeCompletionProposals (续)
if (selectedRange.y > 0) {
try {
// Retrieve selected text
String text = doc.get(selectedRange.x, selectedRange.y);
// Compute completion proposals
computeStyleProposals(text, selectedRange, propList);
} catch (BadLocationException e) {
}
} else {
否则,设法从文档中检索一个限定符,如清单 5 所示。这样的限定符包含部分地进入 HTML 标签的所有字符,用来限制可能的建议集。
清单 5. computeCompletionProposals (续)
// Retrieve qualifier
String qualifier = getQualifier(doc, documentOffset);
// Compute completion proposals
computeStructureProposals(qualifier, documentOffset, propList);
}
最后,将自动完成建议列表转换为一个数组,并将这个数组作为结果返回,如清单 6 所示。
清单 6. computeCompletionProposals (续)
// Create completion proposal array
ICompletionProposal[] proposals = new ICompletionProposal[propList.size()];
// and fill with list elements
propList.toArray(proposals);
// Return the proposals
return proposals;
}
构造限定符
现在,让我们看看如何从当前文档检索限定符。我们需要实现方法 getQualifier(),如清单 7 所示。
清单 7. getQualifier
private String getQualifier(IDocument doc, int documentOffset) {
// Use string buffer to collect characters
StringBuffer buf = new StringBuffer();
while (true) {
try {
// Read character backwards
char c = doc.getChar(--documentOffset);
// This was not the start of a tag
if (c == '>' || Character.isWhitespace(c))
return "";
// Collect character
buf.append(c);
// Start of tag. Return qualifier
if (c == '<')
return buf.reverse().toString();
} catch (BadLocationException e) {
// Document start reached, no tag found
return "";
}
}
}
这是相当简单的。我们从当前文档偏移位置开始,向后读取文档字符。当检测到一个开括号时,我们就找到了一个标签的开头,并将收集到的字符在逆转顺序之后返回。在无法找到标签开头的其他所有情况下,我们返回一个空字符串。在这样的情况下,建议集是不受限制的。
编译自动完成建议
现在让我们编译一个建议集合。清单 8 显示了构成这些建议的相关标签集。如果您愿意,还可以添加更多的标签。
清单 8. 建议集合
// Proposal part before cursor
private final static String[] STRUCTTAGS1 =
new String[] { "<P>", "<A SRC=\"", "<TABLE>", "<TR>", "<TD>" };
// Proposal part after cursor
private final static String[] STRUCTTAGS2 =
new String[] { "", "\"></A>", "</TABLE>", "</TR>", "</TD>" }
可以看到,我们将每个标签建议划分为两个部分:一部分在预计的光标位置之前,一部分在预计的光标位置之后。清单 9 显示了编译这些建议的 computeStructureProposals() 方法。
清单 9. computeStructureProposals
private void computeStructureProposals(String qualifier, int documentOffset, List propList) {
int qlen = qualifier.length();
// Loop through all proposals
for (int i = 0; i < STRUCTTAGS1.length; i++) {
String startTag = STRUCTTAGS1[i];
// Check if proposal matches qualifier
if (startTag.startsWith(qualifier)) {
// Yes -- compute whole proposal text
String text = startTag + STRUCTTAGS2[i];
// Derive cursor position
int cursor = startTag.length();
// Construct proposal
CompletionProposal proposal =
new CompletionProposal(text, documentOffset - qlen, qlen, cursor);
// and add to result list
propList.add(proposal);
}
}
}
我们遍历标签数组,选择以指定限定符开头的所有标签。对于每个选定的标签,我们创建一个新的 CompletionProposal 实例。对于参数,我们传递完整的标签文本、这段文本应该插入的位置、文档中应该被替换的文本的长度(也就是限定符的长度),以及相对于插入文本开头的预计光标位置。
这个方法将为我们提供 WYSIWYG(“所见即所得”)的建议。内容助理的弹出窗口将列出建议,其形式与它们被选定时插入文档的形式精确一致。
处理复杂建议
前述方法并不适合于我们还必须实现的方法 computeStyleProposals()。这里我们需要将选中的文本包装到选定的样式标签中,并使用这个新的字符串替换文档里选中的文本。由于这样的替换可能具有任何长度,在内容助理选择窗口中显示它是没有意义的。相反,显示一段简短而有意义的说明性文字,一旦选定明确的样式建议,就显示一个包含完整替换文本的预览窗口,这样会更有意义。我们可以通过使用 CompletionProposal() 构造函数的一种扩展形式来实现这点。
清单 10 显示了我们想要支持的样式标签以及关联的说明文字。同样,您可能希望添加更多的标签。
清单 10. 样式标签集合
private final static String[] STYLETAGS = new String[] {
"b", "i", "code", "strong"
};
private final static String[] STYLELABELS = new String[] {
"bold", "italic", "code", "strong"
};
清单 11 显示了方法 computeStyleProposals()。
清单 11. computeStyleProposals
private void computeStyleProposals(String selectedText, Point selectedRange, List propList) {
// Loop through all styles
for (int i = 0; i < STYLETAGS.length; i++) {
String tag = STYLETAGS[i];
// Compute replacement text
String replacement = "<" + tag + ">" + selectedText + "</" + tag + ">";
// Derive cursor position
int cursor = tag.length()+2;
// Compute a suitable context information
IContextInformation contextInfo =
new ContextInformation(null, STYLELABELS[i]+" Style");
// Construct proposal
CompletionProposal proposal = new CompletionProposal(replacement,
selectedRange.x, selectedRange.y, cursor, null, STYLELABELS[i],
contextInfo, replacement);
// and add to result list
propList.add(proposal);
}
}
对于每种受支持的样式标签,我们将构造一个替换字符串,并创建一个新的自动完成建议。当然,这种解决办法是相当简单的。恰当的实现应该进一步检查替换字符串。如果这个字符串包含标签,我们将相应地对该字符串分段,分别将单独的段包括在新的样式标签内。
显示额外信息
CompletionProposal() 构造函数的前四个参数与它们在 computeStructureProposals() 方法中一样具有相同的含义(替换字符串、插入点、被替换的文本的长度,以及相对于插入点的光标位置)。第五个参数(在本例中我们将 null 传递给它)接受一个图像实例。这个图像将显示在弹出窗口中相应条目的左侧。第六个参数接受出现在建议选择窗口中的画面说明文字。第七个参数用于 IContextInformation 实例,我们很快就会讨论它。最后,第八个参数接受附加信息窗口中的文本,当某条建议被选定时,这段文本就应该显示出来。然而,仅只是为这个参数提供一个值,并不足以实际获得这样的信息窗口。我们必须相应地配置内容助理。同样地,这是在类 XMLConfiguration 中完成的。我们只需向方法 getContentAssistant() 添加如清单 12 所示的行。
清单 12. 向 getContentAssistant 添加行
assistant.setInformationControlCreator(getInformationControlCreator(sourceViewer));
这里发生了什么呢?首先,我们从当前源代码查看器配置中获得了一个 IInformationControlCreator 类型的实例。这个实例是一个负责创建类 DefaultInformationControl 的实例的工厂,所创建的实例将负责管理信息窗口。然后我们告诉内容助理关于这个工厂的信息。内容助理最终将在某个自动完成建议被选定时,使用这个工厂来创建一个新的信息控制实例。
格式化信息文本
默认情况下,这个信息控制实例将以纯文本的形式提供附加的信息文本。然而,添加一些美妙的文本表示形式是可以做到的。例如,我们可能希望以粗体打印所有标签。为此,我们需要相应地配置 IInformationControlCreator 创建的 DefaultInformationControl 实例。实现这点的惟一办法是使用一个不同的 IInformationControlCreator,而这可以通过重写 XMLConfiguration 方法 getInformationControlCreator()来完成。
这个方法在 SourceViewerConfiguration 类中的标准实现如清单 13 所示。
清单 13. getInformationControlCreator
public IInformationControlCreator getInformationControlCreator
(ISourceViewer sourceViewer) {
return new IInformationControlCreator() {
public IInformationControl createInformationControl(Shell parent) {
return new DefaultInformationControl(parent);
}
};
}
我们通过向 DefaultInformationControl() 构造函数添加一个 DefaultInformationControl.IInformationPresenter 类型的文本展示器(presenter),从而修改 DefaultInformationControl 实例的创建,如清单 14 所示。
清单 14. 添加文本展示器
return new DefaultInformationControl(parent, presenter);
最后剩下的事情就是实现这个文本展示器,如清单 15 所示。
清单 15. 文本展示器
private static final DefaultInformationControl.IInformationPresenter
presenter = new DefaultInformationControl.IInformationPresenter() {
public String updatePresentation(Display display, String infoText,
TextPresentation presentation, int maxWidth, int maxHeight) {
int start = -1;
// Loop over all characters of information text
for (int i = 0; i < infoText.length(); i++) {
switch (infoText.charAt(i)) {
case '<' :
// Remember start of tag
start = i;
break;
case '>' :
if (start >= 0) {
// We have found a tag and create a new style range
StyleRange range =
new StyleRange(start, i - start + 1, null, null, SWT.BOLD)
// Add this style range to the presentation
presentation.addStyleRange(range);
// Reset tag start indicator
start = -1;
}
break;
}
}
// Return the information text
return infoText;
}
};
处理过程是在 updatePresentation() 方法中完成的。这个方法接受要显示的文本和一个默认的 TextPresentation 实例。我们只需循环遍历该文本的字符,针对从该文本中找到的每个 XML 标签,我们向它的这个文本展示实例添加一个新的样式范围。在这个新的样式范围中,我们保留前景色和背景色不变,但是把字体样式设为粗体。
上下文信息
现在研究一下我们已在 computeStyleProposals() 方法中创建的 ContextInformation 实例。这个上下文信息将在某个建议已被插入文档之后显示出来。它可用于通知用户关于某个自动完成建议已被成功应用的信息。然而,仅只是向 CompletionProposal() 构造函数传递一个 ContextInformation 实例是不足够的。我们还必须完成方法 getContextInformationValidator() 来为这个上下文信息提供验证器。清单 16 显示了这是如何进行的。
清单 16. getContextInformationValidator
public IContextInformationValidator getContextInformationValidator() {
return new ContextInformationValidator(this);
}
这里使用了 ContextInformationValidator 的默认实现。这个验证器将检查要显示的上下文信息是否包含在方法 computeContextInformation() 返回的上下文信息项数组中。如果没有,该信息将不会显示。因此我们还必须完成方法 computeContextInformation(),如清单 17 所示。
清单 17. computeContextInformation
public IContextInformation[] computeContextInformation(ITextViewer viewer,
int documentOffset) {
// Retrieve selected range
Point selectedRange = viewer.getSelectedRange();
if (selectedRange.y > 0) {
// Text is selected. Create a context information array.
ContextInformation[] contextInfos = new ContextInformation[STYLELABELS.length];
// Create one context information item for each style
for (int i = 0; i < STYLELABELS.length; i++)
contextInfos[i] = new ContextInformation(null, STYLELABELS[i]+" Style");
return contextInfos;
}
return new ContextInformation[0];
}
这里我们仅为每个样式标签创建一个 IContextInformation 项。当然,这种解决办法相当简单。更高级的实现应该检查选中文本的周围,并确定哪些具体的样式标签适用于选中的文本。
如果我们不想实现这个方法,还可以选择实现我们自己的 IContextInformationValidator,它总是返回 true。
激活助理
到目前为止,我们已经完成了新内容助理的主要逻辑。但是在测试该插件时,按下 Ctrl + 空格键的时候仍然什么事情也没有发生。当然,它为什么要发生呢?当这个组合键按下时,源代码查看器仍然不知道我们想要一个自动完成建议列表。
在独立的 SWT/JFace 应用程序中,我们会向源代码查看器添加一个验证侦听器(参见清单 18),并检查这个组合键。按下 Ctrl + 空格键将触发内容助理操作,并且会禁止这个组合键的事件,以便源代码查看器不会进一步处理它。
清单 18. VerifyKeyListener
sourceViewer.appendVerifyKeyListener(
new VerifyKeyListener() {
public void verifyKey(VerifyEvent event) {
// Check for Ctrl+Spacebar
if (event.stateMask == SWT.CTRL && event.character == ' ') {
// Check if source viewer is able to perform operation
if (sourceViewer.canDoOperation(SourceViewer.CONTENTASSIST_PROPOSALS))
// Perform operation
sourceViewer.doOperation(SourceViewer.CONTENTASSIST_PROPOSALS);
// Veto this key press to avoid further processing
event.doit = false;
}
}
});
然而在工作台编辑器环境中(我们的 HTML 编辑器插件就是这种情况),我们不需要深入事件处理的细节。相反,我们会创建一个适当的 TextOperationAction 来调用内容助理操作。这是通过扩展类 HTMLEditor 中的方法 createActions() 来完成的。只需确保在包 SampleHTMLEditor 中创建一个文件 SampleHTMLEditorPluginResources.properties 来满足该插件资源包的请求即可!
清单 19. TextOperationAction
private static final String CONTENTASSIST_PROPOSAL_ID =
"com.bdaum.HTMLeditor.ContentAssistProposal";
protected void createActions() {
super.createActions();
// This action will fire a CONTENTASSIST_PROPOSALS operation
// when executed
IAction action =
new TextOperationAction(SampleHTMLEditorPlugin.getDefault().getResourceBundle(),
"ContentAssistProposal", this, SourceViewer.CONTENTASSIST_PROPOSALS);
action.setActionDefinitionId(CONTENTASSIST_PROPOSAL_ID);
// Tell the editor about this new action
setAction(CONTENTASSIST_PROPOSAL_ID, action);
// Tell the editor to execute this action
// when Ctrl+Spacebar is pressed
setActionActivationCode(CONTENTASSIST_PROPOSAL_ID,' ', -1, SWT.CTRL);
}
现在可以再次测试这个插件。这时应该能够按 Ctrl + 空格键来调用内容助理了。您也许想要根据文本是否被选中来尝试内容助理的不同行为。
而且,我们还可以添加更多的代码。例如,当键入 '<'字符时,内容助理可以自动激活它自己。这可以通过向内容助理处理器指定这个自动激活字符来实现(就像每种文档类型能够具有特定的处理器一样,同样可以针对每种文档类型使用不同的自动激活字符)。为此,我们完成了类 HtmlContentAssistProcessor 中的方法 getCompletionProposalAutoActivationCharacters 的定义,如清单 20 所示。
清单 20. getCompletionProposalAutoActivationCharacters
public char[] getCompletionProposalAutoActivationCharacters() {
return new char[] { '<' };
}
此外,我们必须启用自动激活并设置自动激活延迟。这是在类 XMLConfiguration 中完成的。我们向方法 getContentAssistant() 添加如清单 21 所示的行。
清单 21. 向 getContentAssistant 添加行
assistant.enableAutoActivation(true);
assistant.setAutoActivationDelay(500);
最后,我们也许希望改变内容助理弹出窗口的背景色,以把它和附加信息窗口区别开来。因此,我们向方法 getContentAssistant() 添加了额外两行,如清单 22 所示。
清单 22. 向 getContentAssistant 添加行
Color bgColor = colorManager.getColor(new RGB(230,255,230));
assistant.setProposalSelectorBackground(bgColor);
注意,我们使用了 XMLConfiguration 实例的颜色管理器来创建新的颜色。当颜色不再需要时,这样为我们省去了清除它的麻烦,因为颜色管理器会负责颜色的清除。
高级概念
现在,在成功地为我们的 HTML 管理器实现内容助理之后,您或许想知道基于模板的内容助理是如何工作的,以及它是如何实现的。我们从 Java 源代码编辑器中知道,这些内容助理具有一个特别的特性:它们的自动完成建议是可以参数化的。这类建议中的特定名称可由用户修改,结果使得该名称在整个建议中的所有实例被同时更新。
遗憾的是,这个功能是 Eclipse Java 开发工具包(JDT)插件的一部分,因而不管是基于 SWT/JFace 的独立应用程序,还是最简单的 Eclipse 平台,凡是没有这个插件的应用程序,都无法使用这个功能。幸运的是,这个功能的源代码可供使用,并且调整它以适应其他环境并不困难。特别是,org.eclipse.jdt.internal.ui.text.java 包中的类 ExperimentalProposal 以及 org.eclipse.jdt.internal.ui.text.link 包中的类型 ILinkedPositionListener、LinkedPositionUI 和 LinkedPositionManager 实现了这个功能。