Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: f778e7a9546072b0fc7978155fc6899679a652d4 (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
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
/*******************************************************************************
 * Copyright (c) 2000, 2016 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jface.text;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.runtime.Assert;


/**
 * A collection of text functions.
 * <p>
 * This class is neither intended to be instantiated nor subclassed.
 * </p>
 * @noinstantiate This class is not intended to be instantiated by clients.
 * @noextend This class is not intended to be subclassed by clients.
 */
public class TextUtilities {

	/**
	 * Default line delimiters used by the text functions of this class.
	 */
	public final static String[] DELIMITERS= new String[] { "\n", "\r", "\r\n" }; //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$

	/**
	 * Default line delimiters used by these text functions.
	 *
	 * @deprecated use DELIMITERS instead
	 */
	@Deprecated
	public final static String[] fgDelimiters= DELIMITERS;



	/**
	 * Determines which one of default line delimiters appears first in the list. If none of them the
	 * hint is returned.
	 *
	 * @param text the text to be checked
	 * @param hint the line delimiter hint
	 * @return the line delimiter
	 */
	public static String determineLineDelimiter(String text, String hint) {
		try {
			int[] info= indexOf(DELIMITERS, text, 0);
			return DELIMITERS[info[1]];
		} catch (ArrayIndexOutOfBoundsException x) {
		}
		return hint;
	}

	/**
	 * Returns the starting position and the index of the first matching search string
	 * in the given text that is greater than the given offset. If more than one search
	 * string matches with the same starting position then the longest one is returned.
	 *
	 * @param searchStrings the strings to search for
	 * @param text the text to be searched
	 * @param offset the offset at which to start the search
	 * @return an <code>int[]</code> with two elements where the first is the starting offset, the second the index of the found
	 * 		search string in the given <code>searchStrings</code> array, returns <code>[-1, -1]</code> if no match exists
	 */
	public static int[] indexOf(String[] searchStrings, String text, int offset) {

		int[] result= { -1, -1 };
		int zeroIndex= -1;

		for (int i= 0; i < searchStrings.length; i++) {

			int length= searchStrings[i].length();

			if (length == 0) {
				zeroIndex= i;
				continue;
			}

			int index= text.indexOf(searchStrings[i], offset);
			if (index >= 0) {

				if (result[0] == -1) {
					result[0]= index;
					result[1]= i;
				} else if (index < result[0]) {
					result[0]= index;
					result[1]= i;
				} else if (index == result[0] && length > searchStrings[result[1]].length()) {
					result[0]= index;
					result[1]= i;
				}
			}
		}

		if (zeroIndex > -1 && result[0] == -1) {
			result[0]= 0;
			result[1]= zeroIndex;
		}

		return result;
	}

	/**
	 * Returns the index of the longest search string with which the given text ends or
	 * <code>-1</code> if none matches.
	 *
	 * @param searchStrings the strings to search for
	 * @param text the text to search
	 * @return the index in <code>searchStrings</code> of the longest string with which <code>text</code> ends or <code>-1</code>
	 */
	public static int endsWith(String[] searchStrings, String text) {

		int index= -1;

		for (int i= 0; i < searchStrings.length; i++) {
			if (text.endsWith(searchStrings[i])) {
				if (index == -1 || searchStrings[i].length() > searchStrings[index].length())
					index= i;
			}
		}

		return index;
	}

	/**
	 * Returns the index of the longest search string with which the given text starts or <code>-1</code>
	 * if none matches.
	 *
	 * @param searchStrings the strings to search for
	 * @param text the text to search
	 * @return the index in <code>searchStrings</code> of the longest string with which <code>text</code> starts or <code>-1</code>
	 */
	public static int startsWith(String[] searchStrings, String text) {

		int index= -1;

		for (int i= 0; i < searchStrings.length; i++) {
			if (text.startsWith(searchStrings[i])) {
				if (index == -1 || searchStrings[i].length() > searchStrings[index].length())
					index= i;
			}
		}

		return index;
	}

	/**
	 * Returns the index of the first compare string that equals the given text or <code>-1</code>
	 * if none is equal.
	 *
	 * @param compareStrings the strings to compare with
	 * @param text the text to check
	 * @return the index of the first equal compare string or <code>-1</code>
	 */
	public static int equals(String[] compareStrings, String text) {
		for (int i= 0; i < compareStrings.length; i++) {
			if (text.equals(compareStrings[i]))
				return i;
		}
		return -1;
	}

	/**
	 * Returns a document event which is an accumulation of a list of document events,
	 * <code>null</code> if the list of documentEvents is empty.
	 * The document of the document events are ignored.
	 *
	 * @param unprocessedDocument the document to which the document events would be applied
	 * @param documentEvents the list of document events to merge
	 * @return returns the merged document event
	 * @throws BadLocationException might be thrown if document is not in the correct state with respect to document events
	 */
	public static DocumentEvent mergeUnprocessedDocumentEvents(IDocument unprocessedDocument, List<? extends DocumentEvent> documentEvents) throws BadLocationException {

		if (documentEvents.isEmpty())
			return null;

		final Iterator<? extends DocumentEvent> iterator= documentEvents.iterator();
		final DocumentEvent firstEvent= iterator.next();

		// current merged event
		final IDocument document= unprocessedDocument;
		int offset= firstEvent.getOffset();
		int length= firstEvent.getLength();
		final StringBuilder text= new StringBuilder(firstEvent.getText() == null ? "" : firstEvent.getText()); //$NON-NLS-1$

		while (iterator.hasNext()) {

			final int delta= text.length() - length;

			final DocumentEvent event= iterator.next();
			final int eventOffset= event.getOffset();
			final int eventLength= event.getLength();
			final String eventText= event.getText() == null ? "" : event.getText(); //$NON-NLS-1$

			// event is right from merged event
			if (eventOffset > offset + length + delta) {
				final String string= document.get(offset + length, (eventOffset - delta) - (offset + length));
				text.append(string);
				text.append(eventText);

				length= (eventOffset - delta) + eventLength - offset;

			// event is left from merged event
			} else if (eventOffset + eventLength < offset) {
				final String string= document.get(eventOffset + eventLength, offset - (eventOffset + eventLength));
				text.insert(0, string);
				text.insert(0, eventText);

				length= offset + length - eventOffset;
				offset= eventOffset;

			// events overlap each other
			} else {
				final int start= Math.max(0, eventOffset - offset);
				final int end= Math.min(text.length(), eventLength + eventOffset - offset);
				text.replace(start, end, eventText);

				offset= Math.min(offset, eventOffset);
				final int totalDelta= delta + eventText.length() - eventLength;
				length= text.length() - totalDelta;
			}
		}

		return new DocumentEvent(document, offset, length, text.toString());
	}

	/**
	 * Returns a document event which is an accumulation of a list of document events,
	 * <code>null</code> if the list of document events is empty.
	 * The document events being merged must all refer to the same document, to which
	 * the document changes have been already applied.
	 *
	 * @param documentEvents the list of document events to merge
	 * @return returns the merged document event
	 * @throws BadLocationException might be thrown if document is not in the correct state with respect to document events
	 */
	public static DocumentEvent mergeProcessedDocumentEvents(List<? extends DocumentEvent> documentEvents) throws BadLocationException {

		if (documentEvents.isEmpty())
			return null;

		final ListIterator<? extends DocumentEvent> iterator= documentEvents.listIterator(documentEvents.size());
		final DocumentEvent firstEvent= iterator.previous();

		// current merged event
		final IDocument document= firstEvent.getDocument();
		int offset= firstEvent.getOffset();
		int length= firstEvent.getLength();
		int textLength= firstEvent.getText() == null ? 0 : firstEvent.getText().length();

		while (iterator.hasPrevious()) {

			final int delta= length - textLength;

			final DocumentEvent event= iterator.previous();
			final int eventOffset= event.getOffset();
			final int eventLength= event.getLength();
			final int eventTextLength= event.getText() == null ? 0 : event.getText().length();

			// event is right from merged event
			if (eventOffset > offset + textLength + delta) {
				length= (eventOffset - delta) - (offset + textLength) + length + eventLength;
				textLength= (eventOffset - delta) + eventTextLength - offset;

			// event is left from merged event
			} else if (eventOffset + eventTextLength < offset) {
				length= offset - (eventOffset + eventTextLength) + length + eventLength;
				textLength= offset + textLength - eventOffset;
				offset= eventOffset;

			// events overlap each other
			} else {
				final int start= Math.max(0, eventOffset - offset);
				final int end= Math.min(length, eventTextLength + eventOffset - offset);
				length += eventLength - (end - start);

				offset= Math.min(offset, eventOffset);
				final int totalDelta= delta + eventLength - eventTextLength;
				textLength= length - totalDelta;
			}
		}

		final String text= document.get(offset, textLength);
		return new DocumentEvent(document, offset, length, text);
	}

	/**
	 * Removes all connected document partitioners from the given document and stores them
	 * under their partitioning name in a map. This map is returned. After this method has been called
	 * the given document is no longer connected to any document partitioner.
	 *
	 * @param document the document
	 * @return the map containing the removed partitioners
	 */
	public static Map<String, IDocumentPartitioner> removeDocumentPartitioners(IDocument document) {
		Map<String, IDocumentPartitioner> partitioners= new HashMap<>();
		if (document instanceof IDocumentExtension3) {
			IDocumentExtension3 extension3= (IDocumentExtension3) document;
			String[] partitionings= extension3.getPartitionings();
			for (String partitioning : partitionings) {
				IDocumentPartitioner partitioner= extension3.getDocumentPartitioner(partitioning);
				if (partitioner != null) {
					extension3.setDocumentPartitioner(partitioning, null);
					partitioner.disconnect();
					partitioners.put(partitioning, partitioner);
				}
			}
		} else {
			IDocumentPartitioner partitioner= document.getDocumentPartitioner();
			if (partitioner != null) {
				document.setDocumentPartitioner(null);
				partitioner.disconnect();
				partitioners.put(IDocumentExtension3.DEFAULT_PARTITIONING, partitioner);
			}
		}
		return partitioners;
	}

	/**
	 * Connects the given document with all document partitioners stored in the given map under
	 * their partitioning name. This method cleans the given map.
	 *
	 * @param document the document
	 * @param partitioners the map containing the partitioners to be connected
	 * @since 3.0
	 */
	public static void addDocumentPartitioners(IDocument document, Map<String, ? extends IDocumentPartitioner> partitioners) {
		if (document instanceof IDocumentExtension3) {
			IDocumentExtension3 extension3= (IDocumentExtension3) document;
			for (Entry<String, ? extends IDocumentPartitioner> entry : partitioners.entrySet()) {
				String partitioning= entry.getKey();
				IDocumentPartitioner partitioner= entry.getValue();
				partitioner.connect(document);
				extension3.setDocumentPartitioner(partitioning, partitioner);
			}
			partitioners.clear();
		} else {
			IDocumentPartitioner partitioner= partitioners.get(IDocumentExtension3.DEFAULT_PARTITIONING);
			partitioner.connect(document);
			document.setDocumentPartitioner(partitioner);
		}
	}

	/**
	 * Returns the content type at the given offset of the given document.
	 *
	 * @param document the document
	 * @param partitioning the partitioning to be used
	 * @param offset the offset
	 * @param preferOpenPartitions <code>true</code> if precedence should be
	 *        given to a open partition ending at <code>offset</code> over a
	 *        closed partition starting at <code>offset</code>
	 * @return the content type at the given offset of the document
	 * @throws BadLocationException if offset is invalid in the document
	 * @since 3.0
	 */
	public static String getContentType(IDocument document, String partitioning, int offset, boolean preferOpenPartitions) throws BadLocationException {
		if (document instanceof IDocumentExtension3) {
			IDocumentExtension3 extension3= (IDocumentExtension3) document;
			try {
				return extension3.getContentType(partitioning, offset, preferOpenPartitions);
			} catch (BadPartitioningException x) {
				return IDocument.DEFAULT_CONTENT_TYPE;
			}
		}

		return document.getContentType(offset);
	}

	/**
	 * Returns the partition of the given offset of the given document.
	 *
	 * @param document the document
	 * @param partitioning the partitioning to be used
	 * @param offset the offset
	 * @param preferOpenPartitions <code>true</code> if precedence should be
	 *        given to a open partition ending at <code>offset</code> over a
	 *        closed partition starting at <code>offset</code>
	 * @return the content type at the given offset of this viewer's input
	 *         document
	 * @throws BadLocationException if offset is invalid in the given document
	 * @since 3.0
	 */
	public static ITypedRegion getPartition(IDocument document, String partitioning, int offset, boolean preferOpenPartitions) throws BadLocationException {
		if (document instanceof IDocumentExtension3) {
			IDocumentExtension3 extension3= (IDocumentExtension3) document;
			try {
				return extension3.getPartition(partitioning, offset, preferOpenPartitions);
			} catch (BadPartitioningException x) {
				return new TypedRegion(0, document.getLength(), IDocument.DEFAULT_CONTENT_TYPE);
			}
		}

		return document.getPartition(offset);
	}

	/**
	 * Computes and returns the partitioning for the given region of the given
	 * document for the given partitioning name.
	 *
	 * @param document the document
	 * @param partitioning the partitioning name
	 * @param offset the region offset
	 * @param length the region length
	 * @param includeZeroLengthPartitions whether to include zero-length partitions
	 * @return the partitioning for the given region of the given document for
	 *         the given partitioning name
	 * @throws BadLocationException if the given region is invalid for the given
	 *         document
	 * @since 3.0
	 */
	public static ITypedRegion[] computePartitioning(IDocument document, String partitioning, int offset, int length, boolean includeZeroLengthPartitions) throws BadLocationException {
		if (document instanceof IDocumentExtension3) {
			IDocumentExtension3 extension3= (IDocumentExtension3) document;
			try {
				return extension3.computePartitioning(partitioning, offset, length, includeZeroLengthPartitions);
			} catch (BadPartitioningException x) {
				return new ITypedRegion[0];
			}
		}

		return document.computePartitioning(offset, length);
	}

	/**
	 * Computes and returns the partition managing position categories for the
	 * given document or <code>null</code> if this was impossible.
	 *
	 * @param document the document
	 * @return the partition managing position categories or <code>null</code>
	 * @since 3.0
	 */
	public static String[] computePartitionManagingCategories(IDocument document) {
		if (document instanceof IDocumentExtension3) {
			IDocumentExtension3 extension3= (IDocumentExtension3) document;
			String[] partitionings= extension3.getPartitionings();
			if (partitionings != null) {
				Set<String> categories= new HashSet<>();
				for (String partitioning : partitionings) {
					IDocumentPartitioner p= extension3.getDocumentPartitioner(partitioning);
					if (p instanceof IDocumentPartitionerExtension2) {
						IDocumentPartitionerExtension2 extension2= (IDocumentPartitionerExtension2) p;
						String[] c= extension2.getManagingPositionCategories();
						if (c != null) {
							Collections.addAll(categories, c);
						}
					}
				}
				String[] result= new String[categories.size()];
				categories.toArray(result);
				return result;
			}
		}
		return null;
	}

	/**
	 * Returns the default line delimiter for the given document. This is
	 * {@link IDocumentExtension4#getDefaultLineDelimiter()} if available.
	 * Otherwise, this is either the delimiter of the first line, or the platform line delimiter if it is
	 * a legal line delimiter, or the first one of the legal line delimiters. The default line delimiter should be used when performing document
	 * manipulations that span multiple lines.
	 *
	 * @param document the document
	 * @return the document's default line delimiter
	 * @since 3.0
	 */
	public static String getDefaultLineDelimiter(IDocument document) {
		String lineDelimiter= null;

		if (document instanceof IDocumentExtension4) {
			lineDelimiter= ((IDocumentExtension4) document).getDefaultLineDelimiter();
			if (lineDelimiter != null)
				return lineDelimiter;
		}

		try {
			lineDelimiter= document.getLineDelimiter(0);
		} catch (BadLocationException x) {
		}

		if (lineDelimiter != null)
			return lineDelimiter;

		String sysLineDelimiter= System.getProperty("line.separator"); //$NON-NLS-1$
		String[] delimiters= document.getLegalLineDelimiters();
		Assert.isTrue(delimiters.length > 0);
		for (String delimiter : delimiters) {
			if (delimiter.equals(sysLineDelimiter)) {
				lineDelimiter= sysLineDelimiter;
				break;
			}
		}

		if (lineDelimiter == null)
			lineDelimiter= delimiters[0];

		return lineDelimiter;
	}

	/**
	 * Returns <code>true</code> if the two regions overlap. Returns <code>false</code> if one of the
	 * arguments is <code>null</code>.
	 *
	 * @param left the left region
	 * @param right the right region
	 * @return <code>true</code> if the two regions overlap, <code>false</code> otherwise
	 * @since 3.0
	 */
	public static boolean overlaps(IRegion left, IRegion right) {

		if (left == null || right == null)
			return false;

		int rightEnd= right.getOffset() + right.getLength();
		int leftEnd= left.getOffset()+ left.getLength();

		if (right.getLength() > 0) {
			if (left.getLength() > 0)
				return left.getOffset() < rightEnd && right.getOffset() < leftEnd;
			return  right.getOffset() <= left.getOffset() && left.getOffset() < rightEnd;
		}

		if (left.getLength() > 0)
			return left.getOffset() <= right.getOffset() && right.getOffset() < leftEnd;

		return left.getOffset() == right.getOffset();
	}

	/**
	 * Returns a copy of the given string array.
	 *
	 * @param array the string array to be copied
	 * @return a copy of the given string array or <code>null</code> when <code>array</code> is <code>null</code>
	 * @since 3.1
	 */
	public static String[] copy(String[] array) {
		if (array != null) {
			String[] copy= new String[array.length];
			System.arraycopy(array, 0, copy, 0, array.length);
			return copy;
		}
		return null;
	}

	/**
	 * Returns a copy of the given integer array.
	 *
	 * @param array the integer array to be copied
	 * @return a copy of the given integer array or <code>null</code> when <code>array</code> is <code>null</code>
	 * @since 3.1
	 */
	public static int[] copy(int[] array) {
		if (array != null) {
			int[] copy= new int[array.length];
			System.arraycopy(array, 0, copy, 0, array.length);
			return copy;
		}
		return null;
	}
}

Back to the top