/* * jEdit - Programmer's Text Editor * :tabSize=4:indentSize=4:noTabs=false: * :folding=explicit:collapseFolds=1: * * Copyright © 2010 jEdit contributors * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.gjt.sp.jedit.textarea; import java.util.Vector; import javax.swing.text.Segment; import org.gjt.sp.jedit.buffer.BufferAdapter; import org.gjt.sp.jedit.buffer.JEditBuffer; import org.gjt.sp.jedit.textarea.TextArea; /** Buffer Listener for Elastic Tabstops. */ public class ElasticTabStopBufferListener extends BufferAdapter { TextArea textArea; private boolean handledInsertion = true; private boolean handledDeletion = true; private boolean singleTabDeleted = false; //{{{ ElasticTabStopBufferListener() method public ElasticTabStopBufferListener(TextArea textArea) { this.textArea = textArea; }//}}} //{{{ fullyUpdateColumnBlocks() method private void fullyUpdateColumnBlocks(JEditBuffer buffer) { buffer.indentUsingElasticTabstops(); handledInsertion = true; handledDeletion = true; buffer.getColumnBlock().setDirtyStatus(false); textArea.chunkCache.invalidateChunksFromPhys(0); textArea.invalidateLineRange(0, buffer.getLineCount()-1); //textArea.getPainter().repaint(); }//}}} //{{{ contentInserted() method public void contentInserted(JEditBuffer buffer, int startLine, int offset, int numLines, int length) { if(!buffer.elasticTabstopsOn) { return; } String charDeleted; boolean isASimpleChar = false; boolean singleTabInserted = false; if((numLines==0)&(length==1)) { isASimpleChar = true; charDeleted = buffer.getText(offset, length); if(charDeleted.equals("\t")) { singleTabInserted = true; } } ColumnBlock rootBlock = buffer.getColumnBlock(); if(rootBlock==null) { return; } //System.out.println("BEFORE UPDATING COLUMN BLOCKS-----"); //System.out.println(rootBlock); int indexofBlockAbove = -1; ColumnBlock block = rootBlock.searchChildren(startLine); ColumnBlock blockjustAbove = null; boolean liesWithinBlock = false; int startIndex =-1; if(block!=null) { startIndex = rootBlock.getChildren().indexOf(block); indexofBlockAbove=startIndex-1; if(block.isLineWithinThisBlock(startLine)==0) { //if the line lies within this block we need to redraw it startIndex++; liesWithinBlock = true; } } else { startIndex = rootBlock.getChildren().size(); indexofBlockAbove = startIndex-1; } if(indexofBlockAbove>=0&&((ColumnBlock)(rootBlock.getChildren().get(indexofBlockAbove))).endLine+1==startLine) { blockjustAbove = (ColumnBlock)(rootBlock.getChildren().get(indexofBlockAbove)); } if(numLines>0) { rootBlock.endLine += numLines; for(int i=startIndex;i<rootBlock.getChildren().size();i++) { ((ColumnBlock)(rootBlock.getChildren().get(i))).updateLineNo(numLines); } } int startingLine =-1; int endLine = -1; if(liesWithinBlock) { ColumnBlock innerContainingBlock = block.getContainingBlock(startLine, offset); if((isASimpleChar)&&!(innerContainingBlock==null&&singleTabInserted)) { //a simple char has been entered (no newline ) //if this lies inside a column block update the startIndex and endIndex of this blocks corresponding ColumnBlockLine //and all subsequent ColumnBlock Lines after this one //check whether columnBlockWidth is valid //do nothing if this char does not lie inside a column block if(innerContainingBlock!=null) { if(!singleTabInserted) { innerContainingBlock.updateColumnBlockLineOffset(startLine, length, false); startingLine = innerContainingBlock.startLine; innerContainingBlock.setTabSizeDirtyStatus(true,false); endLine = innerContainingBlock.endLine; } else { //no need to update line offset as ColumnBlock would be rebuilt ColumnBlock innerParent = (ColumnBlock)innerContainingBlock.getParent(); //startingLine = innerContainingBlock.startLine; //endLine = innerContainingBlock.endLine; //innerParent.getChildren().remove(innerContainingBlock); startingLine = innerParent.startLine; endLine = innerParent.endLine; innerParent.getChildren().removeAllElements(); buffer.updateColumnBlocks(startingLine, endLine,(int)innerParent.columnBlockWidth , innerParent); } } /*else if(innerContainingBlock==null&&singleTabInserted) { //TODO handle this case when tab has been inserted in FRONT and not OUTSIDE of a column block //currently whole column block gets repainted in this case }*/ else { //this line must have been retokenized and repainted by the BufferHandler so repaint it again here after column blocks dirty status is updated startingLine = startLine; endLine = startLine; } } if((!isASimpleChar)||(innerContainingBlock==null&&singleTabInserted)) { startingLine = block.getStartLine(); endLine = block.getEndLine()+numLines; rootBlock.getChildren().remove(block); buffer.updateColumnBlocks(startingLine, endLine, 0, rootBlock); } } else { Segment seg = new Segment(); buffer.getText(offset, length, seg); if(buffer.getTabStopPosition(seg)>=0) { if(blockjustAbove!=null) { rootBlock.getChildren().remove(blockjustAbove); startingLine=blockjustAbove.startLine; } else { startingLine=startLine; } if((block!=null)&&(block.startLine==startLine+numLines+1)) { rootBlock.getChildren().remove(block); endLine = block.endLine; } else { endLine = startLine + numLines; } buffer.updateColumnBlocks(startingLine, endLine, 0, rootBlock); } } handledInsertion = true; rootBlock.setDirtyStatus(false); //System.out.println("AFTER UPDATING COLUMN BLOCKS-----"); //System.out.println(rootBlock); if(startingLine!=-1&&endLine!=-1&&handledDeletion) { textArea.chunkCache.invalidateChunksFromPhys(startingLine); textArea.invalidateLineRange(startingLine, endLine); } }//}}} //{{{ contentRemoved() method /** * Called when text is removed from the buffer. * @param buffer The buffer in question * @param startLine The first line * @param offset The start offset, from the beginning of the buffer * @param numLines The number of lines removed * @param length The number of characters removed * @since jEdit 4.3pre3 */ public void contentRemoved(JEditBuffer buffer, int startLine, int offset, int numLines, int length) { if(!buffer.elasticTabstopsOn) { return; } boolean isASimpleChar = false; ColumnBlock rootBlock = buffer.getColumnBlock(); if(rootBlock==null) { return; } if((numLines==0)&(length==1)) { isASimpleChar = true; } if((!isASimpleChar)) { //we need to remove column blocks //find the column block lying just below the first line deleted ColumnBlock firstBlockEffected = rootBlock.searchChildren(startLine); //info we need to determine inside this if block int startLineToBuild = -1; int endLineToBuild = -1; ColumnBlock firstBlockToBeUpdated = null; ColumnBlock firstBlockToBeRemoved = null; ColumnBlock lastBlockToBeRemoved = null; if(firstBlockEffected!=null) { int indexFirstBlockEffected =rootBlock.getChildren().indexOf(firstBlockEffected); ColumnBlock blockAboveFirstEffected = null; boolean justBelowBlock = false; if(indexFirstBlockEffected>0) { blockAboveFirstEffected = (ColumnBlock)rootBlock.getChildren().get(indexFirstBlockEffected-1); if(blockAboveFirstEffected.endLine==startLine-1 ) { justBelowBlock = true; } } int posFirstLine = firstBlockEffected.isLineWithinThisBlock(startLine); boolean firstLineLiesInside =posFirstLine==0; boolean firstLineLiesAbove =posFirstLine<0; int posLastLine = firstBlockEffected.isLineWithinThisBlock(startLine+numLines); boolean lastLineLiesInside =posLastLine==0; boolean lastLineLiesAbove = posLastLine<0; boolean lastLineLiesBelow = posLastLine>0; //deletion above block if(lastLineLiesAbove ) { //if last line lies above this block cannot be connected to a block above in this deletion without touching the block above /*if(justBelowBlock&&startLine+numLines+1==firstBlockEffected.startLine) { startLineToBuild=blockAboveFirstEffected.startLine; endLineToBuild= firstBlockEffected.endLine; firstBlockToBeRemoved = blockAboveFirstEffected; lastBlockToBeRemoved = firstBlockEffected; }*/ firstBlockToBeUpdated = firstBlockEffected; //else //{ firstBlockToBeRemoved =lastBlockToBeRemoved= null; startLineToBuild=endLineToBuild=-1; //} } //deletion inside block else if((firstLineLiesInside||firstLineLiesAbove)&&lastLineLiesInside) { startLineToBuild = Math.min( firstBlockEffected.startLine,startLine); endLineToBuild = firstBlockEffected.endLine-numLines; //if(indexFirstBlockEffected<rootBlock.getChildren().size()-1) //{ //firstBlockToBeUpdated =(ColumnBlock)rootBlock.getChildren().get(indexFirstBlockEffected+1) ; //} firstBlockToBeRemoved =lastBlockToBeRemoved= firstBlockEffected; if(justBelowBlock) { startLineToBuild =blockAboveFirstEffected.startLine ; firstBlockToBeRemoved = blockAboveFirstEffected; } } //deletion might cover other blocks as well else if(((firstLineLiesInside)||(firstLineLiesAbove))&&lastLineLiesBelow) { startLineToBuild = Math.min(startLine, firstBlockEffected.startLine); firstBlockToBeRemoved = firstBlockEffected; ColumnBlock blockBelow = rootBlock.searchChildren(startLine+numLines); int indexLastBlock = rootBlock.getChildren().indexOf(blockBelow); if(blockBelow!=null) { //deletion partially overlaps this block if(blockBelow.isLineWithinThisBlock(startLine+numLines)==0) { if(justBelowBlock) { startLineToBuild =blockAboveFirstEffected.startLine ; firstBlockToBeRemoved = blockAboveFirstEffected; } lastBlockToBeRemoved = blockBelow; endLineToBuild = blockBelow.endLine-numLines; //if(indexLastBlock<rootBlock.getChildren().size()-1) //{ //firstBlockToBeUpdated = (ColumnBlock)rootBlock.getChildren().get(indexLastBlock+1); //} } //deletion lies above this block else { //do not need to consider blockJustAbove here as we cannot connect two column blocks without //ending on one of the lines of either //firstBlockToBeUpdated = blockBelow; //if we have reached here there is surely a block above this one lastBlockToBeRemoved = (ColumnBlock)rootBlock.getChildren().get(indexLastBlock-1); //if the first Block is wholly covered then all column blocks are being deleted completely and there is nothing to build endLineToBuild = firstLineLiesAbove?-1:startLine; //consider the case where last line deleted is just above the column block block below if((blockBelow.startLine==startLine+numLines+1)&&(endLineToBuild!=-1)) { endLineToBuild = blockBelow.endLine-numLines; lastBlockToBeRemoved = blockBelow; } if(endLineToBuild==-1) { startLineToBuild = -1; } } } //no block below last line else { lastBlockToBeRemoved = (ColumnBlock)rootBlock.getChildren().get(rootBlock.getChildren().size()-1); //firstBlockToBeUpdated = null; if(firstLineLiesInside) { endLineToBuild = startLine; } else { startLineToBuild = -1; endLineToBuild= -1; } } } } //deletion lies below all column blocks else { startLineToBuild = -1; endLineToBuild = -1; //firstBlockToBeUpdated = null; firstBlockToBeRemoved = null; lastBlockToBeRemoved = null; } //once we reach here we have three things to do //1)delete columnBlocks using firstBlockToBeDeleted and lastBlockToBeDeleted Vector<Node> blocksToBeRemoved =null; if(firstBlockToBeRemoved!=null) { int startIndex = rootBlock.getChildren().indexOf(firstBlockToBeRemoved); blocksToBeRemoved = new Vector<Node>(); if(lastBlockToBeRemoved == null) { throw new IllegalArgumentException("Deletion not handled properly"); } int endIndex = rootBlock.getChildren().indexOf(lastBlockToBeRemoved); for(int i=startIndex;i<=endIndex;i++) { blocksToBeRemoved.add(rootBlock.getChildren().get(i)); } } //2)update startLine/endLine in column blocks using firstBlockToBeUpdated if(numLines>0) { rootBlock.endLine-=numLines; if((lastBlockToBeRemoved!=null)||(firstBlockToBeUpdated!=null)) { int startIndex=-1; if(lastBlockToBeRemoved!=null) { startIndex = rootBlock.getChildren().indexOf(lastBlockToBeRemoved); //start just after the last block to be removed startIndex++; } else if(firstBlockToBeUpdated!=null) { startIndex = rootBlock.getChildren().indexOf(firstBlockToBeUpdated); } for(int i=startIndex;i<rootBlock.getChildren().size();i++) { ((ColumnBlock)rootBlock.getChildren().get(i)).updateLineNo(-1*numLines); } } } //once we are done with (2) we can safely change rootBlock if(blocksToBeRemoved!=null) { rootBlock.getChildren().removeAll(blocksToBeRemoved); } //3)rebuild column blocks using endLine and startLine if(startLineToBuild!=-1&&endLineToBuild!=-1) { buffer.updateColumnBlocks(startLineToBuild, endLineToBuild, 0, rootBlock); rootBlock.setDirtyStatus(false); textArea.chunkCache.invalidateChunksFromPhys(startLineToBuild); textArea.invalidateLineRange(startLineToBuild, endLineToBuild); } rootBlock.setDirtyStatus(false); handledDeletion = true; } else { int startingLine = -1; int endLine = -1; //a simple char has been entered //if this lies inside a column block update the startIndex and endIndex of this blocks corresponding ColumnBlockLine //and all subsequent ColumnBlock Lines after this one //check whether columnBlockWidth is valid ColumnBlock innerContainingBlock = rootBlock.getContainingBlock(startLine, offset); //do nothing if this char does not lie inside a column block if(innerContainingBlock!=null) { if(!singleTabDeleted) { innerContainingBlock.updateColumnBlockLineOffset(startLine, -1*length, false); startingLine = innerContainingBlock.startLine; endLine = innerContainingBlock.endLine; innerContainingBlock.setTabSizeDirtyStatus(true,false); } else { //no need to update line offset as ColumnBlock would be rebuilt ColumnBlock innerParent = (ColumnBlock)innerContainingBlock.getParent(); startingLine = innerContainingBlock.startLine; endLine = innerContainingBlock.endLine; innerParent.getChildren().remove(innerContainingBlock); //startingLine = innerParent.startLine; //endLine = innerParent.endLine; //innerParent.getChildren().removeAllElements(); buffer.updateColumnBlocks(startingLine, endLine,(int)innerParent.columnBlockWidth , innerParent); } } else { //this line must have been retokenized and repainted by the BufferHandler so repaint it again here after column blocks dirty status is updated startingLine = startLine; endLine = startLine; } handledDeletion = true; rootBlock.setDirtyStatus(false); if(startingLine!=-1&&endLine!=-1) { textArea.chunkCache.invalidateChunksFromPhys(startingLine); textArea.invalidateLineRange(startingLine, endLine); } } }//}}} //{{{ transactionComplete() method public void transactionComplete(JEditBuffer buffer) { if(!buffer.getBooleanProperty("elasticTabstops")) { return; } if((buffer.getBooleanProperty("elasticTabstops"))&&((!handledInsertion)||(!handledDeletion))) { //if we reach here use brute force as a last resolve fullyUpdateColumnBlocks(buffer); } } //}}} //{{{ preContentInserted() method public void preContentInserted(JEditBuffer buffer, int startLine, int offset, int numLines, int length) { if(!buffer.elasticTabstopsOn) { return; } handledInsertion = false; if(buffer.getColumnBlock()!=null) buffer.getColumnBlock().setDirtyStatus(true); } //}}} //{{{ preContentRemoved() method public void preContentRemoved(JEditBuffer buffer, int startLine, int offset, int numLines, int length) { if(!buffer.elasticTabstopsOn) { return; } handledDeletion = false; singleTabDeleted = false; if(buffer.getColumnBlock()!=null) { buffer.getColumnBlock().setDirtyStatus(true); if((numLines==0)&(length==1)) { String str = buffer.getText(offset, length); if(str.equals("\t")) { singleTabDeleted = true; } } } } //}}} }