org.netbeans.editor.CustomFoldManager Maven / Gradle / Ivy
/*
* 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.editor;
import javax.swing.text.Document;
import javax.swing.text.BadLocationException;
import javax.swing.text.Position;
import javax.swing.event.DocumentEvent;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import org.netbeans.api.editor.fold.Fold;
import org.netbeans.api.editor.fold.FoldHierarchy;
import org.netbeans.api.editor.fold.FoldType;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.spi.editor.fold.FoldHierarchyTransaction;
import org.netbeans.spi.editor.fold.FoldManager;
import org.netbeans.spi.editor.fold.FoldManagerFactory;
import org.netbeans.spi.editor.fold.FoldOperation;
import org.openide.util.RequestProcessor;
/**
* Fold maintainer that creates and updates custom folds.
*
* @author Dusan Balek, Miloslav Metelka
* @version 1.00
*/
final class CustomFoldManager implements FoldManager, Runnable {
private static final Logger LOG = Logger.getLogger(CustomFoldManager.class.getName());
public static final FoldType CUSTOM_FOLD_TYPE = new FoldType("custom-fold"); // NOI18N
private FoldOperation operation;
private Document doc;
private org.netbeans.editor.GapObjectArray markArray = new org.netbeans.editor.GapObjectArray();
private int minUpdateMarkOffset = Integer.MAX_VALUE;
private int maxUpdateMarkOffset = -1;
private List removedFoldList;
private HashMap customFoldId = new HashMap();
private static final RequestProcessor RP = new RequestProcessor(CustomFoldManager.class.getName(),
1, false, false);
private final RequestProcessor.Task task = RP.create(this);
public void init(FoldOperation operation) {
this.operation = operation;
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Initialized: {0}", System.identityHashCode(this));
}
}
private FoldOperation getOperation() {
return operation;
}
public void initFolds(FoldHierarchyTransaction transaction) {
doc = getOperation().getHierarchy().getComponent().getDocument();
task.schedule(300);
}
public void insertUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) {
processRemovedFolds(transaction);
task.schedule(300);
}
public void removeUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) {
processRemovedFolds(transaction);
removeAffectedMarks(evt, transaction);
task.schedule(300);
}
public void changedUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) {
}
public void removeEmptyNotify(Fold emptyFold) {
removeFoldNotify(emptyFold);
}
public void removeDamagedNotify(Fold damagedFold) {
removeFoldNotify(damagedFold);
}
public void expandNotify(Fold expandedFold) {
}
public void release() {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Released: {0}", System.identityHashCode(this));
}
}
public void run() {
if (operation.isReleased()) {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Update skipped, already relaesed: {0}", System.identityHashCode(this));
}
return;
}
((BaseDocument) doc).readLock();
try {
TokenHierarchy th = TokenHierarchy.get(doc);
if (th != null && th.isActive()) {
FoldHierarchy hierarchy = getOperation().getHierarchy();
hierarchy.lock();
try {
if (operation.isReleased()) {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Update skipped, already relaesed: {0}", System.identityHashCode(this));
}
return;
}
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Updating: {0}", System.identityHashCode(this));
}
FoldHierarchyTransaction transaction = getOperation().openTransaction();
try {
updateFolds(th.tokenSequence(), transaction);
} finally {
transaction.commit();
}
} finally {
hierarchy.unlock();
}
}
} finally {
((BaseDocument) doc).readUnlock();
}
}
private void removeFoldNotify(Fold removedFold) {
if (removedFoldList == null) {
removedFoldList = new ArrayList(3);
}
removedFoldList.add(removedFold);
}
private void removeAffectedMarks(DocumentEvent evt, FoldHierarchyTransaction transaction) {
int removeOffset = evt.getOffset();
int markIndex = findMarkIndex(removeOffset);
if (markIndex < getMarkCount()) {
FoldMarkInfo mark;
while (markIndex >= 0 && (mark = getMark(markIndex)).getOffset() == removeOffset) {
mark.release(false, transaction);
removeMark(markIndex);
markIndex--;
}
}
}
private void processRemovedFolds(FoldHierarchyTransaction transaction) {
if (removedFoldList != null) {
for (int i = removedFoldList.size() - 1; i >= 0; i--) {
Fold removedFold = (Fold)removedFoldList.get(i);
FoldMarkInfo startMark = (FoldMarkInfo)getOperation().getExtraInfo(removedFold);
if (startMark.getId() != null)
customFoldId.put(startMark.getId(), Boolean.valueOf(removedFold.isCollapsed())); // remember the last fold's state before remove
FoldMarkInfo endMark = startMark.getPairMark(); // get prior releasing
if (getOperation().isStartDamaged(removedFold)) { // start mark area was damaged
startMark.release(true, transaction); // forced remove
}
if (getOperation().isEndDamaged(removedFold)) {
endMark.release(true, transaction);
}
}
}
removedFoldList = null;
}
private void markUpdate(FoldMarkInfo mark) {
markUpdate(mark.getOffset());
}
private void markUpdate(int offset) {
if (offset < minUpdateMarkOffset) {
minUpdateMarkOffset = offset;
}
if (offset > maxUpdateMarkOffset) {
maxUpdateMarkOffset = offset;
}
}
private FoldMarkInfo getMark(int index) {
return (FoldMarkInfo)markArray.getItem(index);
}
private int getMarkCount() {
return markArray.getItemCount();
}
private void removeMark(int index) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Removing mark from ind=" + index + ": " + getMark(index)); // NOI18N
}
markArray.remove(index, 1);
}
private void insertMark(int index, FoldMarkInfo mark) {
markArray.insertItem(index, mark);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Inserted mark at ind=" + index + ": " + mark); // NOI18N
}
}
private int findMarkIndex(int offset) {
int markCount = getMarkCount();
int low = 0;
int high = markCount - 1;
while (low <= high) {
int mid = (low + high) / 2;
int midMarkOffset = getMark(mid).getOffset();
if (midMarkOffset < offset) {
low = mid + 1;
} else if (midMarkOffset > offset) {
high = mid - 1;
} else {
// mark starting exactly at the given offset found
// If multiple -> find the one with highest index
mid++;
while (mid < markCount && getMark(mid).getOffset() == offset) {
mid++;
}
mid--;
return mid;
}
}
return low; // return higher index (e.g. for insert)
}
private List getMarkList(TokenSequence seq) {
List markList = null;
for(seq.moveStart(); seq.moveNext(); ) {
Token token = seq.token();
FoldMarkInfo info;
try {
info = scanToken(token);
} catch (BadLocationException e) {
LOG.log(Level.WARNING, null, e);
info = null;
}
if (info != null) {
if (markList == null) {
markList = new ArrayList();
}
markList.add(info);
}
}
return markList;
}
private void processTokenList(TokenSequence seq, FoldHierarchyTransaction transaction) {
List markList = getMarkList(seq);
int markListSize;
if (markList != null && ((markListSize = markList.size()) > 0)) {
// Find the index for insertion
int offset = ((FoldMarkInfo)markList.get(0)).getOffset();
int arrayMarkIndex = findMarkIndex(offset);
// Remember the corresponding mark in the array as well
FoldMarkInfo arrayMark;
int arrayMarkOffset;
if (arrayMarkIndex < getMarkCount()) {
arrayMark = getMark(arrayMarkIndex);
arrayMarkOffset = arrayMark.getOffset();
} else { // at last mark
arrayMark = null;
arrayMarkOffset = Integer.MAX_VALUE;
}
for (int i = 0; i < markListSize; i++) {
FoldMarkInfo listMark = (FoldMarkInfo)markList.get(i);
int listMarkOffset = listMark.getOffset();
if (i == 0 || i == markListSize - 1) {
// Update the update-offsets by the first and last marks in the list
markUpdate(listMarkOffset);
}
while (listMarkOffset >= arrayMarkOffset) {
if (listMarkOffset == arrayMarkOffset) {
// At the same offset - likely the same mark
// -> retain the collapsed state
listMark.setCollapsed(arrayMark.isCollapsed());
}
if (!arrayMark.isReleased()) { // make sure that the mark is released
arrayMark.release(false, transaction);
}
removeMark(arrayMarkIndex);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Removed dup mark from ind=" + arrayMarkIndex + ": " + arrayMark); // NOI18N
}
if (arrayMarkIndex < getMarkCount()) {
arrayMark = getMark(arrayMarkIndex);
arrayMarkOffset = arrayMark.getOffset();
} else { // no more marks
arrayMark = null;
arrayMarkOffset = Integer.MAX_VALUE;
}
}
// Insert the listmark
insertMark(arrayMarkIndex, listMark);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Inserted mark at ind=" + arrayMarkIndex + ": " + listMark); // NOI18N
}
arrayMarkIndex++;
}
}
}
private void updateFolds(TokenSequence seq, FoldHierarchyTransaction transaction) {
if (seq != null && !seq.isEmpty()) {
processTokenList(seq, transaction);
}
if (maxUpdateMarkOffset == -1) { // no updates
return;
}
// Find the first mark to update and init the prevMark and parentMark prior the loop
int index = findMarkIndex(minUpdateMarkOffset);
FoldMarkInfo prevMark;
FoldMarkInfo parentMark;
if (index == 0) { // start from begining
prevMark = null;
parentMark = null;
} else {
prevMark = getMark(index - 1);
parentMark = prevMark.getParentMark();
}
// Iterate through the changed marks in the mark array
int markCount = getMarkCount();
while (index < markCount) { // process the marks
FoldMarkInfo mark = getMark(index);
// If the mark was released then it must be removed
if (mark.isReleased()) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Removing released mark at ind=" + index + ": " + mark); // NOI18N
}
removeMark(index);
markCount--;
continue;
}
// Update mark's status (folds, parentMark etc.)
if (mark.isStartMark()) { // starting a new fold
if (prevMark == null || prevMark.isStartMark()) { // new level
mark.setParentMark(prevMark); // prevMark == null means root level
parentMark = prevMark;
} // same level => parent to the parent of the prevMark
} else { // end mark
if (prevMark != null) {
if (prevMark.isStartMark()) { // closing nearest fold
prevMark.setEndMark(mark, false, transaction);
} else { // prevMark is end mark - closing its parent fold
if (parentMark != null) {
// mark's parent gets set as well
parentMark.setEndMark(mark, false, transaction);
parentMark = parentMark.getParentMark();
} else { // prevMark's parentMark is null (top level)
mark.makeSolitaire(false, transaction);
}
}
} else { // prevMark is null
mark.makeSolitaire(false, transaction);
}
}
// Set parent mark of the mark
mark.setParentMark(parentMark);
prevMark = mark;
index++;
}
minUpdateMarkOffset = Integer.MAX_VALUE;
maxUpdateMarkOffset = -1;
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("MARKS DUMP:\n" + this); //NOI18N
}
}
public @Override String toString() {
StringBuffer sb = new StringBuffer();
int markCount = getMarkCount();
int markCountDigitCount = Integer.toString(markCount).length();
for (int i = 0; i < markCount; i++) {
sb.append("["); // NOI18N
String iStr = Integer.toString(i);
appendSpaces(sb, markCountDigitCount - iStr.length());
sb.append(iStr);
sb.append("]:"); // NOI18N
FoldMarkInfo mark = getMark(i);
// Add extra indent regarding the depth in hierarchy
int indent = 0;
FoldMarkInfo parentMark = mark.getParentMark();
while (parentMark != null) {
indent += 4;
parentMark = parentMark.getParentMark();
}
appendSpaces(sb, indent);
sb.append(mark);
sb.append('\n');
}
return sb.toString();
}
private static void appendSpaces(StringBuffer sb, int spaces) {
while (--spaces >= 0) {
sb.append(' ');
}
}
private static Pattern pattern = Pattern.compile(
"(<\\s*editor-fold" +
// id="x"[opt] defaultstate="y"[opt] desc="z"[opt] defaultstate="a"[opt]
// id must be first, the rest of attributes in random order
"(?:(?:\\s+id=\"(\\S*)\")?(?:\\s+defaultstate=\"(\\S*?)\")?(?:\\s+desc=\"([\\S \\t]*?)\")?(?:\\s+defaultstate=\"(\\S*?)\")?)" +
"\\s*>)|(?:\\s*editor-fold\\s*>)"); // NOI18N
private FoldMarkInfo scanToken(Token token) throws BadLocationException {
// ignore any token that is not comment
if (token.id().primaryCategory() != null && token.id().primaryCategory().startsWith("comment")) { //NOI18N
Matcher matcher = pattern.matcher(token.text());
if (matcher.find()) {
if (matcher.group(1) != null) { // fold's start mark found
boolean state;
if (matcher.group(3) != null) {
state = "collapsed".equals(matcher.group(3)); // remember the defaultstate // NOI18N
} else {
state = "collapsed".equals(matcher.group(5));
}
if (matcher.group(2) != null) { // fold's id exists
Boolean collapsed = (Boolean)customFoldId.get(matcher.group(2));
if (collapsed != null)
state = collapsed.booleanValue(); // fold's state is already known from the past
else
customFoldId.put(matcher.group(2), Boolean.valueOf(state));
}
return new FoldMarkInfo(true, token.offset(null), matcher.end(0), matcher.group(2), state, matcher.group(4)); // NOI18N
} else { // fold's end mark found
return new FoldMarkInfo(false, token.offset(null), matcher.end(0), null, false, null);
}
}
}
return null;
}
private final class FoldMarkInfo {
private boolean startMark;
private Position pos;
private int length;
private String id;
private boolean collapsed;
private String description;
/** Matching pair mark used for fold construction */
private FoldMarkInfo pairMark;
/** Parent mark defining nesting in the mark hierarchy. */
private FoldMarkInfo parentMark;
/**
* Fold that corresponds to this mark (if it's start mark).
* It can be null if this mark is end mark or if it currently
* does not have the fold assigned.
*/
private Fold fold;
private boolean released;
private FoldMarkInfo(boolean startMark, int offset,
int length, String id, boolean collapsed, String description)
throws BadLocationException {
this.startMark = startMark;
this.pos = doc.createPosition(offset);
this.length = length;
this.id = id;
this.collapsed = collapsed;
this.description = description;
}
public String getId() {
return id;
}
public String getDescription() {
return description;
}
public boolean isStartMark() {
return startMark;
}
public int getLength() {
return length;
}
public int getOffset() {
return pos.getOffset();
}
public int getEndOffset() {
return getOffset() + getLength();
}
public boolean isCollapsed() {
return (fold != null) ? fold.isCollapsed() : collapsed;
}
public boolean hasFold() {
return (fold != null);
}
public void setCollapsed(boolean collapsed) {
this.collapsed = collapsed;
}
public boolean isSolitaire() {
return (pairMark == null);
}
public void makeSolitaire(boolean forced, FoldHierarchyTransaction transaction) {
if (!isSolitaire()) {
if (isStartMark()) {
setEndMark(null, forced, transaction);
} else { // end mark
getPairMark().setEndMark(null, forced, transaction);
}
}
}
public boolean isReleased() {
return released;
}
/**
* Release this mark and mark for update.
*/
public void release(boolean forced, FoldHierarchyTransaction transaction) {
if (!released) {
makeSolitaire(forced, transaction);
released = true;
markUpdate(this);
}
}
public FoldMarkInfo getPairMark() {
return pairMark;
}
private void setPairMark(FoldMarkInfo pairMark) {
this.pairMark = pairMark;
}
public void setEndMark(FoldMarkInfo endMark, boolean forced,
FoldHierarchyTransaction transaction) {
if (!isStartMark()) {
throw new IllegalStateException("Not start mark"); // NOI18N
}
if (pairMark == endMark) {
return;
}
if (pairMark != null) { // is currently paired to an end mark
releaseFold(forced, transaction);
pairMark.setPairMark(null);
}
pairMark = endMark;
if (endMark != null) {
if (!endMark.isSolitaire()) { // make solitaire first
endMark.makeSolitaire(false, transaction); // not forced here
}
endMark.setPairMark(this);
endMark.setParentMark(this.getParentMark());
ensureFoldExists(transaction);
}
}
public FoldMarkInfo getParentMark() {
return parentMark;
}
public void setParentMark(FoldMarkInfo parentMark) {
this.parentMark = parentMark;
}
private void releaseFold(boolean forced, FoldHierarchyTransaction transaction) {
if (isSolitaire() || !isStartMark()) {
throw new IllegalStateException();
}
if (fold != null) {
setCollapsed(fold.isCollapsed()); // serialize the collapsed info
if (!forced) {
getOperation().removeFromHierarchy(fold, transaction);
}
fold = null;
}
}
public Fold getFold() {
if (isSolitaire()) {
return null;
}
if (!isStartMark()) {
return pairMark.getFold();
}
return fold;
}
public void ensureFoldExists(FoldHierarchyTransaction transaction) {
if (isSolitaire() || !isStartMark()) {
throw new IllegalStateException();
}
if (fold == null) {
try {
if (!startMark) {
throw new IllegalStateException("Not start mark: " + this); // NOI18N
}
if (pairMark == null) {
throw new IllegalStateException("No pairMark for mark:" + this); // NOI18N
}
int startOffset = getOffset();
int startGuardedLength = getLength();
int endGuardedLength = pairMark.getLength();
int endOffset = pairMark.getOffset() + endGuardedLength;
fold = getOperation().addToHierarchy(
CUSTOM_FOLD_TYPE, getDescription(), collapsed,
startOffset, endOffset,
startGuardedLength, endGuardedLength,
this,
transaction
);
} catch (BadLocationException e) {
LOG.log(Level.WARNING, null, e);
}
}
}
public @Override String toString() {
StringBuffer sb = new StringBuffer();
sb.append(isStartMark() ? 'S' : 'E'); // NOI18N
// Check whether this mark (or its pair) has fold
if (hasFold() || (!isSolitaire() && getPairMark().hasFold())) {
sb.append("F"); // NOI18N
// Check fold's status
if (isStartMark() && (isSolitaire()
|| getOffset() != fold.getStartOffset()
|| getPairMark().getEndOffset() != fold.getEndOffset())
) {
sb.append("!!<"); // NOI18N
sb.append(fold.getStartOffset());
sb.append(","); // NOI18N
sb.append(fold.getEndOffset());
sb.append(">!!"); // NOI18N
}
}
// Append mark's internal status
sb.append(" ("); // NOI18N
sb.append("o="); // NOI18N
sb.append(pos.getOffset());
sb.append(", l="); // NOI18N
sb.append(length);
sb.append(", d='"); // NOI18N
sb.append(description);
sb.append('\'');
if (getPairMark() != null) {
sb.append(", <->"); // NOI18N
sb.append(getPairMark().getOffset());
}
if (getParentMark() != null) {
sb.append(", ^"); // NOI18N
sb.append(getParentMark().getOffset());
}
sb.append(')');
return sb.toString();
}
}
public static final class Factory implements FoldManagerFactory {
public FoldManager createFoldManager() {
return new CustomFoldManager();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy