前面介紹的內容集中在兩點:StructuredTextEditor框架和WTP數據模型,在本節中就可以定制一個我們最常用的WTP StructuredTextEditor的功能,那就是自動提示。
【WTP StructuredTextEditor提示功能實現分析】
有關Eclipse文本編輯器框架、JFace Text Framework和WTP StructuredTextEditor的簡要知識,參見:
【Eclipse插件開發】基于WTP開發自定義的JSP編輯器(二):基于WTP StructuredTextEditor創建自己的JSPEditor
【SourceViewer提示策略配置】
在章節二中,我們說過如果要對一個ISourceViewer進行自動提示策略的定制,在ISourceViewer對應的SourceViewerConfiguration中配置就可以了。對于WTP JSP StructuredTextEditor而言,這里的ISourceViewer就是StructuredTextViewer,這里的SourceViewerConfiguration就是StructuredTextViewerConfigurationJSP。那我們來看一下WTP StructuredTextViewerConfigurationJSP中對自動提示策略的配置:
(以下代碼摘取子StructuredTextViewerConfigurationJSP類中):
protected IContentAssistProcessor[] getContentAssistProcessors(ISourceViewer sourceViewer, String partitionType) {
IContentAssistProcessor[] processors = null;
//其他代碼省略......
else if ((partitionType == IXMLPartitions.XML_DEFAULT) || (partitionType == IHTMLPartitions.HTML_DEFAULT) || (partitionType == IHTMLPartitions.HTML_COMMENT) || (partitionType == IJSPPartitions.JSP_DEFAULT) || (partitionType == IJSPPartitions.JSP_DIRECTIVE) || (partitionType == IJSPPartitions.JSP_CONTENT_DELIMITER) || (partitionType == IJSPPartitions.JSP_CONTENT_JAVASCRIPT) || (partitionType == IJSPPartitions.JSP_COMMENT)) {
// jsp
processors = new IContentAssistProcessor[]{new JSPContentAssistProcessor()};
}
else if ((partitionType == IXMLPartitions.XML_CDATA) || (partitionType == IJSPPartitions.JSP_CONTENT_JAVA)) {
// jsp java
processors = new IContentAssistProcessor[]{new JSPJavaContentAssistProcessor()};
}
//其他代碼省略......
return processors;
}
以上代碼,我們可以看的出來,IContentAssistProcessor是和具體
分區類型(partition type)相關聯的。想搞懂這個問題,就需要看一下這個具體分區類型(partition type)是怎么計算出來的。
PS:分區類型是JFace Text Framework中的概念,相關的知識大家有興趣可以進一步去了解一下JFace Text Framework。
【分區類型(partition type)】
我們先來看一下JFace Text Framework中的基礎知識吧。再JFace Text Framework中有個分區劃分器的角色(org.eclipse.jface.text.IDocumentPartitioner),這個角色中一個核心操作就是判斷文檔(org.eclipse.jface.text.IDocument)中特定位置所在區域(region)的分區類型是什么,其實這里的分區類型說白了就是在一定程度上反應了該區域(region)的內容是什么語義性質的^_^。
我們接著看一下,WTP提供了什么樣的IDocumentPartitioner呢?
上圖中的org.eclipse.jst.jsp.core.internal.text.StructuredTextPartitionerForJSP就是我們針對jsp文件類型的分區器了,看一下相應的實現代碼:
public String getPartitionType(ITextRegion region, int offset) {
String result = null;
final String region_type = region.getType();
if (region_type == DOMJSPRegionContexts.JSP_CONTENT) {
result = getPartitionTypeForDocumentLanguage();
}
else if (region_type == DOMJSPRegionContexts.JSP_COMMENT_TEXT || region_type == DOMJSPRegionContexts.JSP_COMMENT_OPEN || region_type == DOMJSPRegionContexts.JSP_COMMENT_CLOSE)
result = IJSPPartitions.JSP_COMMENT;
else if (region_type == DOMJSPRegionContexts.JSP_DIRECTIVE_NAME || region_type == DOMJSPRegionContexts.JSP_DIRECTIVE_OPEN || region_type == DOMJSPRegionContexts.JSP_DIRECTIVE_CLOSE)
result = IJSPPartitions.JSP_DIRECTIVE;
else if (region_type == DOMJSPRegionContexts.JSP_CLOSE || region_type == DOMJSPRegionContexts.JSP_SCRIPTLET_OPEN || region_type == DOMJSPRegionContexts.JSP_EXPRESSION_OPEN || region_type == DOMJSPRegionContexts.JSP_DECLARATION_OPEN)
result = IJSPPartitions.JSP_CONTENT_DELIMITER;
else if (region_type == DOMJSPRegionContexts.JSP_ROOT_TAG_NAME)
result = IJSPPartitions.JSP_DEFAULT;
else if (region_type == DOMJSPRegionContexts.JSP_EL_OPEN || region_type == DOMJSPRegionContexts.JSP_EL_CONTENT || region_type == DOMJSPRegionContexts.JSP_EL_CLOSE || region_type == DOMJSPRegionContexts.JSP_EL_DQUOTE || region_type == DOMJSPRegionContexts.JSP_EL_SQUOTE || region_type == DOMJSPRegionContexts.JSP_EL_QUOTED_CONTENT)
result = IJSPPartitions.JSP_DEFAULT_EL;
//其他代碼省略。。。
else {
result = getEmbeddedPartitioner().getPartitionType(region, offset);
}
return result;
}
我們可以看到,
對于WTP結構化文本(當然包括JSP)而言,分區類型(partition type)基本上是根據ITextRegion的type信息確定的。有關ITextRegion的type相關知識,也是我們前面在介紹語法Document(IStructuredDocument)的時候重點內容之一,忘記的話,去看一下。
【自動提示流程】
既然在StructuredTextViewerConfigurationJSP中根據分區類型(partition type)對提示進行了配置,那么是如何來利用這個配置的呢?
我們在source viewer中觸發提示的時候,會有一個相應的offset信息,前面也說過IDocumentPartitioner(WTP 對應于JSP的實現為StructuredTextPartitionerForJSP)提供了根據offset判斷相應區域分區類型(partition type)的接口操作,那提示的流程也就出來了:
上圖中的WTP StructuredTextViewerConfigurationJSP對應于我們自己的jsp編輯器中的類型為jspeditor.configuration.JSPStructuredTextViewerConfiguration,這個我們在前面第二節中就定義了。
【定制WTP StructuredTextEditor的提示功能】
通過上面的自動提示流程的分析,我們可以看的出來,如果想在我們自己的JSP編輯器中定制WTP提供的特定分區類型下的自動提示,只要覆寫WTP StructuredTextViewerConfigurationJSP中的getContentAssistProcessors實現,用我們自定義的IContentAssistProcessor實現和特定的分區類型想綁定就可以了。
【需求】
1、提供針對標簽屬性值提示的定制。
2、提供屬性值提示擴展點,允許用戶以動態掛入的方式提供特定標簽的屬性值提示
【實現摘要】
1、
定義自己的IContentAssistProcessor實現。
public class CustomizedJSPContentAssistantProcessor extends AbstractContentAssistProcessor{
/*
* 定制自動提示策略:提供自定義的屬性值提示。
*
* @see org.eclipse.wst.xml.ui.internal.contentassist.AbstractContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer, int)
*/
public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
if (!this.isOffsetValid(viewer, offset))
return new ICompletionProposal[0];
//利用IModelManager獲取對應的模型
IStructuredDocument structuredDocument = (IStructuredDocument)viewer.getDocument();
IStructuredModel structuredModel = StructuredModelManager.getModelManager().getModelForRead(structuredDocument);
if (structuredModel == null)
return new ICompletionProposal[0];
try {
//如果當前offset不是位于屬性區域
IDOMAttr attrNode = StructuredModelUtil.getAttrAtOffset(structuredModel, offset);
if (attrNode != null)
return this.computeCustomizedCompletionProposals(viewer, offset, attrNode);
else
return new ICompletionProposal[0];
} catch (Exception e) {
//log exception
IStatus status = new Status(IStatus.ERROR, "jspeditor", 100, "自動提示失敗", e);
Activator.getDefault().getLog().log(status);
return new ICompletionProposal[0];
} finally {
//注意,削減引用計數
if (structuredModel != null)
structuredModel.releaseFromRead();
}
}
/**
* 判斷當前位置是否需要啟動我們自定義的JSP標簽屬性值自動提示,標準:
* 1、當前offset對應的區域的分區類型(partition type)為IJSPPartitions.JSP_DIRECTIVE
* 2、當前offset對應的text region的類型為DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE
*
* @param viewer
* @param offset
* @return
*/
private boolean isOffsetValid(ITextViewer viewer, int offset) {
try {
IStructuredDocument structuredDocument = (IStructuredDocument)viewer.getDocument();
//判斷分區類型
if (IJSPPartitions.JSP_DIRECTIVE != structuredDocument.getPartition(offset).getType())
return false;
//判斷葉子text region對應的region type信息
ITextRegion textRegion = StructuredDocumentUtil.getTextRegion(structuredDocument, offset);
return DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE == textRegion.getType();
} catch (Exception e) {
return false;
}
}
/**
* 自定義提示結果:自定義屬性值提示
*
* @param viewer
* @param offset
* @param attrNode
* @return
* @throws Exception
*/
private ICompletionProposal[] computeCustomizedCompletionProposals(ITextViewer viewer, int offset, IDOMAttr attrNode) throws Exception{
//準備上下文數據
String tagName = attrNode.getOwnerElement().getNodeName();
String attrbuteName = attrNode.getName();
String inputText = attrNode.getStructuredDocument().get(attrNode.getValueRegionStartOffset() + 1, offset - attrNode.getValueRegionStartOffset() - 1);
//獲取相應通過擴展點注冊的屬性值提示擴展
IAssistantContributor contributor = AssistantContributorManager.getInstance().getAssistantContributor(tagName);
return contributor.computeProposals(attrbuteName, inputText, viewer, offset);
}
}
關于細節實現暫且不說,后面會附上相應的源碼。目前只需要了解大致的算法流程就可以:1、如果offset位于特定的JSP標簽屬性值范圍內,則去獲取對應的屬性值自定義提示。
2、
將自定義的IContentAssistProcessor配置到自定義的SourceViewerConfiguration中。再配置之前,我們首先看一下如果offset位于一個屬性值的ITextRegion中,那么分區類型(partition type)是怎樣的。回過頭看一下,上面WTP StructuredTextPartitionerForJSP代碼黑體加粗部分,我們的屬性值區域對應的分區類型為org.eclipse.jst.jsp.JSP_DIRECTIVE(
IJSPPartitions.JSP_DIRECTIVE)。所以在我們自定義的JSPStructuredTextViewerConfiguration中覆寫WTP提供的StructuredTextViewerConfigurationJSP中的相應方法:
public class JSPStructuredTextViewerConfiguration extends
StructuredTextViewerConfigurationJSP {
/*
*
*
* @see org.eclipse.jst.jsp.ui.StructuredTextViewerConfigurationJSP#getContentAssistProcessors(org.eclipse.jface.text.source.ISourceViewer, java.lang.String)
*/
protected IContentAssistProcessor[] getContentAssistProcessors(ISourceViewer sourceViewer, String partitionType) {
//我們目前只自定義JSP標簽屬性值自動提示的情況
if (partitionType == IJSPPartitions.JSP_DIRECTIVE) {
return new IContentAssistProcessor[]{new CustomizedJSPContentAssistantProcessor(), new JSPContentAssistProcessor()};
}
return super.getContentAssistProcessors(sourceViewer, partitionType);
}
}
由于我們只想定制JSP標簽的屬性值提示,所以我們將我們自定義的Content Assistant Processor配置為和
IJSPPartitions.JSP_DIRECTIVE分區類型相綁定。
說明:如果你想定制類型的自動提示呢? 前面已經說過了^_^
3、
定義相關抽象接口。
由于針對不同JSP標簽的屬性值自動提示的邏輯是不同的,因為提示的內容相同,但是屬性值提示這一概念是一致的。所以,我們就定義了IAssistantContributor接口來代表這一抽象概念,行為的變化通過對該接口的繼承來封裝。
public interface IAssistantContributor {
/**
* 提供屬性值內容提示
*
* @param attrbuteName 屬性名
* @param inputText 已輸入屬性值
* @param viewer structured text viewer
* @param offset 光標位置
* @return
*/
public ICompletionProposal[] computeProposals(String attributeName, String inputText, ITextViewer viewer, int offset);
}
上面接口一看就知道是用的策略模式的手法,我在博客的前面的文章中說明了在使用策略模式時候的注意點,其中重點之一就是要關注上下文信息。 我們在本接口中提供了屬性名(attributeName)和用戶已經輸入的屬性值(inputText),這兩個上下文信息對于一般的簡單提示情況下已經足夠了。但是,也有可能有比較為復雜的情況,例如要處理嵌套標簽或者標簽之間有依賴等等情況,通過viewer參數可以獲取到對應的IStructuredDocument和IStructuredModel,在加上offset信息,用戶可以利用這兩個信息自己去分析出進一步的上下文信息^_^。
PS:我們在代碼中針對IAssistantContributor接口提供了一個默認適配器類,針對的也就是簡單的提示情況,即只提供屬性名(attributeName)和用戶已經輸入的屬性值(inputText)就可以完成提示的情況了。
public abstract class AbstractAssistantContributor implements
IAssistantContributor {
/*
* 子類可以覆寫,提供較為簡單的模版方法,分為計算替代字符串和構建completion proposals兩步。
*
* @see jspeditor.assist.contributor.IAssistantContributor#computeProposals(java.lang.String, java.lang.String, org.eclipse.jface.text.ITextViewer, int)
*/
public ICompletionProposal[] computeProposals(String attrbuteName, String inputText, ITextViewer viewer, int offset) {
String[] replaceStrings = this.computeReplaceStrings(attrbuteName, inputText, viewer, offset);
return this.buildCompletionProposals(replaceStrings, inputText, viewer, offset);
}
/**
* 子類可以覆寫.
* 說明:如果需要提供自定義的display string、image等信息,請直接覆寫computeProposals方法。
*
* @param attrbuteName
* @param inputText
* @return
*/
protected String[] computeReplaceStrings(String attrbuteName, String inputText, ITextViewer viewer, int offset) {
return new String[0];
}
protected final IDOMAttr getDOMAttr(ITextViewer viewer, int offset) {
IStructuredDocument structuredDocument = (IStructuredDocument)viewer.getDocument();
IStructuredModel structuredModel = StructuredModelManager.getModelManager().getModelForRead(structuredDocument);
IDOMAttr attr = StructuredModelUtil.getAttrAtOffset(structuredModel, offset);
structuredModel.releaseFromRead();
return attr;
}
/**
* 構造ICompletionProposal實例,只提供簡單的ICompletionProposal實例
*
* @param replaceStrings
* @param inputText
* @param viewer
* @param offset
* @return
*/
protected ICompletionProposal[] buildCompletionProposals(String[] replaceStrings, String inputText, ITextViewer viewer, int offset) {
if (replaceStrings == null || replaceStrings.length == 0)
return new ICompletionProposal[0];
//計算ICompletionProposal相關參數
IDOMAttr attrNode = getDOMAttr(viewer, offset);
int replaceOffset = attrNode.getValueRegionStartOffset() + 1;
int replaceLength = attrNode.getValueSource().length();
List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for (int i = 0; i < replaceStrings.length; i++) {
//根據用戶輸入執行過濾
if (replaceStrings[i].toLowerCase().startsWith(inputText.trim().toLowerCase())) {
int cursorPosition = replaceStrings[i].length();
ICompletionProposal proposal = new CompletionProposal(replaceStrings[i], replaceOffset, replaceLength, cursorPosition);
proposals.add(proposal);
}
}
return proposals.toArray(new ICompletionProposal[proposals.size()]);
}
}
可以看的出來,我們這個默認適配器類簡單的應用了模版方法的手法去處理簡單情景下的自動提示。
4、
定義屬性值自動提示擴展點
由于JSP默認提供的一些標簽實際業務意義不強,而我們自己在開發應用的時候,往往會不斷提供有業務意義的標簽供用戶使用,所以我們假設我們要處理的屬性值自動提示需要允許后續開發人員以動態開發的方式掛入,支持方便擴展。我們想到了擴展點機制^_^。
上面的擴展點定義其實很簡單,就是針對特定的JSP tag id(也就是tag名稱,這是唯一的)提供一個 IAssistantContributor接口的實現。
我們針對用戶掛入的擴展提供了一個管理器AssistantContributorManager,能夠支持以tag id獲取對應的IAssistantContributor實現,看一下上面我們定義的content assistant processor實現中黑體部分代碼就利用了該manager實例。具體請在附件源碼中看吧^_^
5、
示例
我們首先提供一個非常簡單的tld(test.tld),里面包含了一個簡單的測試標簽test,如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN" "http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd">
<taglib>
<tlibversion>1.0</tlibversion>
<jspversion>1.0</jspversion>
<shortname>test</shortname>
<uri>http://www.tkk7.com/zhuxing/tags/test</uri>
<tag>
<name>test</name>
<tagclass>any</tagclass>
<bodycontent>empty</bodycontent>
<attribute>
<name>scope</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
</taglib>
我們在其中定義了一個scope屬性,下面我們就以擴展點的方式掛入我們的擴展。
首先實現提供一個IAssistantContributor接口實現:
public class TestTagAssistantContributor extends AbstractAssistantContributor {
/**
* scope attribute name
*/
private static final String ATTR_NAME_SCOPE = "scope";
/**
* scope attribute value
*/
private static final String[] ATTR_VALUE_SCOPE = {"request", "session"};
/* (non-Javadoc)
* @see jspeditor.assist.contributor.AbstractAssistantContributor#computeReplaceStrings(java.lang.String, java.lang.String, org.eclipse.jface.text.ITextViewer, int)
*/
protected String[] computeReplaceStrings(String attrbuteName, String inputText, ITextViewer viewer, int offset) {
if (ATTR_NAME_SCOPE.equals(attrbuteName)) {
return ATTR_VALUE_SCOPE;
}
return new String[0];
}
}
通過上面簡單的代碼可以看的出來,我們的提示邏輯很簡單,如果是scope屬性,就提供“requst”和“session”兩種選擇。
下面,我們將我們提供的針對test標簽的屬性值提示擴展掛入:
6、效果演示

看到了嗎,我們為test標簽掛入了自動提示實現之后,還真的提示了哈^_^
【后記】
其實本節中的內容為定制自動提示提供了一個完整解決方案的雛形^_^
【源碼下載】
源碼下載(自動提示定制)
本博客中的所有文章、隨筆除了標題中含有引用或者轉載字樣的,其他均為原創。轉載請注明出處,謝謝!