|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Boost end-user convenience and productivity with intelligent
context-sensitive content completion proposals
Berthold
Daum Consultant, bdaum industrial communications 19
November 2003
For users of the Eclipse Java editor,
content assistants are a well-known feature. You press Ctrl + spacebar,
and a window with a set of completion proposals pops up. Selection of a
specific proposal opens another window showing a preview of the
insertion of the selected proposal. Committing a proposal with the Enter
key or a double-click inserts the proposal into the current document.
This article shows how you can easily add this feature to any SWT-based
application, either a stand-alone application or a plug-in to the
Eclipse workbench.
Creating an HTML
editor The concept of content assistants is related to a
specific implementation of JFace text viewers, the class
org.eclipse.jface.text.source.SourceViewer . Instances of this
class are used throughout the Eclipse workbench to implement the various
editors. However, SourceViewers are not restricted to the
Eclipse workbench but may be used in any application that builds upon the
SWT and JFace JARs. This article demonstrates the implementation of
content assistants in the context of an Eclipse editor plug-in, and gives
some hints on how to use content assistants with "naked"
SourceViewers .
Let's implement a simple HTML editor. Here a content assistant can be
very helpful. For example, a content assistant could generate typical HTML
structures such as tables or links, or could wrap selected text areas into
style tags.
To save time, we will implement this editor by using one of the New Plug-in
Project wizards to generate an appropriate editor plug-in. Because
this generated editor is an XML editor, and HTML is an XML-based mark-up
language, we only need to make some minor modifications to convert this
generated editor into an HTML editor. Let's begin.
After invoking the New wizard,
select Plug-in Development and Plug-in Project. On the following screen,
enter the project name "Sample HTML Editor". In the next screen, define a
suitable plug-in ID, such as "com.bdaum.SampleHTMLEditor". The following
screen allows you to select an appropriate code generation wizard. Choose
Plug-in with an
editor, as shown in Figure 1.
Figure 1. Plug-in with an editor
On the next screen, modify the proposed plug-in name (if desired) and
plug-in class name and specify a provider name. Leave everything as
is.
Continue to the next screen, and modify the proposed Editor Class
Name to "HTMLEditor", the Editor Name to
"Sample HTML Editor", and the File Extension
to "html, htm", as shown in Figure 2. This latter entry will associate the
new editor with all files with a file extension of .html or .htm.
Figure 2. Editor options
Click the Finish button to
generate the new editor. Now launch a new workbench via Run > Run as ...
> Run-time workbench. After creating a new file with a .htm or
.html file extension (or importing such a file), open it with the new
editor.
Adding a content
assistant As you'll soon find, this editor does not feature
a content assistant; pressing Ctrl + spacebar has no effect.
SourceViewers by default are not equipped with content
assistants. We need to configure the SourceViewer used in our
HTML editor appropriately.
The configuration of the HTML editor's SourceViewer is
represented by the generated class XMLConfiguration , a
subclass of SourceViewerConfiguration (if you wish, you can
rename this class to HTMLConfiguration , but this is not
essential). To add a content assistant to the source viewer, we need to
override the SourceViewerConfiguration method
getContentAssistant() . This is best done with the Java
editor's context function Source >
Override/Implement Methods..., which will create a stub for this
method. We now need to implement this method and return an appropriate
instance of type IContentAssistant .
Content assistants consist of one or more content processors, one for
each content type that we want to support. Documents processed by source
viewers can be segmented into several partitions of different content
type. Such partitions are determined by partition scanners and, in fact,
we find a class XMLPartitionScanner in the package
com.bdaum.HTMLEditor.editors . This class defines three
different content types for our document type: XML_DEFAULT ,
XML_COMMENT , and XML_TAG . In addition, documents
may contain partitions of type
IDocument.DEFAULT_CONTENT_TYPE .
In the new method getContentAssistant() , we first create a
new instance of the default implementation of
IContentAssistant and equip it with one and the same content
assist processor for the content types XML_DEFAULT ,
XML_TAG , and IDocument.DEFAULT_CONTENT_TYPE .
Since we do not plan to offer assistance within HTML comments, we do not
create a content assist processor for content type
XML_COMMENT . Listing 1 shows the code. Listing 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;
}
|
Implementing a content assist
processor The class HtmlContentAssistProcessor
does not yet exist. Create it by clicking the yellow QuickFix bulb.
In this new class, we only need to complete the pre-generated methods that
were inherited from interface IContentAssistProcessor . The
method that interests us most at the moment is
computeCompletionProposals() . This method returns an array of
CompletionProposal instances , one for each proposal we have
to offer. For example, we could offer a collection of all HTML tags for
selection. However, we want it a bit more sophisticated. When a text range
is selected in the editor, we want to offer a collection of style tags
with which this text can be wrapped. Otherwise, we offer tags for creating
new HTML structures. Figures 3 and 4 show what we want to achieve.
Figure 3. structProposal
Figure 4. styleProposal
So, first retrieve the current selection from the editor's
SourceViewer instance (see Listing 2). Listing 2. computeCompletionProposals
public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer,
int documentOffset) {
// Retrieve current document
IDocument doc = viewer.getDocument();
// Retrieve current selection range
Point selectedRange = viewer.getSelectedRange();
|
Then create an ArrayList instance for collecting the
generated ICompletionProposal instance, as shown in Listing
3. Listing 3. computeCompletionProposals
(continued)
List propList = new ArrayList();
|
If a text range was selected, retrieve the selected text and compute
the proposals for style tags, as shown in Listing 4. Listing 4. computeCompletionProposals
(continued)
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 {
|
Otherwise, try to retrieve a qualifier from the document, as shown in
Listing 5. Such a qualifier consists of all characters of a partially
entered HTML tag and is used to restrict the set of possible
proposals. Listing 5. computeCompletionProposals
(continued)
// Retrieve qualifier
String qualifier = getQualifier(doc, documentOffset);
// Compute completion proposals
computeStructureProposals(qualifier, documentOffset, propList);
}
|
Finally, convert the list of completion proposals to an array and
return this array as the result, as shown in Listing 6. Listing 6. computeCompletionProposals
(continued)
// Create completion proposal array
ICompletionProposal[] proposals = new ICompletionProposal[propList.size()];
// and fill with list elements
propList.toArray(proposals);
// Return the proposals
return proposals;
}
|
Constructing a
qualifier Now, let's see how we can retrieve a qualifier
from the current document. We need to implement the method
getQualifier() , as shown in Listing 7. Listing 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 "";
}
}
}
|
This is pretty simple. Starting at the current document offset, we read
document characters backwards. When we detect an open bracket, we have
found the start of a tag and return the collected characters after we have
reversed their sequence. In all other cases where we cannot find the
beginning of a tag, we return the empty string. In this case, the set of
proposals is not restricted.
Compiling completion
proposals Now, let's compile a collection of proposals.
Listing 8 shows the set of relevant tags constituting these proposals. If
you like, you may add some more. Listing 8.
Collection of proposals
// 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>" }
|
As you can see, we have separated each tag proposal into two parts: in
a part before the planned cursor position and in a part after the planned
cursor position. Listing 9 shows the method
computeStructureProposals() that compiles these
proposals. Listing 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);
}
}
}
|
We loop through the tag arrays and select all tags that start with the
specified qualifier. For each selected tag, we create a new
CompletionProposal instance. For parameters, we pass the
complete tag text, the position where this text should be inserted, the
length of the text in the document that should be replaced (in other
words, the qualifier length), and the planned cursor position relative to
the start of the inserted text.
This method will supply us with WYSIWYG ("what you see is what you
get") completion proposals. The pop-up window of the content assistant
will list the proposals in exactly the same form as they are inserted into
the document when a proposal is selected.
Dealing with complex
proposals The previous approach is not suitable for the
method computeStyleProposals() that we still have to
implement. Here, we need to wrap the selected text into the selected style
tags and replace the selected text in the document with this new string.
As such a replacement can be of any length, it would not make sense to
show it in the content assistants proposal selection window. Instead, it
would be better to display a short but meaningful label, and to display a
preview window containing the full replacement text, as soon as a specific
style proposal is selected. We can achieve such behavior by using an
extended form of the CompletionProposal() constructor.
Listing 10 shows the style tags that we want to support and the
associated labels. Again, you may want to add some more. Listing 10. Collection of style tags
private final static String[] STYLETAGS = new String[] {
"b", "i", "code", "strong"
};
private final static String[] STYLELABELS = new String[] {
"bold", "italic", "code", "strong"
};
|
Listing 11 shows the method computeStyleProposals() . Listing 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);
}
}
|
For each supported style tag, we construct a replacement string and
create a new completion proposal. Of course, this solution is rather
simplistic. A proper implementation would take a closer look at the
replacement string. If this string contained tags, we would segment the
string accordingly and enclose each single segment separately between the
new style tags.
Displaying extra
information The first four parameters in the
CompletionProposal() constructor have the same meaning as in
the method computeStructureProposals() (replacement string,
insertion point, length of replaced text, and cursor position relative to
the insertion point). The fifth parameter -- to which we pass
null in this example -- accepts an image instance. This image
would be shown on the left side of the respective entry in the pop-up
window. The sixth parameter accepts the display label that is shown in the
proposal selection window. Parameter seven is used for
IContextInformation instances, which we will discuss shortly.
Finally, parameter eight accepts the text that should be shown in an
additional information window when a proposal is selected. However, just
supplying a value for this parameter is not sufficient to actually obtain
such an information window. We must configure the content assistant
accordingly. This is, again, done in the class
XMLConfiguration . We simply add the line shown in Listing 12
to the method getContentAssistant() . Listing 12. Add to getContentAssistant
assistant.setInformationControlCreator(getInformationControlCreator(sourceViewer));
|
What happens here? First, we obtain an instance of type
IInformationControlCreator from the current source viewer
configuration. This instance is a factory responsible for creating
instances of class DefaultInformationControl , which will be
responsible for managing the information window. We then tell the content
assistant about this factory. The content assistant will eventually use
this factory to create a new information control instance when a
completion proposal is selected.
Formatting information
texts By default, this information control instance will
present the additional information text as plain text. However, it is
possible to add some fancy text presentation. For example, we may wish to
print all tags in bold. To do this, we need to configure the
DefaultInformationControl instance created by the
IInformationControlCreator accordingly. The only way to do
this is to use a different IInformationControlCreator , and
this can be done by overriding the XMLConfiguration method
getInformationControlCreator() .
The standard implementation of this method in class
SourceViewerConfiguration is shown in Listing 13. Listing 13. getInformationControlCreator
public IInformationControlCreator getInformationControlCreator
(ISourceViewer sourceViewer) {
return new IInformationControlCreator() {
public IInformationControl createInformationControl(Shell parent) {
return new DefaultInformationControl(parent);
}
};
}
|
We modify the creation of a DefaultInformationControl
instance by adding a text presenter of type
DefaultInformationControl.IInformationPresenter to the
DefaultInformationControl() constructor, as shown in Listing
14. Listing 14. Add text presenter
return new DefaultInformationControl(parent, presenter);
|
What remains to do is to implement this text presenter, as shown in
Listing 15. Listing 15. Text presenter
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;
}
};
|
The processing is done in method updatePresentation() .
This method receives the text to be presented and a default
TextPresentation instance. We just loop over the characters
of the text and add a new style range to this text presentation instance
for each XML tag found in the text. In these new style ranges, we leave
the foreground and background colors unchanged but set the font style to
bold.
Context
information Now let's look at the
ContextInformation instance that we had created in method
computeStyleProposals() . This context information is shown
after a proposal has been inserted into the document. It can be used to
inform the user about the successful application of a completion proposal.
However, it is not sufficient to just pass a
ContextInformation instance to the
CompletionProposal() constructor. We must also provide a
validator for this context information by completing the method
getContextInformationValidator() . Listing 16 shows how it's
done. Listing 16.
getContextInformationValidator
public IContextInformationValidator getContextInformationValidator() {
return new ContextInformationValidator(this);
}
|
Here, we have used the default implementation
ContextInformationValidator . This validator will check if the
context information to be shown is contained in the array of context
information items returned by method
computeContextInformation() . If not, the information will not
be shown. We must therefore also complete the method
computeContextInformation() , as shown in Listing 17. Listing 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];
}
|
Here, we just create an IContextInformation item for each
style tag. Of course, this solution is rather simplistic. More advanced
implementations would look at the surroundings of the selected text and
determine which style tags actually apply to the selected text.
If we don't want to implement this method, we still have the option to
implement our own IContextInformationValidator that always
returns true.
Activating the
assistant By now, we have completed the main logic of our
new content assistant. But when we test the plug-in we will find that
still nothing happens when we press Ctrl + spacebar. Of course, why should
it? The source viewer still does not know that we want a list of
completion proposals when this key combination is pressed.
In a stand-alone SWT/JFace application, we would add a verify listener
(see Listing 18) to the source viewer and check for this key combination.
Pressing Ctrl + spacebar would trigger the content assist operation and
would veto the key event so that it is not processed any further by the
source viewer. Listing 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;
}
}
});
|
Within a workbench editor setting, however -- as is the case with our
HTML editor plug-in -- we don't need to dig into the details of event
processing. Here we would rather create an appropriate
TextOperationAction (see Listing 19) that invokes the content
assist operation. We do this by extending the method
createActions() in class HTMLEditor . Just make
sure to create a file
SampleHTMLEditorPluginResources.properties in package
SampleHTMLEditor to satisfy the request for the plug-in
resource bundle! Listing 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);
}
|
Now we can test our plug-in again. We should now be able to invoke the
content assistant by pressing Ctrl + spacebar. You may want to try the
different behavior of the assistant depending on whether text has been
selected or not.
Still, we could add a bit more code. For example, the content assistant
could auto-activate itself when a '<' character is typed. This can be
done by specifying this auto-activation character to the content assistant
processor (as we can have specific processors for each document content
type, we could also use different auto-activation characters for each
content type). We do this by completing the definition of method
getCompletionProposalAutoActivationCharacters in class
HtmlContentAssistProcessor , as shown in Listing 20. Listing 20.
getCompletionProposalAutoActivationCharacters
public char[] getCompletionProposalAutoActivationCharacters() {
return new char[] { '<' };
}
|
In addition, we have to enable auto-activation and to set an
auto-activation delay. This is done in class
XMLConfiguration . We add the following lines shown in Listing
21 to method getContentAssistant() . Listing 21. Add to getContentAssistant
assistant.enableAutoActivation(true);
assistant.setAutoActivationDelay(500);
|
Finally, we may wish to change the background color of the content
assistant pop-up window to differentiate it from the additional
information window. So, we add two more lines to method
getContentAssistant() , as shown in Listing 22. Listing 22. Add to getContentAssistant
Color bgColor = colorManager.getColor(new RGB(230,255,230));
assistant.setProposalSelectorBackground(bgColor);
|
Note, that we use the color manager of the
XMLConfiguration instance to create a new color. This saves
us from having to dispose of the color when it is no longer needed, as the
color manager will care about disposal.
Advanced
concepts Now, after we have successfully implemented a
content assistant for our HTML manager, you probably want to know how a
template-based content assistant works and how it can be implemented.
These content assistants -- as we know them from the Java source editor --
have one special feature: their completion proposals can be parameterized.
Specific names within such a proposal can be modified by the user with the
effect that all occurrences of that name are synchronously updated
throughout the whole proposal.
The bad news is that this functionality is part of the Eclipse Java
Development Toolkit (JDT) plug-in, and thus is not available in
applications where this plug-in is absent -- either stand-alone
SWT/JFace-based applications, or minimal Eclipse platforms. The good news
is that the source code of this functionality is available and is not too
difficult to adapt to other environments. In particular, the classes
ExperimentalProposal from package
org.eclipse.jdt.internal.ui.text.java and the types
ILinkedPositionListener , LinkedPositionUI , and
LinkedPositionManager from package
org.eclipse.jdt.internal.ui.text.link implement this
functionality.
Resources
|
|