/*******************************************************************************
* 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;
}
}