/*
 * JEditBuffer.java - jEdit buffer
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 1998, 2005 Slava Pestov
 * Portions copyright (C) 1999, 2000 mike dillon
 *
 * 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.buffer;

//{{{ Imports
import org.gjt.sp.jedit.*;
import org.gjt.sp.jedit.indent.IndentAction;
import org.gjt.sp.jedit.indent.IndentRule;
import org.gjt.sp.jedit.syntax.*;
import org.gjt.sp.jedit.textarea.ColumnBlock;
import org.gjt.sp.jedit.textarea.ColumnBlockLine;
import org.gjt.sp.jedit.textarea.Node;
import org.gjt.sp.jedit.textarea.Selection;
import org.gjt.sp.jedit.textarea.TextArea;
import org.gjt.sp.util.IntegerArray;
import org.gjt.sp.util.Log;
import org.gjt.sp.util.StandardUtilities;

import javax.annotation.Nonnull;
import javax.swing.text.Position;
import javax.swing.text.Segment;
import java.util.*;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Pattern;

import static org.gjt.sp.jedit.LargeFileMode.ask;
import static org.gjt.sp.jedit.buffer.WordWrap.none;
//}}}

/**
 * A <code>JEditBuffer</code> represents the contents of an open text
 * file as it is maintained in the computer's memory (as opposed to
 * how it may be stored on a disk).<p>
 *
 * This class is partially thread-safe, however you must pay attention to two
 * very important guidelines:
 * <ul>
 * <li>Operations such as insert() and remove(),
 * undo(), change Buffer data in a writeLock(), and must
 * be called from the AWT thread.
 * <li>When accessing the buffer from another thread, you must
 * call readLock() before and readUnLock() after,  if you plan on performing
 * more than one read, to ensure that  the buffer contents are not changed by
 * the AWT thread for the duration of the lock. Only methods whose descriptions
 * specify thread safety can be invoked from other threads.
 * </ul>
 *
 * @author Slava Pestov
 * @version $Id: JEditBuffer.java 25698 2023-11-17 01:31:50Z vampire0 $
 *
 * @since jEdit 4.3pre3
 */
public class JEditBuffer
{
	/**
	 * Line separator property.
	 */
	public static final String LINESEP = "lineSeparator";

	/**
	 * Character encoding used when loading and saving.
	 * @since jEdit 3.2pre4
	 */
	public static final String ENCODING = "encoding";
	/**
	 * Wrap mode, the value must be one of {@link org.gjt.sp.jedit.buffer.WordWrap}
	 * @since jEdit 5.7pre1
	 */
	public static final String WRAP = "wrap";
	/**
	 * This property contains the large file mode option for this buffer.
	 * @since jEdit 5.7pre1
	 */
	public static final String LARGE_MODE_FILE = "largefilemode";

	//{{{ JEditBuffer constructors
	{
		bufferListeners = new Vector<>();
		lock = new ReentrantReadWriteLock();
		contentMgr = new ContentManager();
		lineMgr = new LineManager();
		positionMgr = new PositionManager(this);
		undoMgr = new UndoManager(this);
		integerArray = new IntegerArray();
		propertyLock = new Object();
		properties = new HashMap<>();
	}

	@SuppressWarnings({"unchecked"})
	public JEditBuffer(Map props)
	{
		//{{{ need to convert entries of 'props' to PropValue instances
		Set<Map.Entry> set = props.entrySet();
		for (Map.Entry entry : set)
		{
			properties.put(entry.getKey(),new PropValue(entry.getValue(),false));
		} //}}}

		// fill in defaults for these from system properties if the
		// corresponding buffer.XXX properties not set
		if(!hasProperty(ENCODING))
			properties.put(ENCODING,new PropValue(System.getProperty("file.encoding"),false));
		if(!hasProperty(LINESEP))
			properties.put(LINESEP,new PropValue(System.getProperty("line.separator"),false));

		setFoldHandler(new DummyFoldHandler());
	}

	/**
	 * Create a new JEditBuffer.
	 * It is used by independent textarea only
	 */
	public JEditBuffer()
	{
		setWordWrap(none);
		properties.put("folding",new PropValue("none",false));
		tokenMarker = new TokenMarker();
		tokenMarker.addRuleSet(new ParserRuleSet("text","MAIN"));
		setTokenMarker(tokenMarker);

		loadText(null,null);
		// corresponding buffer.XXX properties not set
		if(!hasProperty(ENCODING))
			properties.put(ENCODING,new PropValue(System.getProperty("file.encoding"),false));
		if(!hasProperty(LINESEP))
			properties.put(LINESEP,new PropValue(System.getProperty("line.separator"),false));

		setFoldHandler(new DummyFoldHandler());
	} //}}}

	//{{{ Flags

	//{{{ isDirty() method
	/**
	 * @return whether there have been unsaved changes to this buffer.
	 * This method is thread-safe.
	 */
	public boolean isDirty()
	{
		return dirty;
	} //}}}

	//{{{ isLoading() method
	public boolean isLoading()
	{
		return loading;
	} //}}}

	//{{{ setLoading() method
	public void setLoading(boolean loading)
	{
		this.loading = loading;
	} //}}}

	//{{{ isElasticTabstopsOn() method
	public boolean isElasticTabstopsOn()
	{
		return elasticTabstopsOn;
	} //}}}

	//{{{ setElasticTabstopsOn() method
	public void setElasticTabstopsOn(boolean elasticTabstopsOn)
	{
		this.elasticTabstopsOn = elasticTabstopsOn;
	} //}}}

	//{{{ isPerformingIO() method
	/**
	 * @return true if the buffer is currently performing I/O.
	 * This method is thread-safe.
	 * @since jEdit 2.7pre1
	 */
	public boolean isPerformingIO()
	{
		return isLoading() || io;
	} //}}}

	//{{{ setPerformingIO() method
	/**
	 * @param io true if the buffer is currently performing I/O.
	 * This method is thread-safe.
	 * @since jEdit 2.7pre1
	 */
	public void setPerformingIO(boolean io)
	{
		this.io = io;
	} //}}}

	//{{{ isEditable() method
	/**
	 * @return true if this file is editable, false otherwise. A file may
	 * become uneditable if it is read only, or if I/O is in progress.
	 * This method is thread-safe.
	 * @since jEdit 2.7pre1
	 */
	public boolean isEditable()
	{
		return !(isPerformingIO()) && editable;
	} //}}}

	//{{{ setEditable() method
	/**
	 * @param editable {@code true} to set the buffer editable, {@code false} otherwise. Default is {@code true}.
	 * This does not change the read only flag on the file, just makes the buffer
	 * editable or not.
	 */
	public void setEditable(boolean editable)
	{
		this.editable = editable;
	} //}}}

	//{{{ isReadOnly() method
	/**
	 * @return true if this file is read only, false otherwise.
	 * This method is thread-safe.
	 */
	public boolean isReadOnly()
	{
		return readOnly || readOnlyOverride;
	} //}}}

	//{{{ setReadOnly() method
	/**
	 * Sets the read only flag.
	 * @param readOnly The read only flag
	 */
	public void setReadOnly(boolean readOnly)
	{
		readOnlyOverride = readOnly;
	} //}}}

	//{{{ setDirty() method
	/**
	 * @param d Sets the 'dirty' (changed since last save) flag of this buffer.
	 */
	public void setDirty(boolean d)
	{
		boolean editable = isEditable();

		if(d)
		{
			if(editable)
				dirty = true;
		}
		else
		{
			dirty = false;

			// fixes dirty flag not being reset on
			// save/insert/undo/redo/undo
			if(!isUndoInProgress())
			{
				// this ensures that undo can clear the dirty flag properly
				// when all edits up to a save are undone
				undoMgr.resetClearDirty();
			}
		}
	} //}}}

	//}}}

	//{{{ Thread safety

	//{{{ readLock() method
	/**
	 * The buffer is guaranteed not to change between calls to
	 * {@link #readLock()} and {@link #readUnlock()}.
	 * Calls to this method may be nested.
	 */
	public void readLock()
	{
		lock.readLock().lock();
	} //}}}

	//{{{ readUnlock() method
	/**
	 * The buffer is guaranteed not to change between calls to
	 * {@link #readLock()} and {@link #readUnlock()}.
	 */
	public void readUnlock()
	{
		lock.readLock().unlock();
	} //}}}

	//{{{ writeLock() method
	/**
	 * Attempting to obtain read lock will block between calls to
	 * {@link #writeLock()} and {@link #writeUnlock()}.
	 * Calls to this method may be nested.
	 */
	public void writeLock()
	{
		lock.writeLock().lock();
	} //}}}

	//{{{ writeUnlock() method
	/**
	 * Attempting to obtain read lock will block between calls to
	 * {@link #writeLock()} and {@link #writeUnlock()}.
	 */
	public void writeUnlock()
	{
		lock.writeLock().unlock();
	} //}}}

	//}}}

	//{{{ Line offset methods

	//{{{ getLength() method
	/**
	 * @return the number of characters in the buffer. This method is thread-safe.
	 */
	public int getLength()
	{
		// no need to lock since this just returns a value and that's it
		return contentMgr.getLength();
	} //}}}

	//{{{ getLineCount() method
	/**
	 * @return the number of physical lines in the buffer.
	 * This method is thread-safe.
	 * @since jEdit 3.1pre1
	 */
	public int getLineCount()
	{
		// no need to lock since this just returns a value and that's it
		return lineMgr.getLineCount();
	} //}}}

	//{{{ getLineOfOffset() method
	/**
	 * Returns the line containing the specified offset.
	 * This method is thread-safe.
	 * @param offset The offset
	 * @return the line of the offset
	 * @since jEdit 4.0pre1
	 */
	public int getLineOfOffset(int offset)
	{
		try
		{
			readLock();

			if(offset < 0 || offset > getLength())
				throw new ArrayIndexOutOfBoundsException(offset);

			return lineMgr.getLineOfOffset(offset);
		}
		finally
		{
			readUnlock();
		}
	} //}}}

	//{{{ getLineStartOffset() method
	/**
	 * Returns the start offset of the specified line.
	 * This method is thread-safe.
	 * @param line The line
	 * @return The start offset of the specified line, that is the offset
	 * after the end-of-line character before that line.
	 * @since jEdit 4.0pre1
	 */
	public int getLineStartOffset(int line)
	{
		try
		{
			readLock();

			if(line < 0 || line >= lineMgr.getLineCount())
				throw new ArrayIndexOutOfBoundsException(line);
			else if(line == 0)
				return 0;

			return lineMgr.getLineEndOffset(line - 1);
		}
		finally
		{
			readUnlock();
		}
	} //}}}

	//{{{ getLineEndOffset() method
	/**
	 * Returns the end offset of the specified line.
	 * This method is thread-safe.
	 * @param line The line
	 * @return The end offset of the specified line, that is the offset
	 * after the end-of-line character. Note that
	 * <code>buffer.getLineOfOffset(buffer.getLineEndOffset(x))</code>
	 * does not return <code>x</code> but <code>x+1</code>.
	 * @since jEdit 4.0pre1
	 */
	public int getLineEndOffset(int line)
	{
		try
		{
			readLock();

			if(line < 0 || line >= lineMgr.getLineCount())
				throw new ArrayIndexOutOfBoundsException(line);

			return lineMgr.getLineEndOffset(line);
		}
		finally
		{
			readUnlock();
		}
	} //}}}

	//{{{ getLineLength() method
	/**
	 * @return the length of the specified line.
	 * This method is thread-safe.
	 * @param line The line
	 * @since jEdit 4.0pre1
	 */
	public int getLineLength(int line)
	{
		try
		{
			readLock();

			return getLineEndOffset(line)
				- getLineStartOffset(line) - 1;
		}
		finally
		{
			readUnlock();
		}
	} //}}}

	//{{{ getPriorNonEmptyLine() method
	/**
	 * Auto indent needs this.
	 * @param lineIndex the line
	 * @return the previous non-empty line
	 */
	public int getPriorNonEmptyLine(int lineIndex)
	{
		if (!mode.getIgnoreWhitespace())
		{
			return lineIndex - 1;
		}

		int returnValue = -1;
		for(int i = lineIndex - 1; i >= 0; i--)
		{
			Segment seg = new Segment();
			getLineText(i,seg);
			if(seg.count != 0)
				returnValue = i;
			for(int j = 0; j < seg.count; j++)
			{
				char ch = seg.array[seg.offset + j];
				if(!Character.isWhitespace(ch))
					return i;
			}
		}

		// didn't find a line that contains non-whitespace chars
		// so return index of prior whitespace line
		return returnValue;
	} //}}}

	//}}}

	//{{{ Text getters and setters

	//{{{ getLineText() methods
	/**
	 * Returns the text on the specified line.
	 * This method is thread-safe.
	 * @param line The line
	 * @return The text, or null if the line is invalid
	 * @since jEdit 4.0pre1
	 */
	public String getLineText(int line)
	{
		if(line < 0 || line >= lineMgr.getLineCount())
			throw new ArrayIndexOutOfBoundsException(line);

		try
		{
			readLock();

			int start = line == 0 ? 0 : lineMgr.getLineEndOffset(line - 1);
			int end = lineMgr.getLineEndOffset(line);

			return getText(start,end - start - 1);
		}
		finally
		{
			readUnlock();
		}
	}

	/**
	 * Returns the specified line in a <code>Segment</code>.<p>
	 *
	 * Using a <code>Segment</code> is generally more
	 * efficient than using a <code>String</code> because it
	 * results in less memory allocation and array copying.<p>
	 *
	 * This method is thread-safe.
	 *
	 * @param line The line
	 * @param segment the segment
	 * @since jEdit 4.0pre1
	 */
	public void getLineText(int line, Segment segment)
	{
		getLineText(line, 0, segment);
	}

	/**
	 * Returns the specified line from the starting point passed in relativeStartOffset  in a <code>Segment</code>.<p>
	 *
	 * Using a <code>Segment</code> is generally more
	 * efficient than using a <code>String</code> because it
	 * results in less memory allocation and array copying.<p>
	 *
	 * This method is thread-safe.
	 *
	 * @param line The line
	 * @param segment the segment
	 * @param relativeStartOffset the relative start offset
	 * @since jEdit 4.0pre1
	 */
	public void getLineText(int line,int relativeStartOffset, Segment segment)
	{
		if(line < 0 || line >= lineMgr.getLineCount())
			throw new ArrayIndexOutOfBoundsException(line);

		try
		{
			readLock();

			int start = (line == 0 ? 0 : lineMgr.getLineEndOffset(line - 1));
			int end = lineMgr.getLineEndOffset(line);
			if((start+relativeStartOffset)>end)
			{
				throw new IllegalArgumentException("This index is outside the line length (start+relativeOffset):"+start+" + "+relativeStartOffset+" > "+"endffset:"+end);
			}
			else
			{
				getText(start+relativeStartOffset,end - start -relativeStartOffset- 1,segment);
			}
		}
		finally
		{
			readUnlock();
		}
	} //}}}

	//{{{ getLineSegment() method
	/**
	 * Returns the text on the specified line.
	 * This method is thread-safe.
	 *
	 * @param line The line index.
	 * @return The text, or null if the line is invalid
	 *
	 * @since jEdit 4.3pre15
	 */
	public CharSequence getLineSegment(int line)
	{
		if(line < 0 || line >= lineMgr.getLineCount())
			throw new ArrayIndexOutOfBoundsException(line);

		try
		{
			readLock();

			int start = line == 0 ? 0 : lineMgr.getLineEndOffset(line - 1);
			int end = lineMgr.getLineEndOffset(line);

			return getSegment(start,end - start - 1);
		}
		finally
		{
			readUnlock();
		}
	} //}}}

	//{{{ getText() methods
	/**
	 * @return the specified text range. This method is thread-safe.
	 * @param start The start offset
	 * @param length The number of characters to get
	 */
	public String getText(int start, int length)
	{
		try
		{
			readLock();

			if(start < 0 || length < 0
				|| start + length > contentMgr.getLength())
				throw new ArrayIndexOutOfBoundsException(start + ":" + length);

			return contentMgr.getText(start,length);
		}
		finally
		{
			readUnlock();
		}
	}

	/**
	 * @return the full buffer content. This method is thread-safe
	 * @since 4.4.1
	 */
	public String getText()
	{
		try
		{
			readLock();
			return contentMgr.getText(0, getLength());
		}
		finally
		{
			readUnlock();
		}
	}

	/**
	 * Returns the specified text range in a <code>Segment</code>.<p>
	 *
	 * Using a <code>Segment</code> is generally more
	 * efficient than using a <code>String</code> because it
	 * results in less memory allocation and array copying.<p>
	 *
	 * This method is thread-safe.
	 *
	 * @param start The start offset
	 * @param length The number of characters to get
	 * @param seg The segment to copy the text to
	 */
	public void getText(int start, int length, Segment seg)
	{
		try
		{
			readLock();

			if(start < 0 || length < 0
				|| start + length > contentMgr.getLength())
				throw new ArrayIndexOutOfBoundsException(start + ":" + length);

			contentMgr.getText(start,length,seg);
		}
		finally
		{
			readUnlock();
		}
	} //}}}

	//{{{ getSegment() method
	/**
	 * Returns the specified text range. This method is thread-safe.
	 * It doesn't copy the text
	 *
	 * @param start The start offset
	 * @param length The number of characters to get
	 *
	 * @return a CharSequence that contains the text wanted text
	 * @since jEdit 4.3pre15
	 */
	public CharSequence getSegment(int start, int length)
	{
		try
		{
			readLock();

			if(start < 0 || length < 0
				|| start + length > contentMgr.getLength())
				throw new ArrayIndexOutOfBoundsException(start + ":" + length);

			return contentMgr.getSegment(start,length);
		}
		finally
		{
			readUnlock();
		}
	} //}}}

	//{{{ insert() methods
	/**
	 * Inserts a string into the buffer.
	 * @param offset The offset
	 * @param str The string
	 * @since jEdit 4.0pre1
	 */
	public void insert(int offset, String str)
	{
		insert(offset, (CharSequence) str);
	}

	/**
	 * Inserts a string into the buffer.
	 * @param offset The offset
	 * @param seg The segment
	 * @since jEdit 4.0pre1
	 */
	public void insert(int offset, Segment seg)
	{
		insert(offset, (CharSequence) seg);
	}

	/**
	 * Inserts a string into the buffer.
	 * @param offset The offset
	 * @param seq The charsequence
	 * @since jEdit 5.0pre1
	 */
	public void insert(int offset, CharSequence seq)
	{
		if(seq == null)
			return;

		int len = seq.length();

		if(len == 0)
			return;

		try
		{
			writeLock();

			if(offset < 0 || offset > contentMgr.getLength())
				throw new ArrayIndexOutOfBoundsException(offset);

			contentMgr.insert(offset,seq);

			integerArray.clear();

			for(int i = 0; i < len; i++)
			{
				if(seq.charAt(i) == '\n')
					integerArray.add(i + 1);
			}

			if(!undoInProgress)
			{
				undoMgr.contentInserted(offset,len,
							seq.toString(),!dirty);
			}

			contentInserted(offset,len,integerArray);
		}
		finally
		{
			writeUnlock();
		}
	} //}}}

	//{{{ remove() method
	/**
	 * Removes the specified rang efrom the buffer.
	 * @param offset The start offset
	 * @param length The number of characters to remove
	 */
	public void remove(int offset, int length)
	{
		if(length == 0)
			return;

		try
		{
			transaction = true;

			writeLock();

			if(offset < 0 || length < 0
				|| offset + length > contentMgr.getLength())
				throw new ArrayIndexOutOfBoundsException(offset + ":" + length);

			int startLine = lineMgr.getLineOfOffset(offset);
			int endLine = lineMgr.getLineOfOffset(offset + length);

			int numLines = endLine - startLine;

			if(!undoInProgress && !loading)
			{
				undoMgr.contentRemoved(offset,length,
					getText(offset,length),
					!dirty);
			}

			firePreContentRemoved(startLine,offset,numLines,length);

			contentMgr.remove(offset,length);
			lineMgr.contentRemoved(startLine,offset,numLines,length);
			positionMgr.contentRemoved(offset,length);

			setDirty(true);

			fireContentRemoved(startLine,offset,numLines,length);

			/* otherwise it will be delivered later */
			if(!undoInProgress && !insideCompoundEdit())
				fireTransactionComplete();

		}
		finally
		{
			transaction = false;

			writeUnlock();
		}
	} //}}}

	//}}}

	//{{{ Indentation

	//{{{ removeTrailingWhiteSpace() method
	/**
	 * Removes trailing whitespace from all lines in the specified list.
	 * @param lines The line numbers
	 * @since jEdit 3.2pre1
	 */
	public void removeTrailingWhiteSpace(int[] lines)
	{
		try
		{
			beginCompoundEdit();

			for (int line : lines)
			{
				Segment seg = new Segment();
				getLineText(line, seg);

				// blank line
				if (seg.count == 0)
					continue;

				int lineStart = seg.offset;
				int lineEnd = seg.offset + seg.count - 1;

				int pos;
				for (pos = lineEnd; pos >= lineStart; pos--)
				{
					if (!Character.isWhitespace(seg.array[pos]))
						break;
				}

				int tail = lineEnd - pos;

				// no whitespace
				if (tail == 0)
					continue;

				remove(getLineEndOffset(line) - 1 - tail, tail);
			}
		}
		finally
		{
			endCompoundEdit();
		}
	} //}}}

	//{{{ shiftIndentLeft() method
	/**
	 * Shifts the indent of each line in the specified list to the left.
	 * @param lines The line numbers
	 * @since jEdit 3.2pre1
	 */
	public void shiftIndentLeft(int[] lines)
	{
		int tabSize = getTabSize();
		int indentSize = getIndentSize();
		boolean noTabs = getBooleanProperty("noTabs");

		try
		{
			beginCompoundEdit();

			for (int l : lines)
			{
				int lineStart = getLineStartOffset(l);
				CharSequence line = getLineSegment(l);
				int whiteSpace = StandardUtilities.getLeadingWhiteSpace(line);
				if (whiteSpace == 0) continue;
				int whiteSpaceWidth = Math.max(0, StandardUtilities
									  .getLeadingWhiteSpaceWidth(line, tabSize)
								  - indentSize);

				insert(lineStart + whiteSpace,
				       StandardUtilities.createWhiteSpace(whiteSpaceWidth, noTabs ? 0 : tabSize));
				remove(lineStart, whiteSpace);
			}

		}
		finally
		{
			endCompoundEdit();
		}
	} //}}}

	//{{{ shiftIndentRight() method
	/**
	 * Shifts the indent of each line in the specified list to the right.
	 * @param lines The line numbers
	 * @since jEdit 3.2pre1
	 */
	public void shiftIndentRight(int[] lines)
	{
		try
		{
			beginCompoundEdit();

			int tabSize = getTabSize();
			int indentSize = getIndentSize();
			boolean noTabs = getBooleanProperty("noTabs");
			for (int l : lines)
			{
				int lineStart = getLineStartOffset(l);
				CharSequence line = getLineSegment(l);
				int whiteSpace = StandardUtilities.getLeadingWhiteSpace(line);

				// silly usability hack
				//if(lines.length != 1 && whiteSpace == 0)
				//	continue;

				int whiteSpaceWidth =
					StandardUtilities.getLeadingWhiteSpaceWidth(line, tabSize) + indentSize;
				insert(lineStart + whiteSpace,
				       StandardUtilities.createWhiteSpace(whiteSpaceWidth, noTabs ? 0 : tabSize));
				remove(lineStart, whiteSpace);
			}
		}
		finally
		{
			endCompoundEdit();
		}
	} //}}}

	//{{{ indentLines() methods
	/**
	 * Indents all specified lines.
	 * @param start The first line to indent
	 * @param end The last line to indent
	 * @since jEdit 3.1pre3
	 */
	public void indentLines(int start, int end)
	{
		try
		{
			beginCompoundEdit();
			for(int i = start; i <= end; i++)
				indentLine(i,true);
		}
		finally
		{
			endCompoundEdit();
		}
	}

	/**
	 * Indents all specified lines.
	 * @param lines The line numbers
	 * @since jEdit 3.2pre1
	 */
	public void indentLines(int[] lines)
	{
		try
		{
			beginCompoundEdit();
			for (int line : lines)
				indentLine(line, true);
		}
		finally
		{
			endCompoundEdit();
		}
	} //}}}

	//{{{ simpleIndentLine() method
	/**
	 * Simply indents the given line to the same level as the previous nonempty line
	 * @param lineIndex The line number to indent
	 * @since jEdit 5.0
	 */
	public void simpleIndentLine(int lineIndex)
	{
		int[] whitespaceChars = new int[1];

		int prevLineIndex = getPriorNonEmptyLine(lineIndex);
		if (prevLineIndex == -1)
			return;

		String indentString = StandardUtilities.getIndentString(
			getLineText(prevLineIndex));

		// Do it
		try
		{
			beginCompoundEdit();

			int start = getLineStartOffset(lineIndex);
			remove(start, whitespaceChars[0]);
			insert(start, indentString);
		}
		finally
		{
			endCompoundEdit();
		}
	} //}}}

	//{{{ indentLine() methods
	/**
	 * Indents the specified line.
	 * @param lineIndex The line number to indent
	 * @param canDecreaseIndent If true, the indent can be decreased as a
	 * result of this. Set this to false for Tab key.
	 * @return true If indentation took place, false otherwise.
	 * @since jEdit 4.2pre2
	 */
	public boolean indentLine(int lineIndex, boolean canDecreaseIndent)
	{
		int[] whitespaceChars = new int[1];
		int currentIndent = getCurrentIndentForLine(lineIndex,
			whitespaceChars);
		int prevLineIndex = getPriorNonEmptyLine(lineIndex);
		int prevLineIndent = (prevLineIndex == -1) ? 0 :
			StandardUtilities.getLeadingWhiteSpaceWidth(getLineSegment(
				prevLineIndex), getTabSize());
		int idealIndent = getIdealIndentForLine(lineIndex, prevLineIndex,
			prevLineIndent);

		if (idealIndent == -1 || idealIndent == currentIndent ||
			(!canDecreaseIndent && idealIndent < currentIndent))
			return false;

		// Do it
		try
		{
			beginCompoundEdit();

			int start = getLineStartOffset(lineIndex);

			remove(start,whitespaceChars[0]);
			String prevIndentString = (prevLineIndex >= 0) ?
				StandardUtilities.getIndentString(getLineText(
					prevLineIndex)) : null;
			String indentString;
			if (prevIndentString == null)
			{
				indentString = StandardUtilities.createWhiteSpace(
					idealIndent,
					getBooleanProperty("noTabs") ? 0 : getTabSize());
			}
			else if (idealIndent == prevLineIndent)
				indentString = prevIndentString;
			else if (idealIndent < prevLineIndent)
				indentString = StandardUtilities.truncateWhiteSpace(
					idealIndent, getTabSize(), prevIndentString);
			else
				indentString = prevIndentString +
					StandardUtilities.createWhiteSpace(
						idealIndent - prevLineIndent,
						getBooleanProperty("noTabs") ? 0 : getTabSize(),
						prevLineIndent);
			insert(start, indentString);
		}
		finally
		{
			endCompoundEdit();
		}

		return true;
	} //}}}

	//{{{ getCurrentIndentForLine() method
	/**
	 * @return the line's current leading indent.
	 * @param lineIndex The line number
	 * @param whitespaceChars If this is non-null, the number of whitespace
	 * characters is stored at the 0 index
	 * @since jEdit 4.2pre2
	 */
	public int getCurrentIndentForLine(int lineIndex, int[] whitespaceChars)
	{
		Segment seg = new Segment();
		getLineText(lineIndex,seg);

		int tabSize = getTabSize();

		int currentIndent = 0;
loop:		for(int i = 0; i < seg.count; i++)
		{
			char c = seg.array[seg.offset + i];
			switch(c)
			{
			case ' ':
				currentIndent++;
				if(whitespaceChars != null)
					whitespaceChars[0]++;
				break;
			case '\t':
				currentIndent += tabSize - (currentIndent
					% tabSize);
				if(whitespaceChars != null)
					whitespaceChars[0]++;
				break;
			default:
				break loop;
			}
		}

		return currentIndent;
	} //}}}

	//{{{ getIdealIndentForLine() method
	/**
	 * @return the ideal leading indent for the specified line.
	 * This will apply the various auto-indent rules.
	 * @param lineIndex The line number
	 */
	public int getIdealIndentForLine(int lineIndex)
	{
		int prevLineIndex = getPriorNonEmptyLine(lineIndex);
		int oldIndent = prevLineIndex == -1 ? 0 :
			StandardUtilities.getLeadingWhiteSpaceWidth(
			getLineSegment(prevLineIndex),
			getTabSize());
		return getIdealIndentForLine(lineIndex, prevLineIndex,
			oldIndent);
	} //}}}

	//{{{ getIdealIndentForLine() method
	/**
	 * Returns the ideal leading indent for the specified line.
	 * This will apply the various auto-indent rules.
	 * @param lineIndex The line number
	 * @param prevLineIndex The index of the previous non-empty line
	 * @param oldIndent The indent width of the previous line (or 0)
	 */
	private int getIdealIndentForLine(int lineIndex, int prevLineIndex,
		int oldIndent)
	{
		int prevPrevLineIndex = prevLineIndex < 0 ? -1
			: getPriorNonEmptyLine(prevLineIndex);
		int newIndent = oldIndent;

		List<IndentRule> indentRules = getIndentRules(lineIndex);
		List<IndentAction> actions = new LinkedList<>();
		for (IndentRule rule : indentRules)
			rule.apply(this, lineIndex, prevLineIndex, prevPrevLineIndex, actions);

		for (IndentAction action : actions)
		{
			newIndent = action.calculateIndent(this, lineIndex,
					oldIndent, newIndent);
			if (!action.keepChecking())
				break;
		}
		if (newIndent < 0)
			newIndent = 0;

		return newIndent;
	} //}}}

	//{{{ getVirtualWidth() method
	/**
	 * @return the virtual column number (taking tabs into account) of the
	 * specified position.
	 *
	 * @param line The line number
	 * @param column The column number
	 * @since jEdit 4.1pre1
	 */
	public int getVirtualWidth(int line, int column)
	{
		try
		{
			readLock();

			int start = getLineStartOffset(line);
			Segment seg = new Segment();
			getText(start,column,seg);

			return StandardUtilities.getVirtualWidth(seg,getTabSize());
		}
		finally
		{
			readUnlock();
		}
	} //}}}

	//{{{ getOffsetOfVirtualColumn() method
	/**
	 * Returns the offset of a virtual column number (taking tabs
	 * into account) relative to the start of the line in question.
	 *
	 * @param line The line number
	 * @param column The virtual column number
	 * @param totalVirtualWidth If this array is non-null, the total
	 * virtual width will be stored in its first location if this method
	 * returns -1.
	 *
	 * @return -1 if the column is out of bounds
	 *
	 * @since jEdit 4.1pre1
	 */
	public int getOffsetOfVirtualColumn(int line, int column,
		int[] totalVirtualWidth)
	{
		try
		{
			readLock();

			Segment seg = new Segment();
			getLineText(line,seg);

			return StandardUtilities.getOffsetOfVirtualColumn(seg,
				getTabSize(),column,totalVirtualWidth);
		}
		finally
		{
			readUnlock();
		}
	} //}}}

	//{{{ insertAtColumn() method
	/**
	 * Like the {@link #insert(int,String)} method, but inserts the string at
	 * the specified virtual column. Inserts spaces as appropriate if
	 * the line is shorter than the column.
	 * @param line The line number
	 * @param col The virtual column number
	 * @param str The string
	 */
	public void insertAtColumn(int line, int col, String str)
	{
		try
		{
			writeLock();

			int[] total = new int[1];
			int offset = getOffsetOfVirtualColumn(line,col,total);
			if(offset == -1)
			{
				offset = getLineEndOffset(line) - 1;
				str = StandardUtilities.createWhiteSpace(col - total[0],0) + str;
			}
			else
				offset += getLineStartOffset(line);

			insert(offset,str);
		}
		finally
		{
			writeUnlock();
		}
	} //}}}

	//{{{ insertIndented() method
	/**
	 * Inserts a string into the buffer, indenting each line of the string
	 * to match the indent of the first line.
	 *
	 * @param offset The offset
	 * @param text The text
	 *
	 * @return The number of characters of indent inserted on each new
	 * line. This is used by the abbreviations code.
	 *
	 * @since jEdit 4.2pre14
	 */
	public int insertIndented(int offset, String text)
	{
		try
		{
			beginCompoundEdit();

			// obtain the leading indent for later use
			int firstLine = getLineOfOffset(offset);
			CharSequence lineText = getLineSegment(firstLine);
			int leadingIndent
				= StandardUtilities.getLeadingWhiteSpaceWidth(
				lineText,getTabSize());

			String whiteSpace = StandardUtilities.createWhiteSpace(
				leadingIndent,getBooleanProperty("noTabs")
				? 0 : getTabSize());

			insert(offset,text);

			int lastLine = getLineOfOffset(offset + text.length());

			// note that if firstLine == lastLine, loop does not
			// execute
			for(int i = firstLine + 1; i <= lastLine; i++)
			{
				insert(getLineStartOffset(i),whiteSpace);
			}

			return whiteSpace.length();
		}
		finally
		{
			endCompoundEdit();
		}
	} //}}}

	//{{{ isElectricKey() methods
	/**
	 * Should inserting this character trigger a re-indent of
	 * the current line?
	 * @param ch a character
	 * @param line the line
	 * @return true if is electric key
	 * @since jEdit 4.3pre9
	 */
	public boolean isElectricKey(char ch, int line)
	{
		TokenMarker.LineContext ctx = getLineContext(line);
		Mode mode = ModeProvider.instance.getMode(ctx.rules.getModeName());

		// mode can be null, though that's probably an error "further up":
		if (mode == null)
			return false;
		return mode.isElectricKey(ch);
	} //}}}

	//}}}

	//{{{ Syntax highlighting

	//{{{ getLineContext() method
	/**
	 * Returns the line context of the token marker for the specified line.
	 */
	public TokenMarker.LineContext getLineContext(int line)
	{
		return lineMgr.getLineContext(line);
	}
	//}}}

	//{{{ markTokens() method
	/**
	 * Returns the syntax tokens for the specified line.
	 * @param lineIndex The line number
	 * @param tokenHandler The token handler that will receive the syntax
	 * tokens
	 * @since jEdit 4.1pre1
	 */
	public void markTokens(int lineIndex, TokenHandler tokenHandler)
	{
		Segment seg = new Segment();

		if(lineIndex < 0 || lineIndex >= lineMgr.getLineCount())
			throw new ArrayIndexOutOfBoundsException(lineIndex);

		int firstInvalidLineContext = lineMgr.getFirstInvalidLineContext();
		int start;
		if(contextInsensitive || firstInvalidLineContext == -1)
		{
			start = lineIndex;
		}
		else
		{
			start = Math.min(firstInvalidLineContext,
				lineIndex);
		}

		if(Debug.TOKEN_MARKER_DEBUG)
			Log.log(Log.DEBUG,this,"tokenize from " + start + " to " + lineIndex);
		TokenMarker.LineContext oldContext = null;
		TokenMarker.LineContext context = null;
		for(int i = start; i <= lineIndex; i++)
		{
			getLineText(i,seg);

			oldContext = getLineContext(i);

			TokenMarker.LineContext prevContext = (
				(i == 0 || contextInsensitive) ? null
				: getLineContext(i - 1)
			);

			TokenHandler _tokenHandler = i == lineIndex ? tokenHandler : DummyTokenHandler.INSTANCE;
			context = markTokens(seg, prevContext, _tokenHandler);
			lineMgr.setLineContext(i,context);
		}

		int lineCount = lineMgr.getLineCount();
		if(lineCount - 1 == lineIndex)
			lineMgr.setFirstInvalidLineContext(-1);
		else if(oldContext != context)
			lineMgr.setFirstInvalidLineContext(lineIndex + 1);
		else if(firstInvalidLineContext != -1)
		{
			lineMgr.setFirstInvalidLineContext(Math.max(
				firstInvalidLineContext,lineIndex + 1));
		}
	} //}}}

	//{{{ getTokenMarker() method
	public TokenMarker getTokenMarker()
	{
		return tokenMarker;
	} //}}}

	//{{{ setTokenMarker() method
	public void setTokenMarker(TokenMarker tokenMarker)
	{
		TokenMarker oldTokenMarker = this.tokenMarker;

		this.tokenMarker = tokenMarker;

		// don't do this on initial token marker
		if(oldTokenMarker != null && tokenMarker != oldTokenMarker)
		{
			lineMgr.setFirstInvalidLineContext(0);
		}
	} //}}}

	//{{{ createPosition() method
	/**
	 * Creates a floating position (<code>javax.swing.text.Position</code>).
	 * The position is retained despite text editions.
	 * <p>No explicit removal of position is necessary, only dereferencing it.
	 * @param offset The offset
	 * @return the position
	 */
	public Position createPosition(int offset)
	{
		try
		{
			readLock();

			if(offset < 0 || offset > contentMgr.getLength())
				throw new ArrayIndexOutOfBoundsException(offset);

			return positionMgr.createPosition(offset);
		}
		finally
		{
			readUnlock();
		}
	} //}}}

	//}}}

	//{{{ Property methods

	//{{{ propertiesChanged() method
	/**
	 * Reloads settings from the properties. This should be called
	 * after the <code>syntax</code> or <code>folding</code>
	 * buffer-local properties are changed.
	 */
	public void propertiesChanged()
	{
		String folding = getStringProperty("folding");
		FoldHandler handler = FoldHandler.getFoldHandler(folding);

		if(handler != null)
		{
			setFoldHandler(handler);
		}
		else
		{
			setFoldHandler(new DummyFoldHandler());
		}
	} //}}}

	//{{{ getTabSize() method
	/**
	 * @return the tab size used in this buffer. This is equivalent
	 * to calling <code>getProperty("tabSize")</code>.
	 * This method is thread-safe.
	 */
	public int getTabSize()
	{
		int tabSize = getIntegerProperty("tabSize",8);
		if(tabSize <= 0)
			return 8;
		else
			return tabSize;
	} //}}}

	//{{{ getIndentSize() method
	/**
	 * @return the indent size used in this buffer. This is equivalent
	 * to calling <code>getProperty("indentSize")</code>.
	 * This method is thread-safe.
	 * @since jEdit 2.7pre1
	 */
	public int getIndentSize()
	{
		int indentSize = getIntegerProperty("indentSize",8);
		if(indentSize <= 0)
			return 8;
		else
			return indentSize;
	} //}}}

	//{{{ getProperty() method
	/**
	 * @return the value of a buffer-local property.
	 *
	 * Using this method is generally discouraged, because it returns an
	 * <code>Object</code> which must be cast to another type
	 * in order to be useful, and this can cause problems if the object
	 * is of a different type than what the caller expects.<p>
	 *
	 * The following methods should be used instead:
	 * <ul>
	 * <li>{@link #getStringProperty(String)}</li>
	 * <li>{@link #getBooleanProperty(String)}</li>
	 * <li>{@link #getIntegerProperty(String,int)}</li>
	 * </ul>
	 *
	 * This method is thread-safe.
	 *
	 * @param name The property name. For backwards compatibility, this
	 * is an <code>Object</code>, not a <code>String</code>.
	 */
	public Object getProperty(Object name)
	{
		synchronized(propertyLock)
		{
			// First try the buffer-local properties
			PropValue o = properties.get(name);
			if(o != null)
				return o.value;

			// For backwards compatibility
			if(!(name instanceof String))
				return null;

			Object retVal = getDefaultProperty((String)name);

			if(retVal == null)
				return null;
			else
			{
				properties.put(name,new PropValue(retVal,true));
				return retVal;
			}
		}
	} //}}}

	//{{{ isClosed() method
	public boolean isClosed()
	{
		return closed;
	} //}}}

	//{{{ close() method
	public void close()
	{
		closed = true;
	} //}}}

	//{{{ hasProperty() method
	/**
	 * @return true if the buffer local property exists.
	 *
	 * This method is thread-safe.
	 *
	 * @param name The property name. For backwards compatibility, this
	 * is an {@code Object}, not a {@code String}.
	 * @since jEdit 5.6pre1
	 */
	public boolean hasProperty(Object name)
	{
		return null != getProperty(name);
	} //}}}

	//{{{ getDefaultProperty() method
	public Object getDefaultProperty(String key)
	{
		return null;
	} //}}}

	//{{{ setProperty() method
	/**
	 * Sets the value of a buffer-local property.
	 * @param name The property name
	 * @param value The property value
	 * @since jEdit 4.0pre1
	 */
	public void setProperty(String name, Object value)
	{
		if(value == null)
			properties.remove(name);
		else
		{
			PropValue test = properties.get(name);
			if(test == null)
				properties.put(name,new PropValue(value,false));
			else if(!test.value.equals(value))
			{
				test.value = value;
				test.defaultValue = false;
			}
		}
	} //}}}

	//{{{ setDefaultProperty() method
	public void setDefaultProperty(String name, Object value)
	{
		properties.put(name,new PropValue(value,true));
	} //}}}

	//{{{ unsetProperty() method
	/**
	 * Clears the value of a buffer-local property.
	 * @param name The property name
	 * @since jEdit 4.0pre1
	 */
	public void unsetProperty(String name)
	{
		properties.remove(name);
	} //}}}

	//{{{ resetCachedProperties() method
	public void resetCachedProperties()
	{
		// Need to reset properties that were cached defaults,
		// since the defaults might have changed.
		properties.values().removeIf(value -> value.defaultValue);
	} //}}}

	//{{{ getStringProperty() method
	/**
	 * @return the value of a string property. This method is thread-safe.
	 * @param name The property name
	 * @since jEdit 4.0pre1
	 */
	public String getStringProperty(String name)
	{
		Object obj = getProperty(name);
		if(obj != null)
			return obj.toString();
		else
			return null;
	} //}}}

	//{{{ setStringProperty() method
	/**
	 * Sets a string property.
	 * @param name The property name
	 * @param value The value
	 * @since jEdit 4.0pre1
	 */
	public void setStringProperty(String name, String value)
	{
		setProperty(name,value);
	} //}}}

	//{{{ getBooleanProperty() methods
	/**
	 * @return the value of a boolean property. This method is thread-safe.
	 * @param name The property name
	 * @since jEdit 4.0pre1
	 */
	public boolean getBooleanProperty(String name)
	{
		return getBooleanProperty(name, false);
	}

	/**
	 * @return the value of a boolean property. This method is thread-safe.
	 * @param name The property name
	 * @param def The default value
	 * @since jEdit 4.3pre17
	 */
	public boolean getBooleanProperty(String name, boolean def)
	{
		Object obj = getProperty(name);
		return StandardUtilities.getBoolean(obj, def);
	} //}}}

	//{{{ setBooleanProperty() method
	/**
	 * Sets a boolean property.
	 * @param name The property name
	 * @param value The value
	 * @since jEdit 4.0pre1
	 */
	public void setBooleanProperty(String name, boolean value)
	{
		setProperty(name,value ? Boolean.TRUE : Boolean.FALSE);
	} //}}}

	//{{{ getIntegerProperty() method
	/**
	 * @return the value of an integer property. This method is thread-safe.
	 * @param name The property name
	 * @param defaultValue the default value to use
	 * @since jEdit 4.0pre1
	 */
	public int getIntegerProperty(String name, int defaultValue)
	{
		boolean defaultValueFlag;
		Object obj;
		PropValue value = properties.get(name);
		if(value != null)
		{
			obj = value.value;
			defaultValueFlag = value.defaultValue;
		}
		else
		{
			obj = getProperty(name);
			// will be cached from now on...
			defaultValueFlag = true;
		}

		if(obj == null)
			return defaultValue;
		else if(obj instanceof Number)
			return ((Number)obj).intValue();
		else
		{
			try
			{
				int returnValue = Integer.parseInt(
					obj.toString().trim());
				properties.put(name,new PropValue(
					returnValue,
					defaultValueFlag));
				return returnValue;
			}
			catch(Exception e)
			{
				return defaultValue;
			}
		}
	} //}}}

	//{{{ setIntegerProperty() method
	/**
	 * Sets an integer property.
	 * @param name The property name
	 * @param value The value
	 * @since jEdit 4.0pre1
	 */
	public void setIntegerProperty(String name, int value)
	{
		setProperty(name,value);
	} //}}}

	//{{{ getPatternProperty()
	/**
	 * @return the value of a property as a regular expression.
	 * This method is thread-safe.
	 * @param name The property name
	 * @param flags Regular expression compilation flags
	 * @since jEdit 4.3pre5
	 */
	public Pattern getPatternProperty(String name, int flags)
	{
		synchronized(propertyLock)
		{
			boolean defaultValueFlag;
			Object obj;
			PropValue value = properties.get(name);
			if(value != null)
			{
				obj = value.value;
				defaultValueFlag = value.defaultValue;
			}
			else
			{
				obj = getProperty(name);
				// will be cached from now on...
				defaultValueFlag = true;
			}

			if(obj == null)
				return null;
			else if (obj instanceof Pattern)
				return (Pattern) obj;
			else
			{
				Pattern re = Pattern.compile(obj.toString(),flags);
				properties.put(name,new PropValue(re,
					defaultValueFlag));
				return re;
			}
		}
	} //}}}

	//{{{ getWordWrap() method
	/**
	 * Returns the current word wrap mode
	 *
	 * @return the current word wrap mode
	 * @since jEdit 5.7pre1
	 */
	public WordWrap getWordWrap()
	{
		return WordWrap.valueOf(getStringProperty(WRAP));
	} //}}}

	//{{{ setWordWrap() method
	/**
	 * set word wrap
	 *
	 * @param wordWrap the new word wrap
	 * @since jEdit 5.7pre1
	 */
	public void setWordWrap(WordWrap wordWrap)
	{
		setProperty(WRAP, wordWrap.name());
	} //}}}


	/**
	 * Returns the LargeFileMode option
	 *
	 * @return the option value, never null (if invalid or null {@link LargeFileMode#ask} is returned
	 * @since jEdit 5.7pre1
	 */
	public LargeFileMode getLargeFileMode()
	{
		String largeFileMode = getStringProperty(LARGE_MODE_FILE);
		if (largeFileMode == null)
			return ask;
		try
		{
			return LargeFileMode.valueOf(largeFileMode);
		}
		catch (IllegalArgumentException e)
		{
			return ask;
		}
	}

	/**
	 * Set the large file mode option
	 *
	 * @param largeFileMode the large file mode
	 * @since jEdit 5.7pre1
	 */
	public void setLargeFileMode(LargeFileMode largeFileMode)
	{
		if (largeFileMode == null)
			unsetProperty(LARGE_MODE_FILE);
		else
			setProperty(LARGE_MODE_FILE, largeFileMode.name());
	}

	//{{{ getRuleSetAtOffset() method
	/**
	 * @return the syntax highlighting ruleset at the specified offset.
	 * @param offset the offset
	 * @since jEdit 4.1pre1
	 */
	public ParserRuleSet getRuleSetAtOffset(int offset)
	{
		int line = getLineOfOffset(offset);
		offset -= getLineStartOffset(line);
		if(offset != 0)
			offset--;

		DefaultTokenHandler tokens = new DefaultTokenHandler();
		markTokens(line,tokens);
		Token token = TextUtilities.getTokenAtOffset(tokens.getTokens(),offset);
		return token.rules;
	} //}}}

	//{{{ getKeywordMapAtOffset() method
	/**
	 * @return the syntax highlighting keyword map in effect at the
	 * specified offset. Used by the <b>Complete Word</b> command to
	 * complete keywords.
	 * @param offset The offset
	 * @since jEdit 4.0pre3
	 */
	public KeywordMap getKeywordMapAtOffset(int offset)
	{
		return getRuleSetAtOffset(offset).getKeywords();
	} //}}}

	//{{{ getContextSensitiveProperty() method
	/**
	 * Some settings, like comment start and end strings, can
	 * vary between different parts of a buffer (HTML text and inline
	 * JavaScript, for example).
	 * @param offset The offset
	 * @param name The property name
	 * @return the property value
	 * @since jEdit 4.0pre3
	 */
	public String getContextSensitiveProperty(int offset, String name)
	{
		ParserRuleSet rules = getRuleSetAtOffset(offset);

		Map<String, String> rulesetProps = rules.getProperties();
		return rulesetProps != null ? rulesetProps.get(name) : null;
	} //}}}

	//{{{ getMode() method
	/**
	 * @return this buffer's edit mode. This method is thread-safe.
	 */
	public Mode getMode()
	{
		return mode;
	} //}}}

	//{{{ setMode() methods
	/**
	 * Sets this buffer's edit mode. Note that calling this before a buffer
	 * is loaded will have no effect; in that case, set the "mode" property
	 * to the name of the mode. A bit inelegant, I know...
	 * @param mode The mode name
	 * @since jEdit 4.2pre1
	 */
	public void setMode(String mode)
	{
		setMode(ModeProvider.instance.getMode(mode));
	}

	/**
	 * Sets this buffer's edit mode. Note that calling this before a buffer
	 * is loaded will have no effect; in that case, set the "mode" property
	 * to the name of the mode. A bit inelegant, I know...
	 * @param mode The mode
	 */
	public void setMode(@Nonnull Mode mode)
	{
		setMode(mode, false);
	}

	/**
	 * Sets this buffer's edit mode. Note that calling this before a buffer
	 * is loaded will have no effect; in that case, set the "mode" property
	 * to the name of the mode. A bit inelegant, I know...
	 * @param mode The mode
	 * @param forceContextInsensitive true if you want to force the buffer to be
	 * insensitive to the context. Careful it can break syntax highlight. Default
	 * value is false
	 * @since jEdit 4.5pre1
	 */
	public void setMode(@Nonnull Mode mode, boolean forceContextInsensitive)
	{
		/* This protects against stupid people (like me)
		 * doing stuff like buffer.setMode(jEdit.getMode(...)); */
		Objects.requireNonNull(mode);

		this.mode = mode;

		contextInsensitive = forceContextInsensitive ||
			mode.getBooleanProperty("contextInsensitive");

		setTokenMarker(mode.getTokenMarker());

		resetCachedProperties();
		propertiesChanged();
	}//}}}

	//}}}

	//{{{ Folding methods

	//{{{ isFoldStart() method
	/**
	 * @param line the line
	 * @return if the specified line begins a fold.
	 * @since jEdit 3.1pre1
	 */
	public boolean isFoldStart(int line)
	{
		return line != getLineCount() - 1
			&& getFoldLevel(line) < getFoldLevel(line + 1);
	} //}}}

	//{{{ isFoldEnd() method
	/**
	 * @return if the specified line ends a fold.
	 * @param line the line
	 * @since jEdit 4.2pre5
	 */
	public boolean isFoldEnd(int line)
	{
        int foldLevel = getFoldLevel(line);
        int nextLineFoldLevel = line == getLineCount() - 1 ? 0 : getFoldLevel(line + 1);
        return foldLevel > nextLineFoldLevel;
	} //}}}

	//{{{ invalidateCachedFoldLevels() method
	/**
	 * Invalidates all cached fold level information.
	 * @since jEdit 4.1pre11
	 */
	public void invalidateCachedFoldLevels()
	{
		lineMgr.setFirstInvalidFoldLevel(0);
		fireFoldLevelChanged(0,getLineCount());
	} //}}}

	//{{{ getFoldLevel() method
	/**
	 * @return the fold level of the specified line.
	 * @param line A physical line index
	 * @since jEdit 3.1pre1
	 */
	public int getFoldLevel(int line)
	{
		if(line < 0 || line >= lineMgr.getLineCount())
			throw new ArrayIndexOutOfBoundsException(line);

		if(foldHandler instanceof DummyFoldHandler)
			return 0;

		int firstInvalidFoldLevel = lineMgr.getFirstInvalidFoldLevel();
		if(firstInvalidFoldLevel == -1 || line < firstInvalidFoldLevel)
		{
			return lineMgr.getFoldLevel(line);
		}
		else
		{
			if(Debug.FOLD_DEBUG)
				Log.log(Log.DEBUG,this,"Invalid fold levels from " + firstInvalidFoldLevel + " to " + line);

			int newFoldLevel = 0;
			boolean changed = false;
			int firstUpdatedFoldLevel = firstInvalidFoldLevel;

			for(int i = firstInvalidFoldLevel; i <= line; i++)
			{
				Segment seg = new Segment();
				newFoldLevel = foldHandler.getFoldLevel(this,i,seg);
				if(newFoldLevel != lineMgr.getFoldLevel(i))
				{
					if(Debug.FOLD_DEBUG)
						Log.log(Log.DEBUG,this,i + " fold level changed");
					changed = true;
					// Update preceding fold levels if necessary
					if (i == firstInvalidFoldLevel)
					{
						List<Integer> precedingFoldLevels =
							foldHandler.getPrecedingFoldLevels(
								this,i,seg,newFoldLevel);
						if (precedingFoldLevels != null)
						{
							int j = i;
							for (Integer foldLevel: precedingFoldLevels)
							{
								j--;
								lineMgr.setFoldLevel(j, foldLevel);
							}
							if (j < firstUpdatedFoldLevel)
								firstUpdatedFoldLevel = j;
						}
					}
				}
				lineMgr.setFoldLevel(i,newFoldLevel);
			}

			if(line == lineMgr.getLineCount() - 1)
				lineMgr.setFirstInvalidFoldLevel(-1);
			else
				lineMgr.setFirstInvalidFoldLevel(line + 1);

			if(changed)
			{
				if(Debug.FOLD_DEBUG)
					Log.log(Log.DEBUG,this,"fold level changed: " + firstUpdatedFoldLevel + ',' + line);
				fireFoldLevelChanged(firstUpdatedFoldLevel,line);
			}

			return newFoldLevel;
		}
	} //}}}

	//{{{ getFoldAtLine() method
	/**
	 * @return an array. The first element is the start line, the
	 * second element is the end line, of the fold containing the
	 * specified line number.
	 * @param line The line number
	 * @since jEdit 4.0pre3
	 */
	public int[] getFoldAtLine(int line)
	{
		int start, end;

		if(isFoldStart(line))
		{
			start = line;
			int foldLevel = getFoldLevel(line);

			line++;

			while(getFoldLevel(line) > foldLevel)
			{
				line++;

				if(line == getLineCount())
					break;
			}

			end = line - 1;
		}
		else
		{
			start = line;
			int foldLevel = getFoldLevel(line);
			while(getFoldLevel(start) >= foldLevel)
			{
				if(start == 0)
					break;
				else
					start--;
			}

			end = line;
			while(getFoldLevel(end) >= foldLevel)
			{
				end++;

				if(end == getLineCount())
					break;
			}

			end--;
		}

		while(getLineLength(end) == 0 && end > start)
			end--;

		return new int[] { start, end };
	} //}}}

	//{{{ getFoldHandler() method
	/**
	 * @return the current buffer's fold handler.
	 * @since jEdit 4.2pre1
	 */
	@Nonnull
	public FoldHandler getFoldHandler()
	{
		return foldHandler;
	} //}}}

	//{{{ setFoldHandler() method
	/**
	 * Sets the buffer's fold handler.
	 * @param foldHandler the fold handler
	 * @since jEdit 4.2pre2
	 */
	public void setFoldHandler(@Nonnull FoldHandler foldHandler)
	{
		FoldHandler oldFoldHandler = this.foldHandler;

		if(foldHandler.equals(oldFoldHandler))
			return;

		this.foldHandler = foldHandler;

		lineMgr.setFirstInvalidFoldLevel(0);

		fireFoldHandlerChanged();
	} //}}}

	//}}}

	//{{{ Undo

	//{{{ undo() method
	/**
	 * Undoes the most recent edit.
	 * @param textArea the text area
	 * @since jEdit 4.0pre1
	 */
	public void undo(TextArea textArea)
	{
		if(undoMgr == null)
			return;

		if(!isEditable())
		{
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
			return;
		}

		try
		{
			writeLock();

			undoInProgress = true;
			fireBeginUndo();
			Selection[] s = undoMgr.undo();
			if(s == null || s.length == 0)
				javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
			else
			{
				textArea.setCaretPosition(s[s.length - 1].getEnd());
				textArea.setSelection(s);
			}
			fireEndUndo();
			fireTransactionComplete();
		}
		finally
		{
			undoInProgress = false;

			writeUnlock();
		}
	} //}}}

	//{{{ redo() method
	/**
	 * Redoes the most recently undone edit.
	 * @param textArea the textArea
	 * @since jEdit 2.7pre2
	 */
	public void redo(TextArea textArea)
	{
		if(undoMgr == null)
			return;

		if(!isEditable())
		{
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
			return;
		}

		try
		{
			writeLock();

			undoInProgress = true;
			fireBeginRedo();
			Selection[] s = undoMgr.redo();
			if(s == null || s.length == 0)
				javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
			else
			{
				textArea.setCaretPosition(s[s.length - 1].getEnd());
				textArea.setSelection(s);
			}

			fireEndRedo();
			fireTransactionComplete();
		}
		finally
		{
			undoInProgress = false;

			writeUnlock();
		}
	} //}}}

	//{{{ isTransactionInProgress() method
	/**
	 * @return if an undo or compound edit is currently in progress. If this
	 * method returns true, then eventually a
	 * {@link org.gjt.sp.jedit.buffer.BufferListener#transactionComplete(JEditBuffer)}
	 * buffer event will get fired.
	 * @since jEdit 4.0pre6
	 */
	public boolean isTransactionInProgress()
	{
		return transaction || undoInProgress || insideCompoundEdit() || loading;
	} //}}}

	//{{{ beginCompoundEdit() method
	/**
	 * Starts a compound edit. All edits from now on until
	 * {@link #endCompoundEdit()} are called will be merged
	 * into one. This can be used to make a complex operation
	 * undoable in one step. Nested calls to
	 * {@link #beginCompoundEdit()} behave as expected,
	 * requiring the same number of {@link #endCompoundEdit()}
	 * calls to end the edit.
	 * @see #endCompoundEdit()
	 */
	public void beginCompoundEdit()
	{
		try
		{
			writeLock();

			undoMgr.beginCompoundEdit();
		}
		finally
		{
			writeUnlock();
		}
	} //}}}

	//{{{ endCompoundEdit() method
	/**
	 * Ends a compound edit. All edits performed since
	 * {@link #beginCompoundEdit()} was called can now
	 * be undone in one step by calling {@link #undo(TextArea)}.
	 * @see #beginCompoundEdit()
	 */
	public void endCompoundEdit()
	{
		try
		{
			writeLock();

			undoMgr.endCompoundEdit();

			if(!insideCompoundEdit())
				fireTransactionComplete();
		}
		finally
		{
			writeUnlock();
		}
	}//}}}

	//{{{ insideCompoundEdit() method
	/**
	 * @return if a compound edit is currently active.
	 * @since jEdit 3.1pre1
	 */
	public boolean insideCompoundEdit()
	{
		return undoMgr.insideCompoundEdit();
	} //}}}

	//{{{ isUndoInProgress() method
	/**
	 * @return if an undo or redo is currently being performed.
	 * @since jEdit 4.3pre3
	 */
	public boolean isUndoInProgress()
	{
		return undoInProgress;
	} //}}}

	//{{{ getUndoId() method
	/**
	 * @return an object that identifies the undo operation to which the
	 * current content change belongs. This method can be used by buffer
	 * listeners during content changes (contentInserted/contentRemoved)
	 * to find out which content changes belong to the same "undo" operation.
	 * The same undoId object will be returned for all content changes
	 * belonging to the same undo operation. Only the identity of the
	 * undoId can be used, by comparing it with a previously-returned undoId
	 * using "==".
	 * @since jEdit 4.3pre18
	 */
	public Object getUndoId()
	{
		return undoMgr.getUndoId();
	} //}}}

	//}}}

	//{{{ Buffer events
	public static final int NORMAL_PRIORITY = 0;
	public static final int HIGH_PRIORITY = 1;

	static class Listener
	{
		BufferListener listener;
		int priority;

		Listener(BufferListener listener, int priority)
		{
			this.listener = listener;
			this.priority = priority;
		}
	}

	//{{{ addBufferListener() methods
	/**
	 * Adds a buffer change listener.
	 * @param listener The listener
	 * @param priority Listeners with HIGH_PRIORITY get the event before
	 * listeners with NORMAL_PRIORITY
	 * @since jEdit 4.3pre3
	 */
	public void addBufferListener(BufferListener listener,
		int priority)
	{
		Listener l = new Listener(listener,priority);
		for(int i = 0; i < bufferListeners.size(); i++)
		{
			Listener _l = bufferListeners.get(i);
			if(_l.priority < priority)
			{
				bufferListeners.add(i,l);
				return;
			}
		}
		bufferListeners.add(l);
	}

	/**
	 * Adds a buffer change listener.
	 * @param listener The listener
	 * @since jEdit 4.3pre3
	 */
	public void addBufferListener(BufferListener listener)
	{
		addBufferListener(listener,NORMAL_PRIORITY);
	} //}}}

	//{{{ removeBufferListener() method
	/**
	 * Removes a buffer change listener.
	 * @param listener The listener
	 * @since jEdit 4.3pre3
	 */
	public void removeBufferListener(BufferListener listener)
	{
		for(int i = 0; i < bufferListeners.size(); i++)
		{
			if(bufferListeners.get(i).listener == listener)
			{
				bufferListeners.remove(i);
				return;
			}
		}
	} //}}}

	//{{{ getBufferListeners() method
	/**
	 * @return an array of registered buffer change listeners.
	 * @since jEdit 4.3pre3
	 */
	public BufferListener[] getBufferListeners()
	{
		BufferListener[] returnValue
			= new BufferListener[
			bufferListeners.size()];
		for(int i = 0; i < returnValue.length; i++)
		{
			returnValue[i] = bufferListeners.get(i).listener;
		}
		return returnValue;
	} //}}}

	//{{{ setUndoLimit() method
	/**
	 * Set the undo limit of the Undo Manager.
	 *
	 * @param limit the new limit
	 * @since jEdit 4.3pre16
	 */
	public void setUndoLimit(int limit)
	{
		if (undoMgr != null)
			undoMgr.setLimit(limit);
	} //}}}

	//{{{ canUndo() method
	/**
	 * @return true if an undo operation can be performed.
	 * @since jEdit 4.3pre18
	 */
	public boolean canUndo()
	{
		if (undoMgr == null)
			return false;
		return undoMgr.canUndo();
	} //}}}

	//{{{ canRedo() method
	/**
	 * @return true if a redo operation can be performed.
	 * @since jEdit 4.3pre18
	 */
	public boolean canRedo()
	{
		if (undoMgr == null)
			return false;
		return undoMgr.canRedo();
	} //}}}

	//{{{ isContextInsensitive() method
	/**
	 * Returns true if the buffer highlight is
	 * not sensitive to the context.
	 * @return true if the highlight is insensitive to
	 * the context
	 * @since jEdit 4.5pre1
	 */
	public boolean isContextInsensitive()
	{
		return contextInsensitive;
	}//}}}

	//{{{ setContextInsensitive() method
	/**
	 * Set the buffer to be insensitive to the context during
	 * highlight.
	 * @param contextInsensitive the new contextInsensitive value
	 * the context
	 * @since jEdit 4.5pre1
	 */
	public void setContextInsensitive(boolean contextInsensitive)
	{
		this.contextInsensitive = contextInsensitive;
	}//}}}

	//}}}

	//{{{ Protected members
	/**
	 * The edit mode of the buffer.
	 */
	protected Mode mode;
	/**
	 * If true the syntax highlight is context insensitive.
	 * To highlight a line we don't keed the context of the previous line.
	 */
	protected boolean contextInsensitive;
	protected UndoManager undoMgr;
	protected TokenMarker tokenMarker;

	//{{{ Event firing methods

	//{{{ fireFoldLevelChanged() method
	protected void fireFoldLevelChanged(int start, int end)
	{
		for(int i = 0; i < bufferListeners.size(); i++)
		{
			BufferListener listener = getListener(i);
			try
			{
				listener.foldLevelChanged(this,start,end);
			}
			catch(Throwable t)
			{
				Log.log(Log.ERROR,this,"Exception while sending buffer event to "+ listener +" :");
				Log.log(Log.ERROR,this,t);
			}
		}
	} //}}}

	//{{{ fireContentInserted() method
	protected void fireContentInserted(int startLine, int offset,
		int numLines, int length)
	{
		for(int i = 0; i < bufferListeners.size(); i++)
		{
			BufferListener listener = getListener(i);
			try
			{
				listener.contentInserted(this,startLine,
					offset,numLines,length);
			}
			catch(Throwable t)
			{
				Log.log(Log.ERROR,this,"Exception while sending buffer event to "+ listener +" :");
				Log.log(Log.ERROR,this,t);
			}
		}
	} //}}}

	//{{{ fireContentRemoved() method
	protected void fireContentRemoved(int startLine, int offset,
		int numLines, int length)
	{
		for(int i = 0; i < bufferListeners.size(); i++)
		{
			BufferListener listener = getListener(i);
			try
			{
				listener.contentRemoved(this,startLine,
					offset,numLines,length);
			}
			catch(Throwable t)
			{
				Log.log(Log.ERROR,this,"Exception while sending buffer event to "+ listener +" :");
				Log.log(Log.ERROR,this,t);
			}
		}
	} //}}}

	//{{{ firePreContentInserted() method
	protected void firePreContentInserted(int startLine, int offset,
		int numLines, int length)
	{
		for(int i = 0; i < bufferListeners.size(); i++)
		{
			BufferListener listener = getListener(i);
			try
			{
				listener.preContentInserted(this,startLine,
					offset,numLines,length);
			}
			catch(Throwable t)
			{
				Log.log(Log.ERROR,this,"Exception while sending buffer event to "+ listener +" :");
				Log.log(Log.ERROR,this,t);
			}
		}
	} //}}}

	//{{{ firePreContentRemoved() method
	protected void firePreContentRemoved(int startLine, int offset,
		int numLines, int length)
	{
		for(int i = 0; i < bufferListeners.size(); i++)
		{
			BufferListener listener = getListener(i);
			try
			{
				listener.preContentRemoved(this,startLine,
					offset,numLines,length);
			}
			catch(Throwable t)
			{
				Log.log(Log.ERROR,this,"Exception while sending buffer event to "+ listener +" :");
				Log.log(Log.ERROR,this,t);
			}
		}
	} //}}}

	//{{{ fireBeginUndo() method
	protected void fireBeginUndo()
	{
	} //}}}

	//{{{ fireEndUndo() method
	protected void fireEndUndo()
	{
	} //}}}

	//{{{ fireBeginRedo() method
	protected void fireBeginRedo()
	{
	} //}}}

	//{{{ fireEndRedo() method
	protected void fireEndRedo()
	{
	} //}}}

	//{{{ fireTransactionComplete() method
	protected void fireTransactionComplete()
	{
		for(int i = 0; i < bufferListeners.size(); i++)
		{
			BufferListener listener = getListener(i);
			try
			{
				listener.transactionComplete(this);
			}
			catch(Throwable t)
			{
				Log.log(Log.ERROR,this,"Exception while sending buffer event to "+ listener +" :");
				Log.log(Log.ERROR,this,t);
			}
		}
	} //}}}

	//{{{ fireFoldHandlerChanged() method
	protected void fireFoldHandlerChanged()
	{
		for(int i = 0; i < bufferListeners.size(); i++)
		{
			BufferListener listener = getListener(i);
			try
			{
				listener.foldHandlerChanged(this);
			}
			catch(Throwable t)
			{
				Log.log(Log.ERROR,this,"Exception while sending buffer event to "+ listener +" :");
				Log.log(Log.ERROR,this,t);
			}
		}
	} //}}}

	//{{{ fireBufferLoaded() method
	protected void fireBufferLoaded()
	{
		for(int i = 0; i < bufferListeners.size(); i++)
		{
			BufferListener listener = getListener(i);
			try
			{
				listener.bufferLoaded(this);
			}
			catch(Throwable t)
			{
				Log.log(Log.ERROR,this,"Exception while sending buffer event to "+ listener +" :");
				Log.log(Log.ERROR,this,t);
			}
		}
	} //}}}

	//}}}

	//{{{ isFileReadOnly() method
	protected boolean isFileReadOnly()
	{
		return readOnly;
	} //}}}

	//{{{ setFileReadOnly() method
	protected void setFileReadOnly(boolean readOnly)
	{
		this.readOnly = readOnly;
	} //}}}

	//{{{ loadText() method
	protected void loadText(Segment seg, IntegerArray endOffsets)
	{
		if(seg == null)
			seg = new Segment(new char[1024],0,0);

		if(endOffsets == null)
		{
			endOffsets = new IntegerArray();
			endOffsets.add(1);
		}

		try
		{
			writeLock();

			// For `reload' command
			// contentMgr.remove() changes this!
			int length = getLength();

			firePreContentRemoved(0,0,getLineCount()
				- 1,length);

			contentMgr.remove(0,length);
			lineMgr.contentRemoved(0,0,getLineCount()
				- 1,length);
			positionMgr.contentRemoved(0,length);
			fireContentRemoved(0,0,getLineCount()
				- 1,length);

			firePreContentInserted(0, 0, endOffsets.getSize() - 1, seg.count - 1);
			// theoretically a segment could
			// have seg.offset != 0 but
			// SegmentBuffer never does that
			contentMgr._setContent(seg.array,seg.count);

			lineMgr._contentInserted(endOffsets);
			positionMgr.contentInserted(0,seg.count);

			fireContentInserted(0,0,
				endOffsets.getSize() - 1,
				seg.count - 1);
		}
		finally
		{
			writeUnlock();
		}
	} //}}}

	//{{{ invalidateFoldLevels() method
	protected void invalidateFoldLevels()
	{
		lineMgr.setFirstInvalidFoldLevel(0);
	} //}}}

	//{{{ parseBufferLocalProperties() method
	protected void parseBufferLocalProperties()
	{
		int maxRead = 10000;
		int lineCount = getLineCount();
		int lastLine = Math.min(9, lineCount - 1);
		int max = Math.min(maxRead, getLineEndOffset(lastLine) - 1);
		parseBufferLocalProperties(getSegment(0, max));

		// first line for last 10 lines, make sure not to overlap
		// with the first 10
		int firstLine = Math.max(lastLine + 1, lineCount - 10);
		if(firstLine < lineCount)
		{
			int firstLineStartOffset = getLineStartOffset(firstLine);
			int length = getLineEndOffset(lineCount - 1)
				- (firstLineStartOffset + 1);
			if (length > maxRead)
			{
				firstLineStartOffset += length - maxRead;
				length = maxRead;
			}
			parseBufferLocalProperties(getSegment(firstLineStartOffset,length));
		}
	} //}}}

	//{{{ markTokens() method
	protected TokenMarker.LineContext markTokens(Segment seg, TokenMarker.LineContext prevContext,
						     TokenHandler _tokenHandler)
	{
		TokenMarker.LineContext context = tokenMarker.markTokens(prevContext, _tokenHandler, seg);
		return context;
	} //}}}

	//{{{ Used to store property values
	protected static class PropValue
	{
		PropValue(@Nonnull Object value, boolean defaultValue)
		{
			Objects.requireNonNull(value);
			this.value = value;
			this.defaultValue = defaultValue;
		}

		Object value;

		/**
		 * If this is true, then this value is cached from the mode
		 * or global defaults, so when the defaults change this property
		 * value must be reset.
		 */
		boolean defaultValue;

		/**
		 * For debugging purposes.
		 */
		public String toString()
		{
			return value.toString();
		}
	} //}}}

	//}}}

	//{{{ Private members
	private final List<Listener> bufferListeners;
	private boolean closed;
	private final ReentrantReadWriteLock lock;
	private final ContentManager contentMgr;
	private final LineManager lineMgr;
	private final PositionManager positionMgr;
	@Nonnull
	private FoldHandler foldHandler;
	private final IntegerArray integerArray;
	private boolean undoInProgress;
	private boolean dirty;
	private boolean readOnly;
	private boolean readOnlyOverride;
	private boolean editable = true;
	private boolean transaction;
	private boolean loading;
	private boolean io;
	private final Map<Object, PropValue> properties;
	private final Object propertyLock;
	/** This field should be read instead of "elasticTabstops" property
	  * when efficiency matters. */
	// synchronization done in TextArea.propertiesChanged()
	public boolean elasticTabstopsOn;
	private ColumnBlock columnBlock;

	//{{{ getListener() method
	private BufferListener getListener(int index)
	{
		return bufferListeners.get(index).listener;
	} //}}}

	//{{{ contentInserted() method
	private void contentInserted(int offset, int length,
		IntegerArray endOffsets)
	{
		try
		{
			transaction = true;

			int startLine = lineMgr.getLineOfOffset(offset);
			int numLines = endOffsets.getSize();

			if (!loading)
			{
				firePreContentInserted(startLine, offset, numLines, length);
			}

			lineMgr.contentInserted(startLine,offset,numLines,length,
				endOffsets);
			positionMgr.contentInserted(offset,length);

			setDirty(true);

			if(!loading)
			{
				fireContentInserted(startLine,offset,numLines,length);

				if(!undoInProgress && !insideCompoundEdit())
					fireTransactionComplete();
			}

		}
		finally
		{
			transaction = false;
		}
	} //}}}

	//{{{ parseBufferLocalProperties() method
	@SuppressWarnings({"fallthrough"})
	private void parseBufferLocalProperties(CharSequence prop)
	{
		StringBuilder buf = new StringBuilder();
		String name = null;
		boolean escape = false;
		int length = prop.length();
		for(int i = 0; i < length; i++)
		{
			char c = prop.charAt(i);
			switch(c)
			{
			case ':':
				if(escape)
				{
					escape = false;
					buf.append(':');
					break;
				}
				if(name != null)
				{
					// encoding mustn't be set this way as it must
					// be equal to encoding used to load or save the
					// file.
					if (!name.equals(ENCODING))
					{
						// use the low-level property setting code
						// so that if we have a buffer-local
						// property with the same value as a default,
						// later changes in the default don't affect
						// the buffer-local property
						properties.put(name,new PropValue(buf.toString(),false));
					}
					name = null;
				}
				buf.setLength(0);
				break;
			case '=':
				if(escape)
				{
					escape = false;
					buf.append('=');
					break;
				}
				name = buf.toString();
				buf.setLength(0);
				break;
			case '\\':
				if(escape)
					buf.append('\\');
				escape = !escape;
				break;
			case 'n':
				if(escape)
				{	buf.append('\n');
					escape = false;
					break;
				}
			case 'r':
				if(escape)
				{	buf.append('\r');
					escape = false;
					break;
				}
			case 't':
				if(escape)
				{
					buf.append('\t');
					escape = false;
					break;
				}
			default:
				buf.append(c);
				escape = false;
				break;
			}
		}
	} //}}}

	//{{{ getIndentRules() method
	private List<IndentRule> getIndentRules(int line)
	{
		String modeName = null;
		TokenMarker.LineContext ctx = getLineContext(line);
		if (ctx != null && ctx.rules != null)
			modeName = ctx.rules.getModeName();
		if (modeName == null)
			modeName = tokenMarker.getMainRuleSet().getModeName();
		return ModeProvider.instance.getMode(modeName).getIndentRules();
	} //}}}

	//{{{ updateColumnBlocks() method
	public void updateColumnBlocks(int startLine,int endLine,int startColumn, Node parent)
	{
		if (parent != null && startLine >= 0 && endLine >= 0 && startLine <= endLine)
		{
			int currentLine = startLine;
			int colBlockWidth=0;
			Vector<ColumnBlockLine> columnBlockLines = new Vector<>();
			//while(currentLine<=endLine)
			ColumnBlock parentColumnBlock = (ColumnBlock) parent;
			for(int ik=startLine- parentColumnBlock.getStartLine();currentLine<=endLine;ik++)
			{
				Segment seg = new Segment();
				int actualStart =  startColumn;
				if(!parentColumnBlock.getLines().isEmpty())
				{
					ColumnBlockLine line =
						parentColumnBlock.getLines().elementAt(ik);
					if(currentLine!=line.getLine())
					{
						throw new IllegalArgumentException();
					}
					actualStart = line.getColumnEndIndex()+1;
				}
				getLineText(currentLine, actualStart, seg);
				int tabPos = getTabStopPosition(seg);
				if(tabPos>=0)
				{
					columnBlockLines.add(new ColumnBlockLine(currentLine, actualStart, actualStart+tabPos));
					if(tabPos>colBlockWidth)
					{
						colBlockWidth =  tabPos;
					}
				}
				if (tabPos < 0 && !columnBlockLines.isEmpty()
				    || !columnBlockLines.isEmpty() && currentLine == endLine)
				{
					ColumnBlock  block = new ColumnBlock(this,
									     columnBlockLines.elementAt(0).getLine(),
									     startColumn+colBlockWidth,
									     columnBlockLines.elementAt(columnBlockLines.size()-1).getLine(),
									     startColumn+colBlockWidth);
					block.setLines(columnBlockLines);
					block.setParent(parent);
					block.setWidth(colBlockWidth);
					block.setTabSizeDirtyStatus(true,false);
					//block.populateTabSizes();
					parent.addChild(block);
					colBlockWidth=0;
					columnBlockLines = new Vector<ColumnBlockLine>();
					updateColumnBlocks(block.getStartLine(), block.getEndLine(), startColumn+block.getColumnWidth()+1, block);
				}
				currentLine++;
			}
		}
		else
		{
			throw new IllegalArgumentException();
		}
	}
	//}}}

	//{{{ getTabStopPosition() method
	public int getTabStopPosition(Segment seg)
	{
		for (int i = 0; i < seg.count; i++)
		{
			if(seg.array[i+seg.offset]=='\t')
			{
				return i;
			}
		}
		return -5;
	}
	 //}}}

	public final Object columnBlockLock = new Object();

	//{{{ indentUsingElasticTabstops() method
	public void indentUsingElasticTabstops()
	{
		synchronized(columnBlockLock)
		{
			columnBlock = new ColumnBlock(this,0,getLineCount()-1);
			updateColumnBlocks(0, lineMgr.getLineCount()-1, 0, columnBlock);
		}
	}
	 //}}}

	//{{{ getColumnBlock() method
	public ColumnBlock getColumnBlock()
	{
		return columnBlock;
	}
	 //}}}
//}}}
}
