/*
* 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;
}
}
}
}
//}}}
}