Bug 550621 - [console] Add IConsoleDocumentPartitionerExtension

Used to better test for read-only parts of console document.

Change-Id: I1d8e3b2655080a8b435e8aaf296c40823239cc93
Signed-off-by: Paul Pazderski <paul-eclipse@ppazderski.de>
diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java
index 213f1ae..3381103 100644
--- a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java
+++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java
@@ -42,6 +42,7 @@
 import org.eclipse.ui.console.IConsole;
 import org.eclipse.ui.console.IConsoleConstants;
 import org.eclipse.ui.console.IConsoleDocumentPartitioner;
+import org.eclipse.ui.console.IConsoleDocumentPartitionerExtension;
 import org.eclipse.ui.console.IConsoleManager;
 import org.eclipse.ui.console.IOConsole;
 import org.eclipse.ui.console.IOConsoleOutputStream;
@@ -484,6 +485,56 @@
 			assertTrue("Offset should be read-only.", c.getPartitioner().isReadOnly(loremEnd + 3));
 			assertTrue("Offset should be read-only.", c.getPartitioner().isReadOnly(loremEnd + 4));
 			assertTrue("Offset should be read-only.", c.getPartitioner().isReadOnly(loremEnd + 5));
+
+			if (c.getPartitioner() instanceof IConsoleDocumentPartitionerExtension) {
+				final IConsoleDocumentPartitionerExtension extension = (IConsoleDocumentPartitionerExtension) c.getPartitioner();
+				assertFalse("Writable parts not recognized.", extension.isReadOnly(0, c.getContentLength()));
+				assertTrue("Read-only parts not recognized.", extension.containsReadOnly(0, c.getContentLength()));
+				assertFalse("Writable parts not recognized.", extension.isReadOnly(0, 3));
+				assertTrue("Read-only parts not recognized.", extension.containsReadOnly(0, 3));
+				assertFalse("Area should be writable.", extension.isReadOnly(loremEnd, 3));
+				assertFalse("Area should be writable.", extension.containsReadOnly(loremEnd, 3));
+				assertTrue("Area should be read-only.", extension.isReadOnly(6, 105));
+				assertTrue("Area should be read-only.", extension.containsReadOnly(8, 111));
+
+				assertTrue("Read-only parts not found.", extension.computeReadOnlyPartitions().length > 0);
+				assertTrue("Writable parts not found.", extension.computeWritablePartitions().length > 0);
+				assertTrue("Read-only parts not found.", extension.computeReadOnlyPartitions(loremEnd - 5, 7).length > 0);
+				assertTrue("Writable parts not found.", extension.computeWritablePartitions(loremEnd - 5, 7).length > 0);
+				assertTrue("Area should be read-only.", extension.computeReadOnlyPartitions(5, 100).length > 0);
+				assertEquals("Area should be read-only.", 0, extension.computeWritablePartitions(5, 100).length);
+				assertEquals("Area should be writable.", 0, extension.computeReadOnlyPartitions(loremEnd, 2).length);
+				assertTrue("Area should be writable.", extension.computeWritablePartitions(loremEnd, 2).length > 0);
+
+				assertEquals("Got wrong offset.", 0, extension.getNextOffsetByState(0, false));
+				assertEquals("Got wrong offset.", 2, extension.getNextOffsetByState(0, true));
+				assertEquals("Got wrong offset.", 0, extension.getPreviousOffsetByState(0, false));
+				assertEquals("Got wrong offset.", -1, extension.getPreviousOffsetByState(0, true));
+				assertEquals("Got wrong offset.", 1, extension.getNextOffsetByState(1, false));
+				assertEquals("Got wrong offset.", 2, extension.getNextOffsetByState(1, true));
+				assertEquals("Got wrong offset.", 1, extension.getPreviousOffsetByState(1, false));
+				assertEquals("Got wrong offset.", -1, extension.getPreviousOffsetByState(1, true));
+				assertEquals("Got wrong offset.", 3, extension.getNextOffsetByState(2, false));
+				assertEquals("Got wrong offset.", 2, extension.getNextOffsetByState(2, true));
+				assertEquals("Got wrong offset.", 1, extension.getPreviousOffsetByState(2, false));
+				assertEquals("Got wrong offset.", 2, extension.getPreviousOffsetByState(2, true));
+				for (int i = 3; i < loremEnd; i++) {
+					assertEquals("Got wrong offset.", i, extension.getNextOffsetByState(i, false));
+					assertEquals("Got wrong offset.", loremEnd, extension.getNextOffsetByState(i, true));
+					assertEquals("Got wrong offset.", i, extension.getPreviousOffsetByState(i, false));
+					assertEquals("Got wrong offset.", 2, extension.getPreviousOffsetByState(i, true));
+				}
+				assertEquals("Got wrong offset.", loremEnd + 3, extension.getNextOffsetByState(loremEnd, false));
+				assertEquals("Got wrong offset.", loremEnd, extension.getNextOffsetByState(loremEnd, true));
+				assertEquals("Got wrong offset.", loremEnd - 1, extension.getPreviousOffsetByState(loremEnd, false));
+				assertEquals("Got wrong offset.", loremEnd, extension.getPreviousOffsetByState(loremEnd, true));
+				assertEquals("Got wrong offset.", loremEnd + 3, extension.getNextOffsetByState(loremEnd + 2, false));
+				assertEquals("Got wrong offset.", loremEnd + 2, extension.getNextOffsetByState(loremEnd + 2, true));
+				assertEquals("Got wrong offset.", loremEnd - 1, extension.getPreviousOffsetByState(loremEnd + 2, false));
+				assertEquals("Got wrong offset.", loremEnd + 2, extension.getPreviousOffsetByState(loremEnd + 2, true));
+			} else {
+				TestUtil.log(IStatus.INFO, TestsPlugin.PLUGIN_ID, "IOConsole partitioner does not implement " + IConsoleDocumentPartitionerExtension.class.getName() + ". Skip those tests.");
+			}
 		}
 		c.verifyPartitions();
 		closeConsole(c, "#");
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsoleDocumentPartitioner.java b/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsoleDocumentPartitioner.java
index 87d3c37..c92b878 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsoleDocumentPartitioner.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsoleDocumentPartitioner.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2006 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -21,12 +21,24 @@
  * A document partitioner for a text console.
  * <p>
  * In addition to regular partitioner duties, a console document partitioner
- * dictates which regions in its document are read-only and provides style ranges.
+ * dictates which regions in its document are read-only and provides style
+ * ranges.
  * </p>
  * <p>
  * Clients may implement this interface.
  * </p>
+ * <p>
+ * In order to provided backward compatibility for clients of
+ * <code>IConsoleDocumentPartitioner</code>, extension interfaces are used to
+ * provide a means of evolution. The following extension interfaces exist:
+ * <ul>
+ * <li>{@link org.eclipse.ui.console.IConsoleDocumentPartitionerExtension} since
+ * version 3.9 adding more possibilities to query read-only regions.</li>
+ * </ul>
+ * </p>
+ *
  * @see org.eclipse.ui.console.TextConsole
+ * @see org.eclipse.ui.console.IConsoleDocumentPartitionerExtension
  * @since 3.1
  */
 public interface IConsoleDocumentPartitioner extends IDocumentPartitioner {
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsoleDocumentPartitionerExtension.java b/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsoleDocumentPartitionerExtension.java
new file mode 100644
index 0000000..e03418d
--- /dev/null
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/console/IConsoleDocumentPartitionerExtension.java
@@ -0,0 +1,120 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Paul Pazderski and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Paul Pazderski - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ui.console;
+
+import org.eclipse.jface.text.ITypedRegion;
+
+/**
+ * Extension interface for {@link IConsoleDocumentPartitioner}.
+ * <p>
+ * It adds more possibilities to query read-only regions of the partitioned
+ * document.
+ * </p>
+ *
+ * @see org.eclipse.ui.console.IConsoleDocumentPartitioner
+ * @since 3.9
+ */
+public interface IConsoleDocumentPartitionerExtension {
+
+	/**
+	 * Returns all partitions which are read-only.
+	 *
+	 * @return all read-only partitions. Ordered by offset and never
+	 *         <code>null</code>.
+	 */
+	ITypedRegion[] computeReadOnlyPartitions();
+
+	/**
+	 * Returns all read-only partitions in given range.
+	 *
+	 * @param offset the offset of the range of interest
+	 * @param length the length of the range of interest
+	 * @return read-only partitions in given range. Ordered by offset and never
+	 *         <code>null</code>. Returned regions may start and/or end outside
+	 *         given range.
+	 */
+	ITypedRegion[] computeReadOnlyPartitions(int offset, int length);
+
+	/**
+	 * Returns all partitions which are writable.
+	 *
+	 * @return all writable partitions. Ordered by offset and never
+	 *         <code>null</code>.
+	 */
+	ITypedRegion[] computeWritablePartitions();
+
+	/**
+	 * Returns all writable partitions in given range.
+	 *
+	 * @param offset the offset of the range of interest
+	 * @param length the length of the range of interest
+	 * @return writable partitions in given range. Ordered by offset and never
+	 *         <code>null</code>. Returned regions may start and/or end outside
+	 *         given range.
+	 */
+	ITypedRegion[] computeWritablePartitions(int offset, int length);
+
+	/**
+	 * Returns whether this partitioner's document is read-only in the specified
+	 * range. Only returns <code>true</code> if the whole range is read-only.
+	 *
+	 * @param offset document offset
+	 * @param length range length
+	 * @return whether this partitioner's document is read-only in the specific
+	 *         range
+	 */
+	boolean isReadOnly(int offset, int length);
+
+	/**
+	 * Returns whether this partitioner's document is read-only at any point in the
+	 * specified range.
+	 *
+	 * @param offset document offset
+	 * @param length range length
+	 * @return returns <code>true</code> if any offset in the given range is
+	 *         read-only
+	 */
+	boolean containsReadOnly(int offset, int length);
+
+	/**
+	 * Get this offset or the nearest offset before which is, depending on argument,
+	 * writable or read-only.
+	 *
+	 * @param offset         the offset of interest
+	 * @param searchWritable if <code>true</code> return the nearest writable
+	 *                       offset. If <code>false</code> return the nearest
+	 *                       read-only offset.
+	 * @return the given offset if it has the requested read-only/writable state or
+	 *         the nearest offset before with requested state. Returns
+	 *         <code>-1</code> if there is no offset with requested state before
+	 *         requested offset.
+	 */
+	int getPreviousOffsetByState(int offset, boolean searchWritable);
+
+	/**
+	 * Get this offset or the nearest offset after which is, depending on argument,
+	 * writable or read-only.
+	 *
+	 * @param offset         the offset of interest
+	 * @param searchWritable if <code>true</code> return the nearest writable
+	 *                       offset. If <code>false</code> return the nearest
+	 *                       read-only offset.
+	 * @return the given offset if it has the requested read-only/writable state or
+	 *         the nearest offset after with requested state. Returns the document
+	 *         length if there is no offset with requested state after requested
+	 *         offset.
+	 */
+	int getNextOffsetByState(int offset, boolean searchWritable);
+}
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
index 67e7b8e..fcf3937 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
@@ -14,6 +14,7 @@
  *                          Bug 547064: use binary search for getPartition
  *                          Bug 548356: fixed user input handling
  *                          Bug 550618: getStyleRanges produced invalid overlapping styles
+ *                          Bug 550621: Implementation of IConsoleDocumentPartitionerExtension
  *******************************************************************************/
 package org.eclipse.ui.internal.console;
 
@@ -42,6 +43,7 @@
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.ui.console.ConsolePlugin;
 import org.eclipse.ui.console.IConsoleDocumentPartitioner;
+import org.eclipse.ui.console.IConsoleDocumentPartitionerExtension;
 import org.eclipse.ui.console.IOConsole;
 import org.eclipse.ui.console.IOConsoleInputStream;
 import org.eclipse.ui.console.IOConsoleOutputStream;
@@ -53,7 +55,8 @@
  *
  * @since 3.1
  */
-public class IOConsolePartitioner implements IConsoleDocumentPartitioner, IDocumentPartitionerExtension {
+public class IOConsolePartitioner
+		implements IConsoleDocumentPartitioner, IConsoleDocumentPartitionerExtension, IDocumentPartitionerExtension {
 	/**
 	 * If true validate partitioning after changes and do other additional
 	 * assertions. Useful for developing/debugging.
@@ -221,6 +224,21 @@
 	 * @return the partitioning of the requested range (never <code>null</code>)
 	 */
 	private IOConsolePartition[] computeIOPartitioning(int offset, int length) {
+		return computePartitioning(offset, length, true, true);
+	}
+
+	/**
+	 * Get partitioning for a given range with possibility to filter partitions by
+	 * their read-only property.
+	 *
+	 * @param offset          the offset of the range of interest
+	 * @param length          the length of the range of interest
+	 * @param includeWritable if false writable partitions are skipped
+	 * @param includeReadOnly if false read-only partitions are skipped
+	 * @return the partitioning of the requested range (never <code>null</code>)
+	 */
+	private IOConsolePartition[] computePartitioning(int offset, int length, boolean includeWritable,
+			boolean includeReadOnly) {
 		final List<IOConsolePartition> result = new ArrayList<>();
 		synchronized (partitions) {
 			int index = findPartitionCandidate(offset);
@@ -234,7 +252,9 @@
 				if (partition.getOffset() >= end) {
 					break;
 				}
-				result.add(partition);
+				if ((includeWritable && !partition.isReadOnly()) || (includeReadOnly && partition.isReadOnly())) {
+					result.add(partition);
+				}
 			}
 		}
 		return result.toArray(new IOConsolePartition[0]);
@@ -795,6 +815,84 @@
 		return styles;
 	}
 
+	@Override
+	public ITypedRegion[] computeReadOnlyPartitions() {
+		if (document == null) {
+			return new IOConsolePartition[0];
+		}
+		return computeReadOnlyPartitions(0, document.getLength());
+	}
+
+	@Override
+	public ITypedRegion[] computeReadOnlyPartitions(int offset, int length) {
+		return computePartitioning(offset, length, false, true);
+	}
+
+	@Override
+	public ITypedRegion[] computeWritablePartitions() {
+		if (document == null) {
+			return new IOConsolePartition[0];
+		}
+		return computeWritablePartitions(0, document.getLength());
+	}
+
+	@Override
+	public ITypedRegion[] computeWritablePartitions(int offset, int length) {
+		return computePartitioning(offset, length, true, false);
+	}
+
+	@Override
+	public boolean isReadOnly(int offset, int length) {
+		final ITypedRegion[] readOnlyRegions = computeReadOnlyPartitions(offset, length);
+		int o = offset;
+		int end = offset + length;
+		for (ITypedRegion readOnlyRegion : readOnlyRegions) {
+			if (o < readOnlyRegion.getOffset()) {
+				return false;
+			}
+			o += readOnlyRegion.getLength();
+			if (o >= end) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	@Override
+	public boolean containsReadOnly(int offset, int length) {
+		return computeReadOnlyPartitions(offset, length).length > 0;
+	}
+
+	@Override
+	public int getPreviousOffsetByState(int offset, boolean searchWritable) {
+		if (partitions != null) {
+			int partitionIndex = findPartitionCandidate(offset);
+			for (; partitionIndex >= 0; partitionIndex--) {
+				final IOConsolePartition partition = partitions.get(partitionIndex);
+				if (partition.isReadOnly() != searchWritable) {
+					return Math.min(partition.getOffset() + partition.getLength() - 1, offset);
+				}
+			}
+		}
+		return -1;
+	}
+
+	@Override
+	public int getNextOffsetByState(int offset, boolean searchWritable) {
+		if (partitions != null) {
+			int partitionIndex = findPartitionCandidate(offset);
+			if (partitionIndex >= 0) {
+				for (; partitionIndex < partitions.size(); partitionIndex++) {
+					final IOConsolePartition partition = partitions.get(partitionIndex);
+					if (partition.isReadOnly() != searchWritable) {
+						return Math.max(partition.getOffset(), offset);
+					}
+				}
+			}
+		}
+		return document != null ? document.getLength() : 0;
+	}
+
 	/**
 	 * Get a partition by its index. Safe from out of bounds exceptions.
 	 *