Skip to main content
summaryrefslogtreecommitdiffstats
blob: 3e1dd5ced35f96c87f36e94b23efb98ff22bf96f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-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;

import org.eclipse.core.runtime.Assert;


/**
 * Represents a text modification as a document replace command. The text
 * modification is given as a {@link org.eclipse.swt.events.VerifyEvent} 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.</p>
 */
public class DocumentCommand {

	/**
	 * A command which is added to document commands.
	 * @since 2.1
	 */
	private static class Command implements Comparable<Command> {
		/** 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 listener 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>
		 * @since 3.0
		 */
		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;
		}

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

		@Override
		public int compareTo(Command object) {
			if (isEqual(object))
				return 0;

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

		private boolean isEqual(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.
	 *
	 * @param <E> the type of elements returned by this iterator
	 */
	private static class ReverseListIterator<E> implements Iterator<E> {

		/** The list iterator. */
		private final ListIterator<E> fListIterator;

		/**
		 * Creates a reverse list iterator.
		 * @param listIterator the iterator that this reverse iterator is based upon
		 */
		public ReverseListIterator(ListIterator<E> listIterator) {
			if (listIterator == null)
				throw new IllegalArgumentException();
			fListIterator= listIterator;
		}

		@Override
		public boolean hasNext() {
			return fListIterator.hasPrevious();
		}

		@Override
		public E next() {
			return fListIterator.previous();
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException();
		}
	}

	/**
	 * A command iterator.
	 */
	private static class CommandIterator implements Iterator<Command> {

		/** The command iterator. */
		private final Iterator<Command> 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<Command> 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;
		}

		@Override
		public boolean hasNext() {
			return fCommand != null || fIterator.hasNext();
		}

		@Override
		public Command 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= 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();
			}
		}

		@Override
		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.
	 * @since 2.1
	 */
	private final List<Command> fCommands= new ArrayList<>();
	/**
	 * Indicates whether the caret should be shifted by this command.
	 * @since 3.0
	 */
	public boolean shiftsCaret;


	/**
	 * 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;
		shiftsCaret= true;
		fCommands.clear();
	}

	/**
	 * Fills the given verify event with the replace text and the <code>doit</code>
	 * 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 && caretOffset == -1);
		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 commandOffset the offset of the region to replace
	 * @param commandLength the length of the region to replace
	 * @param commandText the text to replace with, may be <code>null</code>
	 * @param commandOwner 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 commandOffset, int commandLength, String commandText, IDocumentListener commandOwner) throws BadLocationException {
		final Command command= new Command(commandOffset, commandLength, commandText, commandOwner);

		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(fCommands.get(insertionIndex), command))
			throw new BadLocationException();

		// overlaps to the left?
		if (insertionIndex != 0 && intersects(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<Command> 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;
		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;
		return true;
	}

	/**
	 * Executes the document commands on a document.
	 *
	 * @param document the document on which to execute the commands
	 * @throws BadLocationException in case access to the given document fails
	 * @since 2.1
	 */
	void execute(IDocument document) throws BadLocationException {

		if (length == 0 && text == null && fCommands.size() == 0)
			return;

		DefaultPositionUpdater updater= new DefaultPositionUpdater(getCategory());
		Position caretPosition= null;
		try {
			if (updateCaret()) {
				document.addPositionCategory(getCategory());
				document.addPositionUpdater(updater);
				caretPosition= new Position(caretOffset);
				document.addPosition(getCategory(), caretPosition);
			}

			final Command originalCommand= new Command(offset, length, text, owner);
			for (final Iterator<Command> iterator= new CommandIterator(fCommands, originalCommand, false); iterator.hasNext(); )
				iterator.next().execute(document);

		} catch (BadLocationException e) {
			// ignore
		} catch (BadPositionCategoryException e) {
			// ignore
		} finally {
			if (updateCaret()) {
				document.removePositionUpdater(updater);
				try {
					document.removePositionCategory(getCategory());
				} catch (BadPositionCategoryException e) {
					Assert.isTrue(false);
				}
				caretOffset= caretPosition.getOffset();
			}
		}
	}

	/**
	 * Returns <code>true</code> if the caret offset should be updated, <code>false</code> otherwise.
	 *
	 * @return <code>true</code> if the caret offset should be updated, <code>false</code> otherwise
	 * @since 3.0
	 */
	private boolean updateCaret() {
		return shiftsCaret && caretOffset != -1;
	}

	/**
	 * Returns the position category for the caret offset position.
	 *
	 * @return the position category for the caret offset position
	 * @since 3.0
	 */
	private String getCategory() {
		return toString();
	}

}

Back to the top