/******************************************************************************* * Copyright (c) 2001, 2005 IBM Corporation and others. * 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: * IBM Corporation - initial API and implementation * Jens Lukowski/Innoopract - initial renaming/restructuring * *******************************************************************************/ package org.eclipse.wst.sse.ui.internal; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.jobs.ILock; import org.eclipse.jface.text.AbstractDocument; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.ConfigurableLineTracker; import org.eclipse.jface.text.DefaultLineTracker; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentAdapter; import org.eclipse.jface.text.IDocumentAdapterExtension; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.IRepairableDocument; import org.eclipse.jface.text.ITextStore; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.projection.ProjectionDocument; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.custom.TextChangeListener; import org.eclipse.swt.custom.TextChangedEvent; import org.eclipse.swt.custom.TextChangingEvent; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.PlatformUI; import org.eclipse.wst.sse.core.internal.ILockable; import org.eclipse.wst.sse.core.internal.provisional.events.IStructuredDocumentListener; import org.eclipse.wst.sse.core.internal.provisional.events.NewDocumentEvent; import org.eclipse.wst.sse.core.internal.provisional.events.NoChangeEvent; import org.eclipse.wst.sse.core.internal.provisional.events.RegionChangedEvent; import org.eclipse.wst.sse.core.internal.provisional.events.RegionsReplacedEvent; import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentEvent; import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentRegionsReplacedEvent; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegionList; import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; import org.eclipse.wst.sse.core.internal.util.Debug; import org.eclipse.wst.sse.core.internal.util.Utilities; import org.eclipse.wst.sse.ui.internal.util.Assert; /** * Adapts IStructuredDocument events and methods to StyledTextContent events * and methods */ public class StructuredDocumentToTextAdapter implements IDocumentAdapter, IDocumentAdapterExtension { private class DocumentClone extends AbstractDocument { /** * Creates a new document clone with the given content. * * @param content * the content * @param lineDelimiters * the line delimiters */ public DocumentClone(String content, String[] lineDelimiters) { super(); setTextStore(new StringTextStore(content)); ConfigurableLineTracker tracker = new ConfigurableLineTracker(lineDelimiters); setLineTracker(tracker); getTracker().set(content); completeInitialization(); } } // A pre-notification listener for the viewer's Document class DocumentListener implements IDocumentListener { protected boolean allTextChanged = false; protected DocumentEvent currentEvent; synchronized public void documentAboutToBeChanged(DocumentEvent event) { if (isStoppedForwardingChanges()) return; pendingDocumentChangedEvent = true; allTextChanged = event.getOffset() <= 0 && event.getLength() >= StructuredDocumentToTextAdapter.this.getDocument().getLength(); currentEvent = event; StructuredDocumentToTextAdapter.this.relayTextChanging(event.getOffset(), event.getLength(), event.getText()); } synchronized public void documentChanged(DocumentEvent event) { if (isStoppedForwardingChanges()) return; if (currentEvent != null && event == currentEvent) { if (allTextChanged) { StructuredDocumentToTextAdapter.this.relayTextSet(); } else { // temp work around for immediate thread // problem. // should have more general solution // soon. 'syncExec' are rumored to be // prone to hang. StructuredDocumentToTextAdapter.this.relayTextChanged(); } } currentEvent = null; pendingDocumentChangedEvent = false; handlePendingEvents(); lastEvent = null; } } private static class StringTextStore implements ITextStore { private String fContent; /** * Creates a new string text store with the given content. * * @param content * the content */ public StringTextStore(String content) { Assert.isNotNull(content, "content can not be null when setting text store"); //$NON-NLS-1$ fContent = content; } /* * @see org.eclipse.jface.text.ITextStore#get(int) */ public char get(int offset) { return fContent.charAt(offset); } /* * @see org.eclipse.jface.text.ITextStore#get(int, int) */ public String get(int offset, int length) { return fContent.substring(offset, offset + length); } /* * @see org.eclipse.jface.text.ITextStore#getLength() */ public int getLength() { return fContent.length(); } /* * @see org.eclipse.jface.text.ITextStore#replace(int, int, * java.lang.String) */ public void replace(int offset, int length, String text) { } /* * @see org.eclipse.jface.text.ITextStore#set(java.lang.String) */ public void set(String text) { } } /** * Changes to the Document/IStructuredDocument can extend beyond the text * change area and require more redrawing to keep the hilighting correct. * The event must be saved so that the redraw is only sent after a * textChanged event is received. */ class StructuredDocumentListener implements IStructuredDocumentListener { public void newModel(NewDocumentEvent structuredDocumentEvent) { if (isStoppedForwardingChanges()) { // if // (StructuredDocumentToTextAdapter.this.fStopRelayingChanges) // { if (Debug.debugStructuredDocument) { System.out.println("skipped relaying StructuredDocumentEvent " + structuredDocumentEvent.getClass().getName()); //$NON-NLS-1$ } return; } // should use textSet when all contents have // changed // otherwise need to use the pair of // textChanging and // textChanged. StructuredDocumentToTextAdapter.this.lastEvent = structuredDocumentEvent; } public void noChange(final NoChangeEvent structuredDocumentEvent) { if (Debug.debugStructuredDocument) { System.out.println("skipped relaying StructuredDocumentEvent " + structuredDocumentEvent.getClass().getName()); //$NON-NLS-1$ } if (structuredDocumentEvent.reason == NoChangeEvent.READ_ONLY_STATE_CHANGE) { if (pendingDocumentChangedEvent) { if (lastEventQueue == null) { lastEventQueue = new ArrayList(); } lastEventQueue.add(structuredDocumentEvent); } else { StructuredDocumentToTextAdapter.this.lastEvent = structuredDocumentEvent; } } } public void nodesReplaced(StructuredDocumentRegionsReplacedEvent structuredDocumentEvent) { if (isStoppedForwardingChanges()) { // if // (StructuredDocumentToTextAdapter.this.fStopRelayingChanges) // { if (Debug.debugStructuredDocument) { System.out.println("not relaying StructuredDocumentEvent " + structuredDocumentEvent.getClass().getName()); //$NON-NLS-1$ } return; } if (Debug.debugStructuredDocument) { System.out.println("saving StructuredDocumentEvent " + structuredDocumentEvent.getClass().getName()); //$NON-NLS-1$ } StructuredDocumentToTextAdapter.this.lastEvent = structuredDocumentEvent; } public void regionChanged(RegionChangedEvent structuredDocumentEvent) { if (isStoppedForwardingChanges()) { // if // (StructuredDocumentToTextAdapter.this.fStopRelayingChanges) // { if (Debug.debugStructuredDocument) { System.out.println("not relaying StructuredDocumentEvent " + structuredDocumentEvent.getClass().getName()); //$NON-NLS-1$ } return; } if (Debug.debugStructuredDocument) { System.out.println("saving StructuredDocumentEvent " + structuredDocumentEvent.getClass().getName()); //$NON-NLS-1$ } StructuredDocumentToTextAdapter.this.lastEvent = structuredDocumentEvent; } public void regionsReplaced(RegionsReplacedEvent structuredDocumentEvent) { if (isStoppedForwardingChanges()) { // if // (StructuredDocumentToTextAdapter.this.fStopRelayingChanges) // { if (Debug.debugStructuredDocument) { System.out.println("not relaying StructuredDocumentEvent " + structuredDocumentEvent.getClass().getName()); //$NON-NLS-1$ } return; } if (Debug.debugStructuredDocument) { System.out.println("saving StructuredDocumentEvent " + structuredDocumentEvent.getClass().getName()); //$NON-NLS-1$ } StructuredDocumentToTextAdapter.this.lastEvent = structuredDocumentEvent; } } private static final String EMPTY_STRING = ""; //$NON-NLS-1$ private final static boolean redrawBackground = true; /** The visible child document. */ private ProjectionDocument fChildDocument; /** The master document */ private IDocument fDocument; /** The document clone for the non-forwarding case. */ private IDocument fDocumentClone; // only use this temp work around if on GTK // it causes funny "cursor blinking" if used on windows private final boolean forceRedrawOnRegionChanged = Platform.getWS().equals("gtk"); //$NON-NLS-1$ /** The original content */ private String fOriginalContent; /** The original line delimiters */ private String[] fOriginalLineDelimiters; private int fStopRelayingChangesRequests = 0; private StyledText fStyledTextWidget; /** The registered text changed listeners */ TextChangeListener[] fTextChangeListeners; protected DocumentListener internalDocumentListener; // The listeners for relaying DocumentEvents and // requesting repaints // after modification private IStructuredDocumentListener internalStructuredDocumentListener; protected StructuredDocumentEvent lastEvent = null; List lastEventQueue; boolean pendingDocumentChangedEvent; private static final boolean DEBUG = false; /** * TEST ONLY - TEST ONLY - TEST ONLY NOT API use this constructor only for * tests. Creates a new document adapter which is initiallly not connected * to any document. */ public StructuredDocumentToTextAdapter() { internalStructuredDocumentListener = new StructuredDocumentListener(); internalDocumentListener = new DocumentListener(); // for testing only // setDocument(getModelManager().createStructuredDocumentFor(ContentTypeIdentifierForXML.ContentTypeID_XML)); } /** * Creates a new document adapter which is initiallly not connected to any * document. */ public StructuredDocumentToTextAdapter(StyledText styledTextWidget) { // do not use 'this()' in this case super(); internalStructuredDocumentListener = new StructuredDocumentListener(); internalDocumentListener = new DocumentListener(); fStyledTextWidget = styledTextWidget; } private void _setDocument(IDocument newDoc) { if (fDocument instanceof IStructuredDocument) { ((IStructuredDocument) fDocument).removeDocumentChangedListener(internalStructuredDocumentListener); } fDocument = newDoc; if (!isStoppedForwardingChanges()) { fDocumentClone = null; fOriginalContent = getDocument() != null ? getDocument().get() : null; fOriginalLineDelimiters = getDocument() != null ? getDocument().getLegalLineDelimiters() : null; } if (DEBUG && fDocument != null && !(fDocument instanceof ILockable)) { System.out.println("Warning: non ILockable document used in StructuredDocumentToTextAdapter"); //$NON-NLS-1$ System.out.println(" document updates on non-display thread will not be safe if editor open"); //$NON-NLS-1$ } if (fDocument instanceof IStructuredDocument) { ((IStructuredDocument) fDocument).addDocumentChangedListener(internalStructuredDocumentListener); } } /* * (non-Javadoc) * * @see org.eclipse.swt.custom.StyledTextContent#addTextChangeListener(org.eclipse.swt.custom.TextChangeListener) */ public synchronized void addTextChangeListener(TextChangeListener listener) { // make sure listener is not already in listening // (and if it is, print a warning to aid debugging, // if needed) if (Utilities.contains(fTextChangeListeners, listener)) { if (Debug.displayWarnings) { System.out.println("StructuredDocumentToTextAdapter::addTextChangedListeners. listener " + listener + " was added more than once. "); //$NON-NLS-2$//$NON-NLS-1$ } } else { if (Debug.debugStructuredDocument) { System.out.println("StructuredDocumentToTextAdapter::addTextChangedListeners. Adding an instance of " + listener.getClass() + " as a listener on text adapter."); //$NON-NLS-2$//$NON-NLS-1$ } int oldSize = 0; if (fTextChangeListeners != null) { // normally won't be null, but we need to be // sure, for first time through oldSize = fTextChangeListeners.length; } int newSize = oldSize + 1; TextChangeListener[] newListeners = new TextChangeListener[newSize]; if (fTextChangeListeners != null) { System.arraycopy(fTextChangeListeners, 0, newListeners, 0, oldSize); } // add listener to last position newListeners[newSize - 1] = listener; // // now switch new for old fTextChangeListeners = newListeners; // } } /* * @see org.eclipse.swt.custom.StyledTextContent#getCharCount() */ public int getCharCount() { // getDocument can sometimes be null during startup // and dispose int result = 0; IDocument doc = getDocument(); if (doc != null) { result = getSafeDocument().getLength(); } return result; } private IDocument getClonedDocument() { if (fDocumentClone == null) { String content = fOriginalContent == null ? "" : fOriginalContent; //$NON-NLS-1$ String[] delims = fOriginalLineDelimiters == null ? DefaultLineTracker.DELIMITERS : fOriginalLineDelimiters; fDocumentClone = new DocumentClone(content, delims); } return fDocumentClone; } Display getDisplay() { // Note: the workbench should always have a display // (unless running headless), whereas Display.getCurrent() // only returns the display if the currently executing thread // has one. if (PlatformUI.isWorkbenchRunning()) return PlatformUI.getWorkbench().getDisplay(); else return null; } /** * Returns the visible document. * * @return IDocument */ protected IDocument getDocument() { if (fChildDocument == null) return fDocument; return fChildDocument; } /** * Returns region in master document of given region (should be region in * projection document) * * @return region if no projection document exists, region of master * document if possible, null otherwise */ private IRegion getProjectionToMasterRegion(IRegion region) { IRegion originalRegion = region; if (fChildDocument != null) { try { originalRegion = fChildDocument.getProjectionMapping().toOriginRegion(region); } catch (BadLocationException e) { Logger.logException(e); } } return originalRegion; } /** * Returns offset in projection document of given offset (should be offset * in master document) * * @return offset if no projection document exists, offset of projection * document if possible, -1 otherwise */ private int getMasterToProjectionOffset(int offset) { int originalOffset = offset; if (fChildDocument != null) { try { originalOffset = fChildDocument.getProjectionMapping().toImageOffset(offset); } catch (BadLocationException e) { Logger.logException(e); } } return originalOffset; } /** * Return the line at the given character offset without delimiters. * <p> * * @param offset * offset of the line to return. Does not include delimiters of * preceeding lines. Offset 0 is the first character of the * document. * @return the line text without delimiters */ public java.lang.String getLine(int lineNumber) { String result = null; if (lineNumber >= getLineCount()) { if (Debug.displayWarnings) { System.out.println("Development Debug: IStructuredDocument:getLine() error. lineNumber requested (" + lineNumber + ") was greater than number of lines(" + getLineCount() + "). EmptyString returned"); //$NON-NLS-1$//$NON-NLS-3$//$NON-NLS-2$ } result = EMPTY_STRING; } else { IDocument doc = getSafeDocument(); if (doc == null) { result = EMPTY_STRING; } else { try { IRegion r = doc.getLineInformation(lineNumber); if (r.getLength() > 0) { result = doc.get(r.getOffset(), r.getLength()); } else { result = EMPTY_STRING; } } catch (BadLocationException e) { result = EMPTY_STRING; } } } return result; } /** * Tries to repair the line information. * * @param document * the document * @see IRepairableDocument#repairLineInformation() * @see Eclipse 3.0 */ private void repairLineInformation(IDocument document) { if (document instanceof IRepairableDocument) { IRepairableDocument repairable = (IRepairableDocument) document; repairable.repairLineInformation(); } } /** * Return the line index at the given character offset. * <p> * * @param offset * offset of the line to return. The first character of the * document is at offset 0. An offset of getLength() is valid * and should answer the number of lines. * @return the line index. The first line is at index 0. If the character * at offset is a delimiter character, answer the line index of * the line that is delimited. For example, text = "\r\n\r\n", * delimiter = "\r\n", then: getLineAtOffset(0) == 0 * getLineAtOffset(1) == 0 getLineAtOffset(2) == 1 * getLineAtOffset(3) == 1 getLineAtOffset(4) == 2 */ public int getLineAtOffset(int offset) { int result = 0; IDocument doc = getSafeDocument(); if (doc != null) { try { result = doc.getLineOfOffset(offset); } catch (BadLocationException x) { repairLineInformation(doc); try { result = doc.getLineOfOffset(offset); } catch (BadLocationException x2) { // should not occur, but seems to for projection // documents, related to repainting overview ruler result = 0; } } } return result; } public int getLineCount() { int result = 0; IDocument doc = getSafeDocument(); if (doc != null) { result = doc.getNumberOfLines(); } return result; } /* * @see org.eclipse.swt.custom.StyledTextContent#getLineDelimiter */ public String getLineDelimiter() { String result = null; if (getParentDocument() instanceof IStructuredDocument) { result = ((IStructuredDocument) getParentDocument()).getLineDelimiter(); } else { IDocument doc = getSafeDocument(); result = TextUtilities.getDefaultLineDelimiter(doc); } return result; } /** * Return the character offset of the first character of the given line. * <p> * * @param lineIndex * index of the line. The first line is at index 0. * @return offset offset of the first character of the line. The first * character of the document is at offset 0. The return value * should include line delimiters. For example, text = * "\r\ntest\r\n", delimiter = "\r\n", then: getOffsetAtLine(0) == * 0 getOffsetAtLine(1) == 2 getOffsetAtLine(2) == 8 NOTE: When * there is no text (i.e., no lines), getOffsetAtLine(0) is a * valid call that should return 0. */ public int getOffsetAtLine(int lineIndex) { int result = 0; IDocument doc = getSafeDocument(); if (doc != null) { try { result = doc.getLineOffset(lineIndex); } catch (BadLocationException e) { result = 0; } } return result; } /** * Returns the parent document * * @return the parent document */ private IDocument getParentDocument() { return fDocument; } /** * This is the document to use for request from the StyledText widget. Its * either the live documnet or a clone of it, depending on stop/resume * state. */ private IDocument getSafeDocument() { IDocument result = null; if (isStoppedForwardingChanges()) { result = getClonedDocument(); } else { // note, this document can be normal structured text document, // or the projection/child document result = getDocument(); } return result; } /** * @return org.eclipse.swt.custom.StyledText */ StyledText getStyledTextWidget() { return fStyledTextWidget; } /** * Returns a string representing the content at the given range. * <p> * * @param start * the start offset of the text to return. Offset 0 is the * first character of the document. * @param length * the length of the text to return * @return the text at the given range */ public String getTextRange(int start, int length) { String result = null; try { IDocument doc = getSafeDocument(); result = doc.get(start, length); } catch (BadLocationException e) { result = EMPTY_STRING; } return result; } /** * assume only for "no change" events, for now */ protected void handlePendingEvents() { if (lastEventQueue == null) return; Iterator iterator = lastEventQueue.iterator(); while (iterator.hasNext()) { NoChangeEvent noChangeEvent = (NoChangeEvent) iterator.next(); redrawNoChange(noChangeEvent); } lastEventQueue = null; lastEvent = null; } boolean isStoppedForwardingChanges() { return fStopRelayingChangesRequests > 0; } /** * this method is assumed to be called only for read only region changes. */ protected void redrawNoChange(NoChangeEvent structuredDocumentEvent) { if (isStoppedForwardingChanges()) return; if (Debug.debugStructuredDocument) { System.out.println("maybe redraw stuff"); //$NON-NLS-1$ } int startOffset = structuredDocumentEvent.getOffset(); int length = structuredDocumentEvent.getLength(); redrawRangeWithLength(startOffset, length); } /** * Request a redraw of the text range occupied by the given * StructuredDocumentRegionsReplacedEvent * * @param structuredDocumentEvent */ protected void redrawNodesReplaced(StructuredDocumentRegionsReplacedEvent structuredDocumentEvent) { if (isStoppedForwardingChanges()) return; if (Debug.debugStructuredDocument) { System.out.println("maybe redraw stuff"); //$NON-NLS-1$ } // just the new stuff IStructuredDocumentRegionList newStructuredDocumentRegions = structuredDocumentEvent.getNewStructuredDocumentRegions(); int nNewNodes = newStructuredDocumentRegions.getLength(); if (nNewNodes > 0) { IStructuredDocumentRegion firstNode = newStructuredDocumentRegions.item(0); IStructuredDocumentRegion lastNode = newStructuredDocumentRegions.item(nNewNodes - 1); redrawRange(firstNode.getStartOffset(), lastNode.getEndOffset()); } } /** * Redraws the give offsets in terms of the StructuredDocument. If only * part of the model is visible, ensures that only the visible portion of * the given range is redrawn. * * @param startModelOffset * @param endModelOffset */ private void redrawRange(final int startModelOffset, final int endModelOffset) { if (getDocument() == null) return; if (Debug.debugStructuredDocument) { System.out.println("redraw stuff: " + startModelOffset + "-" + endModelOffset); //$NON-NLS-1$ //$NON-NLS-2$ } if (fChildDocument == null) { Runnable runnable = new Runnable() { public void run() { getStyledTextWidget().redrawRange(startModelOffset, endModelOffset - startModelOffset, redrawBackground); } }; runOnDisplayThreadIfNeedede(runnable); } else { int high = getDocument().getLength(); int startOffset = getMasterToProjectionOffset(startModelOffset); int endOffset = getMasterToProjectionOffset(endModelOffset); // if offsets were not visible, just try to redraw everything in // the child document // // not visible // if (endOffset < 0 || startOffset > high) // return; // restrict lower bound if (startOffset < 0) { startOffset = 0; } // restrict upper bound // if (endOffset > high) { // endOffset = high; // } if (endOffset < 0) { endOffset = high; } int length = endOffset - startOffset; // redrawBackground with false would be faster // but assumes background (or font) is not // changing final int finalStartOffset = startOffset; final int finallength = length; Runnable runnable = new Runnable() { public void run() { getStyledTextWidget().redrawRange(finalStartOffset, finallength, redrawBackground); } }; runOnDisplayThreadIfNeedede(runnable); } } /** * Redraws the give offsets in terms of the Flat Node model. If only part * of the model is visible, ensures that only the visible portion of the * given range is redrawn. * * @param startModelOffset * @param endModelOffset */ private void redrawRangeWithLength(final int startModelOffset, final int length) { if (getDocument() == null) return; if (Debug.debugStructuredDocument) { System.out.println("redraw stuff: " + startModelOffset + "-" + length); //$NON-NLS-1$ //$NON-NLS-2$ } if (fChildDocument == null) { Runnable runnable = new Runnable() { public void run() { getStyledTextWidget().redrawRange(startModelOffset, length, redrawBackground); } }; runOnDisplayThreadIfNeedede(runnable); } else { int high = getDocument().getLength(); // TODO need to take into account segmented visible regions int startOffset = getMasterToProjectionOffset(startModelOffset); // not visible if (startOffset > high || length < 1) return; // restrict lower bound if (startOffset < 0) { startOffset = 0; } int endOffset = startOffset + length - 1; // restrict upper bound if (endOffset > high) { endOffset = high; } // note: length of the child documnet should be // updated, // need to investigate why its not at this // point, but is // probably just because the document event // handling is not // completely finished. int newLength = endOffset - startOffset; // d283007 // redrawBackground with false would be faster // but assumes background (or font) is not // changing final int finalStartOffset = startOffset; final int finalNewLength = newLength; Runnable runnable = new Runnable() { public void run() { getStyledTextWidget().redrawRange(finalStartOffset, finalNewLength, redrawBackground); } }; runOnDisplayThreadIfNeedede(runnable); } } /** * Request a redraw of the text range occupied by the given * RegionChangedEvent for certain (not all) ITextRegion contexts * * @param structuredDocumentEvent */ protected void redrawRegionChanged(RegionChangedEvent structuredDocumentEvent) { if (isStoppedForwardingChanges()) { return; } if (Debug.debugStructuredDocument) { System.out.println("maybe redraw stuff"); //$NON-NLS-1$ } // (nsd) TODO: try to make this reliable somehow // without being directly content dependent // if ((region instanceof ITextRegionContainer) || // (type == XMLJSPRegionContexts.BLOCK_TEXT) || // (type == XMLJSPRegionContexts.JSP_CONTENT)) { // IStructuredDocumentRegion flatNode = // structuredDocumentEvent.getStructuredDocumentRegion(); // // redraw background of false is faster, // // but assumes background (or font) is not // changing // redrawRange(flatNode.getStartOffset(region), // flatNode.getEndOffset(region)); // } if (forceRedrawOnRegionChanged) { // workaround for redrawing problems on Linux-GTK int startOffset = structuredDocumentEvent.getOffset(); int endOffset = structuredDocumentEvent.getOffset() + structuredDocumentEvent.getLength(); try { IRegion startLine = structuredDocumentEvent.fDocument.getLineInformationOfOffset(startOffset); IRegion endLine = structuredDocumentEvent.fDocument.getLineInformationOfOffset(endOffset); if (startLine != null && endLine != null) { redrawRange(startLine.getOffset(), endLine.getOffset() + endLine.getLength()); } } catch (BadLocationException e) { // nothing for now } } } /** * Request a redraw of the text range occupied by the given * RegionsReplacedEvent * * @param structuredDocumentEvent */ protected void redrawRegionsReplaced(RegionsReplacedEvent structuredDocumentEvent) { if (isStoppedForwardingChanges()) return; if (Debug.debugStructuredDocument) { System.out.println("maybe redraw stuff"); //$NON-NLS-1$ } ITextRegionList newRegions = structuredDocumentEvent.getNewRegions(); int nRegions = newRegions.size(); if (nRegions > 0) { ITextRegion firstRegion = newRegions.get(0); ITextRegion lastRegion = newRegions.get(nRegions - 1); IStructuredDocumentRegion flatNode = structuredDocumentEvent.getStructuredDocumentRegion(); redrawRange(flatNode.getStartOffset(firstRegion), flatNode.getEndOffset(lastRegion)); } } protected void redrawTextChanged() { if (lastEvent != null) { // update display, since some cases can effect // highlighting beyond the changed text area. if (lastEvent instanceof StructuredDocumentRegionsReplacedEvent) redrawNodesReplaced((StructuredDocumentRegionsReplacedEvent) lastEvent); if (lastEvent instanceof RegionsReplacedEvent) redrawRegionsReplaced((RegionsReplacedEvent) lastEvent); if (lastEvent instanceof RegionChangedEvent) redrawRegionChanged((RegionChangedEvent) lastEvent); // moved following line to 'document changed' so // the "last event" can be // re-drawn after pending re-draws // lastEvent = null; } } /** * Sends a text replace event to all registered listeners. */ protected void relayTextChanged() { if (isStoppedForwardingChanges()) { if (Debug.debugStructuredDocument && getDocument() != null) { System.out.println("NOT relaying text changed (" + getDocument().getLength() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } return; } if (Debug.debugStructuredDocument && getDocument() != null) { System.out.println("relaying text changed (" + getDocument().getLength() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } final TextChangedEvent textChangedEvent = new TextChangedEvent(this); // we must assign listeners to local variable, since // the add and remove listener // methods can change the actual instance of the // listener array from another thread Runnable runnable = new Runnable() { public void run() { if (fTextChangeListeners != null) { Object[] holdListeners = fTextChangeListeners; for (int i = 0; i < holdListeners.length; i++) { // this is a safe cast, since addListeners // requires a IStructuredDocumentListener ((TextChangeListener) holdListeners[i]).textChanged(textChangedEvent); } } } }; runOnDisplayThreadIfNeedede(runnable); redrawTextChanged(); } /** * Sends a text change to all registered listeners */ protected void relayTextChanging(int requestedStart, int requestedLength, String requestedChange) { if (getDocument() == null) return; if (isStoppedForwardingChanges()) { if (Debug.debugStructuredDocument && getDocument() != null) { System.out.println("NOT relaying text changing: " + requestedStart + ":" + getDocument().getLength()); //$NON-NLS-1$ //$NON-NLS-2$ } return; } if (Debug.debugStructuredDocument && getDocument() != null) { System.out.println("relaying text changing: " + requestedStart + ":" + getDocument().getLength()); //$NON-NLS-1$ //$NON-NLS-2$ } lastEvent = null; try { final TextChangingEvent textChangingEvent = new TextChangingEvent(this); textChangingEvent.start = requestedStart; textChangingEvent.replaceCharCount = requestedLength; textChangingEvent.newCharCount = (requestedChange == null ? 0 : requestedChange.length()); textChangingEvent.replaceLineCount = getDocument().getNumberOfLines(requestedStart, requestedLength) - 1; textChangingEvent.newText = requestedChange; textChangingEvent.newLineCount = (requestedChange == null ? 0 : getDocument().computeNumberOfLines(requestedChange)); // we must assign listeners to local variable, // since the add and remove listner // methods can change the actual instance of the // listener array from another thread Runnable runnable = new Runnable() { public void run() { if (fTextChangeListeners != null) { TextChangeListener[] holdListeners = fTextChangeListeners; for (int i = 0; i < holdListeners.length; i++) { // this is a safe cast, since // addListeners requires a // IStructuredDocumentListener holdListeners[i].textChanging(textChangingEvent); } } } }; runOnDisplayThreadIfNeedede(runnable); } catch (BadLocationException e) { // log for now, unless we find reason not to Logger.log(Logger.INFO, e.getMessage()); } } /** * Sends a text set event to all registered listeners. Widget should * redraw itself automatically. */ protected void relayTextSet() { if (isStoppedForwardingChanges()) { if (Debug.debugStructuredDocument && getDocument() != null) { System.out.println("NOT relaying text set (" + getDocument().getLength() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } return; } if (Debug.debugStructuredDocument && getDocument() != null) { System.out.println("relaying text set (" + getDocument().getLength() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } lastEvent = null; final TextChangedEvent textChangedEvent = new TextChangedEvent(this); // we must assign listeners to local variable, since // the add and remove listner // methods can change the actual instance of the // listener array from another thread Runnable runnable = new Runnable() { public void run() { if (fTextChangeListeners != null) { TextChangeListener[] holdListeners = fTextChangeListeners; for (int i = 0; i < holdListeners.length; i++) { holdListeners[i].textSet(textChangedEvent); } } } }; runOnDisplayThreadIfNeedede(runnable); } /* * (non-Javadoc) * * @see org.eclipse.swt.custom.StyledTextContent#removeTextChangeListener(org.eclipse.swt.custom.TextChangeListener) */ public synchronized void removeTextChangeListener(final TextChangeListener listener) { if ((fTextChangeListeners != null) && (listener != null)) { // if its not in the listeners, we'll ignore the // request if (!Utilities.contains(fTextChangeListeners, listener)) { if (Debug.displayWarnings) { System.out.println("StructuredDocumentToTextAdapter::removeTextChangedListeners. listener " + listener + " was not present. "); //$NON-NLS-2$//$NON-NLS-1$ } } else { if (Debug.debugStructuredDocument) { System.out.println("StructuredDocumentToTextAdapter::addTextChangedListeners. Removing an instance of " + listener.getClass() + " as a listener on text adapter."); //$NON-NLS-2$//$NON-NLS-1$ } final int oldSize = fTextChangeListeners.length; int newSize = oldSize - 1; final TextChangeListener[] newListeners = new TextChangeListener[newSize]; Runnable runnable = new Runnable() { public void run() { int index = 0; for (int i = 0; i < oldSize; i++) { if (fTextChangeListeners[i] != listener) { // copy old to new if its not the // one we are removing newListeners[index++] = fTextChangeListeners[i]; } } } }; runOnDisplayThreadIfNeedede(runnable); // now that we have a new array, let's // switch it for the old one fTextChangeListeners = newListeners; } } } /** * Replace the text with "newText" starting at position "start" for a * length of "replaceLength". * <p> * Implementors have to notify TextChanged listeners after the content has * been updated. The TextChangedEvent should be set as follows: * <ul> * <li>event.type = SWT.TextReplaced * <li>event.start = start of the replaced text * <li>event.numReplacedLines = number of replaced lines * <li>event.numNewLines = number of new lines * <li>event.replacedLength = length of the replaced text * <li>event.newLength = length of the new text * </ul> * <b>NOTE: </b> numNewLines is the number of inserted lines and * numReplacedLines is the number of deleted lines based on the change * that occurs visually. For example: * <ul> * <li>(replacedText, newText) ==> (numReplacedLines, numNewLines) * <li>("", "\n") ==> (0, 1) * <li>("\n\n", "a") ==> (2, 0) * <li>("a", "\n\n") ==> (0, 2) * <li>("\n", "") ==> (1, 0) * </ul> * </p> * * @param start * start offset of text to replace, none of the offsets include * delimiters of preceeding lines, offset 0 is the first * character of the document * @param replaceLength * start offset of text to replace * @param newText * start offset of text to replace */ public void replaceTextRange(int start, int replaceLength, String text) { if (getParentDocument() instanceof IStructuredDocument) { // the structuredDocument initiates the "changing" // and "changed" events. // they are both fired by the time this method // returns. IRegion region = getProjectionToMasterRegion(new Region(start, replaceLength)); if (region != null) { ((IStructuredDocument) getParentDocument()).replaceText(this, region.getOffset(), region.getLength(), text); return; } } // default is to just try and replace text range in current document try { getDocument().replace(start, replaceLength, text); } catch (BadLocationException e) { // log for now, unless we find reason not to Logger.log(Logger.INFO, e.getMessage()); } } /** * @see org.eclipse.jface.text.IDocumentAdapterExtension#resumeForwardingDocumentChanges() */ public void resumeForwardingDocumentChanges() { // from re-reading the textSet API in StyledText, we // must call // textSet if all the contents changed. If all the // contents did // not change, we need to call the pair of APIs, // textChanging and // textChanged. So, if we ever keep careful track of // changes // during stop forwarding and resume forwarding, we // can // investigate change make use of the pair of APIs. fStopRelayingChangesRequests--; if (fStopRelayingChangesRequests == 0) { // fIsForwarding= true; fDocumentClone = null; fOriginalContent = null; fOriginalLineDelimiters = null; // fireTextSet(); relayTextSet(); } } /** * This 'Runnable' should be very brief, and should not "call out" to * other code which itself might call syncExec, or deadlock might occur. * * @param r */ private void runOnDisplayThreadIfNeedede(Runnable r) { // if there is no Display at all (that is, running headless), // or if we are already running on the display thread, then // simply execute the runnable. if (getDisplay() == null || (Thread.currentThread() == getDisplay().getThread())) { r.run(); } else { // otherwise force the runnable to run on the display thread. // // Its unclear if we need this at all, once // we "force" document update to always take place on display // thread. IDocument doc = getDocument(); if (doc instanceof ILockable) { ILock lock = null; try { lock = ((ILockable) doc).getLockObject(); lock.acquire(); getDisplay().syncExec(r); } finally { if (lock != null) { lock.release(); } } } else { // else, ignore!, since risk of deadlock throw new IllegalStateException("non lockable document used for structuredDocumentToTextAdapter"); //$NON-NLS-1$ } } } /** * @param newModel */ public void setDocument(IDocument document) { if (getDocument() != null) { getDocument().removePrenotifiedDocumentListener(internalDocumentListener); } lastEvent = null; if (document instanceof ProjectionDocument) { fChildDocument = (ProjectionDocument) document; _setDocument(fChildDocument.getMasterDocument()); } else { fChildDocument = null; _setDocument(document); } if (getDocument() != null) { getDocument().addPrenotifiedDocumentListener(internalDocumentListener); } } /** * @see IDocument#setText */ public void setText(String string) { if (isStoppedForwardingChanges()) { fDocumentClone = null; fOriginalContent = getDocument().get(); fOriginalLineDelimiters = getDocument().getLegalLineDelimiters(); } else if (getParentDocument() instanceof IStructuredDocument) { ((IStructuredDocument) getDocument()).setText(this, string); } else { getDocument().set(string); } relayTextSet(); } /** * This method was added to make testing easier. Normally, the widget is * specified on the constructor. */ public void setWidget(StyledText widget) { fStyledTextWidget = widget; } /** * @see org.eclipse.jface.text.IDocumentAdapterExtension#stopForwardingDocumentChanges() */ public void stopForwardingDocumentChanges() { fStopRelayingChangesRequests++; // only need to take snapshot on first request if (fStopRelayingChangesRequests == 1) { fDocumentClone = null; fOriginalContent = getDocument().get(); fOriginalLineDelimiters = getDocument().getLegalLineDelimiters(); } } }