Die Eclipse 3.0-Plattform stellt die grundlegende Anwendungsumgebung zur Verfügung. Dazu zählen die Verwaltung von Editoren, eine Gliederungsansicht, die Einbettung in ein Versionierungssystem und die Verwaltung der Benutzereinstellungen. Sprachunabhängige Funktionen wie die Unterstützung für Encodings, QuickDiff (Änderungsanzeige) und die grundlegenden Editieroperationen werden von der Texteditor-Infrastruktur bereitgestellt. Der Eclipse-Plug-in-Mechanismus erlaubt es schließlich, einen Editor modular zu gestalten und wiederum für Erweiterungen durch Dritte zu öffnen.
Dieser Artikel zeigt an einem einfachen Beispiel - einem Rezepteditor - wie ein Quelltexteditor programmiert und in die Eclipse-Umgebung eingefügt wird. Der Rezepteditor verfügt über die folgenden Schlüsselfunktionen: Syntaxhervorhebung, Modellabgleich, Inhaltsassistent, Faltung und Schablonen. Wir gehen davon aus, dass der Leser mit dem Plug-in-Mechanismus von Eclipse vertraut ist. Wer noch nie selbst ein Plug-in geschrieben hat, dem empfehlen wir [4]. Die abgedruckten Auszüge aus dem Quelltext sollen die erläuterten Konzepte illustrieren. Daher haben wir Fehler- und Nullpointer-Überprüfung sowie weniger zentrale Quelltextabschnitte ausgelassen. Um den Unterschied zu Rahmenwerkklassen zu verdeutlichen, beginnen alle Beispielklassen mit Recipe*. Der vollständige Quelltext findet sich auf der beigelegten DVD.
Ein Rezepteditor
Der Aufbau von Kochrezepten ist bewusst einfach gehalten: Sie bestehen aus zwei Abschnitten, die je eine Liste von Zutaten oder Zubereitungsschritten enthalten (Abb. 1).
Ein Rezepteditor-Plug-in
Um den Rezepteditor zu Eclipse hinzuzufügen, erzeugen wir ein leeres Plug-in-Projekt. Dessen Manifest (plugin.xml) deklariert die Erweiterungen (Extensions), die wir bei den vorhandenen Erweiterungspunkten (Extension Points) registrieren. Als Erstes wollen wir einen neuen Editor hinzufügen, was über den org.eclipse.ui.editors-Erweiterungspunkt geschieht. Wir registrieren den Editor auf Dateien mit der Endung rec. Abpictureung 2 zeigt die im Plug-in-Manifest-Editor deklarierte Editorerweiterung.
Die Deklaration spezifiziert zwei Klassen, die wir implementieren müssen: die Editorklasse selbst und die Contributor-Klasse. Auch wenn gleichzeitig mehrere Rezepteditoren geöffnet sind, wird nur ein Objekt der Contributor-Klasse erzeugt. Sie hat zur Aufgabe, Ereignisse von der Benutzerschnittstelle (Menü, Symbolleiste, Tastaturkürzel) an den aktiven Editor weiterzuleiten. Eine erste Implementierung leiten wir von der Contributor-Klasse des Texteditors ab:
public class RecipeEditorContributor extends BasicTextEditorActionContributor {}
Für die Implementierung des Editors haben wir die Wahl, den RecipeEditor entweder von AbstractTextEditor oder von AbstractDecoratedTextEditor abzuleiten, wobei Letzterer die folgenden Erweiterungen hinzufügt:
- Zeilennummern
- Übersichtsleiste
- Änderungsanzeige
- Druckrand
- Markierung der aktuellen Zeile
Für unser Beispiel wollen wir diese Erweiterungen verwenden und wählen deshalb AbstractDecoratedTextEditor. Für schlankere Anwendungen oder für eine reine Rich-Client-Applikation [2] verwendet man den AbstractTextEditor. Im Plug-in-Manifest müssen wir damit Abhängigkeiten auf die folgenden Plug-ins eintragen:
- org.eclipse.core.runtime
- org.eclipse.jface.text
- org.eclipse.ui
- org.eclipse.ui.views
- org.eclipse.ui.editors
- org.eclipse.ui.workbench.texteditor
Damit haben wir bereits einen funktionsfähigen Texteditor. Im Folgenden zeigen wir, wie der Editor schrittweise erweitert wird, um dem Benutzer einen vollständigen Rezepteditor zu bieten.
SourceViewerConfiguration
Einige Aspekte eines konkreten Texteditors werden im Eclipse-Rahmenwerk mithilfe einer SourceViewerConfiguration konfiguriert. Dazu gehören:- Syntaxhervorhebung: das Anzeigen von Textstücken in verschiedenen Farben und Schriftschnitten abhängig von ihrem Inhalt und ihrer Position im Text.
- Modellabgleich: das automatische Aktualisieren eines Dokumentmodells, nachdem der Quelltext im Editor verändert wurde.
- Inhaltsassistent: Komplettierungsvorschläge bei unvollständiger Eingabe.
- Hovers: das Anzeigen von Informationen über das Element unter dem Mauszeiger.
Die SourceViewerConfiguration muss im Editor registriert werden, bevor dessen createPartControl(..) aufgerufen wird. Am besten geschieht dies im Konstruktor:
public class RecipeEditor extends AbstractDecoratedTextEditor {public RecipeEditor() {setSourceViewerConfiguration(new RecipeSourceViewerConfiguration(this, getSharedColors()));}}
In der Klasse RecipeSourceViewerConfiguration überschreiben wir die jeweils erforderlichen get*(..)-Methoden. Der Editor verwendet dann unsere Implementierungen an Stelle der Standardimplementierungen.
Syntaxhervorhebung
Ein moderner Texteditor gestaltet einen Text attraktiver, indem er verschiedene Teile des Textes abhängig von ihrem Inhalt und ihrer Position in unterschiedlichen Farben oder Schriftstilen (fett oder kursiv) darstellt. Diese Syntaxhervorhebung verbessert die Lesbarkeit von strukturierten Dokumenten und kann dem Benutzer helfen, Syntaxfehler schneller zu erkennen.
Damit der Editor weiß, wie er das Dokument darstellen soll, müssen wir in der RecipeSourceViewerConfiguration die Methode getPresentationReconciler(..) überschreiben. Der zurückgegebene IPresentationReconciler besteht aus einem Damager und einem Repairer, die im Zusammenspiel dafür sorgen, dass bei einer Änderung im Dokument jeweils die richtigen Stellen neu koloriert werden. Der Damager stellt fest, welche Textregion nach einer Änderung aufgefrischt werden muss. Der Repairer ist für die Kolorierung zuständig. Wenn wir die Klasse DefaultDamagerRepairer verwenden, müssen wir praktisch nichts implementieren (Abb. 3).
public IPresentationReconciler getPresentationReconciler(ISourceViewer sourceViewer) {PresentationReconciler reconciler= new PresentationReconciler();DefaultDamagerRepairer dr= new DefaultDamagerRepairer(getRecipeScanner());reconciler.setDamager(dr, IDocument.DEFAULT_CONTENT_TYPE);reconciler.setRepairer(dr, IDocument.DEFAULT_CONTENT_TYPE);return reconciler;}
Der DefaultDamagerRepairer wird mit einem ITokenScanner konfiguriert, welcher den Text in Fragmente gleicher Art unterteilt und deren Darstellung über ein TextAttribute im Token beschreibt. Der RuleBasedScanner aus dem Texteditor-Rahmenwerk löst diese Aufgabe, indem er das Dokument unter Verwendung sprachspezifischer Regeln durchsucht.
Für den Rezepteditor benötigen wir lediglich vier Regeln. Eine WordRule erkennt zum Beispiel eine Menge von gegebenen Worten, und eine SingleLineRule erkennt Fragmente auf einer Zeile, die mit den gegebenen Zeichenfolgen beginnen oder enden (Listing 1).
Listing 1
private ITokenScanner getRecipeScanner() {RuleBasedScanner scanner= new RuleBasedScanner();IRule[] rules= new IRule[4];rules[0]= createSectionTitleRule();rules[1]= createQuantityRule();rules[2]= createLeadingDashRule();rules[3]= createStepRule();scanner.setRules(rules);return scanner;}...private IRule createStepRule() {IToken stepToken= new Token(new TextAttribute(fColors.getColor(new RGB(200, 100, 100)), null, SWT.BOLD));SingleLineRule stepRule= new SingleLineRule("(", ")", stepToken);stepRule.setColumnConstraint(0);return stepRule;}
Rezeptmodell
Für viele Aufgaben ist es hilfreich, wenn man auf ein Modell des Quelltextes zugreifen kann. So erstellt zum Beispiel der Java Compiler aus einer *.java-Datei ein Modell, das unter anderem aus Typen und Methoden besteht. Für das Rezeptmodell definieren wir eine Basisklasse RecipeElement, von der alle anderen Klassen des Modells erben. Das RecipeElement kennt den ihm entsprechenden Textbereich sowie seine Unterelemente.Ein Parser erstellt das Modell aus dem aktuellen Dokument und liefert ein Objekt der Klasse Recipe. Dieses enhält je eine IngredientsSection (Abschnitt Zutaten) und eine PreparationSection (Abschnitt Zubereitung). Die IngredientsSection enthält eine Liste von Ingredients und die PreparationSection eine Liste von Steps. Als zentraler Zugriffspunkt für das Modell bietet sich die Editorklasse RecipeEditor an (Abb. 4).
Modellabgleich
Während des Editierens eines Dokuments verändert sich das dazugehörige Modell ständig. Daher muss beispielsweise die Gliederungsansicht immer wieder mit einem neuen Modell abgeglichen werden. Da das Parsen des Dokuments und das Erstellen eines Modells im Allgemeinen zu viel Zeit benötigt, als dass dies bei jeder Dokumentänderung geschehen könnte, bietet das Textrahmenwerk die so genannte Reconciling-Infrastruktur an. Diese erlaubt es, zeitaufwendige Operationen wie das Parsen des Dokuments und die entsprechenden Aktualisierungen in den modellabhängigen Teilen des Editors in einem Hintergrund-Thread durchzuführen. Damit dies nicht bei jedem Tastendruck geschieht, läuft der Hintergrund-Thread erst, wenn für eine gewisse Zeit keine Dokumentänderungen vorgenommen wurden.Um von der Infrastruktur für den Modellabgleich Gebrauch zu machen, überschreiben wir in der RecipeSourceViewerConfiguration zunächst die Methode getReconciler(..) und erzeugen dort einen Reconciler mit unserer eigenen ReconcilingStrategy. Der hier benutzte MonoReconciler übernimmt das Management des Hintergrund-Threads und unterstützt genau eine ReconcilingStrategy für das ganze Dokument. Wir setzen einen ProgressMonitor, mithilfe dessen die Strategie ihren Fortschritt melden und unterbrochen werden kann, und eine Wartezeit, nach deren Ablauf der Modellabgleich beginnt (Abb. 5).
public IReconciler getReconciler(ISourceViewer sourceViewer) {RecipeReconcilingStrategy strategy= new RecipeReconcilingStrategy(fEditor);MonoReconciler reconciler= new MonoReconciler(strategy, false);reconciler.setProgressMonitor(new NullProgressMonitor());reconciler.setDelay(500);return reconciler;}
Die RecipeReconcilingStrategy implementiert die Interfaces IReconcilingStrategy und IReconcilingStrategyExtension. Der Reconciler übergibt zunächst das Dokument und den ProgressMonitor einer ReconcilingStrategy und ruft während des Modellabgleichs die reconcile(..)-Methode auf. Implementiert eine ReconcilingStrategy zusätzlich das Interface IReconcilingStrategyExtension wird vor der ersten Dokumentänderung die Methode initialReconcile() aufgerufen. Dies ermöglicht es der Strategie, sich zu initialisieren. Sowohl reconcile(..) als auch initialReconcile() rufen die private Methode reconcile() auf.
private void reconcile() {final Recipe recipe= fParser.parse(fDocument);Shell shell= fEditor.getSite().getShell();shell.getDisplay().asyncExec(new Runnable() {public void run() {fEditor.setRecipe(recipe);}});}
Beim Aktualisieren des Modells gilt es zu beachten, dass der Modellabgleich in einem eigenen Hintergrund-Thread abläuft und eine geeignete Synchronisation mit dem SWT Thread benötigt wird. So lassen wir bei jedem Abgleich den Parser ein neues Modell des Dokuments erstellen, aktualisieren aber den Editor erst in einem Runnable, welches im SWT Thread zur Ausführung kommt. Auf vollständige Synchronisation wurde in unserem Beispiel der Einfachheit halber verzichtet. Der Java-Editor von Eclipse benutzt beispielsweise eine synchronisierte Variante der Dokumentklasse.
Gliederung
Damit der Benutzer in einem längeren Rezept nicht den Überblick verliert, wollen wir ihm eine Gliederung (Outline) anbieten. Das Eclipse-Rahmenwerk bietet bereits eine Gliederungsansicht an, welche immer die zum aktiven Editor gehörende Gliederung anzeigt. Die Verbindung zwischen einem Editor und der Gliederungsansicht wird über einen Adapter im Editor hergestellt. Wenn ein neuer Editor geöffnet wird, fragt die Gliederungsansicht mit editor.getAdapter(IContentOutlinePage.class) nach einer Gliederungsseite und zeigt diese an.In RecipeEditor fügen wir einen Adapter auf IContentOutlinePage.class hinzu, und sorgen in setRecipe(..) dafür, dass eine Aktualisierung des Modells an die Gliederungsseite weitergeleitet wird (Listings 2 & 3).
Listing 2
private RecipeOutlinePage fOutlinePage;private Recipe fRecipe;...public Object getAdapter(Class required) {if (IContentOutlinePage.class.equals(required)) {if (fOutlinePage == null)fOutlinePage= new RecipeOutlinePage(this);return fOutlinePage;}}...public void setRecipe(Recipe recipe) {fRecipe= recipe;if (fOutlinePage != null)fOutlinePage.setRecipe(recipe);}
Listing 3
public class RecipeOutlinePage extends ContentOutlinePage {private RecipeEditor fEditor;public RecipeOutlinePage(RecipeEditor editor) {fEditor= editor;}public void createControl(Composite parent) {super.createControl(parent);TreeViewer treeViewer= getTreeViewer();treeViewer.setLabelProvider(new RecipeLabelProvider());treeViewer.setContentProvider(new RecipeContentProvider());treeViewer.setAutoExpandLevel(AbstractTreeViewer.ALL_LEVELS);setRecipe(fEditor.getRecipe());}public void setRecipe(Recipe recipe) {getTreeViewer().setInput(recipe);}...}
Die Implementierungen von RecipeLabelProvider und RecipeContentProvider delegieren im Wesentlichen an Methoden der Modellklasse RecipeElement. Mehr Informationen über den verwendeten TreeViewer finden sich zum Beispiel in [1].
Inhaltsassistent
Es ist praktisch, wenn ein Quelltexteditor kontextabhängig Komplettierungsvorschläge anzeigen kann. Eclipse-Benutzer rufen den Inhaltsassistenten mit Ctrl + Space auf. Sie ersparen sich damit einiges an Tipparbeit und vermeiden auch Tippfehler.Wir wollen im Rezepteditor die aufgelisteten Zutaten als Komplettierungsvorschläge anbieten. Diese Funktion wird wie schon die Syntaxhervorhebung in der RecipeSourceViewerConfiguration konfiguriert.
public IContentAssistant getContentAssistant(ISourceViewer sourceViewer) {ContentAssistant assistant= new ContentAssistant();IContentAssistProcessor processor= new RecipeCompletionProcessor(fEditor);assistant.setContentAssistProcessor(processor, IDocument.DEFAULT_CONTENT_TYPE);return assistant;}
Dem ContentAssistant wird ein RecipeCompletionProcessor mitgegeben, welcher die Vorschläge auf Anfrage berechnet. Aus dem aktuellen Modell des RecipeEditor liest die Funktion computeCompletionProposals(..) die vorhandenen Zutaten und erstellt mögliche Komplettierungen (Listing 4).
Listing 4
public class RecipeCompletionProcessor implements IContentAssistProcessor {private final RecipeEditor fEditor;public RecipeCompletionProcessor(RecipeEditor editor) {fEditor= editor;}public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {Recipe recipe= fEditor.getRecipe();Ingredient[] ingredients= recipe.getIngredients();String prefix= getPrefix(viewer.getDocument(), offset);ArrayList proposals= new ArrayList();for (int i= 0; i < ingredients.length; i++) {String ingredient= ingredients[i].getName();if (ingredient.startsWith(prefix))proposals.add(new CompletionProposal(ingredient, offset - prefix.length(), prefix.length(), ingredient.length()));}return (ICompletionProposal[]) proposals.toArray(new ICompletionProposal[proposals.size()]);}// alle anderen Methoden geben null zurück}
Damit der Benutzer die Komplettierungsvorschläge per Tastendruck anzeigen kann, muss in RecipeEditor#createActions() noch eine TextOperationAction erzeugt werden. In RecipeEditorContributor wird die Aktion dem Inhaltsassistenten zugeordnet.
Annotationen
Wir haben gesehen, wie ein textbasierter Editor erstellt und wie das auf dessen Textinhalt basierende Modell im Hintergrund aktualisiert werden. Syntax Coloring dient dazu, den Text zu strukturieren, und die Gliederung ermöglicht es dem Benutzer, sich schnell einen Überblick über ein Dokument zu verschaffen. Das im Hintergrund aktualisierte Modell ermöglicht aber noch mehr. Es erlaubt beispielsweise, Fehler im Quelltext zu erkennen. Zudem möchten wir je nach Eingabeposition dem Benutzer zusätzliche Informationen anzeigen können bzw. Aktionen anbieten. Dies wird durch so genannte Annotationen ermöglicht.Der in einem Editor dargestellte Inhalt besteht aus zwei Teilen: Neben dem Text, auf den über das IDocument-Interface zugegriffen wird, enthält das Annotationsmodell eine Menge von Annotationen, die dem Text zugeordnet sind. Eine Annotation ist ein Stück zusätzliche Information zu einem Text an einer bestimmten Position (Abb. 6).
Eine Position repräsentiert eine Stelle (Offset und Länge) in einem Dokument. Ist eine Position bei einem IDocument registriert, wird sie bei Dokumentänderungen automatisch aktualisiert. Eine Position wird beispielsweise nach hinten verschoben, wenn vor ihr Text eingefügt wurde.
Der Editor stellt Annotationen je nach Typ unterschiedlich dar. Sie können als Ikonen am linken Editorrand, als Farbmarkierungen in der Übersichtsleiste (rechts des Editors) oder direkt im Text als Wellenlinie oder durch eine spezielle Hintergrundfarbe angezeigt werden.
Im Rezepteditor möchten wir dynamisch alle Vorkommen einer Zutat gelb hinterlegen, wenn der Textcursor in deren Bereich bewegt wird. Dazu erzeugen wir im RecipeEditor eine Instanz der neuen Klasse RecipeOccurrencesUpdater (Listing 5). Um über Selektionsänderungen (einschließlich Cursorbewegungen) informiert zu werden, registriert sich der Updater beim IPostSelectionProvider des Editors. Dieser Provider sendet seine SelectionEvents erst aus, wenn der Benutzer die Selektion für eine kurze Zeit unverändert gelassen hat. Dies hat den Vorteil, dass schnell wechselnde Selektionen (zum Beispiel, wenn der Benutzer die Pfeiltasten gedrückt hält) nicht empfangen werden. Erhält der Updater eine Selektionsänderung, so extrahiert er das aktuelle Wort aus dem Text. Ist es laut Rezeptmodell in der Zutatenlisten enthalten, werden alle gleich lautenden Textstellen im Rezept mit einer Annotation markiert (Listing 6). Wird die Zutatenliste editiert, entspricht das Rezeptmodell während dem Markieren vielleicht noch nicht dem aktuellen Zustand. Deshalb werden die Annotationen auch erneuert, wenn der Editor ein neues Modell erhält.
Listing 5
public void createPartControl(Composite parent) {super.createPartControl(parent);fOccurrencesUpdater= new RecipeOccurrencesUpdater(this);}public void setRecipe(Recipe recipe) {...if (fOccurrencesUpdater != null)fOccurrencesUpdater.update(getSourceViewer());}
class RecipeOccurrencesUpdater implements ISelectionChangedListener {private final List fOldAnnotations= new LinkedList();public RecipeOccurrencesUpdater(RecipeEditor editor) {((IPostSelectionProvider) editor.getSelectionProvider()).addPostSelectionChangedListener(this);fEditor= editor;}public void selectionChanged(SelectionChangedEvent event) {update((ISourceViewer) event.getSource());}public void update(ISourceViewer viewer) {IDocument document= viewer.getDocument();IAnnotationModel model= viewer.getAnnotationModel();removeOldAnnotations(model);String word= getWordAtSelection(fEditor.getSelectionProvider().getSelection(), document);if (isIngredient(word)) {createNewAnnotations(word, document, model);}}// getWordAtSelection() und isIngredient() ausgelassenprivate void removeOldAnnotations(IAnnotationModel model) {for (Iterator it= fOldAnnotations.iterator(); it.hasNext();) {Annotation annotation= (Annotation) it.next();model.removeAnnotation(annotation);}fOldAnnotations.clear();}private void createNewAnnotations(String ingredient, IDocument document, IAnnotationModel model) {String content= document.get();int idx= content.indexOf(ingredient);while (idx != -1) {Annotation annotation= new Annotation(ANNOTATION_TYPE, false, ingredient);Position position= new Position(idx, ingredient.length());model.addAnnotation(annotation, position);fOldAnnotations.add(annotation);idx= content.indexOf(ingredient, idx + 1);}}}
Für die Darstellung der Annotationen liest der Editor die Erweiterungen des org.eclipse.ui.editors.markerAnnotationSpecification-Erweiterungspunkts. In einer solchen Erweiterung kann spezifiziert werden, wie ein bestimmter Annotationstyp dem Benutzer präsentiert wird. Wir wählen die Einstellungen wie in Listing 7, um jedes Vorkommen einer Zutat im Text gelb zu hinterlegen und am linken Editorrand sowie in der Übersichtsleiste zu markieren.
Listing 7
<extensionpoint="org.eclipse.ui.editors.markerAnnotationSpecification"><specificationannotationType="org.eclipse.ui.examples.recipeeditor.highlightannotation"verticalRulerPreferenceKey="highlight.rulers.vertical"textPreferenceKey="highlight.text"colorPreferenceKey="highlight.color"highlightPreferenceKey="highlight.background"textPreferenceValue="false"textStylePreferenceValue="UNDERLINE"overviewRulerPreferenceKey="highlight.rulers.overview"presentationLayer="4"highlightPreferenceValue="true"label="Zutaten"icon="icons/occurrence.gif"colorPreferenceValue="253,255,157"verticalRulerPreferenceValue="true"overviewRulerPreferenceValue="true"textStylePreferenceKey="highlight.text.style"></specification></extension>
Ausgehend von den Standardeinstellungen kann der Benutzer die Darstellung des hinzugefügten Annotationstyps selbst konfigurieren: In den Benutzervorgaben kann unter Workbench | Editors | Annotations beispielsweise eingestellt werden, dass die Zutaten eingerahmt statt markiert werden sollen (Abb. 7).
Faltung
Faltung, wie es der Java-Editor unterstützt, erlaubt dem Benutzer, ganze Textfragmente zu einer Zeile zusammenzufalten. Nachfolgend wollen wir den Rezepteditor um Faltung ergänzen, sodass alle im Modell repräsentierten mehrzeiligen Regionen weggefaltet werden können (Abb. 8).
Um einen Editor um Faltung zu erweitern, verwenden wir einen ProjectionViewer anstelle eines normalen SourceViewer. Dabei konfiguriert man die Faltfunktionen über einen ProjectionSupport. Die Faltregionen definiert man durch ProjectionAnnotations, die man auf dem ProjectionAnnotationModel des ProjectionViewer registriert. Das Zusammenspiel dieser Klassen ist in Abpictureung 9 illustriert.
Den Rezepteditor ergänzen wir um Faltung, indem wir in der Editorklasse createSourceViewer(..) überschreiben und anstelle des normalen SourceViewer den ProjectionViewer benutzen. In createPartControl(..) konfigurieren und installieren wir den ProjectionSupport und schalten Faltung auf dem ProjectionViewer ein. Zudem erweitern wir getAdapter(), sodass auch Adapter des ProjectionSupport berücksichtigt werden (Listing 8).
Listing 8
private ProjectionSupport fProjectionSupport;protected ISourceViewer createSourceViewer(Composite parent, IVerticalRuler ruler, int styles) {...ISourceViewer viewer= new ProjectionViewer(parent, ruler, fOverviewRuler, true, styles);...return viewer;}public void createPartControl(Composite parent) {super.createPartControl(parent);ProjectionViewer projectionViewer= (ProjectionViewer) getSourceViewer();fProjectionSupport= new ProjectionSupport(projectionViewer, getAnnotationAccess(), getSharedColors());fProjectionSupport.install();projectionViewer.doOperation(ProjectionViewer.TOGGLE);}public Object getAdapter(Class required) {Object adapter= fProjectionSupport.getAdapter(getSourceViewer(), required);if (adapter != null)return adapter;return super.getAdapter(required);}
Was nun noch fehlt, ist die Definition von Faltregionen. Da wir bereits ein Modell haben, welches uns die Textregionen der einzelnen Knoten zur Verfügung stellt, leiten wir die Faltregionen direkt davon ab. Dazu ergänzen wir die RecipeReconcilingStrategy um den RecipeFoldingStructureProvider. Dieser wird jedes Mal aufgerufen, nachdem ein neues Modell generiert wurde. Er aktualisiert auf dem ProjectionAnnotationModel die ProjectionAnnotations, welche die Faltregionen repräsentieren.
private void reconcile() {final Recipe recipe= fParser.parse(fDocument);...fFoldingStructureProvider.updateFoldingRegions(recipe);}
Der RecipeFoldingStructureProvider traversiert zunächst in addFoldingRegions() das Modell und sammelt alle Faltregionen als Positions. Da der ProjectionViewer zeilenbasiert arbeitet, müssen die Textregionen der einzelnen Modellknoten zu ganzen Zeilen ergänzt werden. Erstreckt sich eine Textregion über mehr als eine Zeile, erzeugen wir dafür eine Faltregion.
Würden wir nun die alten Faltregionen vom ProjectionAnnotationModel entfernen und die neuen hinzufügen, so ginge der Faltzustand der alten Regionen verloren und alle gefalteten Regionen würden sich öffnen. In computeDifferences() berechnen wir die Differenz zwischen den alten und den neuen Faltregionen. Als Ergebnis erhalten wir somit zwei Mengen: die hinzuzufügenden und die zu löschenden Faltregionen. Hierbei können wir uns den Umstand zunutze machen, dass die zu Annotationen gehörenden Positionen nach jeder Dokumentänderung aktualisiert wurden (Listing 9).
Listing 9
public void updateFoldingRegions(Recipe recipe, IProgressMonitor progressMonitor) {ProjectionAnnotationModel model= (ProjectionAnnotationModel) fEditor.getAdapter(ProjectionAnnotationModel.class);Set additions= new HashSet();addFoldingRegions(additions, recipe.getChildren());Annotation[] deletions= computeDifferences(model, additions);Map additionsMap= new HashMap();for (Iterator iter= additions.iterator(); iter.hasNext();)additionsMap.put(new ProjectionAnnotation(), iter.next());if ((deletions.length != 0 || additionsMap.size() != 0) && !progressMonitor.isCanceled())model.modifyAnnotations(deletions, additionsMap, new Annotation[] {});}
Damit haben wir den Rezepteditor um Faltung ergänzt. Der ProjectionSupport bietet noch weitere Optionen zur Konfiguration. So kann man beispielsweise zusammenfassbare Annotationstypen definieren. Annotationen zusammenfassbaren Typs werden, wenn sie weggefaltet sind, als einzelne Annotation dieses Typs auf der Faltzeile dargestellt.
Schablonen
In den meisten Sprachen gibt es oft wiederkehrende Textmuster, deren Eingabe durch Schablonen (Templates) erleichtert werden kann. Sie werden als zusätzliche Einträge im Inhaltsassistenten angezeigt. In unserem Beispiel wollen wir Schablonen für häufige Zubereitungsschritte anbieten (Abb. 10 und 11).
Eclipse 3.0 bietet Klassen an, mittels derer Textmuster als Vorschläge des Inhaltsassistenten angeboten werden. Im Gegensatz zu normalen Vorschlägen wird dem Benutzer bei der Auswahl eines Textmusters eine Schablone präsentiert, deren Lücken er mit konkreten Werten füllen kann. Bevor ein Textmuster als Vorschlag präsentiert wird, erstellt der Inhaltsassistent einen TemplateContext. Der Kontext enthält alle benötigten Informationen über die Umgebung, in der das Textmuster eingefügt wird. Spezialisierte Kontexte können das Muster vor dem Einfügen noch verändern. Der Java-Editor bietet beispielsweise für eine for-Schleife eine Schablone an, für deren Lücken (die Variablen der for-Schleife) passende Referenzen eingefügt werden. Der Editor legt fest, welche Kontexttypen er unterstützt.
Ein TemplateStore dient dazu, Textmuster zu speichern und für den Editor zugänglich zu machen. Um ein Muster als Komplettierungsvorschlag zu präsentieren, wird ein TemplateProposal verwendet, das bei Aktivierung das Muster im aktuellen Kontext auswertet, in den Text einfügt und als Schablone anzeigt. In unserem Beispiel benutzen wir den org.eclipse.ui.editors.templates-Erweiterungspunkt, um einen Kontexttyp zu definieren (Listing 10) und die angegebene XML-Datei mit zwei Textmustern hinzuzufügen (Listing 11).
Listing 10
<extensionpoint="org.eclipse.ui.editors.templates"><contextTypeclass="org.eclipse.jface.text.templates.TemplateContextType"name="Zubereitung"id="org.eclipse.ui.examples.recipeeditor.preparation"/><include file="templates/templates.xml"/></extension>
Listing 11
<templates><template id="backen"name="backen"description="eine Zutat backen"context="org.eclipse.ui.examples.recipeeditor.preparation"enabled="true">(${N}) ${Zutat} bei ${temperatur} Grad backen</template></templates>
Der RecipeCompletionProcessor greift über unsere Plugin-in-Klasse auf den TemplateStore zu, der alle relevanten Textmuster für den definierten Kontext enthält. Die Muster werden dann als TemplateProposals vom Inhaltsassistenten angeboten (Listing 12).
Listing 12
private ICompletionProposal[] computeTemplateProposals(ITextViewer viewer, IRegion region, Recipe recipe, String prefix) {TemplateContext context= createContext(viewer, region, recipe);String id= context.getContextType().getId();Template[] templates= RecipeEditorPlugin.getDefault().getTemplateStore().getTemplates(id);List matches= new ArrayList();for (int i= 0; i < templates.length; i++)matches.add(new TemplateProposal(templates[i], context, region, getImage(template), getRelevance(template, prefix)));return (ICompletionProposal[]) matches.toArray(new ICompletionProposal[matches.size()]);}
- Wird eine TemplatePreferencePage beim org.eclipse.ui.preferencePages-Erweiterungspunkt registriert, kann der Benutzer die Textmuster verändern und eigene hinzufügen.
- Eine Erweiterung der Standardimplementierung TemplateContextType kann für bestimmte Variablen (beispielsweise ${Zutat}) Vorschläge berechnen, die darauf in der Schablone angezeigt werden.
Fazit
Wir haben gesehen, wie mit wenig Programmieraufwand ein leistungsfähiger textbasierter Editor realisiert wird. Das Texteditor-Rahmenwerk von Eclipse erlaubt dem Entwickler, sich auf die sprachspezifischen Einzelheiten zu konzentrieren. Es stellt die grundlegenden Editieroperationen zur Verfügung und ist bereits für komplexere Funktionen ausgelegt.Links und Literatur
[1] How to use the JFace Tree Viewer: www.eclipse.org/articles/treeviewer-cg/TreeViewerArticle.htm
[2] Eclipse Rich Client Platform: dev.eclipse.org/viewcvs/index.cgi/~checkout~/platform-ui-home/rcp/
[3] Eclipse Help: Eclipse Platform Plug-in Developer Guide | Programmer's Guide | Editors
[4] PDE does Plug-ins: www.eclipse.org/articles/Article-PDE-does-plugins/PDE-intro.html









