129
|
1 /*******************************************************************************
|
|
2 * Copyright (c) 2000, 2007 IBM Corporation and others.
|
|
3 * All rights reserved. This program and the accompanying materials
|
|
4 * are made available under the terms of the Eclipse Public License v1.0
|
|
5 * which accompanies this distribution, and is available at
|
|
6 * http://www.eclipse.org/legal/epl-v10.html
|
|
7 *
|
|
8 * Contributors:
|
|
9 * IBM Corporation - initial API and implementation
|
|
10 * Port to the D programming language:
|
|
11 * Frank Benoit <benoit@tionex.de>
|
|
12 *******************************************************************************/
|
|
13
|
|
14 module dwtx.jface.text.DocumentCommand;
|
|
15
|
|
16 import dwt.dwthelper.utils;
|
|
17
|
|
18
|
|
19 import java.util.ArrayList;
|
|
20 import java.util.Collections;
|
|
21 import java.util.Iterator;
|
|
22 import java.util.List;
|
|
23 import java.util.ListIterator;
|
|
24 import java.util.NoSuchElementException;
|
|
25
|
|
26 import dwt.events.VerifyEvent;
|
|
27 import dwtx.core.runtime.Assert;
|
|
28
|
|
29
|
|
30 /**
|
|
31 * Represents a text modification as a document replace command. The text
|
|
32 * modification is given as a {@link dwt.events.VerifyEvent} and
|
|
33 * translated into a document replace command relative to a given offset. A
|
|
34 * document command can also be used to initialize a given
|
|
35 * <code>VerifyEvent</code>.
|
|
36 * <p>
|
|
37 * A document command can also represent a list of related changes.</p>
|
|
38 */
|
|
39 public class DocumentCommand {
|
|
40
|
|
41 /**
|
|
42 * A command which is added to document commands.
|
|
43 * @since 2.1
|
|
44 */
|
|
45 private static class Command : Comparable {
|
|
46 /** The offset of the range to be replaced */
|
|
47 private final int fOffset;
|
|
48 /** The length of the range to be replaced. */
|
|
49 private final int fLength;
|
|
50 /** The replacement text */
|
|
51 private final String fText;
|
|
52 /** The listener who owns this command */
|
|
53 private final IDocumentListener fOwner;
|
|
54
|
|
55 /**
|
|
56 * Creates a new command with the given specification.
|
|
57 *
|
|
58 * @param offset the offset of the replace command
|
|
59 * @param length the length of the replace command
|
|
60 * @param text the text to replace with, may be <code>null</code>
|
|
61 * @param owner the document command owner, may be <code>null</code>
|
|
62 * @since 3.0
|
|
63 */
|
|
64 public Command(int offset, int length, String text, IDocumentListener owner) {
|
|
65 if (offset < 0 || length < 0)
|
|
66 throw new IllegalArgumentException();
|
|
67 fOffset= offset;
|
|
68 fLength= length;
|
|
69 fText= text;
|
|
70 fOwner= owner;
|
|
71 }
|
|
72
|
|
73 /**
|
|
74 * Returns the length delta for this command.
|
|
75 *
|
|
76 * @return the length delta for this command
|
|
77 */
|
|
78 public int getDeltaLength() {
|
|
79 return (fText is null ? 0 : fText.length()) - fLength;
|
|
80 }
|
|
81
|
|
82 /**
|
|
83 * Executes the document command on the specified document.
|
|
84 *
|
|
85 * @param document the document on which to execute the command.
|
|
86 * @throws BadLocationException in case this commands cannot be executed
|
|
87 */
|
|
88 public void execute(IDocument document) throws BadLocationException {
|
|
89
|
|
90 if (fLength is 0 && fText is null)
|
|
91 return;
|
|
92
|
|
93 if (fOwner !is null)
|
|
94 document.removeDocumentListener(fOwner);
|
|
95
|
|
96 document.replace(fOffset, fLength, fText);
|
|
97
|
|
98 if (fOwner !is null)
|
|
99 document.addDocumentListener(fOwner);
|
|
100 }
|
|
101
|
|
102 /*
|
|
103 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
|
|
104 */
|
|
105 public int compareTo(final Object object) {
|
|
106 if (isEqual(object))
|
|
107 return 0;
|
|
108
|
|
109 final Command command= (Command) object;
|
|
110
|
|
111 // diff middle points if not intersecting
|
|
112 if (fOffset + fLength <= command.fOffset || command.fOffset + command.fLength <= fOffset) {
|
|
113 int value= (2 * fOffset + fLength) - (2 * command.fOffset + command.fLength);
|
|
114 if (value !is 0)
|
|
115 return value;
|
|
116 }
|
|
117 // the answer
|
|
118 return 42;
|
|
119 }
|
|
120
|
|
121 private bool isEqual(Object object) {
|
|
122 if (object is this)
|
|
123 return true;
|
|
124 if (!(object instanceof Command))
|
|
125 return false;
|
|
126 final Command command= (Command) object;
|
|
127 return command.fOffset is fOffset && command.fLength is fLength;
|
|
128 }
|
|
129 }
|
|
130
|
|
131 /**
|
|
132 * An iterator, which iterates in reverse over a list.
|
|
133 */
|
|
134 private static class ReverseListIterator : Iterator {
|
|
135
|
|
136 /** The list iterator. */
|
|
137 private final ListIterator fListIterator;
|
|
138
|
|
139 /**
|
|
140 * Creates a reverse list iterator.
|
|
141 * @param listIterator the iterator that this reverse iterator is based upon
|
|
142 */
|
|
143 public ReverseListIterator(ListIterator listIterator) {
|
|
144 if (listIterator is null)
|
|
145 throw new IllegalArgumentException();
|
|
146 fListIterator= listIterator;
|
|
147 }
|
|
148
|
|
149 /*
|
|
150 * @see java.util.Iterator#hasNext()
|
|
151 */
|
|
152 public bool hasNext() {
|
|
153 return fListIterator.hasPrevious();
|
|
154 }
|
|
155
|
|
156 /*
|
|
157 * @see java.util.Iterator#next()
|
|
158 */
|
|
159 public Object next() {
|
|
160 return fListIterator.previous();
|
|
161 }
|
|
162
|
|
163 /*
|
|
164 * @see java.util.Iterator#remove()
|
|
165 */
|
|
166 public void remove() {
|
|
167 throw new UnsupportedOperationException();
|
|
168 }
|
|
169 }
|
|
170
|
|
171 /**
|
|
172 * A command iterator.
|
|
173 */
|
|
174 private static class CommandIterator : Iterator {
|
|
175
|
|
176 /** The command iterator. */
|
|
177 private final Iterator fIterator;
|
|
178
|
|
179 /** The original command. */
|
|
180 private Command fCommand;
|
|
181
|
|
182 /** A flag indicating the direction of iteration. */
|
|
183 private bool fForward;
|
|
184
|
|
185 /**
|
|
186 * Creates a command iterator.
|
|
187 *
|
|
188 * @param commands an ascending ordered list of commands
|
|
189 * @param command the original command
|
|
190 * @param forward the direction
|
|
191 */
|
|
192 public CommandIterator(final List commands, final Command command, final bool forward) {
|
|
193 if (commands is null || command is null)
|
|
194 throw new IllegalArgumentException();
|
|
195 fIterator= forward ? commands.iterator() : new ReverseListIterator(commands.listIterator(commands.size()));
|
|
196 fCommand= command;
|
|
197 fForward= forward;
|
|
198 }
|
|
199
|
|
200 /*
|
|
201 * @see java.util.Iterator#hasNext()
|
|
202 */
|
|
203 public bool hasNext() {
|
|
204 return fCommand !is null || fIterator.hasNext();
|
|
205 }
|
|
206
|
|
207 /*
|
|
208 * @see java.util.Iterator#next()
|
|
209 */
|
|
210 public Object next() {
|
|
211
|
|
212 if (!hasNext())
|
|
213 throw new NoSuchElementException();
|
|
214
|
|
215 if (fCommand is null)
|
|
216 return fIterator.next();
|
|
217
|
|
218 if (!fIterator.hasNext()) {
|
|
219 final Command tempCommand= fCommand;
|
|
220 fCommand= null;
|
|
221 return tempCommand;
|
|
222 }
|
|
223
|
|
224 final Command command= (Command) fIterator.next();
|
|
225 final int compareValue= command.compareTo(fCommand);
|
|
226
|
|
227 if ((compareValue < 0) ^ !fForward) {
|
|
228 return command;
|
|
229
|
|
230 } else if ((compareValue > 0) ^ !fForward) {
|
|
231 final Command tempCommand= fCommand;
|
|
232 fCommand= command;
|
|
233 return tempCommand;
|
|
234
|
|
235 } else {
|
|
236 throw new IllegalArgumentException();
|
|
237 }
|
|
238 }
|
|
239
|
|
240 /*
|
|
241 * @see java.util.Iterator#remove()
|
|
242 */
|
|
243 public void remove() {
|
|
244 throw new UnsupportedOperationException();
|
|
245 }
|
|
246 }
|
|
247
|
|
248 /** Must the command be updated */
|
|
249 public bool doit= false;
|
|
250 /** The offset of the command. */
|
|
251 public int offset;
|
|
252 /** The length of the command */
|
|
253 public int length;
|
|
254 /** The text to be inserted */
|
|
255 public String text;
|
|
256 /**
|
|
257 * The owner of the document command which will not be notified.
|
|
258 * @since 2.1
|
|
259 */
|
|
260 public IDocumentListener owner;
|
|
261 /**
|
|
262 * The caret offset with respect to the document before the document command is executed.
|
|
263 * @since 2.1
|
|
264 */
|
|
265 public int caretOffset;
|
|
266 /**
|
|
267 * Additional document commands.
|
|
268 * @since 2.1
|
|
269 */
|
|
270 private final List fCommands= new ArrayList();
|
|
271 /**
|
|
272 * Indicates whether the caret should be shifted by this command.
|
|
273 * @since 3.0
|
|
274 */
|
|
275 public bool shiftsCaret;
|
|
276
|
|
277
|
|
278 /**
|
|
279 * Creates a new document command.
|
|
280 */
|
|
281 protected DocumentCommand() {
|
|
282 }
|
|
283
|
|
284 /**
|
|
285 * Translates a verify event into a document replace command using the given offset.
|
|
286 *
|
|
287 * @param event the event to be translated
|
|
288 * @param modelRange the event range as model range
|
|
289 */
|
|
290 void setEvent(VerifyEvent event, IRegion modelRange) {
|
|
291
|
|
292 doit= true;
|
|
293 text= event.text;
|
|
294
|
|
295 offset= modelRange.getOffset();
|
|
296 length= modelRange.getLength();
|
|
297
|
|
298 owner= null;
|
|
299 caretOffset= -1;
|
|
300 shiftsCaret= true;
|
|
301 fCommands.clear();
|
|
302 }
|
|
303
|
|
304 /**
|
|
305 * Fills the given verify event with the replace text and the <code>doit</code>
|
|
306 * flag of this document command. Returns whether the document command
|
|
307 * covers the same range as the verify event considering the given offset.
|
|
308 *
|
|
309 * @param event the event to be changed
|
|
310 * @param modelRange to be considered for range comparison
|
|
311 * @return <code>true</code> if this command and the event cover the same range
|
|
312 */
|
|
313 bool fillEvent(VerifyEvent event, IRegion modelRange) {
|
|
314 event.text= text;
|
|
315 event.doit= (offset is modelRange.getOffset() && length is modelRange.getLength() && doit && caretOffset is -1);
|
|
316 return event.doit;
|
|
317 }
|
|
318
|
|
319 /**
|
|
320 * Adds an additional replace command. The added replace command must not overlap
|
|
321 * with existing ones. If the document command owner is not <code>null</code>, it will not
|
|
322 * get document change notifications for the particular command.
|
|
323 *
|
|
324 * @param commandOffset the offset of the region to replace
|
|
325 * @param commandLength the length of the region to replace
|
|
326 * @param commandText the text to replace with, may be <code>null</code>
|
|
327 * @param commandOwner the command owner, may be <code>null</code>
|
|
328 * @throws BadLocationException if the added command intersects with an existing one
|
|
329 * @since 2.1
|
|
330 */
|
|
331 public void addCommand(int commandOffset, int commandLength, String commandText, IDocumentListener commandOwner) throws BadLocationException {
|
|
332 final Command command= new Command(commandOffset, commandLength, commandText, commandOwner);
|
|
333
|
|
334 if (intersects(command))
|
|
335 throw new BadLocationException();
|
|
336
|
|
337 final int index= Collections.binarySearch(fCommands, command);
|
|
338
|
|
339 // a command with exactly the same ranges exists already
|
|
340 if (index >= 0)
|
|
341 throw new BadLocationException();
|
|
342
|
|
343 // binary search result is defined as (-(insertionIndex) - 1)
|
|
344 final int insertionIndex= -(index + 1);
|
|
345
|
|
346 // overlaps to the right?
|
|
347 if (insertionIndex !is fCommands.size() && intersects((Command) fCommands.get(insertionIndex), command))
|
|
348 throw new BadLocationException();
|
|
349
|
|
350 // overlaps to the left?
|
|
351 if (insertionIndex !is 0 && intersects((Command) fCommands.get(insertionIndex - 1), command))
|
|
352 throw new BadLocationException();
|
|
353
|
|
354 fCommands.add(insertionIndex, command);
|
|
355 }
|
|
356
|
|
357 /**
|
|
358 * Returns an iterator over the commands in ascending position order.
|
|
359 * The iterator includes the original document command.
|
|
360 * Commands cannot be removed.
|
|
361 *
|
|
362 * @return returns the command iterator
|
|
363 */
|
|
364 public Iterator getCommandIterator() {
|
|
365 Command command= new Command(offset, length, text, owner);
|
|
366 return new CommandIterator(fCommands, command, true);
|
|
367 }
|
|
368
|
|
369 /**
|
|
370 * Returns the number of commands including the original document command.
|
|
371 *
|
|
372 * @return returns the number of commands
|
|
373 * @since 2.1
|
|
374 */
|
|
375 public int getCommandCount() {
|
|
376 return 1 + fCommands.size();
|
|
377 }
|
|
378
|
|
379 /**
|
|
380 * Returns whether the two given commands intersect.
|
|
381 *
|
|
382 * @param command0 the first command
|
|
383 * @param command1 the second command
|
|
384 * @return <code>true</code> if the commands intersect
|
|
385 * @since 2.1
|
|
386 */
|
|
387 private bool intersects(Command command0, Command command1) {
|
|
388 // diff middle points if not intersecting
|
|
389 if (command0.fOffset + command0.fLength <= command1.fOffset || command1.fOffset + command1.fLength <= command0.fOffset)
|
|
390 return (2 * command0.fOffset + command0.fLength) - (2 * command1.fOffset + command1.fLength) is 0;
|
|
391 return true;
|
|
392 }
|
|
393
|
|
394 /**
|
|
395 * Returns whether the given command intersects with this command.
|
|
396 *
|
|
397 * @param command the command
|
|
398 * @return <code>true</code> if the command intersects with this command
|
|
399 * @since 2.1
|
|
400 */
|
|
401 private bool intersects(Command command) {
|
|
402 // diff middle points if not intersecting
|
|
403 if (offset + length <= command.fOffset || command.fOffset + command.fLength <= offset)
|
|
404 return (2 * offset + length) - (2 * command.fOffset + command.fLength) is 0;
|
|
405 return true;
|
|
406 }
|
|
407
|
|
408 /**
|
|
409 * Executes the document commands on a document.
|
|
410 *
|
|
411 * @param document the document on which to execute the commands
|
|
412 * @throws BadLocationException in case access to the given document fails
|
|
413 * @since 2.1
|
|
414 */
|
|
415 void execute(IDocument document) throws BadLocationException {
|
|
416
|
|
417 if (length is 0 && text is null && fCommands.size() is 0)
|
|
418 return;
|
|
419
|
|
420 DefaultPositionUpdater updater= new DefaultPositionUpdater(getCategory());
|
|
421 Position caretPosition= null;
|
|
422 try {
|
|
423 if (updateCaret()) {
|
|
424 document.addPositionCategory(getCategory());
|
|
425 document.addPositionUpdater(updater);
|
|
426 caretPosition= new Position(caretOffset);
|
|
427 document.addPosition(getCategory(), caretPosition);
|
|
428 }
|
|
429
|
|
430 final Command originalCommand= new Command(offset, length, text, owner);
|
|
431 for (final Iterator iterator= new CommandIterator(fCommands, originalCommand, false); iterator.hasNext(); )
|
|
432 ((Command) iterator.next()).execute(document);
|
|
433
|
|
434 } catch (BadLocationException e) {
|
|
435 // ignore
|
|
436 } catch (BadPositionCategoryException e) {
|
|
437 // ignore
|
|
438 } finally {
|
|
439 if (updateCaret()) {
|
|
440 document.removePositionUpdater(updater);
|
|
441 try {
|
|
442 document.removePositionCategory(getCategory());
|
|
443 } catch (BadPositionCategoryException e) {
|
|
444 Assert.isTrue(false);
|
|
445 }
|
|
446 caretOffset= caretPosition.getOffset();
|
|
447 }
|
|
448 }
|
|
449 }
|
|
450
|
|
451 /**
|
|
452 * Returns <code>true</code> if the caret offset should be updated, <code>false</code> otherwise.
|
|
453 *
|
|
454 * @return <code>true</code> if the caret offset should be updated, <code>false</code> otherwise
|
|
455 * @since 3.0
|
|
456 */
|
|
457 private bool updateCaret() {
|
|
458 return shiftsCaret && caretOffset !is -1;
|
|
459 }
|
|
460
|
|
461 /**
|
|
462 * Returns the position category for the caret offset position.
|
|
463 *
|
|
464 * @return the position category for the caret offset position
|
|
465 * @since 3.0
|
|
466 */
|
|
467 private String getCategory() {
|
|
468 return toString();
|
|
469 }
|
|
470
|
|
471 }
|