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