blob: 5345cb99950332e2c2dcf5471ab7a1a0c617cdbf [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.text;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import org.eclipse.swt.events.VerifyEvent;
/**
* Represents a text modification as a document replace command. The text modification is given
* as a <code>VerifyEvent</code> and translated into a document replace command relative
* to a given offset. A document command can also be used to initialize a given <code>VerifyEvent</code>.<p>
* A document command can also represent a list of related changes.
*/
public class DocumentCommand {
/**
* A command which is added to document commands.
* @since 2.1
*/
private static class Command implements Comparable {
/** The offset of the range to be replaced */
private final int fOffset;
/** The length of the range to be replaced. */
private final int fLength;
/** The replacement text */
private final String fText;
/** The listern who owns this command */
private final IDocumentListener fOwner;
/**
* Creates a new command with the given specification.
*
* @param offset the offset of the replace command
* @param length the length of the replace command
* @param text the text to replace with, may be <code>null</code>
* @param owner the document command owner, may be <code>null</code>
*/
public Command(int offset, int length, String text, IDocumentListener owner) {
if (offset < 0 || length < 0)
throw new IllegalArgumentException();
fOffset= offset;
fLength= length;
fText= text;
fOwner= owner;
}
/**
* Returns the length delta for this command.
*
* @return the length delta for this command
*/
public int getDeltaLength() {
return (fText == null ? 0 : fText.length()) - fLength;
}
/**
* Executes the document command on the specified document.
*
* @param document the document on which to execute the command.
* @throws BadLocationException in case this commands cannot be executed
*/
public void execute(IDocument document) throws BadLocationException {
if (fLength == 0 && fText == null)
return;
if (fOwner != null)
document.removeDocumentListener(fOwner);
document.replace(fOffset, fLength, fText);
if (fOwner != null)
document.addDocumentListener(fOwner);
}
/*
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
public int compareTo(final Object object) {
if (equals(object))
return 0;
final Command command= (Command) object;
// diff middle points if not intersecting
if (fOffset + fLength <= command.fOffset || command.fOffset + command.fLength <= fOffset) {
int value= (2 * fOffset + fLength) - (2 * command.fOffset + command.fLength);
if (value != 0)
return value;
}
// the answer
return 42;
}
/*
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object object) {
if (object == this)
return true;
if (! (object instanceof Command))
return false;
final Command command= (Command) object;
return command.fOffset == fOffset && command.fLength == fLength;
}
}
/**
* An iterator, which iterates in reverse over a list.
*/
private static class ReverseListIterator implements Iterator {
/** The list iterator. */
private final ListIterator fListIterator;
/**
* Creates a reverse list iterator.
*/
public ReverseListIterator(ListIterator listIterator) {
if (listIterator == null)
throw new IllegalArgumentException();
fListIterator= listIterator;
}
/*
* @see java.util.Iterator#hasNext()
*/
public boolean hasNext() {
return fListIterator.hasPrevious();
}
/*
* @see java.util.Iterator#next()
*/
public Object next() {
return fListIterator.previous();
}
/*
* @see java.util.Iterator#remove()
*/
public void remove() {
throw new UnsupportedOperationException();
}
}
/**
* A command iterator.
*/
private static class CommandIterator implements Iterator {
/** The command iterator. */
private final Iterator fIterator;
/** The original command. */
private Command fCommand;
/** A flag indicating the direction of iteration. */
private boolean fForward;
/**
* Creates a command iterator.
*
* @param commands an ascending ordered list of commands
* @param command the original command
* @param forward the direction
*/
public CommandIterator(final List commands, final Command command, final boolean forward) {
if (commands == null || command == null)
throw new IllegalArgumentException();
fIterator= forward ? commands.iterator() : new ReverseListIterator(commands.listIterator(commands.size()));
fCommand= command;
fForward= forward;
}
/*
* @see java.util.Iterator#hasNext()
*/
public boolean hasNext() {
return fCommand != null || fIterator.hasNext();
}
/*
* @see java.util.Iterator#next()
*/
public Object next() {
if (! hasNext())
throw new NoSuchElementException();
if (fCommand == null)
return fIterator.next();
if (!fIterator.hasNext()) {
final Command tempCommand= fCommand;
fCommand= null;
return tempCommand;
}
final Command command= (Command) fIterator.next();
final int compareValue= command.compareTo(fCommand);
if ((compareValue < 0) ^ ! fForward) {
return command;
} else if ((compareValue > 0) ^ ! fForward) {
final Command tempCommand= fCommand;
fCommand= command;
return tempCommand;
} else {
throw new IllegalArgumentException();
}
}
/*
* @see java.util.Iterator#remove()
*/
public void remove() {
throw new UnsupportedOperationException();
}
}
/** Must the command be updated */
public boolean doit= false;
/** The offset of the command. */
public int offset;
/** The length of the command */
public int length;
/** The text to be inserted */
public String text;
/**
* The owner of the document command which will not be notified.
* @since 2.1
*/
public IDocumentListener owner;
/**
* The caret offset with respect to the document before the document command is executed.
* @since 2.1
*/
public int caretOffset;
/** Additional document commands. */
private final List fCommands= new ArrayList();
/**
* Creates a new document command.
*/
protected DocumentCommand() {
}
/**
* Translates a verify event into a document replace command using the given offset.
*
* @param event the event to be translated
* @param modelRange the event range as model range
*/
void setEvent(VerifyEvent event, IRegion modelRange) {
doit= true;
text= event.text;
offset= modelRange.getOffset();
length= modelRange.getLength();
owner= null;
caretOffset= -1;
fCommands.clear();
}
/**
* Fills the given verify event with the replace text and the doit
* flag of this document command. Returns whether the document command
* covers the same range as the verify event considering the given offset.
*
* @param event the event to be changed
* @param modelRange to be considered for range comparison
* @return <code>true</code> if this command and the event cover the same range
*/
boolean fillEvent(VerifyEvent event, IRegion modelRange) {
event.text= text;
event.doit= (offset == modelRange.getOffset() && length == modelRange.getLength() && doit);
return event.doit;
}
/**
* Adds an additional replace command. The added replace command must not overlap
* with existing ones. If the document command owner is not <code>null</code>, it will not
* get document change notifications for the particular command.
*
* @param offset the offset of the region to replace
* @param length the length of the region to replace
* @param text the text to replace with, may be <code>null</code>
* @param owner the command owner, may be <code>null</code>
* @throws BadLocationException if the added command intersects with an existing one
* @since 2.1
*/
public void addCommand(int offset, int length, String text, IDocumentListener owner) throws BadLocationException {
final Command command= new Command(offset, length, text, owner);
if (intersects(command))
throw new BadLocationException();
final int index= Collections.binarySearch(fCommands, command);
// a command with exactly the same ranges exists already
if (index >= 0)
throw new BadLocationException();
// binary search result is defined as (-(insertionIndex) - 1)
final int insertionIndex= -(index + 1);
// overlaps to the right?
if (insertionIndex != fCommands.size() && intersects((Command) fCommands.get(insertionIndex), command))
throw new BadLocationException();
// overlaps to the left?
if (insertionIndex != 0 && intersects((Command) fCommands.get(insertionIndex - 1), command))
throw new BadLocationException();
fCommands.add(insertionIndex, command);
}
/**
* Returns an iterator over the commands in ascending position order.
* The iterator includes the original document command.
* Commands cannot be removed.
*
* @return returns the command iterator
*/
public Iterator getCommandIterator() {
Command command= new Command(offset, length, text, owner);
return new CommandIterator(fCommands, command, true);
}
/**
* Returns the number of commands including the original document command.
*
* @return returns the number of commands
* @since 2.1
*/
public int getCommandCount() {
return 1 + fCommands.size();
}
/**
* Returns whether the two given commands intersect.
*
* @param command0 the first command
* @param command1 the second command
* @return <code>true</code> if the commands intersect
* @since 2.1
*/
private boolean intersects(Command command0, Command command1) {
// diff middle points if not intersecting
if (command0.fOffset + command0.fLength <= command1.fOffset || command1.fOffset + command1.fLength <= command0.fOffset)
return (2 * command0.fOffset + command0.fLength) - (2 * command1.fOffset + command1.fLength) == 0;
else
return true;
}
/**
* Returns whether the given command intersects with this command.
*
* @param command the command
* @return <code>true</code> if the command intersects with this command
* @since 2.1
*/
private boolean intersects(Command command) {
// diff middle points if not intersecting
if (offset + length <= command.fOffset || command.fOffset + command.fLength <= offset)
return (2 * offset + length) - (2 * command.fOffset + command.fLength) == 0;
else
return true;
}
/**
* Executes the document commands on a document.
*
* @param document the document on which to execute the commands
* @since 2.1
*/
void execute(IDocument document) throws BadLocationException {
if (length == 0 && text == null && fCommands.size() == 0)
return;
final Command originalCommand= new Command(offset, length, text, owner);
for (final Iterator iterator= new CommandIterator(fCommands, originalCommand, false); iterator.hasNext(); ) {
final Command command= (Command) iterator.next();
command.execute(document);
if (caretOffset != -1 && command.fOffset + command.fLength <= caretOffset)
caretOffset += command.getDeltaLength();
}
}
}