Java tutorial
/******************************************************************************* * Copyright (c) 2009 Stephan Muehlstrasser. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Stephan Muehlstrasser - initial implementation *******************************************************************************/ package ccw.editors.clojure; import java.util.Map; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.preference.PreferenceConverter; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextInputListener; import org.eclipse.jface.text.ITextOperationTarget; import org.eclipse.jface.text.ITextViewerExtension5; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TextSelection; import org.eclipse.jface.text.TextViewer; import org.eclipse.jface.text.source.DefaultCharacterPairMatcher; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.text.source.ICharacterPairMatcher; import org.eclipse.jface.text.source.IOverviewRuler; import org.eclipse.jface.text.source.IVerticalRuler; import org.eclipse.jface.text.source.SourceViewerConfiguration; import org.eclipse.jface.text.source.projection.ProjectionViewer; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Listener; import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; import org.eclipse.ui.texteditor.AbstractTextEditor; import org.eclipse.ui.texteditor.StatusLineContributionItem; import ccw.CCWPlugin; import ccw.ClojureCore; import ccw.preferences.PreferenceConstants; import ccw.repl.REPLView; import ccw.util.ClojureInvoker; import ccw.util.DisplayUtil; public abstract class ClojureSourceViewer extends ProjectionViewer implements IClojureEditor, IPropertyChangeListener { private final ClojureInvoker editorSupport = ClojureInvoker.newInvoker(CCWPlugin.getDefault(), "ccw.editors.clojure.editor-support"); private final ClojureInvoker handlers = ClojureInvoker.newInvoker(CCWPlugin.getDefault(), "ccw.editors.clojure.handlers"); /** * Status category used e.g. with TextEditors embedding a ClojureSourceViewer * reporting the status of the structural edition mode (Strict/Default). * */ public static final String STATUS_CATEGORY_STRUCTURAL_EDITION = "CCW.STATUS_CATEGORY_STRUCTURAL_EDITING_POSSIBLE"; /** * Due to Eclipse idiosyncracies, it is not possible for the viewer to * directly manage lifecycle of StatusLineContributionItems. * * But it is still required, to stay DRY, to centralise as much as possible * of the state reporting of the ClojureSourceViewer. * * This interface must be implemented by "components" (Editors, Viewers, whatever) * which are capable of reporting status to status line managers */ public static interface IStatusLineHandler { StatusLineContributionItem getEditingModeStatusContributionItem(); } /** * The preference store. */ private IPreferenceStore fPreferenceStore; public static class EditorColors { /** * This viewer's foreground color. */ public Color fForegroundColor; /** * The viewer's background color. */ public Color fBackgroundColor; /** * This viewer's selection foreground color. */ public Color fSelectionForegroundColor; /** * The viewer's selection background color. */ public Color fSelectionBackgroundColor; /** * The viewer's background color for the selected line */ public Color fCurrentLineBackgroundColor; public void unconfigure() { fForegroundColor = unconfigure(fForegroundColor); fBackgroundColor = unconfigure(fBackgroundColor = null); fSelectionForegroundColor = unconfigure(fSelectionForegroundColor); fSelectionBackgroundColor = unconfigure(fSelectionBackgroundColor); fCurrentLineBackgroundColor = unconfigure(fCurrentLineBackgroundColor); } private Color unconfigure(Color c) { if (c != null) { c.dispose(); } return null; } } private EditorColors editorColors = new EditorColors(); /** * The source viewer configuration. Needed for property change events * for reconfiguring. */ private ClojureSourceViewerConfiguration fConfiguration; /** * Is this source viewer configured? */ private boolean fIsConfigured; private boolean inEscapeSequence; private boolean isContentAssistantActive; /** * Set to true if the editor is in "Strict" Structural editing mode */ private boolean useStrictStructuralEditing; /** * Set to true to have the viewer show rainbow parens */ private boolean isShowRainbowParens; /** * Set to true to indicate a Damager to consider that the whole document * must be considered damaged, e.g. to force syntax coloring & al. * to refresh. */ private boolean isForceRepair; /** History for structure select action * STOLEN FROM THE JDT */ private SelectionHistory fSelectionHistory; /** can be null */ private IStatusLineHandler statusLineHandler; /** The error message shown in the status line in case of failed information look up. */ protected final String fErrorLabel = "An unexpected error occured"; public SelectionHistory getSelectionHistory() { return fSelectionHistory; } /** * Set to false if structural editing is not possible, because the document * is not parseable. */ private boolean structuralEditingPossible; public ClojureSourceViewer(Composite parent, IVerticalRuler verticalRuler, IOverviewRuler overviewRuler, boolean showAnnotationsOverview, int styles, IPreferenceStore store, IStatusLineHandler statusLineHandler) { super(parent, verticalRuler, overviewRuler, showAnnotationsOverview, styles); setPreferenceStore(store); KeyListener escListener = new KeyListener() { public void keyPressed(KeyEvent e) { if (e.character == SWT.ESC) { if (!isContentAssistantActive) { inEscapeSequence = true; updateTabsToSpacesConverter(); updateStructuralEditingModeStatusField(); } } } public void keyReleased(KeyEvent e) { if (inEscapeSequence && !(e.character == SWT.ESC)) { inEscapeSequence = false; updateTabsToSpacesConverter(); updateStructuralEditingModeStatusField(); } } }; // add before all other listeners so that we're certain we enable/disable //the Esc key feature based on accurate state information addKeyListenerFirst(getTextWidget(), escListener); addTextInputListener(new ITextInputListener() { public void inputDocumentChanged(IDocument oldInput, IDocument newInput) { if (newInput != null) { newInput.addDocumentListener(parseTreeConstructorDocumentListener); String text = newInput.get(); updateTextBuffer(text, 0, -1, text); } } public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) { if (oldInput != null) oldInput.removeDocumentListener(parseTreeConstructorDocumentListener); } }); useStrictStructuralEditing = store .getBoolean(ccw.preferences.PreferenceConstants.USE_STRICT_STRUCTURAL_EDITING_MODE_BY_DEFAULT); isShowRainbowParens = store.getBoolean(ccw.preferences.PreferenceConstants.SHOW_RAINBOW_PARENS_BY_DEFAULT); this.statusLineHandler = statusLineHandler; } private void addKeyListenerFirst(Control control, KeyListener listener) { Listener[] keyDownListeners = control.getListeners(SWT.KeyDown); Listener[] keyUpListeners = control.getListeners(SWT.KeyUp); removeAll(control, SWT.KeyDown, keyDownListeners); removeAll(control, SWT.KeyUp, keyUpListeners); control.addKeyListener(listener); addAll(control, SWT.KeyDown, keyDownListeners); addAll(control, SWT.KeyUp, keyUpListeners); } private void removeAll(Control control, int eventType, Listener[] listeners) { for (Listener listener : listeners) { control.removeListener(eventType, listener); } } private void addAll(Control control, int eventType, Listener[] listeners) { for (Listener listener : listeners) { control.addListener(eventType, listener); } } public static StatusLineContributionItem createStructuralEditionModeStatusContributionItem() { return new StatusLineContributionItem(ClojureSourceViewer.STATUS_CATEGORY_STRUCTURAL_EDITION, true, STATUS_STRUCTURAL_EDITION_CHARS_WIDTH); } public void propertyChange(PropertyChangeEvent event) { if (fConfiguration != null) { ClojureSourceViewerConfiguration tmp = fConfiguration; unconfigure(); initializeViewerColors(); tmp.initTokenScanner(); configure(tmp); // TODO this causes setRange() to be called twice (does the reinitialization of things } } /** * Sets the preference store on this viewer. * * @param store the preference store * * @since 3.0 */ public void setPreferenceStore(IPreferenceStore store) { if (fIsConfigured && fPreferenceStore != null) fPreferenceStore.removePropertyChangeListener(this); fPreferenceStore = store; if (fIsConfigured && fPreferenceStore != null) { fPreferenceStore.addPropertyChangeListener(this); initializeViewerColors(); } } public void configure(SourceViewerConfiguration configuration) { super.configure(configuration); if (fPreferenceStore != null) { fPreferenceStore.addPropertyChangeListener(this); initializeViewerColors(); } if (configuration instanceof ClojureSourceViewerConfiguration) fConfiguration = (ClojureSourceViewerConfiguration) configuration; fSelectionHistory = new SelectionHistory(this); fIsConfigured = true; } /** * Creates a color from the information stored in the given preference store. * Returns <code>null</code> if there is no such information available. * * @param store the store to read from * @param key the key used for the lookup in the preference store * @param display the display used create the color * @return the created color according to the specification in the preference store */ static public Color createColor(IPreferenceStore store, String key, Display display) { RGB rgb = getRGBColor(store, key); return (rgb != null) ? new Color(display, rgb) : null; } static public RGB getRGBColor(IPreferenceStore store, String key) { RGB rgb = null; if (store.contains(key)) { if (store.isDefault(key)) rgb = PreferenceConverter.getDefaultColor(store, key); else rgb = PreferenceConverter.getColor(store, key); } return rgb; } public void initializeViewerColors() { initializeViewerColors(getTextWidget(), fPreferenceStore, editorColors); if (fPreferenceStore != null) { CCWPlugin.registerEditorColors(fPreferenceStore, getTextWidget().getForeground().getRGB()); } } public static void initializeViewerColors(StyledText styledText, IPreferenceStore preferenceStore, EditorColors editorColors) { if (preferenceStore != null) { // ----------- foreground color -------------------- Color color = preferenceStore.getBoolean(AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND_SYSTEM_DEFAULT) ? null : createColor(preferenceStore, AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND, styledText.getDisplay()); styledText.setForeground(color); if (editorColors.fForegroundColor != null) editorColors.fForegroundColor.dispose(); editorColors.fForegroundColor = color; // ---------- background color ---------------------- color = preferenceStore.getBoolean(AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND_SYSTEM_DEFAULT) ? null : createColor(preferenceStore, AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND, styledText.getDisplay()); styledText.setBackground(color); if (editorColors.fBackgroundColor != null) editorColors.fBackgroundColor.dispose(); editorColors.fBackgroundColor = color; // ----------- selection foreground color -------------------- color = preferenceStore.getBoolean( AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_FOREGROUND_DEFAULT_COLOR) ? null : createColor(preferenceStore, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_FOREGROUND_COLOR, styledText.getDisplay()); styledText.setSelectionForeground(color); if (editorColors.fSelectionForegroundColor != null) editorColors.fSelectionForegroundColor.dispose(); editorColors.fSelectionForegroundColor = color; // ---------- selection background color ---------------------- color = preferenceStore.getBoolean( AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_BACKGROUND_DEFAULT_COLOR) ? null : createColor(preferenceStore, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SELECTION_BACKGROUND_COLOR, styledText.getDisplay()); styledText.setSelectionBackground(color); if (editorColors.fSelectionBackgroundColor != null) editorColors.fSelectionBackgroundColor.dispose(); editorColors.fSelectionBackgroundColor = color; // ---------- current line background color ---------------------- color = createColor(preferenceStore, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_CURRENT_LINE_COLOR, styledText.getDisplay()); if (editorColors.fCurrentLineBackgroundColor != null) editorColors.fCurrentLineBackgroundColor.dispose(); editorColors.fCurrentLineBackgroundColor = color; } } /* * @see org.eclipse.jface.text.source.ISourceViewerExtension2#unconfigure() * @since 3.0 */ public void unconfigure() { editorColors.unconfigure(); if (fPreferenceStore != null) fPreferenceStore.removePropertyChangeListener(this); if (fSelectionHistory != null) { fSelectionHistory.dispose(); fSelectionHistory = null; } super.unconfigure(); fIsConfigured = false; fConfiguration = null; } /** This is manipulated by clojure functions. * It's a ref, holding a map {:text "the raw text file" :parser parser} * where state is a future holding the parser's state */ private Object parseState; private IDocumentListener parseTreeConstructorDocumentListener = new IDocumentListener() { public void documentAboutToBeChanged(DocumentEvent event) { // TODO ?? maybe call updateTextBuffer directly from within an overriden method of AbstractDocument (so creating our own ClojureDocument ?) => maintaining parse tree with document ... String newText = replace(event.getDocument().get(), event.getOffset(), event.getLength(), event.getText()); updateTextBuffer(newText, event.getOffset(), event.getLength(), event.getText()); } public void documentChanged(DocumentEvent event) { } }; private String replace(String doc, int offset, int length, String text) { return doc.substring(0, offset) + text + doc.substring(offset + length); } private void updateTextBuffer(String finalText, long offset, long length, String text) { boolean firstTime = (parseState == null); parseState = editorSupport._("updateTextBuffer", parseState, finalText, offset, length, text); if (firstTime) { editorSupport._("startWatchParseRef", parseState, this); } } // TODO rename getParseInfo or get.. ? public Object getParseState() { if (parseState == null) { String text = getDocument().get(); updateTextBuffer(text, 0, -1, text); } return editorSupport._("getParseState", getDocument().get(), parseState); } public boolean isParseTreeBroken() { return (Boolean) editorSupport._("brokenParseTree?", getParseState()); } public Object getPreviousParseTree() { if (parseState == null) { return null; } else { return editorSupport._("getPreviousParseTree", parseState); } } private boolean structuralEditionPossible = true; public void setStructuralEditionPossible(final boolean state) { structuralEditionPossible = state; syncWithStructuralEditionPossibleState(); } public boolean isStructuralEditionPossible() { return structuralEditionPossible; } private void syncWithStructuralEditionPossibleState() { DisplayUtil.asyncExec(new Runnable() { public void run() { getTextWidget().setBackground(structuralEditionPossible ? editorColors.fBackgroundColor : Display.getCurrent().getSystemColor(SWT.COLOR_GRAY)); getTextWidget().setToolTipText(structuralEditionPossible ? null : "Unparseable source code. Structural Edition temporarily disabled."); } }); } public IRegion getSignedSelection() { StyledText text = getTextWidget(); Point selection = text.getSelectionRange(); if (text.getCaretOffset() == selection.x) { selection.x = selection.x + selection.y; selection.y = -selection.y; } selection.x = widgetOffset2ModelOffset(selection.x); return new Region(selection.x, selection.y); } public IRegion getUnSignedSelection() { StyledText text = getTextWidget(); Point selection = text.getSelectionRange(); selection.x = widgetOffset2ModelOffset(selection.x); return new Region(selection.x, selection.y); } public void selectAndReveal(int start, int length) { setSelection(new TextSelection(start, length), true); } // TODO rename because it's really "should we be in strict mode or not?" public boolean isStructuralEditingEnabled() { return useStrictStructuralEditing; } public boolean isShowRainbowParens() { return isShowRainbowParens; } public boolean isInEscapeSequence() { return inEscapeSequence; } public String findDeclaringNamespace() { return ClojureCore.findDeclaringNamespace((Map) editorSupport._("getParseTree", getParseState())); } public IJavaProject getAssociatedProject() { return null; } public REPLView getCorrespondingREPL() { // this gets overridden in REPLView as appropriate so that the toolConnection there gets returned return null; } public void updateTabsToSpacesConverter() { } // TODO get rid of this way of handling document initialization @Override public void setDocument(IDocument document, IAnnotationModel annotationModel, int modelRangeOffset, int modelRangeLength) { super.setDocument(document, annotationModel, modelRangeOffset, modelRangeLength); if (document != null) { String text = document.get(); updateTextBuffer(text, 0, -1, text); } } /** Preference key for matching brackets color */ //PreferenceConstants.EDITOR_MATCHING_BRACKETS_COLOR; public final static char[] PAIRS = { '{', '}', '(', ')', '[', ']' }; public static final int STATUS_STRUCTURAL_EDITION_CHARS_WIDTH = 33; private DefaultCharacterPairMatcher pairsMatcher = new DefaultCharacterPairMatcher(PAIRS, ClojurePartitionScanner.CLOJURE_PARTITIONING) { /* tries to match a pair be the cursor after or before a pair start/end element */ @Override public IRegion match(IDocument doc, int offset) { IRegion region = super.match(doc, offset); if (region == null && offset < (doc.getLength() - 1)) { return super.match(doc, offset + 1); } else { return region; } } }; /** * Jumps to the matching bracket. */ public void gotoMatchingBracket() { IDocument document = getDocument(); if (document == null) return; IRegion selection = getSignedSelection(); int selectionLength = Math.abs(selection.getLength()); if (selectionLength > 1) { setStatusLineErrorMessage(ClojureEditorMessages.GotoMatchingBracketAction_error_invalidSelection); getTextWidget().getDisplay().beep(); return; } // // #26314 int sourceCaretOffset = selection.getOffset() + selection.getLength(); // From JavaEditor, but I don't understand what it does so I maintain it commented out // if (isSurroundedByBrackets(document, sourceCaretOffset)) // sourceCaretOffset -= selection.getLength(); // IRegion region = pairsMatcher.match(document, sourceCaretOffset); if (region == null) { setStatusLineErrorMessage(ClojureEditorMessages.GotoMatchingBracketAction_error_noMatchingBracket); getTextWidget().getDisplay().beep(); return; } int offset = region.getOffset(); int length = region.getLength(); if (length < 1) return; int anchor = pairsMatcher.getAnchor(); // http://dev.eclipse.org/bugs/show_bug.cgi?id=34195 int targetOffset = (ICharacterPairMatcher.RIGHT == anchor) ? offset + 1 : offset + length; boolean visible = false; if (this instanceof ITextViewerExtension5) { ITextViewerExtension5 extension = (ITextViewerExtension5) this; visible = (extension.modelOffset2WidgetOffset(targetOffset) > -1); } else { IRegion visibleRegion = getVisibleRegion(); // http://dev.eclipse.org/bugs/show_bug.cgi?id=34195 visible = (targetOffset >= visibleRegion.getOffset() && targetOffset <= visibleRegion.getOffset() + visibleRegion.getLength()); } if (!visible) { setStatusLineErrorMessage( ClojureEditorMessages.GotoMatchingBracketAction_error_bracketOutsideSelectedElement); getTextWidget().getDisplay().beep(); return; } if (selection.getLength() < 0) targetOffset -= selection.getLength(); setSelectedRange(targetOffset, selection.getLength()); revealRange(targetOffset, selection.getLength()); } public DefaultCharacterPairMatcher getPairsMatcher() { return pairsMatcher; } public void setStructuralEditingPossible(boolean state) { if (state != this.structuralEditingPossible) { this.structuralEditingPossible = state; updateStructuralEditingModeStatusField(); } } public void toggleStructuralEditionMode() { useStrictStructuralEditing = !useStrictStructuralEditing; updateStructuralEditingModeStatusField(); } public void toggleShowRainbowParens() { isShowRainbowParens = !isShowRainbowParens; markDamagedAndRedraw(); } public void markDamagedAndRedraw() { try { isForceRepair = true; this.invalidateTextPresentation(); } finally { isForceRepair = false; } } /** * @return true to indicate a Damager to consider that the whole document * must be considered damaged, e.g. to force syntax coloring & al. * to refresh. */ public boolean isForceRepair() { return isForceRepair; } public void updateStructuralEditingModeStatusField() { if (this.statusLineHandler == null) { return; } StatusLineContributionItem field = this.statusLineHandler.getEditingModeStatusContributionItem(); if (field != null) { field.setText((isStructuralEditingEnabled() ? "strict/paredit" : "unrestricted") + " edit mode" + (inEscapeSequence ? " ESC" : "")); field.setToolTipText((isStructuralEditingEnabled() ? "strict/paredit edit mode:\neditor does its best to prevent you from breaking the structure of the code (requires you to know shortcut commands well)." : "unrestricted edit mode:\nhelps you with edition, but does not get in your way.")); } } /* * Eclipse TextEditor framework uses old "Action" framework. So it is impossible * to use handlers declaratively, one must plug the new behaviour via code, * some way or the other. * It was decided here to plug new behaviour by overriding directly the * doOperation(operation) call, at the most central point, that is. * * (non-Javadoc) * @see org.eclipse.jface.text.source.projection.ProjectionViewer#doOperation(int) */ @Override public void doOperation(int operation) { if (operation == TextViewer.PASTE) { if (!getTextWidget().getBlockSelection()) { handlers._("smart-paste", this); return; } else { // We're not trying (at least yet) to handle paste inside // block selections super.doOperation(operation); } } else { super.doOperation(operation); } } public Object getAdapter(Class adapter) { if (IClojureEditor.class == adapter) { return this; } if (ITextOperationTarget.class == adapter) { return this; } return null; } public boolean isEscapeInStringLiteralsEnabled() { return fPreferenceStore.getBoolean(PreferenceConstants.EDITOR_ESCAPE_ON_PASTE); } public boolean isContentAssistantActive() { return isContentAssistantActive; } public void setContentAssistantActive(boolean isContentAssistantActive) { this.isContentAssistantActive = isContentAssistantActive; } }