前面介绍的内容集中在两点: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.blogjava.net/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标签挂入了自动提示实现之后,还真的提示了哈^_^            
            
            【后记】
              其实本节中的内容为定制自动提示提供了一个完整解决方案的雏形^_^               
           【源码下载】
              源码下载(自动提示定制) 
 本博客中的所有文章、随笔除了标题中含有引用或者转载字样的,其他均为原创。转载请注明出处,谢谢!