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