SelectWordUtil cleanup (IDEA-CR-15854)
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / editorActions / SelectWordUtil.java
1 /*
2  * Copyright 2000-2014 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
17 package com.intellij.codeInsight.editorActions;
18
19 import com.intellij.lang.ASTNode;
20 import com.intellij.lang.FileASTNode;
21 import com.intellij.lexer.Lexer;
22 import com.intellij.openapi.editor.Editor;
23 import com.intellij.openapi.editor.actions.EditorActionUtil;
24 import com.intellij.openapi.extensions.Extensions;
25 import com.intellij.openapi.project.DumbService;
26 import com.intellij.openapi.project.IndexNotReadyException;
27 import com.intellij.openapi.util.TextRange;
28 import com.intellij.psi.FileViewProvider;
29 import com.intellij.psi.PsiElement;
30 import com.intellij.psi.PsiFile;
31 import com.intellij.psi.StringEscapesTokenTypes;
32 import com.intellij.util.ArrayUtil;
33 import com.intellij.util.Processor;
34 import com.intellij.util.containers.ContainerUtil;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37
38 import java.util.List;
39
40 /**
41  * @author Mike
42  */
43 public class SelectWordUtil {
44   private static ExtendWordSelectionHandler[] SELECTIONERS = new ExtendWordSelectionHandler[]{
45   };
46
47   private static boolean ourExtensionsLoaded = false;
48
49   private SelectWordUtil() {
50   }
51
52   /**
53    * @see ExtendWordSelectionHandler#EP_NAME
54    */
55   @Deprecated
56   public static void registerSelectioner(ExtendWordSelectionHandler selectioner) {
57     SELECTIONERS = ArrayUtil.append(SELECTIONERS, selectioner);
58   }
59
60   static ExtendWordSelectionHandler[] getExtendWordSelectionHandlers() {
61     if (!ourExtensionsLoaded) {
62       ourExtensionsLoaded = true;
63       for (ExtendWordSelectionHandler handler : Extensions.getExtensions(ExtendWordSelectionHandler.EP_NAME)) {
64         registerSelectioner(handler);        
65       }
66     }
67     return SELECTIONERS;
68   }
69
70   public static final CharCondition JAVA_IDENTIFIER_PART_CONDITION = new CharCondition() {
71     @Override
72     public boolean value(char ch) {
73       return Character.isJavaIdentifierPart(ch);
74     }
75   };
76
77   public static void addWordSelection(boolean camel, CharSequence editorText, int cursorOffset, @NotNull List<TextRange> ranges) {
78     addWordSelection(camel, editorText, cursorOffset, ranges, JAVA_IDENTIFIER_PART_CONDITION);
79   }
80
81   public static void addWordOrLexemeSelection(boolean camel, @NotNull Editor editor, int cursorOffset, @NotNull List<TextRange> ranges) {
82     addWordOrLexemeSelection(camel, editor, cursorOffset, ranges, JAVA_IDENTIFIER_PART_CONDITION);
83   }
84
85   public static void addWordSelection(boolean camel,
86                                       CharSequence editorText,
87                                       int cursorOffset,
88                                       @NotNull List<TextRange> ranges,
89                                       CharCondition isWordPartCondition) {
90     TextRange camelRange = camel ? getCamelSelectionRange(editorText, cursorOffset, isWordPartCondition) : null;
91     if (camelRange != null) {
92       ranges.add(camelRange);
93     }
94
95     TextRange range = getWordSelectionRange(editorText, cursorOffset, isWordPartCondition);
96     if (range != null && !range.equals(camelRange)) {
97       ranges.add(range);
98     }
99   }
100
101   public static void addWordOrLexemeSelection(boolean camel,
102                                               @NotNull Editor editor,
103                                               int cursorOffset,
104                                               @NotNull List<TextRange> ranges,
105                                               CharCondition isWordPartCondition) {
106     TextRange camelRange = camel ? getCamelSelectionRange(editor.getDocument().getImmutableCharSequence(),
107                                                           cursorOffset, isWordPartCondition) : null;
108     if (camelRange != null) {
109       ranges.add(camelRange);
110     }
111
112     TextRange range = getWordOrLexemeSelectionRange(editor, cursorOffset, isWordPartCondition);
113     if (range != null && !range.equals(camelRange)) {
114       ranges.add(range);
115     }
116   }
117
118   @Nullable
119   private static TextRange getCamelSelectionRange(CharSequence editorText, int cursorOffset, CharCondition isWordPartCondition) {
120     if (cursorOffset < 0 || cursorOffset >= editorText.length()) {
121       return null;
122     }
123     if (cursorOffset > 0 && !isWordPartCondition.value(editorText.charAt(cursorOffset)) &&
124         isWordPartCondition.value(editorText.charAt(cursorOffset - 1))) {
125       cursorOffset--;
126     }
127
128     if (isWordPartCondition.value(editorText.charAt(cursorOffset))) {
129       int start = cursorOffset;
130       int end = cursorOffset + 1;
131       final int textLen = editorText.length();
132
133       while (start > 0 && isWordPartCondition.value(editorText.charAt(start - 1)) && !EditorActionUtil.isHumpBound(editorText, start, true)) {
134         start--;
135       }
136
137       while (end < textLen && isWordPartCondition.value(editorText.charAt(end)) && !EditorActionUtil.isHumpBound(editorText, end, false)) {
138         end++;
139       }
140
141       if (start + 1 < end) {
142         return new TextRange(start, end);
143       }
144     }
145
146     return null;
147   }
148
149   @Nullable
150   public static TextRange getWordOrLexemeSelectionRange(@NotNull Editor editor, int cursorOffset,
151                                                          @NotNull CharCondition isWordPartCondition) {
152     return getWordOrLexemeSelectionRange(editor, editor.getDocument().getImmutableCharSequence(), cursorOffset, isWordPartCondition);
153   }
154
155   @Nullable
156   public static TextRange getWordSelectionRange(@NotNull CharSequence editorText, int cursorOffset,
157                                                 @NotNull CharCondition isWordPartCondition) {
158     return getWordOrLexemeSelectionRange(null, editorText, cursorOffset, isWordPartCondition);
159   }
160
161   @Nullable
162   private static TextRange getWordOrLexemeSelectionRange(@Nullable Editor editor, @NotNull CharSequence editorText, int cursorOffset,
163                                                          @NotNull CharCondition isWordPartCondition) {
164     int length = editorText.length();
165     if (length == 0) return null;
166     if (cursorOffset == length ||
167         cursorOffset > 0 && !isWordPartCondition.value(editorText.charAt(cursorOffset)) &&
168         isWordPartCondition.value(editorText.charAt(cursorOffset - 1))) {
169       cursorOffset--;
170     }
171
172     if (isWordPartCondition.value(editorText.charAt(cursorOffset))) {
173       int start = cursorOffset;
174       int end = cursorOffset;
175
176       while (start > 0 && isWordPartCondition.value(editorText.charAt(start - 1)) &&
177              (editor == null || !EditorActionUtil.isLexemeBoundary(editor, start))) {
178         start--;
179       }
180
181       while (end < length && isWordPartCondition.value(editorText.charAt(end)) &&
182              (end == start || editor == null || !EditorActionUtil.isLexemeBoundary(editor, end))) {
183         end++;
184       }
185
186       return new TextRange(start, end);
187     }
188
189     return null;
190   }
191
192   public static void processRanges(@Nullable PsiElement element,
193                                    CharSequence text,
194                                    int cursorOffset,
195                                    Editor editor,
196                                    Processor<TextRange> consumer) {
197     if (element == null) return;
198
199     PsiFile file = element.getContainingFile();
200
201     FileViewProvider viewProvider = file.getViewProvider();
202
203     processInFile(element, consumer, text, cursorOffset, editor);
204
205     for (PsiFile psiFile : viewProvider.getAllFiles()) {
206       if (psiFile == file) continue;
207
208       FileASTNode fileNode = psiFile.getNode();
209       if (fileNode == null) continue;
210
211       ASTNode nodeAt = fileNode.findLeafElementAt(element.getTextOffset());
212       if (nodeAt == null) continue;
213
214       PsiElement elementAt = nodeAt.getPsi();
215
216       while (!(elementAt instanceof PsiFile) && elementAt != null) {
217         if (elementAt.getTextRange().contains(element.getTextRange())) break;
218
219         elementAt = elementAt.getParent();
220       }
221
222       if (elementAt == null) continue;
223
224       processInFile(elementAt, consumer, text, cursorOffset, editor);
225     }
226   }
227
228   private static void processInFile(@NotNull final PsiElement element,
229                                     final Processor<TextRange> consumer,
230                                     final CharSequence text,
231                                     final int cursorOffset,
232                                     final Editor editor) {
233     DumbService.getInstance(element.getProject()).withAlternativeResolveEnabled(() -> {
234       PsiElement e = element;
235       while (e != null && !(e instanceof PsiFile)) {
236         if (processElement(e, consumer, text, cursorOffset, editor)) return;
237         e = e.getParent();
238       }
239     });
240   }
241
242   private static boolean processElement(@NotNull PsiElement element,
243                                      Processor<TextRange> processor,
244                                      CharSequence text,
245                                      int cursorOffset,
246                                      Editor editor) {
247     boolean stop = false;
248
249     ExtendWordSelectionHandler[] extendWordSelectionHandlers = getExtendWordSelectionHandlers();
250     int minimalTextRangeLength = 0;
251     List<ExtendWordSelectionHandler> availableSelectioners = ContainerUtil.newLinkedList();
252     for (ExtendWordSelectionHandler selectioner : extendWordSelectionHandlers) {
253       if (selectioner.canSelect(element)) {
254         int selectionerMinimalTextRange = selectioner instanceof ExtendWordSelectionHandlerBase
255           ? ((ExtendWordSelectionHandlerBase)selectioner).getMinimalTextRangeLength(element, text, cursorOffset)
256           : 0;
257         minimalTextRangeLength = Math.max(minimalTextRangeLength, selectionerMinimalTextRange);
258         availableSelectioners.add(selectioner);
259       }
260     }
261     for (ExtendWordSelectionHandler selectioner : availableSelectioners) {
262       List<TextRange> ranges = askSelectioner(element, text, cursorOffset, editor, selectioner);
263       if (ranges == null) continue;
264
265       for (TextRange range : ranges) {
266         if (range == null || range.getLength() < minimalTextRangeLength) continue;
267
268         stop |= processor.process(range);
269       }
270     }
271
272     return stop;
273   }
274
275   @Nullable
276   private static List<TextRange> askSelectioner(@NotNull PsiElement element,
277                                                 CharSequence text,
278                                                 int cursorOffset,
279                                                 Editor editor,
280                                                 ExtendWordSelectionHandler selectioner) {
281     try {
282       long stamp = editor.getDocument().getModificationStamp();
283       List<TextRange> ranges = selectioner.select(element, text, cursorOffset, editor);
284       if (stamp != editor.getDocument().getModificationStamp()) {
285         throw new AssertionError("Selectioner " + selectioner + " has changed the document");
286       }
287       return ranges;
288     }
289     catch (IndexNotReadyException e) {
290       return null;
291     }
292   }
293
294   public static void addWordHonoringEscapeSequences(CharSequence editorText,
295                                                     TextRange literalTextRange,
296                                                     int cursorOffset,
297                                                     Lexer lexer,
298                                                     List<TextRange> result) {
299     lexer.start(editorText, literalTextRange.getStartOffset(), literalTextRange.getEndOffset());
300
301     while (lexer.getTokenType() != null) {
302       if (lexer.getTokenStart() <= cursorOffset && cursorOffset < lexer.getTokenEnd()) {
303         if (StringEscapesTokenTypes.STRING_LITERAL_ESCAPES.contains(lexer.getTokenType())) {
304           result.add(new TextRange(lexer.getTokenStart(), lexer.getTokenEnd()));
305         }
306         else {
307           TextRange word = getWordSelectionRange(editorText, cursorOffset, JAVA_IDENTIFIER_PART_CONDITION);
308           if (word != null) {
309             result.add(new TextRange(Math.max(word.getStartOffset(), lexer.getTokenStart()),
310                                      Math.min(word.getEndOffset(), lexer.getTokenEnd())));
311           }
312         }
313         break;
314       }
315       lexer.advance();
316     }
317   }
318   
319   public interface CharCondition { boolean value(char ch); }
320 }