8875202d6f07ddaf30dae0428ac51e0e454d819c
[idea/community.git] / platform / util / src / com / intellij / openapi / util / text / StringUtil.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.util.text;
3
4 import com.intellij.ReviseWhenPortedToJDK;
5 import com.intellij.openapi.diagnostic.Logger;
6 import com.intellij.openapi.progress.ProcessCanceledException;
7 import com.intellij.openapi.util.Pair;
8 import com.intellij.openapi.util.TextRange;
9 import com.intellij.util.*;
10 import com.intellij.util.containers.ContainerUtil;
11 import com.intellij.util.text.CharArrayUtil;
12 import com.intellij.util.text.CharSequenceSubSequence;
13 import com.intellij.util.text.MergingCharSequence;
14 import com.intellij.util.text.StringFactory;
15 import org.jetbrains.annotations.*;
16
17 import javax.swing.text.MutableAttributeSet;
18 import javax.swing.text.html.HTML;
19 import javax.swing.text.html.HTMLEditorKit;
20 import javax.swing.text.html.parser.ParserDelegator;
21 import java.beans.Introspector;
22 import java.io.IOException;
23 import java.io.Reader;
24 import java.io.StringReader;
25 import java.time.Duration;
26 import java.util.*;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29 import java.util.stream.Collector;
30 import java.util.stream.Collectors;
31
32 //TeamCity inherits StringUtil: do not add private constructors!!!
33 @SuppressWarnings("MethodOverridesStaticMethodOfSuperclass")
34 public class StringUtil extends StringUtilRt {
35   @SuppressWarnings("SpellCheckingInspection") private static final String VOWELS = "aeiouy";
36   private static final Pattern EOL_SPLIT_KEEP_SEPARATORS = Pattern.compile("(?<=(\r\n|\n))|(?<=\r)(?=[^\n])");
37   private static final Pattern EOL_SPLIT_PATTERN = Pattern.compile(" *(\r|\n|\r\n)+ *");
38   private static final Pattern EOL_SPLIT_PATTERN_WITH_EMPTY = Pattern.compile(" *(\r|\n|\r\n) *");
39   private static final Pattern EOL_SPLIT_DONT_TRIM_PATTERN = Pattern.compile("(\r|\n|\r\n)+");
40   public static final String ELLIPSIS = "\u2026";
41
42   /**
43    * @return a lightweight CharSequence which results from replacing {@code [start, end)} range in the {@code charSeq} with {@code replacement}.
44    * Works in O(1), but retains references to the passed char sequences, so please use something else if you want them to be garbage-collected.
45    */
46   public static @NotNull MergingCharSequence replaceSubSequence(@NotNull CharSequence charSeq, int start, int end, @NotNull CharSequence replacement) {
47     return new MergingCharSequence(
48           new MergingCharSequence(new CharSequenceSubSequence(charSeq, 0, start), replacement),
49           new CharSequenceSubSequence(charSeq, end, charSeq.length()));
50   }
51
52   private static final class MyHtml2Text extends HTMLEditorKit.ParserCallback {
53     private final @NotNull StringBuilder myBuffer = new StringBuilder();
54     private final boolean myIsSkipStyleTag;
55
56     private boolean myIsStyleTagOpened;
57
58     private MyHtml2Text(boolean isSkipStyleTag) {
59       myIsSkipStyleTag = isSkipStyleTag;
60     }
61
62     public void parse(@NotNull Reader in) throws IOException {
63       myBuffer.setLength(0);
64       new ParserDelegator().parse(in, this, Boolean.TRUE);
65     }
66
67     @Override
68     public void handleText(char @NotNull [] text, int pos) {
69       if (!myIsStyleTagOpened) {
70         myBuffer.append(text);
71       }
72     }
73
74     @Override
75     public void handleStartTag(@NotNull HTML.Tag tag, MutableAttributeSet set, int i) {
76       if (myIsSkipStyleTag && "style".equals(tag.toString())) {
77         myIsStyleTagOpened = true;
78       }
79       handleTag(tag);
80     }
81
82     @Override
83     public void handleEndTag(@NotNull HTML.Tag tag, int pos) {
84       if (myIsSkipStyleTag && "style".equals(tag.toString())) {
85         myIsStyleTagOpened = false;
86       }
87     }
88
89     @Override
90     public void handleSimpleTag(HTML.Tag tag, MutableAttributeSet set, int i) {
91       handleTag(tag);
92     }
93
94     private void handleTag(@NotNull HTML.Tag tag) {
95       if (tag.breaksFlow() && myBuffer.length() > 0) {
96         myBuffer.append(SystemProperties.getLineSeparator());
97       }
98     }
99
100     public @NotNull String getText() {
101       return myBuffer.toString();
102     }
103   }
104
105   private static final MyHtml2Text html2TextParser = new MyHtml2Text(false);
106
107   public static final NotNullFunction<String, String> QUOTER = s -> "\"" + s + "\"";
108
109   public static final NotNullFunction<String, String> SINGLE_QUOTER = s -> "'" + s + "'";
110
111   @Contract(pure = true)
112   public static @NotNull List<String> getWordsInStringLongestFirst(@NotNull String find) {
113     List<String> words = getWordsIn(find);
114     // hope long words are rare
115     words.sort((o1, o2) -> o2.length() - o1.length());
116     return words;
117   }
118
119   @Contract(pure = true)
120   public static @NotNull String escapePattern(final @NotNull String text) {
121     return replace(replace(text, "'", "''"), "{", "'{'");
122   }
123
124   @Contract(pure = true)
125   public static @NotNull <T> Function<T, String> createToStringFunction(@SuppressWarnings("unused") @NotNull Class<T> cls) {
126     return Object::toString;
127   }
128
129   public static final @NotNull Function<String, String> TRIMMER = StringUtil::trim;
130
131   // Unlike String.replace(CharSequence,CharSequence) does not allocate intermediate objects on non-match
132   // TODO revise when JDK9 arrives - its String.replace(CharSequence, CharSequence) is more optimized
133   @ReviseWhenPortedToJDK("9")
134   @Contract(pure = true)
135   public static @NotNull String replace(@NotNull String text, @NotNull String oldS, @NotNull String newS) {
136     return replace(text, oldS, newS, false);
137   }
138
139   @Contract(pure = true)
140   public static @NotNull String replaceIgnoreCase(@NotNull String text, @NotNull String oldS, @NotNull String newS) {
141     return replace(text, oldS, newS, true);
142   }
143
144   /**
145    * @deprecated Use {@link String#replace(char,char)} instead
146    */
147   @Contract(pure = true)
148   @Deprecated
149   public static @NotNull String replaceChar(@NotNull String buffer, char oldChar, char newChar) {
150     return buffer.replace(oldChar, newChar);
151   }
152
153   @Contract(pure = true)
154   public static String replace(final @NotNull String text, final @NotNull String oldS, final @NotNull String newS, final boolean ignoreCase) {
155     if (text.length() < oldS.length()) return text;
156
157     StringBuilder newText = null;
158     int i = 0;
159
160     while (i < text.length()) {
161       final int index = ignoreCase? indexOfIgnoreCase(text, oldS, i) : text.indexOf(oldS, i);
162       if (index < 0) {
163         if (i == 0) {
164           return text;
165         }
166
167         newText.append(text, i, text.length());
168         break;
169       }
170       else {
171         if (newText == null) {
172           if (text.length() == oldS.length()) {
173             return newS;
174           }
175           newText = new StringBuilder(text.length() - i);
176         }
177
178         newText.append(text, i, index);
179         newText.append(newS);
180         i = index + oldS.length();
181       }
182     }
183     return newText != null ? newText.toString() : "";
184   }
185
186   @Contract(pure = true)
187   public static int indexOfIgnoreCase(@NotNull String where, @NotNull String what, int fromIndex) {
188     return Strings.indexOfIgnoreCase(where, what, fromIndex);
189   }
190
191   /**
192    * Implementation copied from {@link String#indexOf(String, int)} except character comparisons made case insensitive
193    */
194   @Contract(pure = true)
195   public static int indexOfIgnoreCase(@NotNull CharSequence where, @NotNull CharSequence what, int fromIndex) {
196     return Strings.indexOfIgnoreCase(where, what, fromIndex);
197   }
198
199   @Contract(pure = true)
200   public static int indexOfIgnoreCase(@NotNull String where, char what, int fromIndex) {
201     return Strings.indexOfIgnoreCase(where, what, fromIndex);
202   }
203
204   @Contract(pure = true)
205   public static int lastIndexOfIgnoreCase(@NotNull String where, char what, int fromIndex) {
206     for (int i = Math.min(fromIndex, where.length() - 1); i >= 0; i--) {
207       if (charsEqualIgnoreCase(where.charAt(i), what)) {
208         return i;
209       }
210     }
211
212     return -1;
213   }
214
215   @Contract(pure = true)
216   public static boolean containsIgnoreCase(@NotNull String where, @NotNull String what) {
217     return indexOfIgnoreCase(where, what, 0) >= 0;
218   }
219
220   @Contract(pure = true)
221   public static boolean endsWithIgnoreCase(@NotNull String str, @NotNull String suffix) {
222     return Strings.endsWithIgnoreCase(str, suffix);
223   }
224
225   @Contract(pure = true)
226   public static boolean startsWithIgnoreCase(@NotNull String str, @NotNull String prefix) {
227     return StringUtilRt.startsWithIgnoreCase(str, prefix);
228   }
229
230   @Contract(pure = true)
231   public static @NotNull String stripHtml(@NotNull String html, boolean convertBreaks) {
232     if (convertBreaks) {
233       html = html.replaceAll("<br/?>", "\n\n");
234     }
235
236     return html.replaceAll("<(.|\n)*?>", "");
237   }
238
239   @Contract(value = "null -> null; !null -> !null", pure = true)
240   public static String toLowerCase(@Nullable String str) {
241     return Strings.toLowerCase(str);
242   }
243
244   @Contract(pure = true)
245   public static @NotNull String getPackageName(@NotNull String fqName) {
246     return getPackageName(fqName, '.');
247   }
248
249   /**
250    * Given a fqName returns the package name for the type or the containing type.
251    * <p/>
252    * <ul>
253    * <li>{@code java.lang.String} -> {@code java.lang}</li>
254    * <li>{@code java.util.Map.Entry} -> {@code java.util.Map}</li>
255    * </ul>
256    *
257    * @param fqName    a fully qualified type name. Not supposed to contain any type arguments
258    * @param separator the separator to use. Typically '.'
259    * @return the package name of the type or the declarator of the type. The empty string if the given fqName is unqualified
260    */
261   @Contract(pure = true)
262   public static @NotNull String getPackageName(@NotNull String fqName, char separator) {
263     int lastPointIdx = fqName.lastIndexOf(separator);
264     if (lastPointIdx >= 0) {
265       return fqName.substring(0, lastPointIdx);
266     }
267     return "";
268   }
269
270   @Contract(pure = true)
271   public static int getLineBreakCount(@NotNull CharSequence text) {
272     int count = 0;
273     for (int i = 0; i < text.length(); i++) {
274       char c = text.charAt(i);
275       if (c == '\n') {
276         count++;
277       }
278       else if (c == '\r') {
279         if (i + 1 < text.length() && text.charAt(i + 1) == '\n') {
280           //noinspection AssignmentToForLoopParameter
281           i++;
282         }
283         count++;
284       }
285     }
286     return count;
287   }
288
289   @Contract(pure = true)
290   public static boolean containsLineBreak(@NotNull CharSequence text) {
291     for (int i = 0; i < text.length(); i++) {
292       char c = text.charAt(i);
293       if (isLineBreak(c)) return true;
294     }
295     return false;
296   }
297
298   @Contract(pure = true)
299   public static boolean isLineBreak(char c) {
300     return c == '\n' || c == '\r';
301   }
302
303   @Contract(pure = true)
304   public static @NotNull String escapeLineBreak(@NotNull String text) {
305     StringBuilder buffer = new StringBuilder(text.length());
306     for (int i = 0; i < text.length(); i++) {
307       char c = text.charAt(i);
308       switch (c) {
309         case '\n':
310           buffer.append("\\n");
311           break;
312         case '\r':
313           buffer.append("\\r");
314           break;
315         default:
316           buffer.append(c);
317       }
318     }
319     return buffer.toString();
320   }
321
322   @Contract(pure = true)
323   public static boolean endsWithLineBreak(@NotNull CharSequence text) {
324     int len = text.length();
325     return len > 0 && isLineBreak(text.charAt(len - 1));
326   }
327
328   @Contract(pure = true)
329   public static int lineColToOffset(@NotNull CharSequence text, int line, int col) {
330     int curLine = 0;
331     int offset = 0;
332     while (line != curLine) {
333       if (offset == text.length()) return -1;
334       char c = text.charAt(offset);
335       if (c == '\n') {
336         curLine++;
337       }
338       else if (c == '\r') {
339         curLine++;
340         if (offset < text.length() - 1 && text.charAt(offset + 1) == '\n') {
341           offset++;
342         }
343       }
344       offset++;
345     }
346     return offset + col;
347   }
348
349   @Contract(pure = true)
350   public static int offsetToLineNumber(@NotNull CharSequence text, int offset) {
351     LineColumn lineColumn = offsetToLineColumn(text, offset);
352     return lineColumn != null ? lineColumn.line : -1;
353   }
354
355   @Contract(pure = true)
356   public static LineColumn offsetToLineColumn(@NotNull CharSequence text, int offset) {
357     int curLine = 0;
358     int curLineStart = 0;
359     int curOffset = 0;
360     while (curOffset < offset) {
361       if (curOffset == text.length()) return null;
362       char c = text.charAt(curOffset);
363       if (c == '\n') {
364         curLine++;
365         curLineStart = curOffset + 1;
366       }
367       else if (c == '\r') {
368         curLine++;
369         if (curOffset < text.length() - 1 && text.charAt(curOffset + 1) == '\n') {
370           curOffset++;
371         }
372         curLineStart = curOffset + 1;
373       }
374       curOffset++;
375     }
376
377     return LineColumn.of(curLine, offset - curLineStart);
378   }
379
380   /**
381    * Classic dynamic programming algorithm for string differences.
382    */
383   @Contract(pure = true)
384   public static int difference(@NotNull String s1, @NotNull String s2) {
385     int[][] a = new int[s1.length()][s2.length()];
386
387     for (int i = 0; i < s1.length(); i++) {
388       a[i][0] = i;
389     }
390
391     for (int j = 0; j < s2.length(); j++) {
392       a[0][j] = j;
393     }
394
395     for (int i = 1; i < s1.length(); i++) {
396       for (int j = 1; j < s2.length(); j++) {
397
398         a[i][j] = Math.min(Math.min(a[i - 1][j - 1] + (s1.charAt(i) == s2.charAt(j) ? 0 : 1), a[i - 1][j] + 1), a[i][j - 1] + 1);
399       }
400     }
401
402     return a[s1.length() - 1][s2.length() - 1];
403   }
404
405   @Contract(pure = true)
406   public static @NotNull String wordsToBeginFromUpperCase(@NotNull String s) {
407     return fixCapitalization(s, ArrayUtil.mergeArrays(ourPrepositions, ourOtherNonCapitalizableWords), true);
408   }
409
410   @Contract(pure = true)
411   public static @NotNull String wordsToBeginFromLowerCase(@NotNull String s) {
412     return fixCapitalization(s, ourPrepositions, false);
413   }
414
415   @Contract(pure = true)
416   public static @NotNull String toTitleCase(@NotNull String s) {
417     return fixCapitalization(s, ArrayUtilRt.EMPTY_STRING_ARRAY, true);
418   }
419
420   private static @NotNull String fixCapitalization(@NotNull String s, String @NotNull [] prepositions, boolean title) {
421     StringBuilder buffer = null;
422     for (int i = 0; i < s.length(); i++) {
423       char prevChar = i == 0 ? ' ' : s.charAt(i - 1);
424       char currChar = s.charAt(i);
425       if (!Character.isLetterOrDigit(prevChar) && prevChar != '\'') {
426         if (Character.isLetterOrDigit(currChar)) {
427           if (title || Character.isUpperCase(currChar)) {
428             int j = i;
429             for (; j < s.length(); j++) {
430               if (!Character.isLetterOrDigit(s.charAt(j))) {
431                 break;
432               }
433             }
434             if (!title && j > i + 1 && !Character.isLowerCase(s.charAt(i + 1))) {
435               // filter out abbreviations like I18n, SQL and CSS
436               continue;
437             }
438             if (!isPreposition(s, i, j - 1, prepositions)) {
439               if (buffer == null) {
440                 buffer = new StringBuilder(s);
441               }
442               buffer.setCharAt(i, title ? toUpperCase(currChar) : toLowerCase(currChar));
443             }
444           }
445         }
446       }
447     }
448     return buffer == null ? s : buffer.toString();
449   }
450
451   private static final String[] ourPrepositions = {
452     "a", "an", "and", "as", "at", "but", "by", "down", "for", "from", "if", "in", "into", "not", "of", "on", "onto", "or", "out", "over",
453     "per", "nor", "the", "to", "up", "upon", "via", "with"
454   };
455
456   private static final String[] ourOtherNonCapitalizableWords = {
457     "iOS", "iPhone", "iPad", "iMac"
458   };
459
460   /**
461    * @deprecated Use {@link #isPreposition(String, int, int, String[])}.
462    */
463   @ApiStatus.ScheduledForRemoval(inVersion = "2020.1")
464   @Deprecated
465   @Contract(pure = true)
466   public static boolean isPreposition(@NotNull String s, int firstChar, int lastChar) {
467     return isPreposition(s, firstChar, lastChar, ourPrepositions);
468   }
469
470   @Contract(pure = true)
471   public static boolean isPreposition(@NotNull String s, int firstChar, int lastChar, String @NotNull [] prepositions) {
472     for (String preposition : prepositions) {
473       boolean found = false;
474       if (lastChar - firstChar + 1 == preposition.length()) {
475         found = true;
476         for (int j = 0; j < preposition.length(); j++) {
477           if (toLowerCase(s.charAt(firstChar + j)) != toLowerCase(preposition.charAt(j))) {
478             found = false;
479             break;
480           }
481         }
482       }
483       if (found) {
484         return true;
485       }
486     }
487     return false;
488   }
489
490   @Contract(pure = true)
491   public static @NotNull NotNullFunction<String, String> escaper(final boolean escapeSlash, final @Nullable String additionalChars) {
492     return dom -> {
493       final StringBuilder builder = new StringBuilder(dom.length());
494       escapeStringCharacters(dom.length(), dom, additionalChars, escapeSlash, builder);
495       return builder.toString();
496     };
497   }
498
499
500   public static void escapeStringCharacters(int length, @NotNull String str, @NotNull StringBuilder buffer) {
501     escapeStringCharacters(length, str, "\"", buffer);
502   }
503
504   public static @NotNull StringBuilder escapeStringCharacters(int length,
505                                                               @NotNull String str,
506                                                               @Nullable String additionalChars,
507                                                               @NotNull StringBuilder buffer) {
508     return escapeStringCharacters(length, str, additionalChars, true, buffer);
509   }
510
511   public static @NotNull StringBuilder escapeStringCharacters(int length,
512                                                               @NotNull String str,
513                                                               @Nullable String additionalChars,
514                                                               boolean escapeSlash,
515                                                               @NotNull StringBuilder buffer) {
516     return escapeStringCharacters(length, str, additionalChars, escapeSlash, true, buffer);
517   }
518
519   public static @NotNull StringBuilder escapeStringCharacters(int length,
520                                                               @NotNull String str,
521                                                               @Nullable String additionalChars,
522                                                               boolean escapeSlash,
523                                                               boolean escapeUnicode,
524                                                               @NotNull StringBuilder buffer) {
525     char prev = 0;
526     for (int idx = 0; idx < length; idx++) {
527       char ch = str.charAt(idx);
528       switch (ch) {
529         case '\b':
530           buffer.append("\\b");
531           break;
532
533         case '\t':
534           buffer.append("\\t");
535           break;
536
537         case '\n':
538           buffer.append("\\n");
539           break;
540
541         case '\f':
542           buffer.append("\\f");
543           break;
544
545         case '\r':
546           buffer.append("\\r");
547           break;
548
549         default:
550           if (escapeSlash && ch == '\\') {
551             buffer.append("\\\\");
552           }
553           else if (additionalChars != null && additionalChars.indexOf(ch) > -1 && (escapeSlash || prev != '\\')) {
554             buffer.append("\\").append(ch);
555           }
556           else if (escapeUnicode && !isPrintableUnicode(ch)) {
557             CharSequence hexCode = toUpperCase(Integer.toHexString(ch));
558             buffer.append("\\u");
559             int paddingCount = 4 - hexCode.length();
560             while (paddingCount-- > 0) {
561               buffer.append(0);
562             }
563             buffer.append(hexCode);
564           }
565           else {
566             buffer.append(ch);
567           }
568       }
569       prev = ch;
570     }
571     return buffer;
572   }
573
574   @Contract(pure = true)
575   public static boolean isPrintableUnicode(char c) {
576     int t = Character.getType(c);
577     return t != Character.UNASSIGNED && t != Character.LINE_SEPARATOR && t != Character.PARAGRAPH_SEPARATOR &&
578            t != Character.CONTROL && t != Character.FORMAT && t != Character.PRIVATE_USE && t != Character.SURROGATE;
579   }
580
581   @Contract(pure = true)
582   public static @NotNull String escapeStringCharacters(@NotNull String s) {
583     StringBuilder buffer = new StringBuilder(s.length());
584     escapeStringCharacters(s.length(), s, "\"", buffer);
585     return buffer.toString();
586   }
587
588   @Contract(pure = true)
589   public static @NotNull String escapeCharCharacters(@NotNull String s) {
590     StringBuilder buffer = new StringBuilder(s.length());
591     escapeStringCharacters(s.length(), s, "'", buffer);
592     return buffer.toString();
593   }
594
595   @Contract(pure = true)
596   public static @NotNull String unescapeStringCharacters(@NotNull String s) {
597     StringBuilder buffer = new StringBuilder(s.length());
598     unescapeStringCharacters(s.length(), s, buffer);
599     return buffer.toString();
600   }
601
602   private static boolean isQuoteAt(@NotNull String s, int ind) {
603     char ch = s.charAt(ind);
604     return ch == '\'' || ch == '\"';
605   }
606
607   @Contract(pure = true)
608   public static boolean isQuotedString(@NotNull String s) {
609     return StringUtilRt.isQuotedString(s);
610   }
611
612   /**
613    * @return string with paired quotation marks (quote (") or apostrophe (')) removed
614    */
615   @Contract(pure = true)
616   public static @NotNull String unquoteString(@NotNull String s) {
617     return StringUtilRt.unquoteString(s);
618   }
619
620   @Contract(pure = true)
621   public static @NotNull String unquoteString(@NotNull String s, char quotationChar) {
622     return StringUtilRt.unquoteString(s, quotationChar);
623   }
624
625   private static void unescapeStringCharacters(int length, @NotNull String s, @NotNull StringBuilder buffer) {
626     boolean escaped = false;
627     for (int idx = 0; idx < length; idx++) {
628       char ch = s.charAt(idx);
629       if (!escaped) {
630         if (ch == '\\') {
631           escaped = true;
632         }
633         else {
634           buffer.append(ch);
635         }
636       }
637       else {
638         int octalEscapeMaxLength = 2;
639         switch (ch) {
640           case 'n':
641             buffer.append('\n');
642             break;
643
644           case 'r':
645             buffer.append('\r');
646             break;
647
648           case 'b':
649             buffer.append('\b');
650             break;
651
652           case 't':
653             buffer.append('\t');
654             break;
655
656           case 'f':
657             buffer.append('\f');
658             break;
659
660           case '\'':
661             buffer.append('\'');
662             break;
663
664           case '\"':
665             buffer.append('\"');
666             break;
667
668           case '\\':
669             buffer.append('\\');
670             break;
671
672           case 'u':
673             if (idx + 4 < length) {
674               try {
675                 int code = Integer.parseInt(s.substring(idx + 1, idx + 5), 16);
676                 //noinspection AssignmentToForLoopParameter
677                 idx += 4;
678                 buffer.append((char)code);
679               }
680               catch (NumberFormatException e) {
681                 buffer.append("\\u");
682               }
683             }
684             else {
685               buffer.append("\\u");
686             }
687             break;
688
689           case '0':
690           case '1':
691           case '2':
692           case '3':
693             octalEscapeMaxLength = 3;
694             //noinspection fallthrough
695           case '4':
696           case '5':
697           case '6':
698           case '7':
699             int escapeEnd = idx + 1;
700             while (escapeEnd < length && escapeEnd < idx + octalEscapeMaxLength && isOctalDigit(s.charAt(escapeEnd))) escapeEnd++;
701             try {
702               buffer.append((char)Integer.parseInt(s.substring(idx, escapeEnd), 8));
703             }
704             catch (NumberFormatException e) {
705               throw new RuntimeException("Couldn't parse " + s.substring(idx, escapeEnd), e); // shouldn't happen
706             }
707             //noinspection AssignmentToForLoopParameter
708             idx = escapeEnd - 1;
709             break;
710
711           default:
712             buffer.append(ch);
713             break;
714         }
715         escaped = false;
716       }
717     }
718
719     if (escaped) buffer.append('\\');
720   }
721
722   @Contract(pure = true)
723   public static @NotNull String pluralize(@NotNull String word) {
724     return Strings.pluralize(word);
725   }
726
727   @Contract(pure = true)
728   public static @NotNull String capitalizeWords(@NotNull String text,
729                                                 boolean allWords) {
730     return capitalizeWords(text, " \t\n\r\f([<", allWords, true);
731   }
732
733   @Contract(pure = true)
734   public static @NotNull String capitalizeWords(@NotNull String text,
735                                                 @NotNull String tokenizerDelim,
736                                                 boolean allWords,
737                                                 boolean leaveOriginalDelims) {
738     final StringTokenizer tokenizer = new StringTokenizer(text, tokenizerDelim, leaveOriginalDelims);
739     final StringBuilder out = new StringBuilder(text.length());
740     boolean toCapitalize = true;
741     while (tokenizer.hasMoreTokens()) {
742       final String word = tokenizer.nextToken();
743       if (!leaveOriginalDelims && out.length() > 0) {
744         out.append(' ');
745       }
746       out.append(toCapitalize ? capitalize(word) : word);
747       if (!allWords) {
748         toCapitalize = false;
749       }
750     }
751     return out.toString();
752   }
753
754   @Contract(pure = true)
755   public static @NotNull String decapitalize(@NotNull String s) {
756     return Introspector.decapitalize(s);
757   }
758
759   @Contract(pure = true)
760   public static boolean isVowel(char c) {
761     return VOWELS.indexOf(c) >= 0;
762   }
763
764   /**
765    * Capitalize the first letter of the sentence.
766    */
767   @Contract(pure = true)
768   public static @NotNull String capitalize(@NotNull String s) {
769     return Strings.capitalize(s);
770   }
771
772   @Contract(value = "null -> false", pure = true)
773   public static boolean isCapitalized(@Nullable String s) {
774     return Strings.isCapitalized(s);
775   }
776
777   @Contract(pure = true)
778   public static @NotNull String capitalizeWithJavaBeanConvention(@NotNull String s) {
779     if (s.length() > 1 && Character.isUpperCase(s.charAt(1))) {
780       return s;
781     }
782     return capitalize(s);
783   }
784
785   @Contract(pure = true)
786   public static int stringHashCode(@NotNull CharSequence chars) {
787     if (chars instanceof String || chars instanceof CharSequenceWithStringHash) {
788       // we know for sure these classes have conformant (and maybe faster) hashCode()
789       return chars.hashCode();
790     }
791
792     return stringHashCode(chars, 0, chars.length());
793   }
794
795   @Contract(pure = true)
796   public static int stringHashCode(@NotNull CharSequence chars, int from, int to) {
797     return Strings.stringHashCode(chars, from, to);
798   }
799
800   @Contract(pure = true)
801   public static int stringHashCode(@NotNull CharSequence chars, int from, int to, int prefixHash) {
802     return Strings.stringHashCode(chars, from, to, prefixHash);
803   }
804
805   @Contract(pure = true)
806   public static int stringHashCode(char[] chars, int from, int to) {
807     return Strings.stringHashCode(chars, from, to);
808   }
809
810   @Contract(pure = true)
811   public static int stringHashCodeInsensitive(char @NotNull [] chars, int from, int to) {
812     return Strings.stringHashCodeInsensitive(chars, from, to);
813   }
814
815   @Contract(pure = true)
816   public static int stringHashCodeInsensitive(@NotNull CharSequence chars, int from, int to) {
817     return Strings.stringHashCodeInsensitive(chars, from, to);
818   }
819
820   @Contract(pure = true)
821   public static int stringHashCodeInsensitive(@NotNull CharSequence chars, int from, int to, int prefixHash) {
822     return Strings.stringHashCodeInsensitive(chars, from, to, prefixHash);
823   }
824
825   @Contract(pure = true)
826   public static int stringHashCodeInsensitive(@NotNull CharSequence chars) {
827     return Strings.stringHashCodeInsensitive(chars);
828   }
829
830   @Contract(pure = true)
831   public static int stringHashCodeIgnoreWhitespaces(@NotNull CharSequence chars) {
832     int h = 0;
833     for (int off = 0; off < chars.length(); off++) {
834       char c = chars.charAt(off);
835       if (!isWhiteSpace(c)) {
836         h = 31 * h + c;
837       }
838     }
839     return h;
840   }
841
842   /**
843    * Equivalent to string.startsWith(prefixes[0] + prefixes[1] + ...) but avoids creating an object for concatenation.
844    */
845   @Contract(pure = true)
846   public static boolean startsWithConcatenation(@NotNull String string, String @NotNull ... prefixes) {
847     int offset = 0;
848     for (String prefix : prefixes) {
849       int prefixLen = prefix.length();
850       if (!string.regionMatches(offset, prefix, 0, prefixLen)) {
851         return false;
852       }
853       offset += prefixLen;
854     }
855     return true;
856   }
857
858   @Contract(value = "null -> null; !null -> !null", pure = true)
859   public static String trim(@Nullable String s) {
860     return Strings.trim(s);
861   }
862
863   @Contract(pure = true)
864   public static @NotNull String trimEnd(@NotNull String s, @NotNull String suffix) {
865     return Strings.trimEnd(s, suffix);
866   }
867
868   @Contract(pure = true)
869   public static @NotNull String trimEnd(@NotNull String s, @NotNull String suffix, boolean ignoreCase) {
870     return Strings.trimEnd(s, suffix, ignoreCase);
871   }
872
873   @Contract(pure = true)
874   public static @NotNull String trimEnd(@NotNull String s, char suffix) {
875     return Strings.trimEnd(s, suffix);
876   }
877
878   @Contract(pure = true)
879   public static @NotNull String trimLog(final @NotNull String text, final int limit) {
880     if (limit > 5 && text.length() > limit) {
881       return text.substring(0, limit - 5) + " ...\n";
882     }
883     return text;
884   }
885
886   @Contract(pure = true)
887   public static @NotNull String trimLeading(@NotNull String string) {
888     return trimLeading((CharSequence)string).toString();
889   }
890   @Contract(pure = true)
891   public static @NotNull CharSequence trimLeading(@NotNull CharSequence string) {
892     int index = 0;
893     while (index < string.length() && Character.isWhitespace(string.charAt(index))) index++;
894     return string.subSequence(index, string.length());
895   }
896
897   @Contract(pure = true)
898   public static @NotNull String trimLeading(@NotNull String string, char symbol) {
899     int index = 0;
900     while (index < string.length() && string.charAt(index) == symbol) index++;
901     return string.substring(index);
902   }
903
904   public static @NotNull StringBuilder trimLeading(@NotNull StringBuilder builder, char symbol) {
905     int index = 0;
906     while (index < builder.length() && builder.charAt(index) == symbol) index++;
907     if (index > 0) builder.delete(0, index);
908     return builder;
909   }
910
911   @Contract(pure = true)
912   public static @NotNull String trimTrailing(@NotNull String string) {
913     return trimTrailing((CharSequence)string).toString();
914   }
915
916   @Contract(pure = true)
917   public static @NotNull CharSequence trimTrailing(@NotNull CharSequence string) {
918     int index = string.length() - 1;
919     while (index >= 0 && Character.isWhitespace(string.charAt(index))) index--;
920     return string.subSequence(0, index + 1);
921   }
922
923   @Contract(pure = true)
924   public static @NotNull String trimTrailing(@NotNull String string, char symbol) {
925     int index = string.length() - 1;
926     while (index >= 0 && string.charAt(index) == symbol) index--;
927     return string.substring(0, index + 1);
928   }
929
930   public static @NotNull StringBuilder trimTrailing(@NotNull StringBuilder builder, char symbol) {
931     int index = builder.length() - 1;
932     while (index >= 0 && builder.charAt(index) == symbol) index--;
933     builder.setLength(index + 1);
934     return builder;
935   }
936
937   @Contract(value = "null -> null; !null -> !null", pure = true)
938   public static @Nullable CharSequence trim(@Nullable CharSequence s) {
939     if (s == null) return null;
940     int startIndex = 0;
941     int length = s.length();
942     if (length == 0) return s;
943     while (startIndex < length && Character.isWhitespace(s.charAt(startIndex))) startIndex++;
944
945     if (startIndex == length) {
946       return Strings.EMPTY_CHAR_SEQUENCE;
947     }
948
949     int endIndex = length - 1;
950     while (endIndex >= startIndex && Character.isWhitespace(s.charAt(endIndex))) endIndex--;
951     endIndex++;
952
953     if (startIndex > 0 || endIndex < length) {
954       return s.subSequence(startIndex, endIndex);
955     }
956     return s;
957   }
958
959   @Contract(pure = true)
960   public static boolean startsWithChar(@Nullable CharSequence s, char prefix) {
961     return s != null && s.length() != 0 && s.charAt(0) == prefix;
962   }
963
964   @Contract(pure = true)
965   public static boolean endsWithChar(@Nullable CharSequence s, char suffix) {
966     return Strings.endsWithChar(s, suffix);
967   }
968
969   @Contract(pure = true)
970   public static @NotNull String trimStart(@NotNull String s, @NotNull String prefix) {
971     if (s.startsWith(prefix)) {
972       return s.substring(prefix.length());
973     }
974     return s;
975   }
976
977   @Contract(pure = true)
978   public static @NotNull String trimExtensions(@NotNull String name) {
979     int index = name.indexOf('.');
980     return index < 0 ? name : name.substring(0, index);
981   }
982
983   @Contract(pure = true)
984   public static @NotNull String pluralize(@NotNull String base, int count) {
985     if (count == 1) return base;
986     return pluralize(base);
987   }
988
989   public static void repeatSymbol(@NotNull Appendable buffer, char symbol, int times) {
990     assert times >= 0 : times;
991     try {
992       for (int i = 0; i < times; i++) {
993         buffer.append(symbol);
994       }
995     }
996     catch (IOException e) {
997       Logger.getInstance(StringUtil.class).error(e);
998     }
999   }
1000
1001   @Contract(pure = true)
1002   public static String defaultIfEmpty(@Nullable String value, String defaultValue) {
1003     return isEmpty(value) ? defaultValue : value;
1004   }
1005
1006   @Contract(value = "null -> false", pure = true)
1007   public static boolean isNotEmpty(@Nullable String s) {
1008     return Strings.isNotEmpty(s);
1009   }
1010
1011   @Contract(value = "null -> true", pure = true)
1012   public static boolean isEmpty(@Nullable String s) {
1013     return Strings.isEmpty(s);
1014   }
1015
1016   @Contract(value = "null -> true",pure = true)
1017   public static boolean isEmpty(@Nullable CharSequence cs) {
1018     return Strings.isEmpty(cs);
1019   }
1020
1021   @Contract(pure = true)
1022   public static int length(@Nullable CharSequence cs) {
1023     return cs == null ? 0 : cs.length();
1024   }
1025
1026   @Contract(pure = true)
1027   public static @NotNull String notNullize(@Nullable String s) {
1028     return Strings.notNullize(s);
1029   }
1030
1031   @Contract(pure = true)
1032   public static @NotNull String notNullize(@Nullable String s, @NotNull String defaultValue) {
1033     return Strings.notNullize(s, defaultValue);
1034   }
1035
1036   @Contract(pure = true)
1037   public static @Nullable String nullize(@Nullable String s) {
1038     return Strings.nullize(s, false);
1039   }
1040
1041   @Contract(pure = true)
1042   public static @Nullable String nullize(@Nullable String s, @Nullable String defaultValue) {
1043     return Strings.nullize(s, defaultValue);
1044   }
1045
1046   @Contract(pure = true)
1047   public static @Nullable String nullize(@Nullable String s, boolean nullizeSpaces) {
1048     return Strings.nullize(s, nullizeSpaces);
1049   }
1050
1051   @Contract(value = "null -> true",pure = true)
1052   // we need to keep this method to preserve backward compatibility
1053   public static boolean isEmptyOrSpaces(@Nullable String s) {
1054     return isEmptyOrSpaces((CharSequence)s);
1055   }
1056
1057   @Contract(value = "null -> true", pure = true)
1058   public static boolean isEmptyOrSpaces(@Nullable CharSequence s) {
1059     return Strings.isEmptyOrSpaces(s);
1060   }
1061
1062   /**
1063    * Allows to answer if given symbol is white space, tabulation or line feed.
1064    *
1065    * @param c symbol to check
1066    * @return {@code true} if given symbol is white space, tabulation or line feed; {@code false} otherwise
1067    */
1068   @Contract(pure = true)
1069   public static boolean isWhiteSpace(char c) {
1070     return c == '\n' || c == '\t' || c == ' ';
1071   }
1072
1073   @Contract(pure = true)
1074   public static @NotNull String getThrowableText(@NotNull Throwable aThrowable) {
1075     return ExceptionUtil.getThrowableText(aThrowable);
1076   }
1077
1078   @Contract(pure = true)
1079   public static @Nullable String getMessage(@NotNull Throwable e) {
1080     return ExceptionUtil.getMessage(e);
1081   }
1082
1083   @Contract(pure = true)
1084   public static @NotNull String repeatSymbol(final char aChar, final int count) {
1085     char[] buffer = new char[count];
1086     Arrays.fill(buffer, aChar);
1087     return StringFactory.createShared(buffer);
1088   }
1089
1090   @Contract(pure = true)
1091   public static @NotNull String repeat(@NotNull String s, int count) {
1092     if (count == 0) return "";
1093     assert count >= 0 : count;
1094     StringBuilder sb = new StringBuilder(s.length() * count);
1095     for (int i = 0; i < count; i++) {
1096       sb.append(s);
1097     }
1098     return sb.toString();
1099   }
1100
1101   @Contract(pure = true)
1102   public static @NotNull List<String> splitHonorQuotes(@NotNull String s, char separator) {
1103     return StringUtilRt.splitHonorQuotes(s, separator);
1104   }
1105
1106
1107   @Contract(pure = true)
1108   public static @NotNull List<String> split(@NotNull String s, @NotNull String separator) {
1109     return split(s, separator, true);
1110   }
1111   @Contract(pure = true)
1112   public static @NotNull List<CharSequence> split(@NotNull CharSequence s, @NotNull CharSequence separator) {
1113     return split(s, separator, true, true);
1114   }
1115
1116   @Contract(pure = true)
1117   public static @NotNull List<String> split(@NotNull String s, @NotNull String separator, boolean excludeSeparator) {
1118     return split(s, separator, excludeSeparator, true);
1119   }
1120
1121   @Contract(pure = true)
1122   public static @NotNull List<String> split(@NotNull String s, @NotNull String separator, boolean excludeSeparator, boolean excludeEmptyStrings) {
1123     //noinspection unchecked
1124     return (List)split((CharSequence)s, separator, excludeSeparator, excludeEmptyStrings);
1125   }
1126
1127   @Contract(pure = true)
1128   public static @NotNull List<CharSequence> split(@NotNull CharSequence s, @NotNull CharSequence separator, boolean excludeSeparator, boolean excludeEmptyStrings) {
1129     if (separator.length() == 0) {
1130       return Collections.singletonList(s);
1131     }
1132     List<CharSequence> result = new ArrayList<>();
1133     int pos = 0;
1134     while (true) {
1135       int index = indexOf(s, separator, pos);
1136       if (index == -1) break;
1137       final int nextPos = index + separator.length();
1138       CharSequence token = s.subSequence(pos, excludeSeparator ? index : nextPos);
1139       if (token.length() != 0 || !excludeEmptyStrings) {
1140         result.add(token);
1141       }
1142       pos = nextPos;
1143     }
1144     if (pos < s.length() || !excludeEmptyStrings && pos == s.length()) {
1145       result.add(s.subSequence(pos, s.length()));
1146     }
1147     return result;
1148   }
1149
1150   @Contract(pure = true)
1151   public static @NotNull Iterable<String> tokenize(@NotNull String s, @NotNull String separators) {
1152     final com.intellij.util.text.StringTokenizer tokenizer = new com.intellij.util.text.StringTokenizer(s, separators);
1153     return () -> new Iterator<String>() {
1154       @Override
1155       public boolean hasNext() {
1156         return tokenizer.hasMoreTokens();
1157       }
1158
1159       @Override
1160       public String next() {
1161         return tokenizer.nextToken();
1162       }
1163
1164       @Override
1165       public void remove() {
1166         throw new UnsupportedOperationException();
1167       }
1168     };
1169   }
1170
1171   @Contract(pure = true)
1172   public static @NotNull Iterable<String> tokenize(final @NotNull StringTokenizer tokenizer) {
1173     return () -> new Iterator<String>() {
1174       @Override
1175       public boolean hasNext() {
1176         return tokenizer.hasMoreTokens();
1177       }
1178
1179       @Override
1180       public String next() {
1181         return tokenizer.nextToken();
1182       }
1183
1184       @Override
1185       public void remove() {
1186         throw new UnsupportedOperationException();
1187       }
1188     };
1189   }
1190
1191   /**
1192    * @return list containing all words in {@code text}, or {@link ContainerUtil#emptyList()} if there are none.
1193    * The <b>word</b> here means the maximum sub-string consisting entirely of characters which are {@code Character.isJavaIdentifierPart(c)}.
1194    */
1195   @Contract(pure = true)
1196   public static @NotNull List<String> getWordsIn(@NotNull String text) {
1197     List<String> result = null;
1198     int start = -1;
1199     for (int i = 0; i < text.length(); i++) {
1200       char c = text.charAt(i);
1201       boolean isIdentifierPart = Character.isJavaIdentifierPart(c);
1202       if (isIdentifierPart && start == -1) {
1203         start = i;
1204       }
1205       if (isIdentifierPart && i == text.length() - 1) {
1206         if (result == null) {
1207           result = new SmartList<>();
1208         }
1209         result.add(text.substring(start, i + 1));
1210       }
1211       else if (!isIdentifierPart && start != -1) {
1212         if (result == null) {
1213           result = new SmartList<>();
1214         }
1215         result.add(text.substring(start, i));
1216         start = -1;
1217       }
1218     }
1219     if (result == null) {
1220       return ContainerUtil.emptyList();
1221     }
1222     return result;
1223   }
1224
1225   @Contract(pure = true)
1226   public static @NotNull List<TextRange> getWordIndicesIn(@NotNull String text) {
1227     return getWordIndicesIn(text, null);
1228   }
1229
1230   /**
1231    * @param text text to get word ranges in.
1232    * @param separatorsSet if not null, only these characters will be considered as separators (i.e. not a part of word).
1233    *                   Otherwise {@link Character#isJavaIdentifierPart(char)} will be used to determine whether a symbol is part of word.
1234    * @return ranges ranges of words in passed text.
1235    */
1236   @Contract(pure = true)
1237   public static @NotNull List<TextRange> getWordIndicesIn(@NotNull String text, @Nullable Set<Character> separatorsSet) {
1238     List<TextRange> result = new SmartList<>();
1239     int start = -1;
1240     for (int i = 0; i < text.length(); i++) {
1241       char c = text.charAt(i);
1242       boolean isIdentifierPart = separatorsSet == null ? Character.isJavaIdentifierPart(c) : !separatorsSet.contains(c);
1243       if (isIdentifierPart && start == -1) {
1244         start = i;
1245       }
1246       if (isIdentifierPart && i == text.length() - 1) {
1247         result.add(new TextRange(start, i + 1));
1248       }
1249       else if (!isIdentifierPart && start != -1) {
1250         result.add(new TextRange(start, i));
1251         start = -1;
1252       }
1253     }
1254     return result;
1255   }
1256
1257   @Contract(pure = true)
1258   public static @NotNull String join(final String @NotNull [] strings, final @NotNull String separator) {
1259     return join(strings, 0, strings.length, separator);
1260   }
1261
1262   @Contract(pure = true)
1263   public static @NotNull String join(final String @NotNull [] strings, int startIndex, int endIndex, final @NotNull String separator) {
1264     final StringBuilder result = new StringBuilder();
1265     for (int i = startIndex; i < endIndex; i++) {
1266       if (i > startIndex) result.append(separator);
1267       result.append(strings[i]);
1268     }
1269     return result.toString();
1270   }
1271
1272   @Contract(pure = true)
1273   public static String @NotNull [] zip(String @NotNull [] strings1, String @NotNull [] strings2, String separator) {
1274     if (strings1.length != strings2.length) throw new IllegalArgumentException();
1275
1276     String[] result = ArrayUtil.newStringArray(strings1.length);
1277     for (int i = 0; i < result.length; i++) {
1278       result[i] = strings1[i] + separator + strings2[i];
1279     }
1280
1281     return result;
1282   }
1283
1284   @Contract(pure = true)
1285   public static String @NotNull [] surround(String @NotNull [] strings, @NotNull String prefix, @NotNull String suffix) {
1286     String[] result = ArrayUtil.newStringArray(strings.length);
1287     for (int i = 0; i < result.length; i++) {
1288       result[i] = prefix + strings[i] + suffix;
1289     }
1290     return result;
1291   }
1292
1293   @Contract(pure = true)
1294   public static @NotNull <T> String join(T @NotNull [] items, @NotNull Function<? super T, String> f, @NotNull String separator) {
1295     return join(Arrays.asList(items), f, separator);
1296   }
1297
1298   @Contract(pure = true)
1299   public static @NotNull <T> String join(@NotNull Collection<? extends T> items,
1300                                          @NotNull Function<? super T, String> f,
1301                                          @NotNull String separator) {
1302     if (items.isEmpty()) return "";
1303     if (items.size() == 1) return notNullize(f.fun(items.iterator().next()));
1304     return join((Iterable<? extends T>)items, f, separator);
1305   }
1306
1307   @Contract(pure = true)
1308   public static @NotNull String join(@NotNull Iterable<?> items, @NotNull String separator) {
1309     StringBuilder result = new StringBuilder();
1310     for (Object item : items) {
1311       result.append(item).append(separator);
1312     }
1313     if (result.length() > 0) {
1314       result.setLength(result.length() - separator.length());
1315     }
1316     return result.toString();
1317   }
1318
1319   @Contract(pure = true)
1320   public static @NotNull <T> String join(@NotNull Iterable<? extends T> items,
1321                                          @NotNull Function<? super T, ? extends CharSequence> f,
1322                                          @NotNull String separator) {
1323     StringBuilder result = new StringBuilder();
1324     join(items, f, separator, result);
1325     return result.toString();
1326   }
1327
1328   public static <T> void join(@NotNull Iterable<? extends T> items,
1329                               @NotNull Function<? super T, ? extends CharSequence> f,
1330                               @NotNull String separator,
1331                               @NotNull StringBuilder result) {
1332     boolean isFirst = true;
1333     for (T item : items) {
1334       CharSequence string = f.fun(item);
1335       if (!isEmpty(string)) {
1336         if (isFirst) {
1337           isFirst = false;
1338         }
1339         else {
1340           result.append(separator);
1341         }
1342         result.append(string);
1343       }
1344     }
1345   }
1346
1347   @Contract(pure = true)
1348   public static @NotNull String join(@NotNull Collection<String> strings, @NotNull String separator) {
1349     if (strings.size() <= 1) {
1350       return notNullize(ContainerUtil.getFirstItem(strings));
1351     }
1352     StringBuilder result = new StringBuilder();
1353     join(strings, separator, result);
1354     return result.toString();
1355   }
1356
1357   public static void join(@NotNull Collection<String> strings, @NotNull String separator, @NotNull StringBuilder result) {
1358     boolean isFirst = true;
1359     for (String string : strings) {
1360       if (string != null) {
1361         if (isFirst) {
1362           isFirst = false;
1363         }
1364         else {
1365           result.append(separator);
1366         }
1367         result.append(string);
1368       }
1369     }
1370   }
1371
1372   @Contract(pure = true)
1373   public static @NotNull String join(final int @NotNull [] strings, final @NotNull String separator) {
1374     final StringBuilder result = new StringBuilder();
1375     for (int i = 0; i < strings.length; i++) {
1376       if (i > 0) result.append(separator);
1377       result.append(strings[i]);
1378     }
1379     return result.toString();
1380   }
1381
1382   @Contract(pure = true)
1383   public static @NotNull String join(final String @NotNull ... strings) {
1384     if (strings.length == 0) return "";
1385
1386     final StringBuilder builder = new StringBuilder();
1387     for (final String string : strings) {
1388       builder.append(string);
1389     }
1390     return builder.toString();
1391   }
1392
1393   @Contract(pure = true)
1394   public static @NotNull Collector<CharSequence, ?, String> joining() {
1395     return Collectors.joining(", ");
1396   }
1397
1398   /**
1399    * Consider using {@link StringUtil#unquoteString(String)} instead.
1400    * Note: this method has an odd behavior:
1401    *   Quotes are removed even if leading and trailing quotes are different or
1402    *                           if there is only one quote (leading or trailing).
1403    */
1404   @Contract(pure = true)
1405   public static @NotNull String stripQuotesAroundValue(@NotNull String text) {
1406     final int len = text.length();
1407     if (len > 0) {
1408       final int from = isQuoteAt(text, 0) ? 1 : 0;
1409       final int to = len > 1 && isQuoteAt(text, len - 1) ? len - 1 : len;
1410       if (from > 0 || to < len) {
1411         return text.substring(from, to);
1412       }
1413     }
1414     return text;
1415   }
1416
1417   /** Formats given file size in metric (1 kB = 1000 B) units (example: {@code formatFileSize(1234) = "1.23 KB"}). */
1418   @Contract(pure = true)
1419   public static @NotNull String formatFileSize(long fileSize) {
1420     return Formats.formatFileSize(fileSize);
1421   }
1422
1423   /** Formats given file size in metric (1 kB = 1000 B) units (example: {@code formatFileSize(1234, "") = "1.23KB"}). */
1424   @Contract(pure = true)
1425   public static @NotNull String formatFileSize(long fileSize, @NotNull String unitSeparator) {
1426     return Formats.formatFileSize(fileSize, unitSeparator);
1427   }
1428
1429   /** Formats duration given in milliseconds as a sum of time units (example: {@code formatDuration(123456) = "2 m 3 s 456 ms"}). */
1430   @Contract(pure = true)
1431   public static @NotNull String formatDuration(long duration) {
1432     return Formats.formatDuration(duration);
1433   }
1434
1435   /** Formats {@link Duration} as a sum of time units (calls {@link #formatDuration(long)} with duration converted to milliseconds) */
1436   @Contract(pure = true)
1437   public static @NotNull String formatDuration(@NotNull Duration duration) {
1438     return Formats.formatDuration(duration);
1439   }
1440
1441   /** Formats duration given in milliseconds as a sum of time units (example: {@code formatDuration(123456, "") = "2m 3s 456ms"}). */
1442   @Contract(pure = true)
1443   public static @NotNull String formatDuration(long duration, @NotNull String unitSeparator) {
1444     return Formats.formatDuration(duration, unitSeparator);
1445   }
1446
1447   /**
1448    * Formats duration given in milliseconds as a sum of padded time units, except the most significant unit
1449    * E.g. 234523598 padded as "2 d 03 h 11 min 04 sec 004 ms" accordingly with zeros except "days" here.
1450    */
1451   @Contract(pure = true)
1452   public static @NotNull String formatDurationPadded(long millis, @NotNull String unitSeparator) {
1453     return Formats.formatDurationPadded(millis, unitSeparator);
1454   }
1455
1456   /**
1457    * Formats duration given in milliseconds as a sum of time units with at most two units
1458    * (example: {@code formatDuration(123456) = "2 m 3 s"}).
1459    */
1460   @Contract(pure = true)
1461   public static @NotNull String formatDurationApproximate(long duration) {
1462     return Formats.formatDurationApproximate(duration);
1463   }
1464
1465   /**
1466    * Appends English ordinal suffix to the given number.
1467    */
1468   @Contract(pure = true)
1469   public static @NotNull String formatOrdinal(long num) {
1470     return Formats.formatOrdinal(num);
1471   }
1472
1473   /**
1474    * Returns unpluralized variant using English based heuristics like properties -> property, names -> name, children -> child.
1475    * Returns {@code null} if failed to match appropriate heuristic.
1476    *
1477    * @param word english word in plural form
1478    * @return name in singular form or {@code null} if failed to find one.
1479    */
1480   @Contract(pure = true)
1481   public static @Nullable String unpluralize(@NotNull String word) {
1482     return Strings.unpluralize(word);
1483   }
1484
1485   @Contract(pure = true)
1486   public static boolean containsAlphaCharacters(@NotNull String value) {
1487     for (int i = 0; i < value.length(); i++) {
1488       if (Character.isLetter(value.charAt(i))) return true;
1489     }
1490     return false;
1491   }
1492
1493   @Contract(pure = true)
1494   public static boolean containsAnyChar(final @NotNull String value, final @NotNull @NonNls String chars) {
1495     return Strings.containsAnyChar(value, chars);
1496   }
1497
1498   @Contract(pure = true)
1499   public static boolean containsAnyChar(final @NotNull String value,
1500                                         final @NotNull String chars,
1501                                         final int start, final int end) {
1502     return Strings.containsAnyChar(value, chars, start, end);
1503   }
1504
1505   @Contract(pure = true)
1506   public static boolean containsChar(final @NotNull String value, final char ch) {
1507     return Strings.containsChar(value, ch);
1508   }
1509
1510   /**
1511    * @deprecated use #capitalize(String)
1512    */
1513   @Deprecated
1514   @Contract(value = "null -> null; !null -> !null", pure = true)
1515   public static String firstLetterToUpperCase(final @Nullable String displayString) {
1516     if (displayString == null || displayString.isEmpty()) return displayString;
1517     char firstChar = displayString.charAt(0);
1518     char uppedFirstChar = toUpperCase(firstChar);
1519
1520     if (uppedFirstChar == firstChar) return displayString;
1521
1522     char[] buffer = displayString.toCharArray();
1523     buffer[0] = uppedFirstChar;
1524     return StringFactory.createShared(buffer);
1525   }
1526
1527   /**
1528    * Strip out all characters not accepted by given filter
1529    *
1530    * @param s      e.g. "/n    my string "
1531    * @param filter e.g. {@link CharFilter#NOT_WHITESPACE_FILTER}
1532    * @return stripped string e.g. "mystring"
1533    */
1534   @Contract(pure = true)
1535   public static @NotNull String strip(final @NotNull String s, final @NotNull CharFilter filter) {
1536     final StringBuilder result = new StringBuilder(s.length());
1537     for (int i = 0; i < s.length(); i++) {
1538       char ch = s.charAt(i);
1539       if (filter.accept(ch)) {
1540         result.append(ch);
1541       }
1542     }
1543     return result.toString();
1544   }
1545
1546   /**
1547    * Trim all characters not accepted by given filter
1548    *
1549    * @param s      e.g. "/n    my string "
1550    * @param filter e.g. {@link CharFilter#NOT_WHITESPACE_FILTER}
1551    * @return trimmed string e.g. "my string"
1552    */
1553   @Contract(pure = true)
1554   public static @NotNull String trim(final @NotNull String s, final @NotNull CharFilter filter) {
1555     int start = 0;
1556     int end = s.length();
1557
1558     for (; start < end; start++) {
1559       char ch = s.charAt(start);
1560       if (filter.accept(ch)) break;
1561     }
1562
1563     for (; start < end; end--) {
1564       char ch = s.charAt(end - 1);
1565       if (filter.accept(ch)) break;
1566     }
1567
1568     return s.substring(start, end);
1569   }
1570
1571   @Contract(pure = true)
1572   public static @NotNull List<String> findMatches(@NotNull String s, @NotNull Pattern pattern) {
1573     return findMatches(s, pattern, 1);
1574   }
1575
1576   @Contract(pure = true)
1577   public static @NotNull List<String> findMatches(@NotNull String s, @NotNull Pattern pattern, int groupIndex) {
1578     List<String> result = new SmartList<>();
1579     Matcher m = pattern.matcher(s);
1580     while (m.find()) {
1581       String group = m.group(groupIndex);
1582       if (group != null) {
1583         result.add(group);
1584       }
1585     }
1586     return result;
1587   }
1588
1589   /**
1590    * Find position of the first character accepted by given filter.
1591    *
1592    * @param s      the string to search
1593    * @param filter search filter
1594    * @return position of the first character accepted or -1 if not found
1595    */
1596   @Contract(pure = true)
1597   public static int findFirst(final @NotNull CharSequence s, @NotNull CharFilter filter) {
1598     for (int i = 0; i < s.length(); i++) {
1599       char ch = s.charAt(i);
1600       if (filter.accept(ch)) {
1601         return i;
1602       }
1603     }
1604     return -1;
1605   }
1606
1607   @Contract(pure = true)
1608   public static @NotNull String replaceSubstring(@NotNull String string, @NotNull TextRange range, @NotNull String replacement) {
1609     return range.replace(string, replacement);
1610   }
1611
1612   @Contract(pure = true)
1613   public static boolean startsWithWhitespace(@NotNull String text) {
1614     return !text.isEmpty() && Character.isWhitespace(text.charAt(0));
1615   }
1616
1617   @Contract(pure = true)
1618   public static boolean isChar(CharSequence seq, int index, char c) {
1619     return index >= 0 && index < seq.length() && seq.charAt(index) == c;
1620   }
1621
1622   @Contract(pure = true)
1623   public static boolean startsWith(@NotNull CharSequence text, @NotNull CharSequence prefix) {
1624     return StringUtilRt.startsWith(text, prefix);
1625   }
1626
1627   @Contract(pure = true)
1628   public static boolean startsWith(@NotNull CharSequence text, int startIndex, @NotNull CharSequence prefix) {
1629     return Strings.startsWith(text, startIndex, prefix);
1630   }
1631
1632   @Contract(pure = true)
1633   public static boolean endsWith(@NotNull CharSequence text, @NotNull CharSequence suffix) {
1634     return Strings.endsWith(text, suffix);
1635   }
1636
1637   @Contract(pure = true)
1638   public static boolean endsWith(@NotNull CharSequence text, int start, int end, @NotNull CharSequence suffix) {
1639     int suffixLen = suffix.length();
1640     if (end < suffixLen) return false;
1641
1642     for (int i = end - 1; i >= end - suffixLen && i >= start; i--) {
1643       if (text.charAt(i) != suffix.charAt(i + suffixLen - end)) return false;
1644     }
1645
1646     return true;
1647   }
1648
1649   @Contract(pure = true)
1650   public static @NotNull String commonPrefix(@NotNull String s1, @NotNull String s2) {
1651     return s1.substring(0, commonPrefixLength(s1, s2));
1652   }
1653
1654   @Contract(pure = true)
1655   public static int commonPrefixLength(@NotNull CharSequence s1, @NotNull CharSequence s2) {
1656     return commonPrefixLength(s1, s2, false);
1657   }
1658
1659   @Contract(pure = true)
1660   public static int commonPrefixLength(@NotNull CharSequence s1, @NotNull CharSequence s2, boolean ignoreCase) {
1661     int i;
1662     int minLength = Math.min(s1.length(), s2.length());
1663     for (i = 0; i < minLength; i++) {
1664       if (!Strings.charsMatch(s1.charAt(i), s2.charAt(i), ignoreCase)) {
1665         break;
1666       }
1667     }
1668     return i;
1669   }
1670
1671   @Contract(pure = true)
1672   public static @NotNull String commonSuffix(@NotNull String s1, @NotNull String s2) {
1673     return s1.substring(s1.length() - commonSuffixLength(s1, s2));
1674   }
1675
1676   @Contract(pure = true)
1677   public static int commonSuffixLength(@NotNull CharSequence s1, @NotNull CharSequence s2) {
1678     int s1Length = s1.length();
1679     int s2Length = s2.length();
1680     if (s1Length == 0 || s2Length == 0) return 0;
1681     int i;
1682     for (i = 0; i < s1Length && i < s2Length; i++) {
1683       if (s1.charAt(s1Length - i - 1) != s2.charAt(s2Length - i - 1)) {
1684         break;
1685       }
1686     }
1687     return i;
1688   }
1689
1690   /**
1691    * Allows to answer if target symbol is contained at given char sequence at {@code [start; end)} interval.
1692    *
1693    * @param s     target char sequence to check
1694    * @param start start offset to use within the given char sequence (inclusive)
1695    * @param end   end offset to use within the given char sequence (exclusive)
1696    * @param c     target symbol to check
1697    * @return {@code true} if given symbol is contained at the target range of the given char sequence;
1698    * {@code false} otherwise
1699    */
1700   @Contract(pure = true)
1701   public static boolean contains(@NotNull CharSequence s, int start, int end, char c) {
1702     return Strings.contains(s, start, end, c);
1703   }
1704
1705   @Contract(pure = true)
1706   public static boolean containsWhitespaces(@Nullable CharSequence s) {
1707     if (s == null) return false;
1708
1709     for (int i = 0; i < s.length(); i++) {
1710       if (Character.isWhitespace(s.charAt(i))) return true;
1711     }
1712     return false;
1713   }
1714
1715   @Contract(pure = true)
1716   public static int indexOf(@NotNull CharSequence s, char c) {
1717     return Strings.indexOf(s, c);
1718   }
1719
1720   @Contract(pure = true)
1721   public static int indexOf(@NotNull CharSequence s, char c, int start) {
1722     return Strings.indexOf(s, c, start);
1723   }
1724
1725   @Contract(pure = true)
1726   public static int indexOf(@NotNull CharSequence s, char c, int start, int end) {
1727     return Strings.indexOf(s, c, start, end);
1728   }
1729
1730   @Contract(pure = true)
1731   public static boolean contains(@NotNull CharSequence sequence, @NotNull CharSequence infix) {
1732     return Strings.contains(sequence, infix);
1733   }
1734
1735   @Contract(pure = true)
1736   public static int indexOf(@NotNull CharSequence sequence, @NotNull CharSequence infix) {
1737     return Strings.indexOf(sequence, infix);
1738   }
1739
1740   @Contract(pure = true)
1741   public static int indexOf(@NotNull CharSequence sequence, @NotNull CharSequence infix, int start) {
1742     return Strings.indexOf(sequence, infix, start);
1743   }
1744
1745   @Contract(pure = true)
1746   public static int indexOf(@NotNull CharSequence sequence, @NotNull CharSequence infix, int start, int end) {
1747     return Strings.indexOf(sequence, infix, start, end);
1748   }
1749
1750   @Contract(pure = true)
1751   public static int indexOf(@NotNull CharSequence s, char c, int start, int end, boolean caseSensitive) {
1752     return Strings.indexOf(s, c, start, end, caseSensitive);
1753   }
1754
1755   public static int indexOf(char @NotNull [] s, char c, int start, int end, boolean caseSensitive) {
1756     return Strings.indexOf(s, c, start, end, caseSensitive);
1757   }
1758
1759   @Contract(pure = true)
1760   public static int indexOfSubstringEnd(@NotNull String text, @NotNull String subString) {
1761     int i = text.indexOf(subString);
1762     if (i == -1) return -1;
1763     return i + subString.length();
1764   }
1765
1766   @Contract(pure = true)
1767   public static int indexOfAny(final @NotNull String s, final @NotNull String chars) {
1768     return Strings.indexOfAny(s, chars);
1769   }
1770
1771   @Contract(pure = true)
1772   public static int indexOfAny(final @NotNull CharSequence s, final @NotNull String chars) {
1773     return Strings.indexOfAny(s, chars);
1774   }
1775
1776   @Contract(pure = true)
1777   public static int indexOfAny(final @NotNull String s, final @NotNull String chars, final int start, final int end) {
1778     return Strings.indexOfAny(s, chars, start, end);
1779   }
1780
1781   @Contract(pure = true)
1782   public static int indexOfAny(final @NotNull CharSequence s, final @NotNull String chars, final int start, int end) {
1783     return Strings.indexOfAny(s, chars, start, end);
1784   }
1785
1786   @Contract(pure = true)
1787   public static int lastIndexOfAny(@NotNull CharSequence s, final @NotNull String chars) {
1788     for (int i = s.length() - 1; i >= 0; i--) {
1789       if (containsChar(chars, s.charAt(i))) return i;
1790     }
1791     return -1;
1792   }
1793
1794   @Contract(pure = true)
1795   public static @Nullable String substringBefore(@NotNull String text, @NotNull String subString) {
1796     int i = text.indexOf(subString);
1797     if (i == -1) return null;
1798     return text.substring(0, i);
1799   }
1800
1801   @Contract(pure = true)
1802   public static @NotNull String substringBeforeLast(@NotNull String text, @NotNull String subString) {
1803     int i = text.lastIndexOf(subString);
1804     if (i == -1) return text;
1805     return text.substring(0, i);
1806   }
1807
1808   @Contract(pure = true)
1809   public static @Nullable String substringAfter(@NotNull String text, @NotNull String subString) {
1810     int i = text.indexOf(subString);
1811     if (i == -1) return null;
1812     return text.substring(i + subString.length());
1813   }
1814
1815   @Contract(pure = true)
1816   public static @Nullable String substringAfterLast(@NotNull String text, @NotNull String subString) {
1817     int i = text.lastIndexOf(subString);
1818     if (i == -1) return null;
1819     return text.substring(i + subString.length());
1820   }
1821
1822   /**
1823    * Allows to retrieve index of last occurrence of the given symbols at {@code [start; end)} sub-sequence of the given text.
1824    *
1825    * @param s     target text
1826    * @param c     target symbol which last occurrence we want to check
1827    * @param start start offset of the target text (inclusive)
1828    * @param end   end offset of the target text (exclusive)
1829    * @return index of the last occurrence of the given symbol at the target sub-sequence of the given text if any;
1830    * {@code -1} otherwise
1831    */
1832   @Contract(pure = true)
1833   public static int lastIndexOf(@NotNull CharSequence s, char c, int start, int end) {
1834     return StringUtilRt.lastIndexOf(s, c, start, end);
1835   }
1836
1837   @Contract(pure = true)
1838   public static @NotNull String first(@NotNull String text, final int maxLength, final boolean appendEllipsis) {
1839     return text.length() > maxLength ? text.substring(0, maxLength) + (appendEllipsis ? "..." : "") : text;
1840   }
1841
1842   @Contract(pure = true)
1843   public static @NotNull CharSequence first(@NotNull CharSequence text, final int length, final boolean appendEllipsis) {
1844     if (text.length() <= length) {
1845       return text;
1846     }
1847     if (appendEllipsis) {
1848       return text.subSequence(0, length) + "...";
1849     }
1850     return text.subSequence(0, length);
1851   }
1852
1853   @Contract(pure = true)
1854   public static @NotNull CharSequence last(@NotNull CharSequence text, final int length, boolean prependEllipsis) {
1855     if (text.length() <= length) {
1856       return text;
1857     }
1858     if (prependEllipsis) {
1859       return "..." + text.subSequence(text.length() - length, text.length());
1860     }
1861     return text.subSequence(text.length() - length, text.length());
1862   }
1863
1864   @Contract(pure = true)
1865   public static @NotNull String firstLast(@NotNull String text, int length) {
1866     return text.length() > length
1867            ? text.subSequence(0, length / 2) + ELLIPSIS + text.subSequence(text.length() - length / 2 - 1, text.length())
1868            : text;
1869   }
1870
1871   @Contract(pure = true)
1872   public static @NotNull String escapeChar(final @NotNull String str, final char character) {
1873     return escapeChars(str, character);
1874   }
1875
1876   @Contract(pure = true)
1877   public static @NotNull String escapeChars(final @NotNull String str, final char @NotNull ... character) {
1878     final StringBuilder buf = new StringBuilder(str);
1879     for (char c : character) {
1880       escapeChar(buf, c);
1881     }
1882     return buf.toString();
1883   }
1884
1885   public static void escapeChar(final @NotNull StringBuilder buf, final char character) {
1886     int idx = 0;
1887     while ((idx = indexOf(buf, character, idx)) >= 0) {
1888       buf.insert(idx, "\\");
1889       idx += 2;
1890     }
1891   }
1892
1893   @Contract(pure = true)
1894   public static @NotNull String escapeQuotes(final @NotNull String str) {
1895     return escapeChar(str, '"');
1896   }
1897
1898   public static void escapeQuotes(final @NotNull StringBuilder buf) {
1899     escapeChar(buf, '"');
1900   }
1901
1902   @Contract(pure = true)
1903   public static @NotNull String escapeSlashes(final @NotNull String str) {
1904     return escapeChar(str, '/');
1905   }
1906
1907   @Contract(pure = true)
1908   public static @NotNull String escapeBackSlashes(final @NotNull String str) {
1909     return escapeChar(str, '\\');
1910   }
1911
1912   @Contract(pure = true)
1913   public static @NotNull String unescapeBackSlashes(final @NotNull String str) {
1914     final StringBuilder buf = new StringBuilder(str.length());
1915     unescapeChar(buf, str, '\\');
1916     return buf.toString();
1917   }
1918
1919   @Contract(pure = true)
1920   public static @NotNull String unescapeChar(final @NotNull String str, char unescapeChar) {
1921     final StringBuilder buf = new StringBuilder(str.length());
1922     unescapeChar(buf, str, unescapeChar);
1923     return buf.toString();
1924   }
1925
1926   private static void unescapeChar(@NotNull StringBuilder buf, @NotNull String str, char unescapeChar) {
1927     final int length = str.length();
1928     final int last = length - 1;
1929     for (int i = 0; i < length; i++) {
1930       char ch = str.charAt(i);
1931       if (ch == '\\' && i != last) {
1932         //noinspection AssignmentToForLoopParameter
1933         i++;
1934         ch = str.charAt(i);
1935         if (ch != unescapeChar) buf.append('\\');
1936       }
1937
1938       buf.append(ch);
1939     }
1940   }
1941
1942   public static void quote(final @NotNull StringBuilder builder) {
1943     quote(builder, '\"');
1944   }
1945
1946   public static void quote(final @NotNull StringBuilder builder, final char quotingChar) {
1947     builder.insert(0, quotingChar);
1948     builder.append(quotingChar);
1949   }
1950
1951   @Contract(pure = true)
1952   public static @NotNull String wrapWithDoubleQuote(@NotNull String str) {
1953     return '\"' + str + "\"";
1954   }
1955
1956   private static final List<String> REPLACES_REFS = Arrays.asList("&lt;", "&gt;", "&amp;", "&#39;", "&quot;");
1957   private static final List<String> REPLACES_DISP = Arrays.asList("<", ">", "&", "'", "\"");
1958
1959   /**
1960    * @deprecated Use {@link #unescapeXmlEntities(String)} instead
1961    */
1962   @Contract(value = "null -> null; !null -> !null",pure = true)
1963   @Deprecated
1964   public static String unescapeXml(final @Nullable String text) {
1965     return text == null ? null : unescapeXmlEntities(text);
1966   }
1967
1968   /**
1969    * @deprecated Use {@link #escapeXmlEntities(String)} instead
1970    */
1971   @Contract(value = "null -> null; !null -> !null",pure = true)
1972   @Deprecated
1973   public static String escapeXml(final @Nullable String text) {
1974     return text == null ? null : escapeXmlEntities(text);
1975   }
1976
1977   /**
1978    * @return {@code text} with some standard XML entities replaced with corresponding characters, e.g. '{@code &lt;}' replaced with '<'
1979    */
1980   @Contract(pure = true)
1981   public static @NotNull String unescapeXmlEntities(@NotNull String text) {
1982     return replace(text, REPLACES_REFS, REPLACES_DISP);
1983   }
1984
1985   /**
1986    * @return {@code text} with some characters replaced with standard XML entities, e.g. '<' replaced with '{@code &lt;}'
1987    */
1988   @Contract(pure = true)
1989   public static @NotNull String escapeXmlEntities(@NotNull String text) {
1990     return replace(text, REPLACES_DISP, REPLACES_REFS);
1991   }
1992
1993   public static @NotNull String removeHtmlTags(@NotNull String htmlString) {
1994     return removeHtmlTags(htmlString, false);
1995   }
1996
1997   public static @NotNull String removeHtmlTags(@NotNull String htmlString, boolean isRemoveStyleTag) {
1998     if (isEmpty(htmlString)) {
1999       return "";
2000     }
2001
2002     final MyHtml2Text parser = isRemoveStyleTag ? new MyHtml2Text(true) : html2TextParser;
2003     try {
2004       parser.parse(new StringReader(htmlString));
2005     }
2006     catch (IOException e) {
2007       Logger.getInstance(StringUtil.class).error(e);
2008     }
2009     return parser.getText();
2010   }
2011
2012   @Contract(pure = true)
2013   public static @NotNull String removeEllipsisSuffix(@NotNull String s) {
2014     String THREE_DOTS = "...";
2015     if (s.endsWith(THREE_DOTS)) {
2016       return s.substring(0, s.length() - THREE_DOTS.length());
2017     }
2018     if (s.endsWith(ELLIPSIS)) {
2019       return s.substring(0, s.length() - ELLIPSIS.length());
2020     }
2021     return s;
2022   }
2023
2024   private static final List<String> MN_QUOTED = Arrays.asList("&&", "__");
2025   private static final List<String> MN_CHARS = Arrays.asList("&", "_");
2026
2027   @Contract(pure = true)
2028   public static @NotNull String escapeMnemonics(@NotNull String text) {
2029     return replace(text, MN_CHARS, MN_QUOTED);
2030   }
2031
2032   @Contract(pure = true)
2033   public static @NotNull String htmlEmphasize(@NotNull String text) {
2034     return "<b><code>" + escapeXmlEntities(text) + "</code></b>";
2035   }
2036
2037
2038   @Contract(pure = true)
2039   public static @NotNull String escapeToRegexp(@NotNull String text) {
2040     final StringBuilder result = new StringBuilder(text.length());
2041     return escapeToRegexp(text, result).toString();
2042   }
2043
2044   public static @NotNull StringBuilder escapeToRegexp(@NotNull CharSequence text, @NotNull StringBuilder builder) {
2045     for (int i = 0; i < text.length(); i++) {
2046       final char c = text.charAt(i);
2047       if (c == ' ' || Character.isLetter(c) || Character.isDigit(c) || c == '_') {
2048         builder.append(c);
2049       }
2050       else if (c == '\n') {
2051         builder.append("\\n");
2052       }
2053       else if (c == '\r') {
2054         builder.append("\\r");
2055       }
2056       else {
2057         final Character.UnicodeBlock block = Character.UnicodeBlock.of(c);
2058         if (block == Character.UnicodeBlock.HIGH_SURROGATES || block == Character.UnicodeBlock.LOW_SURROGATES) {
2059           builder.append(c);
2060         } else {
2061           builder.append('\\').append(c);
2062         }
2063       }
2064     }
2065
2066     return builder;
2067   }
2068
2069   @Contract(pure = true)
2070   public static boolean isEscapedBackslash(char @NotNull [] chars, int startOffset, int backslashOffset) {
2071     if (chars[backslashOffset] != '\\') {
2072       return true;
2073     }
2074     boolean escaped = false;
2075     for (int i = startOffset; i < backslashOffset; i++) {
2076       if (chars[i] == '\\') {
2077         escaped = !escaped;
2078       }
2079       else {
2080         escaped = false;
2081       }
2082     }
2083     return escaped;
2084   }
2085
2086   @Contract(pure = true)
2087   public static boolean isEscapedBackslash(@NotNull CharSequence text, int startOffset, int backslashOffset) {
2088     if (text.charAt(backslashOffset) != '\\') {
2089       return true;
2090     }
2091     boolean escaped = false;
2092     for (int i = startOffset; i < backslashOffset; i++) {
2093       if (text.charAt(i) == '\\') {
2094         escaped = !escaped;
2095       }
2096       else {
2097         escaped = false;
2098       }
2099     }
2100     return escaped;
2101   }
2102
2103   /**
2104    * @deprecated Use {@link #replace(String, List, List)}
2105    */
2106   @Deprecated
2107   @Contract(pure = true)
2108   public static @NotNull String replace(@NotNull String text, String @NotNull [] from, String @NotNull [] to) {
2109     return replace(text, Arrays.asList(from), Arrays.asList(to));
2110   }
2111
2112   @Contract(pure = true)
2113   public static @NotNull String replace(@NotNull String text, @NotNull List<String> from, @NotNull List<String> to) {
2114     assert from.size() == to.size();
2115     StringBuilder result = null;
2116     replace:
2117     for (int i = 0; i < text.length(); i++) {
2118       for (int j = 0; j < from.size(); j += 1) {
2119         String toReplace = from.get(j);
2120         String replaceWith = to.get(j);
2121
2122         final int len = toReplace.length();
2123         if (len == 0) continue;
2124         if (text.regionMatches(i, toReplace, 0, len)) {
2125           if (result == null) {
2126             result = new StringBuilder(text.length());
2127             result.append(text, 0, i);
2128           }
2129           result.append(replaceWith);
2130           //noinspection AssignmentToForLoopParameter
2131           i += len - 1;
2132           continue replace;
2133         }
2134       }
2135
2136       if (result != null) {
2137         result.append(text.charAt(i));
2138       }
2139     }
2140     return result == null ? text : result.toString();
2141   }
2142
2143   @Contract(pure = true)
2144   public static String @NotNull [] filterEmptyStrings(String @NotNull [] strings) {
2145     int emptyCount = 0;
2146     for (String string : strings) {
2147       if (string == null || string.isEmpty()) emptyCount++;
2148     }
2149     if (emptyCount == 0) return strings;
2150
2151     String[] result = ArrayUtil.newStringArray(strings.length - emptyCount);
2152     int count = 0;
2153     for (String string : strings) {
2154       if (string == null || string.isEmpty()) continue;
2155       result[count++] = string;
2156     }
2157
2158     return result;
2159   }
2160
2161   @Contract(pure = true)
2162   public static int countNewLines(@NotNull CharSequence text) {
2163     return countChars(text, '\n');
2164   }
2165
2166   @Contract(pure = true)
2167   public static int countChars(@NotNull CharSequence text, char c) {
2168     return countChars(text, c, 0, false);
2169   }
2170
2171   @Contract(pure = true)
2172   public static int countChars(@NotNull CharSequence text, char c, int offset, boolean stopAtOtherChar) {
2173     return countChars(text, c, offset, text.length(), stopAtOtherChar);
2174   }
2175
2176   @Contract(pure = true)
2177   public static int countChars(@NotNull CharSequence text, char c, int start, int end, boolean stopAtOtherChar) {
2178     boolean forward = start <= end;
2179     start = forward ? Math.max(0, start) : Math.min(text.length(), start);
2180     end = forward ? Math.min(text.length(), end) : Math.max(0, end);
2181     int count = 0;
2182     for (int i = forward ? start : start - 1; forward == i < end; i += forward ? 1 : -1) {
2183       if (text.charAt(i) == c) {
2184         count++;
2185       }
2186       else if (stopAtOtherChar) {
2187         break;
2188       }
2189     }
2190     return count;
2191   }
2192
2193   /**
2194    * @param args Strings to join.
2195    * @return {@code null} if any of given Strings is {@code null}.
2196    */
2197   @Contract(pure = true)
2198   public static @Nullable String joinOrNull(String @NotNull ... args) {
2199     StringBuilder r = new StringBuilder();
2200     for (String arg : args) {
2201       if (arg == null) return null;
2202       r.append(arg);
2203     }
2204     return r.toString();
2205   }
2206
2207   @Contract(pure = true)
2208   public static @Nullable String getPropertyName(@NotNull String methodName) {
2209     if (methodName.startsWith("get")) {
2210       return Introspector.decapitalize(methodName.substring(3));
2211     }
2212     if (methodName.startsWith("is")) {
2213       return Introspector.decapitalize(methodName.substring(2));
2214     }
2215     if (methodName.startsWith("set")) {
2216       return Introspector.decapitalize(methodName.substring(3));
2217     }
2218     return null;
2219   }
2220
2221   @Contract(pure = true)
2222   public static boolean isJavaIdentifierStart(char c) {
2223     return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || Character.isJavaIdentifierStart(c);
2224   }
2225
2226   @Contract(pure = true)
2227   public static boolean isJavaIdentifierPart(char c) {
2228     return c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || Character.isJavaIdentifierPart(c);
2229   }
2230
2231   @Contract(pure = true)
2232   public static boolean isJavaIdentifier(@NotNull String text) {
2233     int len = text.length();
2234     if (len == 0) return false;
2235
2236     if (!isJavaIdentifierStart(text.charAt(0))) return false;
2237
2238     for (int i = 1; i < len; i++) {
2239       if (!isJavaIdentifierPart(text.charAt(i))) return false;
2240     }
2241
2242     return true;
2243   }
2244
2245   /**
2246    * Escape property name or key in property file. Unicode characters are escaped as well.
2247    *
2248    * @param input an input to escape
2249    * @param isKey if true, the rules for key escaping are applied. The leading space is escaped in that case.
2250    * @return an escaped string
2251    */
2252   @Contract(pure = true)
2253   public static @NotNull String escapeProperty(@NotNull String input, final boolean isKey) {
2254     final StringBuilder escaped = new StringBuilder(input.length());
2255     for (int i = 0; i < input.length(); i++) {
2256       final char ch = input.charAt(i);
2257       switch (ch) {
2258         case ' ':
2259           if (isKey && i == 0) {
2260             // only the leading space has to be escaped
2261             escaped.append('\\');
2262           }
2263           escaped.append(' ');
2264           break;
2265         case '\t':
2266           escaped.append("\\t");
2267           break;
2268         case '\r':
2269           escaped.append("\\r");
2270           break;
2271         case '\n':
2272           escaped.append("\\n");
2273           break;
2274         case '\f':
2275           escaped.append("\\f");
2276           break;
2277         case '\\':
2278         case '#':
2279         case '!':
2280         case ':':
2281         case '=':
2282           escaped.append('\\');
2283           escaped.append(ch);
2284           break;
2285         default:
2286           if (20 < ch && ch < 0x7F) {
2287             escaped.append(ch);
2288           }
2289           else {
2290             escaped.append("\\u");
2291             escaped.append(Character.forDigit((ch >> 12) & 0xF, 16));
2292             escaped.append(Character.forDigit((ch >> 8) & 0xF, 16));
2293             escaped.append(Character.forDigit((ch >> 4) & 0xF, 16));
2294             escaped.append(Character.forDigit((ch) & 0xF, 16));
2295           }
2296           break;
2297       }
2298     }
2299     return escaped.toString();
2300   }
2301
2302   @Contract(pure = true)
2303   public static @NotNull @NonNls String getQualifiedName(@Nullable @NonNls String packageName, @NotNull @NonNls String className) {
2304     if (packageName == null || packageName.isEmpty()) {
2305       return className;
2306     }
2307     return packageName + '.' + className;
2308   }
2309
2310   @Contract(pure = true)
2311   public static int compareVersionNumbers(@Nullable String v1, @Nullable String v2) {
2312     // todo duplicates com.intellij.util.text.VersionComparatorUtil.compare
2313     // todo please refactor next time you make changes here
2314     if (v1 == null && v2 == null) {
2315       return 0;
2316     }
2317     if (v1 == null) {
2318       return -1;
2319     }
2320     if (v2 == null) {
2321       return 1;
2322     }
2323
2324     String[] part1 = v1.split("[._\\-]");
2325     String[] part2 = v2.split("[._\\-]");
2326
2327     int idx = 0;
2328     for (; idx < part1.length && idx < part2.length; idx++) {
2329       String p1 = part1[idx];
2330       String p2 = part2[idx];
2331
2332       int cmp;
2333       if (p1.matches("\\d+") && p2.matches("\\d+")) {
2334         cmp = new Integer(p1).compareTo(new Integer(p2));
2335       }
2336       else {
2337         cmp = part1[idx].compareTo(part2[idx]);
2338       }
2339       if (cmp != 0) return cmp;
2340     }
2341
2342     if (part1.length != part2.length) {
2343       boolean left = part1.length > idx;
2344       String[] parts = left ? part1 : part2;
2345
2346       for (; idx < parts.length; idx++) {
2347         String p = parts[idx];
2348         int cmp;
2349         if (p.matches("\\d+")) {
2350           cmp = new Integer(p).compareTo(0);
2351         }
2352         else {
2353           cmp = 1;
2354         }
2355         if (cmp != 0) return left ? cmp : -cmp;
2356       }
2357     }
2358     return 0;
2359   }
2360
2361   @Contract(pure = true)
2362   public static int getOccurrenceCount(@NotNull String text, final char c) {
2363     int res = 0;
2364     int i = 0;
2365     while (i < text.length()) {
2366       i = text.indexOf(c, i);
2367       if (i >= 0) {
2368         res++;
2369         i++;
2370       }
2371       else {
2372         break;
2373       }
2374     }
2375     return res;
2376   }
2377
2378   @Contract(pure = true)
2379   public static int getOccurrenceCount(@NotNull String text, @NotNull String s) {
2380     int res = 0;
2381     int i = 0;
2382     while (i < text.length()) {
2383       i = text.indexOf(s, i);
2384       if (i >= 0) {
2385         res++;
2386         i++;
2387       }
2388       else {
2389         break;
2390       }
2391     }
2392     return res;
2393   }
2394
2395   @Contract(pure = true)
2396   public static @NotNull String fixVariableNameDerivedFromPropertyName(@NotNull String name) {
2397     if (isEmptyOrSpaces(name)) return name;
2398     char c = name.charAt(0);
2399     if (isVowel(c)) {
2400       return "an" + Character.toUpperCase(c) + name.substring(1);
2401     }
2402     return "a" + Character.toUpperCase(c) + name.substring(1);
2403   }
2404
2405   @Contract(pure = true)
2406   public static @NotNull String sanitizeJavaIdentifier(@NotNull String name) {
2407     final StringBuilder result = new StringBuilder(name.length());
2408
2409     for (int i = 0; i < name.length(); i++) {
2410       final char ch = name.charAt(i);
2411       if (Character.isJavaIdentifierPart(ch)) {
2412         if (result.length() == 0 && !Character.isJavaIdentifierStart(ch)) {
2413           result.append("_");
2414         }
2415         result.append(ch);
2416       }
2417     }
2418
2419     return result.toString();
2420   }
2421
2422   public static void assertValidSeparators(@NotNull CharSequence s) {
2423     char[] chars = CharArrayUtil.fromSequenceWithoutCopying(s);
2424     int slashRIndex = -1;
2425
2426     if (chars != null) {
2427       for (int i = 0, len = s.length(); i < len; ++i) {
2428         if (chars[i] == '\r') {
2429           slashRIndex = i;
2430           break;
2431         }
2432       }
2433     }
2434     else {
2435       for (int i = 0, len = s.length(); i < len; i++) {
2436         if (s.charAt(i) == '\r') {
2437           slashRIndex = i;
2438           break;
2439         }
2440       }
2441     }
2442
2443     if (slashRIndex != -1) {
2444       String context =
2445         String.valueOf(last(s.subSequence(0, slashRIndex), 10, true)) + first(s.subSequence(slashRIndex, s.length()), 10, true);
2446       context = escapeStringCharacters(context);
2447       throw new AssertionError("Wrong line separators: '" + context + "' at offset " + slashRIndex);
2448     }
2449   }
2450
2451   @Contract(pure = true)
2452   public static @NotNull String tail(@NotNull String s, final int idx) {
2453     return idx >= s.length() ? "" : s.substring(idx);
2454   }
2455
2456   /**
2457    * Splits string by lines.
2458    *
2459    * @param string String to split
2460    * @return array of strings
2461    */
2462   @Contract(pure = true)
2463   public static String @NotNull [] splitByLines(@NotNull String string) {
2464     return splitByLines(string, true);
2465   }
2466
2467   /**
2468    * Splits string by lines. If several line separators are in a row corresponding empty lines
2469    * are also added to result if {@code excludeEmptyStrings} is {@code false}.
2470    *
2471    * @param string String to split
2472    * @return array of strings
2473    */
2474   @Contract(pure = true)
2475   public static String @NotNull [] splitByLines(@NotNull String string, boolean excludeEmptyStrings) {
2476     return (excludeEmptyStrings ? EOL_SPLIT_PATTERN : EOL_SPLIT_PATTERN_WITH_EMPTY).split(string);
2477   }
2478
2479   @Contract(pure = true)
2480   public static String @NotNull [] splitByLinesDontTrim(@NotNull String string) {
2481     return EOL_SPLIT_DONT_TRIM_PATTERN.split(string);
2482   }
2483
2484   /**
2485    * Splits string by lines, keeping all line separators at the line ends and in the empty lines.
2486    * <br> E.g. splitting text
2487    * <blockquote>
2488    *   foo\r\n<br>
2489    *   \n<br>
2490    *   bar\n<br>
2491    *   \r\n<br>
2492    *   baz\r<br>
2493    *   \r<br>
2494    * </blockquote>
2495    * will return the following array: foo\r\n, \n, bar\n, \r\n, baz\r, \r
2496    *
2497    */
2498   @Contract(pure = true)
2499   public static String @NotNull [] splitByLinesKeepSeparators(@NotNull String string) {
2500     return EOL_SPLIT_KEEP_SEPARATORS.split(string);
2501   }
2502
2503   @Contract(pure = true)
2504   public static @NotNull List<Pair<String, Integer>> getWordsWithOffset(@NotNull String s) {
2505     List<Pair<String, Integer>> res = new ArrayList<>();
2506     s += " ";
2507     StringBuilder name = new StringBuilder();
2508     int startInd = -1;
2509     for (int i = 0; i < s.length(); i++) {
2510       if (Character.isWhitespace(s.charAt(i))) {
2511         if (name.length() > 0) {
2512           res.add(Pair.create(name.toString(), startInd));
2513           name.setLength(0);
2514           startInd = -1;
2515         }
2516       }
2517       else {
2518         if (startInd == -1) {
2519           startInd = i;
2520         }
2521         name.append(s.charAt(i));
2522       }
2523     }
2524     return res;
2525   }
2526
2527   @Contract(pure = true)
2528   public static int naturalCompare(@Nullable String string1, @Nullable String string2) {
2529     return NaturalComparator.INSTANCE.compare(string1, string2);
2530   }
2531
2532   @Contract(pure = true)
2533   public static boolean isDecimalDigit(char c) {
2534     return c >= '0' && c <= '9';
2535   }
2536
2537   @Contract("null -> false")
2538   public static boolean isNotNegativeNumber(@Nullable CharSequence s) {
2539     if (s == null) {
2540       return false;
2541     }
2542     for (int i = 0; i < s.length(); i++) {
2543       if (!isDecimalDigit(s.charAt(i))) {
2544         return false;
2545       }
2546     }
2547     return true;
2548   }
2549
2550   @Contract(pure = true)
2551   public static int compare(@Nullable String s1, @Nullable String s2, boolean ignoreCase) {
2552     //noinspection StringEquality
2553     if (s1 == s2) return 0;
2554     if (s1 == null) return -1;
2555     if (s2 == null) return 1;
2556     return ignoreCase ? s1.compareToIgnoreCase(s2) : s1.compareTo(s2);
2557   }
2558
2559   @Contract(pure = true)
2560   public static int compare(@Nullable CharSequence s1, @Nullable CharSequence s2, boolean ignoreCase) {
2561     if (s1 == s2) return 0;
2562     if (s1 == null) return -1;
2563     if (s2 == null) return 1;
2564
2565     int length1 = s1.length();
2566     int length2 = s2.length();
2567     int i = 0;
2568     for (; i < length1 && i < length2; i++) {
2569       int diff = Strings.compare(s1.charAt(i), s2.charAt(i), ignoreCase);
2570       if (diff != 0) {
2571         return diff;
2572       }
2573     }
2574     return length1 - length2;
2575   }
2576
2577   @Contract(pure = true)
2578   public static int comparePairs(@Nullable String s1, @Nullable String t1, @Nullable String s2, @Nullable String t2, boolean ignoreCase) {
2579     final int compare = compare(s1, s2, ignoreCase);
2580     return compare != 0 ? compare : compare(t1, t2, ignoreCase);
2581   }
2582
2583   @Contract(pure = true)
2584   public static boolean equals(@Nullable CharSequence s1, @Nullable CharSequence s2) {
2585     return StringUtilRt.equal(s1, s2, true);
2586   }
2587
2588   @Contract(pure = true)
2589   public static boolean equalsIgnoreCase(@Nullable CharSequence s1, @Nullable CharSequence s2) {
2590     return StringUtilRt.equal(s1, s2, false);
2591   }
2592
2593   @Contract(pure = true)
2594   public static boolean equalsIgnoreWhitespaces(@Nullable CharSequence s1, @Nullable CharSequence s2) {
2595     if (s1 == null ^ s2 == null) {
2596       return false;
2597     }
2598
2599     if (s1 == null) {
2600       return true;
2601     }
2602
2603     int len1 = s1.length();
2604     int len2 = s2.length();
2605
2606     int index1 = 0;
2607     int index2 = 0;
2608     while (index1 < len1 && index2 < len2) {
2609       if (s1.charAt(index1) == s2.charAt(index2)) {
2610         index1++;
2611         index2++;
2612         continue;
2613       }
2614
2615       boolean skipped = false;
2616       while (index1 != len1 && isWhiteSpace(s1.charAt(index1))) {
2617         skipped = true;
2618         index1++;
2619       }
2620       while (index2 != len2 && isWhiteSpace(s2.charAt(index2))) {
2621         skipped = true;
2622         index2++;
2623       }
2624
2625       if (!skipped) return false;
2626     }
2627
2628     for (; index1 != len1; index1++) {
2629       if (!isWhiteSpace(s1.charAt(index1))) return false;
2630     }
2631     for (; index2 != len2; index2++) {
2632       if (!isWhiteSpace(s2.charAt(index2))) return false;
2633     }
2634
2635     return true;
2636   }
2637
2638   @Contract(pure = true)
2639   public static boolean equalsTrimWhitespaces(@NotNull CharSequence s1, @NotNull CharSequence s2) {
2640     int start1 = 0;
2641     int end1 = s1.length();
2642     int end2 = s2.length();
2643
2644     while (start1 < end1) {
2645       char c = s1.charAt(start1);
2646       if (!isWhiteSpace(c)) break;
2647       start1++;
2648     }
2649
2650     while (start1 < end1) {
2651       char c = s1.charAt(end1 - 1);
2652       if (!isWhiteSpace(c)) break;
2653       end1--;
2654     }
2655
2656     int start2 = 0;
2657     while (start2 < end2) {
2658       char c = s2.charAt(start2);
2659       if (!isWhiteSpace(c)) break;
2660       start2++;
2661     }
2662
2663     while (start2 < end2) {
2664       char c = s2.charAt(end2 - 1);
2665       if (!isWhiteSpace(c)) break;
2666       end2--;
2667     }
2668
2669     CharSequence ts1 = new CharSequenceSubSequence(s1, start1, end1);
2670     CharSequence ts2 = new CharSequenceSubSequence(s2, start2, end2);
2671
2672     return equals(ts1, ts2);
2673   }
2674
2675   /**
2676    * Collapses all white-space (including new lines) between non-white-space characters to a single space character.
2677    * Leading and trailing white space is removed.
2678    */
2679   public static @NotNull String collapseWhiteSpace(@NotNull CharSequence s) {
2680     final StringBuilder result = new StringBuilder();
2681     boolean space = false;
2682     for (int i = 0, length = s.length(); i < length; i++) {
2683       final char ch = s.charAt(i);
2684       if (isWhiteSpace(ch)) {
2685         if (!space) space = true;
2686       }
2687       else {
2688         if (space && result.length() > 0) result.append(' ');
2689         result.append(ch);
2690         space = false;
2691       }
2692     }
2693     return result.toString();
2694   }
2695
2696   @Contract(pure = true)
2697   public static boolean findIgnoreCase(@Nullable String toFind, String @NotNull ... where) {
2698     for (String string : where) {
2699       if (equalsIgnoreCase(toFind, string)) return true;
2700     }
2701     return false;
2702   }
2703
2704   @Contract(pure = true)
2705   public static int compare(char c1, char c2, boolean ignoreCase) {
2706     // duplicating String.equalsIgnoreCase logic
2707     int d = c1 - c2;
2708     if (d == 0 || !ignoreCase) {
2709       return d;
2710     }
2711     // If characters don't match but case may be ignored,
2712     // try converting both characters to uppercase.
2713     // If the results match, then the comparison scan should
2714     // continue.
2715     char u1 = StringUtilRt.toUpperCase(c1);
2716     char u2 = StringUtilRt.toUpperCase(c2);
2717     d = u1 - u2;
2718     if (d != 0) {
2719       // Unfortunately, conversion to uppercase does not work properly
2720       // for the Georgian alphabet, which has strange rules about case
2721       // conversion.  So we need to make one last check before
2722       // exiting.
2723       d = StringUtilRt.toLowerCase(u1) - StringUtilRt.toLowerCase(u2);
2724     }
2725     return d;
2726   }
2727
2728   @Contract(pure = true)
2729   public static @NotNull String formatLinks(@NotNull String message) {
2730     Pattern linkPattern = Pattern.compile("http://[a-zA-Z0-9./\\-+]+");
2731     StringBuffer result = new StringBuffer();
2732     Matcher m = linkPattern.matcher(message);
2733     while (m.find()) {
2734       m.appendReplacement(result, "<a href=\"" + m.group() + "\">" + m.group() + "</a>");
2735     }
2736     m.appendTail(result);
2737     return result.toString();
2738   }
2739
2740   @Contract(pure = true)
2741   public static boolean isHexDigit(char c) {
2742     return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F';
2743   }
2744
2745   @Contract(pure = true)
2746   public static boolean isOctalDigit(char c) {
2747     return '0' <= c && c <= '7';
2748   }
2749
2750   @Contract(pure = true)
2751   public static @NotNull String shortenTextWithEllipsis(final @NotNull String text, final int maxLength, final int suffixLength) {
2752     return shortenTextWithEllipsis(text, maxLength, suffixLength, false);
2753   }
2754
2755   @Contract(pure = true)
2756   public static @NotNull String trimMiddle(@NotNull String text, int maxLength) {
2757     return shortenTextWithEllipsis(text, maxLength, maxLength >> 1, true);
2758   }
2759
2760   @Contract(pure = true)
2761   public static @NotNull String shortenTextWithEllipsis(final @NotNull String text,
2762                                                         final int maxLength,
2763                                                         final int suffixLength,
2764                                                         @NotNull String symbol) {
2765     final int textLength = text.length();
2766     if (textLength > maxLength) {
2767       final int prefixLength = maxLength - suffixLength - symbol.length();
2768       assert prefixLength > 0;
2769       return text.substring(0, prefixLength) + symbol + text.substring(textLength - suffixLength);
2770     }
2771     else {
2772       return text;
2773     }
2774   }
2775
2776   @Contract(pure = true)
2777   public static @NotNull String shortenTextWithEllipsis(final @NotNull String text,
2778                                                         final int maxLength,
2779                                                         final int suffixLength,
2780                                                         boolean useEllipsisSymbol) {
2781     String symbol = useEllipsisSymbol ? ELLIPSIS : "...";
2782     return shortenTextWithEllipsis(text, maxLength, suffixLength, symbol);
2783   }
2784
2785   @Contract(pure = true)
2786   public static @NotNull String shortenPathWithEllipsis(final @NotNull String path, final int maxLength, boolean useEllipsisSymbol) {
2787     return shortenTextWithEllipsis(path, maxLength, (int)(maxLength * 0.7), useEllipsisSymbol);
2788   }
2789
2790   @Contract(pure = true)
2791   public static @NotNull String shortenPathWithEllipsis(final @NotNull String path, final int maxLength) {
2792     return shortenPathWithEllipsis(path, maxLength, false);
2793   }
2794
2795   @Contract(pure = true)
2796   public static boolean charsEqualIgnoreCase(char a, char b) {
2797     return Strings.charsEqualIgnoreCase(a, b);
2798   }
2799
2800   @Contract(pure = true)
2801   public static char toUpperCase(char a) {
2802     return Strings.toUpperCase(a);
2803   }
2804
2805   @Contract(value = "null -> null; !null -> !null", pure = true)
2806   public static String toUpperCase(String s) {
2807     return Strings.toUpperCase(s);
2808   }
2809
2810   @Contract(pure = true)
2811   public static char toLowerCase(final char a) {
2812     return Strings.toLowerCase(a);
2813   }
2814
2815   @Contract(pure = true)
2816   public static boolean isUpperCase(@NotNull CharSequence sequence) {
2817     for (int i = 0; i < sequence.length(); i++) {
2818       if (!Character.isUpperCase(sequence.charAt(i))) return false;
2819     }
2820     return true;
2821   }
2822
2823   public static @Nullable LineSeparator detectSeparators(@NotNull CharSequence text) {
2824     int index = indexOfAny(text, "\n\r");
2825     if (index == -1) return null;
2826     LineSeparator lineSeparator = getLineSeparatorAt(text, index);
2827     if (lineSeparator == null) {
2828       throw new AssertionError();
2829     }
2830     return lineSeparator;
2831   }
2832
2833   public static @Nullable LineSeparator getLineSeparatorAt(@NotNull CharSequence text, int index) {
2834     if (index < 0 || index >= text.length()) {
2835       return null;
2836     }
2837     char ch = text.charAt(index);
2838     if (ch == '\r') {
2839       return index + 1 < text.length() && text.charAt(index + 1) == '\n' ? LineSeparator.CRLF : LineSeparator.CR;
2840     }
2841     return ch == '\n' ? LineSeparator.LF : null;
2842   }
2843
2844   @Contract(pure = true)
2845   public static @NotNull String convertLineSeparators(@NotNull String text) {
2846     return StringUtilRt.convertLineSeparators(text);
2847   }
2848
2849   @Contract(pure = true)
2850   public static @NotNull String convertLineSeparators(@NotNull String text, boolean keepCarriageReturn) {
2851     return StringUtilRt.convertLineSeparators(text, keepCarriageReturn);
2852   }
2853
2854   @Contract(pure = true)
2855   public static @NotNull String convertLineSeparators(@NotNull String text, @NotNull String newSeparator) {
2856     return StringUtilRt.convertLineSeparators(text, newSeparator);
2857   }
2858
2859   public static @NotNull String convertLineSeparators(@NotNull String text, @NotNull String newSeparator, int @Nullable [] offsetsToKeep) {
2860     return StringUtilRt.convertLineSeparators(text, newSeparator, offsetsToKeep);
2861   }
2862
2863   @Contract(pure = true)
2864   public static int parseInt(@Nullable String string, int defaultValue) {
2865     return StringUtilRt.parseInt(string, defaultValue);
2866   }
2867
2868   @Contract(pure = true)
2869   public static long parseLong(@Nullable String string, long defaultValue) {
2870     return StringUtilRt.parseLong(string, defaultValue);
2871   }
2872
2873   @Contract(pure = true)
2874   public static double parseDouble(@Nullable String string, double defaultValue) {
2875     return StringUtilRt.parseDouble(string, defaultValue);
2876   }
2877
2878   @Contract(pure = true)
2879   public static <E extends Enum<E>> E parseEnum(@NotNull String string, E defaultValue, @NotNull Class<E> clazz) {
2880     return StringUtilRt.parseEnum(string, defaultValue, clazz);
2881   }
2882
2883   @Contract(pure = true)
2884   public static @NotNull String getShortName(@NotNull Class<?> aClass) {
2885     return StringUtilRt.getShortName(aClass);
2886   }
2887
2888   @Contract(pure = true)
2889   public static @NotNull @NonNls String getShortName(@NotNull @NonNls String fqName) {
2890     return StringUtilRt.getShortName(fqName);
2891   }
2892
2893   @Contract(pure = true)
2894   public static @NotNull @NonNls String getShortName(@NotNull @NonNls String fqName, char separator) {
2895     return StringUtilRt.getShortName(fqName, separator);
2896   }
2897
2898   /**
2899    * Equivalent for {@code getShortName(fqName).equals(shortName)}, but could be faster.
2900    *
2901    * @param fqName    fully-qualified name (dot-separated)
2902    * @param shortName a short name, must not contain dots
2903    * @return true if specified short name is a short name of fully-qualified name
2904    */
2905   public static boolean isShortNameOf(@NotNull String fqName, @NotNull String shortName) {
2906     if (fqName.length() < shortName.length()) return false;
2907     if (fqName.length() == shortName.length()) return fqName.equals(shortName);
2908     int diff = fqName.length() - shortName.length();
2909     if (fqName.charAt(diff - 1) != '.') return false;
2910     return fqName.regionMatches(diff, shortName, 0, shortName.length());
2911   }
2912
2913   /**
2914    * Strips class name from Object#toString if present.
2915    * To be used as custom data type renderer for java.lang.Object.
2916    * To activate just add {@code StringUtil.toShortString(this)}
2917    * expression in <em>Settings | Debugger | Data Views</em>.
2918    */
2919   @Contract("null->null;!null->!null")
2920   @SuppressWarnings("UnusedDeclaration")
2921   static String toShortString(@Nullable Object o) {
2922     if (o == null) return null;
2923     if (o instanceof CharSequence) return o.toString();
2924     String className = o.getClass().getName();
2925     String s = o.toString();
2926     if (!s.startsWith(className)) return s;
2927     return s.length() > className.length() && !Character.isLetter(s.charAt(className.length())) ?
2928            trimStart(s, className) : s;
2929   }
2930
2931   @Contract(pure = true)
2932   public static @NotNull CharSequence newBombedCharSequence(@NotNull CharSequence sequence, long delay) {
2933     final long myTime = System.currentTimeMillis() + delay;
2934     return new BombedCharSequence(sequence) {
2935       @Override
2936       protected void checkCanceled() {
2937         long l = System.currentTimeMillis();
2938         if (l >= myTime) {
2939           throw new ProcessCanceledException();
2940         }
2941       }
2942     };
2943   }
2944
2945   public static boolean trimEnd(@NotNull StringBuilder buffer, @NotNull CharSequence end) {
2946     if (endsWith(buffer, end)) {
2947       buffer.delete(buffer.length() - end.length(), buffer.length());
2948       return true;
2949     }
2950     return false;
2951   }
2952
2953   /**
2954    * Say smallPart = "op" and bigPart="open". Method returns true for "Ope" and false for "ops"
2955    */
2956   @Contract(pure = true)
2957   public static boolean isBetween(@NotNull String string, @NotNull String smallPart, @NotNull String bigPart) {
2958     String s = toLowerCase(string);
2959     return s.startsWith(toLowerCase(smallPart)) && toLowerCase(bigPart).startsWith(s);
2960   }
2961
2962   /**
2963    * Does the string have an uppercase character?
2964    * @param s  the string to test.
2965    * @return   true if the string has an uppercase character, false if not.
2966    */
2967   public static boolean hasUpperCaseChar(@NotNull String s) {
2968       char[] chars = s.toCharArray();
2969       for (char c : chars) {
2970           if (Character.isUpperCase(c)) {
2971               return true;
2972           }
2973       }
2974       return false;
2975   }
2976
2977   /**
2978    * Does the string have a lowercase character?
2979    * @param s  the string to test.
2980    * @return   true if the string has a lowercase character, false if not.
2981    */
2982   public static boolean hasLowerCaseChar(@NotNull String s) {
2983       char[] chars = s.toCharArray();
2984       for (char c : chars) {
2985           if (Character.isLowerCase(c)) {
2986               return true;
2987           }
2988       }
2989       return false;
2990   }
2991
2992   private static final Pattern UNICODE_CHAR = Pattern.compile("\\\\u[0-9a-fA-F]{4}");
2993
2994   public static String replaceUnicodeEscapeSequences(String text) {
2995     if (text == null) return null;
2996
2997     final Matcher matcher = UNICODE_CHAR.matcher(text);
2998     if (!matcher.find()) return text; // fast path
2999
3000     matcher.reset();
3001     int lastEnd = 0;
3002     final StringBuilder sb = new StringBuilder(text.length());
3003     while (matcher.find()) {
3004       sb.append(text, lastEnd, matcher.start());
3005       final char c = (char)Integer.parseInt(matcher.group().substring(2), 16);
3006       sb.append(c);
3007       lastEnd = matcher.end();
3008     }
3009     sb.append(text.substring(lastEnd));
3010     return sb.toString();
3011   }
3012
3013   /**
3014    * Expirable CharSequence. Very useful to control external library execution time,
3015    * i.e. when java.util.regex.Pattern match goes out of control.
3016    */
3017   public abstract static class BombedCharSequence implements CharSequence {
3018     private final CharSequence delegate;
3019     private int i;
3020     private boolean myDefused;
3021
3022     public BombedCharSequence(@NotNull CharSequence sequence) {
3023       delegate = sequence;
3024     }
3025
3026     @Override
3027     public int length() {
3028       check();
3029       return delegate.length();
3030     }
3031
3032     @Override
3033     public char charAt(int i) {
3034       check();
3035       return delegate.charAt(i);
3036     }
3037
3038     protected void check() {
3039       if (myDefused) {
3040         return;
3041       }
3042       if ((++i & 1023) == 0) {
3043         checkCanceled();
3044       }
3045     }
3046
3047     public final void defuse() {
3048        myDefused = true;
3049     }
3050
3051     @Override
3052     public @NotNull String toString() {
3053       check();
3054       return delegate.toString();
3055     }
3056
3057     protected abstract void checkCanceled();
3058
3059     @Override
3060     public @NotNull CharSequence subSequence(int i, int i1) {
3061       check();
3062       return delegate.subSequence(i, i1);
3063     }
3064   }
3065
3066   @Contract(pure = true)
3067   public static @NotNull String toHexString(byte @NotNull [] bytes) {
3068     @SuppressWarnings("SpellCheckingInspection") String digits = "0123456789abcdef";
3069     StringBuilder sb = new StringBuilder(2 * bytes.length);
3070     for (byte b : bytes) sb.append(digits.charAt((b >> 4) & 0xf)).append(digits.charAt(b & 0xf));
3071     return sb.toString();
3072   }
3073
3074   @Contract(pure = true)
3075   public static byte @NotNull [] parseHexString(@NotNull String str) {
3076     int len = str.length();
3077     if (len % 2 != 0) throw new IllegalArgumentException("Non-even-length: " + str);
3078     byte[] bytes = new byte[len / 2];
3079     for (int i = 0; i < len; i += 2) {
3080       bytes[i / 2] = (byte)((Character.digit(str.charAt(i), 16) << 4) + Character.digit(str.charAt(i + 1), 16));
3081     }
3082     return bytes;
3083   }
3084
3085   /** @deprecated use {@link #startsWithConcatenation(String, String...)} */
3086   @Deprecated
3087   @ApiStatus.ScheduledForRemoval(inVersion = "2021.1")
3088   public static boolean startsWithConcatenationOf(@NotNull String string, @NotNull String firstPrefix, @NotNull String secondPrefix) {
3089     return startsWithConcatenation(string, firstPrefix, secondPrefix);
3090   }
3091
3092   /**
3093    * @return {@code true} if the passed string is not {@code null} and not empty
3094    * and contains only latin upper- or lower-case characters and digits; {@code false} otherwise.
3095    */
3096   @Contract(pure = true)
3097   public static boolean isLatinAlphanumeric(@Nullable CharSequence str) {
3098     if (isEmpty(str)) {
3099       return false;
3100     }
3101     for (int i = 0; i < str.length(); i++) {
3102       char c = str.charAt(i);
3103       if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || Character.isDigit(c)) {
3104         continue;
3105       }
3106       return false;
3107     }
3108     return true;
3109   }
3110
3111   @Contract(value = "null -> null; !null->!null", pure = true)
3112   public static String internEmptyString(String s) {
3113     return s == null ? null : s.isEmpty() ? "" : s;
3114   }
3115
3116   /**
3117    * Finds the next position in the supplied CharSequence which is neither a space nor a tab.
3118    * @param text text
3119    * @param pos starting position
3120    * @return position of the first non-whitespace character after or equal to pos; or the length of the CharSequence
3121    * if no non-whitespace character found
3122    */
3123   public static int skipWhitespaceForward(@NotNull CharSequence text, int pos) {
3124     int length = text.length();
3125     while (pos < length && isWhitespaceOrTab(text.charAt(pos))) {
3126       pos++;
3127     }
3128     return pos;
3129   }
3130
3131   /**
3132    * Finds the previous position in the supplied CharSequence which is neither a space nor a tab.
3133    * @param text text
3134    * @param pos starting position
3135    * @return position of the character before or equal to pos which has no space or tab before;
3136    * or zero if no non-whitespace character found
3137    */
3138   public static int skipWhitespaceBackward(@NotNull CharSequence text, int pos) {
3139     while (pos > 0 && isWhitespaceOrTab(text.charAt(pos - 1))) {
3140       pos--;
3141     }
3142     return pos;
3143   }
3144
3145   private static boolean isWhitespaceOrTab(char c) {
3146     return c == ' ' || c == '\t';
3147   }
3148 }