/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.netbeans.modules.editor.hints; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.event.ChangeListener; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Position; import javax.swing.text.StyledDocument; import org.netbeans.editor.BaseDocument; import org.netbeans.editor.Utilities; import org.netbeans.spi.editor.hints.ErrorDescription; import org.netbeans.spi.editor.hints.Fix; import org.netbeans.spi.editor.hints.LazyFixList; import org.openide.cookies.EditorCookie; import org.openide.filesystems.FileObject; import org.openide.loaders.DataObject; import org.openide.text.CloneableEditorSupport; import org.openide.text.EditorSupport; import org.openide.text.NbDocument; import org.openide.text.PositionBounds; import org.openide.text.PositionRef; import org.openide.util.Exceptions; /** * * @author Jan Lahoda */ public final class HintsControllerImpl { private static final Logger LOG = Logger.getLogger(HintsControllerImpl.class.getName()); private HintsControllerImpl() {} public static void setErrors(Document doc, String layer, Collection<? extends ErrorDescription> errors) { DataObject od = (DataObject) doc.getProperty(Document.StreamDescriptionProperty); if (od == null) return ; try { setErrorsImpl(od.getPrimaryFile(), layer, errors); } catch (IOException e) { Exceptions.printStackTrace(e); } } public static void setErrors(FileObject file, String layer, Collection<? extends ErrorDescription> errors) { try { setErrorsImpl(file, layer, errors); } catch (IOException e) { Exceptions.printStackTrace(e); } } private static void setErrorsImpl(FileObject file, String layer, Collection<? extends ErrorDescription> errors) throws IOException { AnnotationHolder holder = AnnotationHolder.getInstance(file); if (holder != null) { holder.setErrorDescriptions(layer,errors); } } private static void computeLineSpan(Document doc, int[] offsets) throws BadLocationException { String text = doc.getText(offsets[0], offsets[1] - offsets[0]); int column = 0; int length = text.length(); while (column < text.length() && Character.isWhitespace(text.charAt(column))) { column++; } while (length > 0 && Character.isWhitespace(text.charAt(length - 1))) length--; offsets[1] = offsets[0] + length; offsets[0] += column; if (offsets[1] < offsets[0]) { //may happen on lines without non-whitespace characters offsets[0] = offsets[1]; } } static int[] computeLineSpan(Document doc, int lineNumber) throws BadLocationException { lineNumber = Math.min(lineNumber, NbDocument.findLineRootElement((StyledDocument) doc).getElementCount()); int lineStartOffset = NbDocument.findLineOffset((StyledDocument) doc, Math.max(0, lineNumber - 1)); int lineEndOffset; if (doc instanceof BaseDocument) { lineEndOffset = Utilities.getRowEnd((BaseDocument) doc, lineStartOffset); } else { //XXX: performance: String lineText = doc.getText(lineStartOffset, doc.getLength() - lineStartOffset); lineText = lineText.indexOf('\n') != (-1) ? lineText.substring(0, lineText.indexOf('\n')) : lineText; lineEndOffset = lineStartOffset + lineText.length(); } int[] span = new int[] {lineStartOffset, lineEndOffset}; computeLineSpan(doc, span); return span; } public static PositionBounds fullLine(final Document doc, final int lineNumber) { final PositionBounds[] result = new PositionBounds[1]; doc.render(new Runnable() { @Override public void run() { result[0] = fullLineImpl(doc, lineNumber); } }); return result[0]; } private static PositionBounds fullLineImpl(Document doc, int lineNumber) { DataObject file = (DataObject) doc.getProperty(Document.StreamDescriptionProperty); if (file == null) return null; try { int[] span = computeLineSpan(doc, lineNumber); return linePart(file.getPrimaryFile(), span[0], span[1]); } catch (BadLocationException e) { Exceptions.printStackTrace(e); return null; } } public static PositionBounds linePart(Document doc, final Position start, final Position end) { DataObject od = (DataObject) doc.getProperty(Document.StreamDescriptionProperty); if (od == null) return null; EditorCookie ec = od.getCookie(EditorCookie.class); if (ec instanceof CloneableEditorSupport) { final CloneableEditorSupport ces = (CloneableEditorSupport) ec; final PositionRef[] refs = new PositionRef[2]; doc.render(new Runnable() { public void run() { checkOffsetsAndLog(start.getOffset(), end.getOffset()); refs[0] = ces.createPositionRef(start.getOffset(), Position.Bias.Forward); refs[1] = ces.createPositionRef(end.getOffset(), Position.Bias.Backward); } }); return new PositionBounds(refs[0], refs[1]); } if (ec instanceof EditorSupport) { final EditorSupport es = (EditorSupport) ec; final PositionRef[] refs = new PositionRef[2]; doc.render(new Runnable() { public void run() { checkOffsetsAndLog(start.getOffset(), end.getOffset()); refs[0] = es.createPositionRef(start.getOffset(), Position.Bias.Forward); refs[1] = es.createPositionRef(end.getOffset(), Position.Bias.Backward); } }); return new PositionBounds(refs[0], refs[1]); } return null; } public static PositionBounds linePart(FileObject file, int start, int end) { try { DataObject od = DataObject.find(file); if (od == null) return null; EditorCookie ec = od.getCookie(EditorCookie.class); if (!(ec instanceof CloneableEditorSupport)) { return null; } final CloneableEditorSupport ces = (CloneableEditorSupport) ec; checkOffsetsAndLog(start, end); return new PositionBounds(ces.createPositionRef(start, Position.Bias.Forward), ces.createPositionRef(end, Position.Bias.Backward)); } catch (IOException e) { LOG.log(Level.INFO, null, e); return null; } } private static void checkOffsetsAndLog(int start, int end) { if (start <= end) { return ; } Logger.getLogger(HintsControllerImpl.class.getName()).log(Level.INFO, "Incorrect span, please attach your messages.log to issue #112566. start=" + start + ", end=" + end, new Exception()); } private static List<ChangeListener> listeners = new ArrayList<ChangeListener>(); public static synchronized void addChangeListener(ChangeListener l) { listeners.add(l); } public static synchronized void removeChangeListener(ChangeListener l) { listeners.remove(l); } private static final Map<Reference<Fix>, Iterable<? extends Fix>> fix2Subfixes = new HashMap<Reference<Fix>, Iterable<? extends Fix>>(); public static void attachSubfixes(Fix fix, Iterable<? extends Fix> subfixes) { synchronized (fix2Subfixes) { fix2Subfixes.put(new CleaningReference(fix), subfixes); } } public static Iterable<? extends Fix> getSubfixes(Fix fix) { synchronized (fix2Subfixes) { Iterable<? extends Fix> ret = fix2Subfixes.get(new CleaningReference(fix)); return ret != null ? ret : Collections.<Fix>emptyList(); } } private static final class CleaningReference extends WeakReference<Fix> implements Runnable { private final int hashCode; public CleaningReference(Fix referent) { super(referent, org.openide.util.Utilities.activeReferenceQueue()); hashCode = System.identityHashCode(referent); } @Override public void run() { synchronized (fix2Subfixes) { for (Iterator<Entry<Reference<Fix>, Iterable<? extends Fix>>> it = fix2Subfixes.entrySet().iterator(); it.hasNext();) { Entry<Reference<Fix>, Iterable<? extends Fix>> e = it.next(); if (e.getKey() == this) { it.remove(); return; } } } } @Override public boolean equals(Object obj) { if (!(obj instanceof Reference)) { return false; } Reference<Fix> that = (Reference<Fix>) obj; Fix thisFix = get(); Fix thatFix = that.get(); return thisFix == thatFix; } @Override public int hashCode() { return hashCode; } } public static class CompoundLazyFixList implements LazyFixList, PropertyChangeListener { final List<LazyFixList> delegates; private List<Fix> fixesCache; private Boolean computedCache; private Boolean probablyContainsFixesCache; private PropertyChangeSupport pcs; public CompoundLazyFixList(List<LazyFixList> delegates) { this.delegates = delegates; this.pcs = new PropertyChangeSupport(this); for (LazyFixList l : delegates) { l.addPropertyChangeListener(this); } } public void addPropertyChangeListener(PropertyChangeListener l) { pcs.addPropertyChangeListener(l); } public void removePropertyChangeListener(PropertyChangeListener l) { pcs.removePropertyChangeListener(l); } public synchronized boolean probablyContainsFixes() { if (probablyContainsFixesCache == null) { boolean result = false; for (LazyFixList l : delegates) { result |= l.probablyContainsFixes(); } probablyContainsFixesCache = Boolean.valueOf(result); } return probablyContainsFixesCache; } public synchronized List<Fix> getFixes() { if (fixesCache == null) { fixesCache = new ArrayList<Fix>(); for (LazyFixList l : delegates) { fixesCache.addAll(l.getFixes()); } } return fixesCache; } public synchronized boolean isComputed() { if (computedCache == null) { boolean result = true; for (LazyFixList l : delegates) { result &= l.isComputed(); } computedCache = Boolean.valueOf(result); } return computedCache; } public void propertyChange(PropertyChangeEvent evt) { if (PROP_FIXES.equals(evt.getPropertyName())) { synchronized (this) { fixesCache = null; } pcs.firePropertyChange(PROP_FIXES, null, null); return; } if (PROP_COMPUTED.equals(evt.getPropertyName())) { synchronized (this) { computedCache = null; } pcs.firePropertyChange(PROP_COMPUTED, null, null); } } } }