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