001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2008 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.modules.java.editor.rename;
042:
043: import com.sun.source.tree.Tree.Kind;
044: import com.sun.source.util.TreePath;
045: import java.awt.Color;
046: import java.awt.event.KeyEvent;
047: import java.awt.event.KeyListener;
048: import java.io.IOException;
049: import java.util.ArrayList;
050: import java.util.EnumSet;
051: import java.util.HashSet;
052: import java.util.List;
053: import java.util.Set;
054: import java.util.logging.Level;
055: import java.util.logging.Logger;
056: import javax.lang.model.element.Element;
057: import javax.lang.model.element.ElementKind;
058: import javax.lang.model.element.ExecutableElement;
059: import javax.lang.model.util.ElementFilter;
060: import javax.swing.Action;
061: import javax.swing.event.CaretEvent;
062: import javax.swing.event.DocumentEvent;
063: import javax.swing.event.DocumentListener;
064: import javax.swing.text.AttributeSet;
065: import javax.swing.text.BadLocationException;
066: import javax.swing.text.Document;
067: import javax.swing.text.JTextComponent;
068: import javax.swing.text.Position;
069: import javax.swing.text.Position.Bias;
070: import javax.swing.text.StyleConstants;
071: import javax.swing.undo.CannotUndoException;
072: import javax.swing.undo.UndoableEdit;
073: import org.netbeans.api.editor.mimelookup.MimeLookup;
074: import org.netbeans.api.editor.mimelookup.MimePath;
075: import org.netbeans.api.editor.settings.AttributesUtilities;
076: import org.netbeans.api.editor.settings.EditorStyleConstants;
077: import org.netbeans.api.editor.settings.FontColorSettings;
078: import org.netbeans.api.java.lexer.JavaTokenId;
079: import org.netbeans.api.java.source.Task;
080: import org.netbeans.api.java.source.CompilationController;
081: import org.netbeans.api.java.source.CompilationInfo;
082: import org.netbeans.api.java.source.JavaSource;
083: import org.netbeans.api.java.source.JavaSource.Phase;
084: import org.netbeans.api.java.source.SourceUtils;
085: import org.netbeans.api.lexer.Token;
086: import org.netbeans.api.lexer.TokenSequence;
087: import org.netbeans.editor.BaseDocument;
088: import org.netbeans.editor.BaseKit;
089: import org.netbeans.editor.GuardedDocument;
090: import org.netbeans.editor.MarkBlock;
091: import org.netbeans.editor.Utilities;
092: import org.netbeans.lib.editor.util.swing.MutablePositionRegion;
093: import org.netbeans.modules.editor.java.JavaKit.JavaDeleteCharAction;
094: import org.netbeans.modules.java.editor.semantic.FindLocalUsagesQuery;
095: import org.netbeans.modules.refactoring.api.ui.RefactoringActionsFactory;
096: import org.netbeans.spi.editor.highlighting.support.OffsetsBag;
097: import org.openide.cookies.EditorCookie;
098: import org.openide.loaders.DataObject;
099: import org.openide.nodes.Node;
100: import org.openide.text.NbDocument;
101: import org.openide.util.Exceptions;
102: import org.openide.util.Lookup;
103: import org.openide.util.lookup.AbstractLookup;
104: import org.openide.util.lookup.InstanceContent;
105:
106: /**
107: *
108: * @author Jan Lahoda
109: */
110: public class InstantRenamePerformer implements DocumentListener,
111: KeyListener {
112:
113: private static final Logger LOG = Logger
114: .getLogger(InstantRenamePerformer.class.getName());
115:
116: private SyncDocumentRegion region;
117: private int span;
118: private Document doc;
119: private JTextComponent target;
120:
121: private AttributeSet attribs = null;
122: private AttributeSet attribsLeft = null;
123: private AttributeSet attribsRight = null;
124: private AttributeSet attribsMiddle = null;
125: private AttributeSet attribsAll = null;
126:
127: /** Creates a new instance of InstantRenamePerformer */
128: private InstantRenamePerformer(JTextComponent target,
129: Set<Token<JavaTokenId>> highlights, int caretOffset)
130: throws BadLocationException {
131: this .target = target;
132: doc = target.getDocument();
133:
134: MutablePositionRegion mainRegion = null;
135: List<MutablePositionRegion> regions = new ArrayList<MutablePositionRegion>();
136:
137: for (Token<JavaTokenId> h : highlights) {
138: Position start = NbDocument.createPosition(doc, h
139: .offset(null), Bias.Backward);
140: Position end = NbDocument.createPosition(doc, h
141: .offset(null)
142: + h.length(), Bias.Forward);
143: MutablePositionRegion current = new MutablePositionRegion(
144: start, end);
145:
146: if (isIn(current, caretOffset)) {
147: mainRegion = current;
148: } else {
149: regions.add(current);
150: }
151: }
152:
153: if (mainRegion == null) {
154: throw new IllegalArgumentException(
155: "No highlight contains the caret.");
156: }
157:
158: regions.add(0, mainRegion);
159:
160: region = new SyncDocumentRegion(doc, regions);
161:
162: if (doc instanceof BaseDocument) {
163: ((BaseDocument) doc)
164: .setPostModificationDocumentListener(this );
165: }
166:
167: target.addKeyListener(this );
168:
169: target.putClientProperty(InstantRenamePerformer.class, this );
170:
171: requestRepaint();
172:
173: target.select(mainRegion.getStartOffset(), mainRegion
174: .getEndOffset());
175:
176: span = region.getFirstRegionLength();
177: }
178:
179: public static void invokeInstantRename(JTextComponent target) {
180: try {
181: final int caret = target.getCaretPosition();
182: String ident = Utilities.getIdentifier(Utilities
183: .getDocument(target), caret);
184:
185: if (ident == null) {
186: Utilities.setStatusBoldText(target,
187: "Cannot perform instant rename here.");
188: return;
189: }
190:
191: DataObject od = (DataObject) target.getDocument()
192: .getProperty(Document.StreamDescriptionProperty);
193: JavaSource js = JavaSource.forFileObject(od
194: .getPrimaryFile());
195: final boolean[] wasResolved = new boolean[1];
196: @SuppressWarnings("unchecked")
197: final Set<Token<JavaTokenId>>[] changePoints = new Set[1];
198:
199: js.runUserActionTask(new Task<CompilationController>() {
200:
201: public void run(CompilationController controller)
202: throws Exception {
203: if (controller.toPhase(Phase.RESOLVED).compareTo(
204: Phase.RESOLVED) < 0)
205: return;
206:
207: changePoints[0] = computeChangePoints(controller,
208: caret, wasResolved);
209: }
210: }, true);
211:
212: if (wasResolved[0]) {
213: if (changePoints[0] != null) {
214: doInstantRename(changePoints[0], target, caret,
215: ident);
216: } else {
217: doFullRename(od.getCookie(EditorCookie.class), od
218: .getNodeDelegate());
219: }
220: } else {
221: Utilities.setStatusBoldText(target,
222: "Cannot perform instant rename here.");
223: }
224: } catch (BadLocationException e) {
225: Exceptions.printStackTrace(e);
226: } catch (IOException ioe) {
227: Exceptions.printStackTrace(ioe);
228: }
229: }
230:
231: private static void doFullRename(EditorCookie ec, Node n) {
232:
233: InstanceContent ic = new InstanceContent();
234: ic.add(ec);
235: ic.add(n);
236: Lookup actionContext = new AbstractLookup(ic);
237:
238: Action a = RefactoringActionsFactory.renameAction()
239: .createContextAwareInstance(actionContext);
240: a.actionPerformed(RefactoringActionsFactory.DEFAULT_EVENT);
241: }
242:
243: private static void doInstantRename(
244: Set<Token<JavaTokenId>> changePoints,
245: JTextComponent target, int caret, String ident)
246: throws BadLocationException {
247: InstantRenamePerformer.performInstantRename(target,
248: changePoints, caret);
249: }
250:
251: static Set<Token<JavaTokenId>> computeChangePoints(
252: final CompilationInfo info, final int caret,
253: final boolean[] wasResolved) throws IOException {
254: final Document doc = info.getDocument();
255:
256: if (doc == null)
257: return null;
258:
259: final int[] adjustedCaret = new int[] { caret };
260:
261: doc.render(new Runnable() {
262: public void run() {
263: TokenSequence<JavaTokenId> ts = SourceUtils
264: .getJavaTokenSequence(info.getTokenHierarchy(),
265: caret);
266:
267: ts.move(caret);
268:
269: if (ts.moveNext() && ts.token() != null
270: && ts.token().id() == JavaTokenId.IDENTIFIER) {
271: adjustedCaret[0] = ts.offset()
272: + ts.token().length() / 2 + 1;
273: }
274: }
275: });
276:
277: TreePath path = info.getTreeUtilities().pathFor(
278: adjustedCaret[0]);
279:
280: //correction for int something[]:
281: if (path != null && path.getParentPath() != null) {
282: Kind leafKind = path.getLeaf().getKind();
283: Kind parentKind = path.getParentPath().getLeaf().getKind();
284:
285: if (leafKind == Kind.ARRAY_TYPE
286: && parentKind == Kind.VARIABLE) {
287: long typeEnd = info.getTrees().getSourcePositions()
288: .getEndPosition(info.getCompilationUnit(),
289: path.getLeaf());
290: long variableEnd = info.getTrees().getSourcePositions()
291: .getEndPosition(info.getCompilationUnit(),
292: path.getLeaf());
293:
294: if (typeEnd == variableEnd) {
295: path = path.getParentPath();
296: }
297: }
298: }
299:
300: Element el = info.getTrees().getElement(path);
301:
302: if (el == null) {
303: wasResolved[0] = false;
304: return null;
305: }
306:
307: //#89736: if the caret is not in the resolved element's name, no rename:
308: final Token<JavaTokenId> name = org.netbeans.modules.java.editor.semantic.Utilities
309: .getToken(info, doc, path);
310:
311: if (name == null)
312: return null;
313:
314: doc.render(new Runnable() {
315: public void run() {
316: wasResolved[0] = name.offset(null) <= caret
317: && caret <= (name.offset(null) + name.length());
318: }
319: });
320:
321: if (!wasResolved[0])
322: return null;
323:
324: if (el.getKind() == ElementKind.CONSTRUCTOR) {
325: //for constructor, work over the enclosing class:
326: el = el.getEnclosingElement();
327: }
328:
329: if (allowInstantRename(el)) {
330: final Set<Token<JavaTokenId>> points = new HashSet<Token<JavaTokenId>>(
331: new FindLocalUsagesQuery()
332: .findUsages(el, info, doc));
333:
334: if (el.getKind().isClass()) {
335: //rename also the constructors:
336: for (ExecutableElement c : ElementFilter
337: .constructorsIn(el.getEnclosedElements())) {
338: TreePath t = info.getTrees().getPath(c);
339:
340: if (t != null) {
341: Token<JavaTokenId> token = org.netbeans.modules.java.editor.semantic.Utilities
342: .getToken(info, doc, t);
343:
344: if (token != null) {
345: points.add(token);
346: }
347: }
348: }
349: }
350:
351: final boolean[] overlapsWithGuardedBlocks = new boolean[1];
352:
353: doc.render(new Runnable() {
354: public void run() {
355: overlapsWithGuardedBlocks[0] = overlapsWithGuardedBlocks(
356: doc, points);
357: }
358: });
359:
360: if (overlapsWithGuardedBlocks[0]) {
361: return null;
362: }
363:
364: return points;
365: }
366:
367: return null;
368: }
369:
370: private static boolean allowInstantRename(Element e) {
371: if (org.netbeans.modules.java.editor.semantic.Utilities
372: .isPrivateElement(e)) {
373: return true;
374: }
375:
376: //#92160: check for local classes:
377: if (e.getKind() == ElementKind.CLASS) {//only classes can be local
378: Element enclosing = e.getEnclosingElement();
379:
380: return LOCAL_CLASS_PARENTS.contains(enclosing.getKind());
381: }
382:
383: return false;
384: }
385:
386: private static boolean overlapsWithGuardedBlocks(Document doc,
387: Set<Token<JavaTokenId>> highlights) {
388: if (!(doc instanceof GuardedDocument))
389: return false;
390:
391: GuardedDocument gd = (GuardedDocument) doc;
392: MarkBlock current = gd.getGuardedBlockChain().getChain();
393:
394: while (current != null) {
395: for (Token<JavaTokenId> h : highlights) {
396: if ((current.compare(h.offset(null), h.offset(null)
397: + h.length()) & MarkBlock.OVERLAP) != 0) {
398: return true;
399: }
400: }
401:
402: current = current.getNext();
403: }
404:
405: return false;
406: }
407:
408: private static final Set<ElementKind> LOCAL_CLASS_PARENTS = EnumSet
409: .of(ElementKind.CONSTRUCTOR, ElementKind.INSTANCE_INIT,
410: ElementKind.METHOD, ElementKind.STATIC_INIT);
411:
412: public static void performInstantRename(JTextComponent target,
413: Set<Token<JavaTokenId>> highlights, int caretOffset)
414: throws BadLocationException {
415: new InstantRenamePerformer(target, highlights, caretOffset);
416: }
417:
418: private boolean isIn(MutablePositionRegion region, int caretOffset) {
419: return region.getStartOffset() <= caretOffset
420: && caretOffset <= region.getEndOffset();
421: }
422:
423: private boolean inSync;
424:
425: public synchronized void insertUpdate(DocumentEvent e) {
426: if (inSync)
427: return;
428:
429: //check for modifications outside the first region:
430: if (e.getOffset() < region.getFirstRegionStartOffset()
431: || (e.getOffset() + e.getLength()) > region
432: .getFirstRegionEndOffset()) {
433: release();
434: return;
435: }
436:
437: inSync = true;
438: region.sync(0);
439: span = region.getFirstRegionLength();
440: inSync = false;
441:
442: requestRepaint();
443: }
444:
445: public synchronized void removeUpdate(DocumentEvent e) {
446: if (inSync)
447: return;
448:
449: if (e.getLength() == 1) {
450: if ((e.getOffset() < region.getFirstRegionStartOffset() || e
451: .getOffset() > region.getFirstRegionEndOffset())) {
452: release();
453: return;
454: }
455:
456: if (e.getOffset() == region.getFirstRegionStartOffset()
457: && region.getFirstRegionLength() > 0
458: && region.getFirstRegionLength() == span) {
459: if (LOG.isLoggable(Level.FINE)) {
460: LOG.fine("e.getOffset()=" + e.getOffset());
461: LOG.fine("region.getFirstRegionStartOffset()="
462: + region.getFirstRegionStartOffset());
463: LOG.fine("region.getFirstRegionEndOffset()="
464: + region.getFirstRegionEndOffset());
465: LOG.fine("span= " + span);
466: }
467: JavaDeleteCharAction jdca = (JavaDeleteCharAction) target
468: .getClientProperty(JavaDeleteCharAction.class);
469:
470: if (jdca != null && !jdca.getNextChar()) {
471: undo();
472: } else {
473: release();
474: }
475:
476: return;
477: }
478:
479: if (e.getOffset() == region.getFirstRegionEndOffset()
480: && region.getFirstRegionLength() > 0
481: && region.getFirstRegionLength() == span) {
482: if (LOG.isLoggable(Level.FINE)) {
483: LOG.fine("e.getOffset()=" + e.getOffset());
484: LOG.fine("region.getFirstRegionStartOffset()="
485: + region.getFirstRegionStartOffset());
486: LOG.fine("region.getFirstRegionEndOffset()="
487: + region.getFirstRegionEndOffset());
488: LOG.fine("span= " + span);
489: }
490: //XXX: moves the caret anyway:
491: // JavaDeleteCharAction jdca = (JavaDeleteCharAction) target.getClientProperty(JavaDeleteCharAction.class);
492: //
493: // if (jdca != null && jdca.getNextChar()) {
494: // undo();
495: // } else {
496: release();
497: // }
498:
499: return;
500: }
501: } else {
502: //selection/multiple characters removed:
503: int removeSpan = e.getLength()
504: + region.getFirstRegionLength();
505:
506: if (span < removeSpan) {
507: release();
508: return;
509: }
510: }
511:
512: //#89997: do not sync the regions for the "remove" part of replace selection,
513: //as the consequent insert may use incorrect offset, and the regions will be synced
514: //after the insert anyway.
515: if (doc.getProperty(BaseKit.DOC_REPLACE_SELECTION_PROPERTY) != null) {
516: return;
517: }
518:
519: inSync = true;
520: region.sync(0);
521: span = region.getFirstRegionLength();
522: inSync = false;
523:
524: requestRepaint();
525: }
526:
527: public void changedUpdate(DocumentEvent e) {
528: }
529:
530: public void caretUpdate(CaretEvent e) {
531: }
532:
533: public void keyTyped(KeyEvent e) {
534: }
535:
536: public void keyPressed(KeyEvent e) {
537: if ((e.getKeyCode() == KeyEvent.VK_ESCAPE && e.getModifiers() == 0)
538: || (e.getKeyCode() == KeyEvent.VK_ENTER && e
539: .getModifiers() == 0)) {
540: release();
541: e.consume();
542: }
543: }
544:
545: public void keyReleased(KeyEvent e) {
546: }
547:
548: private void release() {
549: target.putClientProperty(InstantRenamePerformer.class, null);
550: if (doc instanceof BaseDocument) {
551: ((BaseDocument) doc)
552: .setPostModificationDocumentListener(null);
553: }
554: target.removeKeyListener(this );
555: target = null;
556:
557: region = null;
558: attribs = null;
559:
560: requestRepaint();
561:
562: doc = null;
563: }
564:
565: private void undo() {
566: if (doc instanceof BaseDocument
567: && ((BaseDocument) doc).isAtomicLock()) {
568: ((BaseDocument) doc).atomicUndo();
569: } else {
570: UndoableEdit undoMgr = (UndoableEdit) doc
571: .getProperty(BaseDocument.UNDO_MANAGER_PROP);
572: if (target != null && undoMgr != null) {
573: try {
574: undoMgr.undo();
575: } catch (CannotUndoException e) {
576: Logger.getLogger(
577: InstantRenamePerformer.class.getName())
578: .log(Level.WARNING, null, e);
579: }
580: }
581: }
582: }
583:
584: private void requestRepaint() {
585: if (region == null) {
586: OffsetsBag bag = getHighlightsBag(doc);
587: bag.clear();
588: } else {
589: // Compute attributes
590: if (attribs == null) {
591: attribs = getSyncedTextBlocksHighlight();
592: Color foreground = (Color) attribs
593: .getAttribute(StyleConstants.Foreground);
594: Color background = (Color) attribs
595: .getAttribute(StyleConstants.Background);
596: attribsLeft = AttributesUtilities.createImmutable(
597: StyleConstants.Background, background,
598: EditorStyleConstants.LeftBorderLineColor,
599: foreground,
600: EditorStyleConstants.TopBorderLineColor,
601: foreground,
602: EditorStyleConstants.BottomBorderLineColor,
603: foreground);
604: attribsRight = AttributesUtilities.createImmutable(
605: StyleConstants.Background, background,
606: EditorStyleConstants.RightBorderLineColor,
607: foreground,
608: EditorStyleConstants.TopBorderLineColor,
609: foreground,
610: EditorStyleConstants.BottomBorderLineColor,
611: foreground);
612: attribsMiddle = AttributesUtilities.createImmutable(
613: StyleConstants.Background, background,
614: EditorStyleConstants.TopBorderLineColor,
615: foreground,
616: EditorStyleConstants.BottomBorderLineColor,
617: foreground);
618: attribsAll = AttributesUtilities.createImmutable(
619: StyleConstants.Background, background,
620: EditorStyleConstants.LeftBorderLineColor,
621: foreground,
622: EditorStyleConstants.RightBorderLineColor,
623: foreground,
624: EditorStyleConstants.TopBorderLineColor,
625: foreground,
626: EditorStyleConstants.BottomBorderLineColor,
627: foreground);
628: }
629:
630: OffsetsBag nue = new OffsetsBag(doc);
631: int startOffset = region.getFirstRegionStartOffset();
632: int endOffset = region.getFirstRegionEndOffset();
633: int size = region.getFirstRegionLength();
634: if (size == 1) {
635: nue.addHighlight(startOffset, endOffset, attribsAll);
636: } else if (size > 1) {
637: nue.addHighlight(startOffset, startOffset + 1,
638: attribsLeft);
639: nue
640: .addHighlight(endOffset - 1, endOffset,
641: attribsRight);
642: if (size > 2) {
643: nue.addHighlight(startOffset + 1, endOffset - 1,
644: attribsMiddle);
645: }
646: }
647:
648: OffsetsBag bag = getHighlightsBag(doc);
649: bag.setHighlights(nue);
650: }
651: }
652:
653: // private static final AttributeSet defaultSyncedTextBlocksHighlight = AttributesUtilities.createImmutable(StyleConstants.Background, new Color(138, 191, 236));
654: private static final AttributeSet defaultSyncedTextBlocksHighlight = AttributesUtilities
655: .createImmutable(StyleConstants.Foreground, Color.red);
656:
657: private static AttributeSet getSyncedTextBlocksHighlight() {
658: FontColorSettings fcs = MimeLookup.getLookup(MimePath.EMPTY)
659: .lookup(FontColorSettings.class);
660: AttributeSet as = fcs != null ? fcs
661: .getFontColors("synchronized-text-blocks-ext") : null; //NOI18N
662: return as == null ? defaultSyncedTextBlocksHighlight : as;
663: }
664:
665: public static OffsetsBag getHighlightsBag(Document doc) {
666: OffsetsBag bag = (OffsetsBag) doc
667: .getProperty(InstantRenamePerformer.class);
668:
669: if (bag == null) {
670: doc.putProperty(InstantRenamePerformer.class,
671: bag = new OffsetsBag(doc));
672:
673: Object stream = doc
674: .getProperty(Document.StreamDescriptionProperty);
675:
676: if (stream instanceof DataObject) {
677: Logger.getLogger("TIMER").log(
678: Level.FINE,
679: "Instant Rename Highlights Bag",
680: new Object[] {
681: ((DataObject) stream).getPrimaryFile(),
682: bag }); //NOI18N
683: }
684: }
685:
686: return bag;
687: }
688:
689: }
|