9026286d2778f84a1c6119174190f0b7fa8682ef
[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     long stamp = editor.getDocument().getModificationStamp();
262     for (ExtendWordSelectionHandler selectioner : availableSelectioners) {
263       List<TextRange> ranges = askSelectioner(element, text, cursorOffset, editor, stamp, selectioner);
264       if (ranges == null) continue;
265
266       for (TextRange range : ranges) {
267         if (range == null || range.getLength() < minimalTextRangeLength) continue;
268
269         stop |= processor.process(range);
270       }
271     }
272
273     return stop;
274   }
275
276   @Nullable
277   private static List<TextRange> askSelectioner(@NotNull PsiElement element,
278                                                 CharSequence text,
279                                                 int cursorOffset,
280                                                 Editor editor,
281                                                 long stamp,
282                                                 ExtendWordSelectionHandler selectioner) {
283     try {
284       List<TextRange> ranges = selectioner.select(element, text, cursorOffset, editor);
285       if (stamp != editor.getDocument().getModificationStamp()) {
286         throw new AssertionError("Selectioner " + selectioner + " has changed the document");
287       }
288       return ranges;
289     }
290     catch (IndexNotReadyException e) {
291       return null;
292     }
293   }
294
295   public static void addWordHonoringEscapeSequences(CharSequence editorText,
296                                                     TextRange literalTextRange,
297                                                     int cursorOffset,
298                                                     Lexer lexer,
299                                                     List<TextRange> result) {
300     lexer.start(editorText, literalTextRange.getStartOffset(), literalTextRange.getEndOffset());
301
302     while (lexer.getTokenType() != null) {
303       if (lexer.getTokenStart() <= cursorOffset && cursorOffset < lexer.getTokenEnd()) {
304         if (StringEscapesTokenTypes.STRING_LITERAL_ESCAPES.contains(lexer.getTokenType())) {
305           result.add(new TextRange(lexer.getTokenStart(), lexer.getTokenEnd()));
306         }
307         else {
308           TextRange word = getWordSelectionRange(editorText, cursorOffset, JAVA_IDENTIFIER_PART_CONDITION);
309           if (word != null) {
310             result.add(new TextRange(Math.max(word.getStartOffset(), lexer.getTokenStart()),
311                                      Math.min(word.getEndOffset(), lexer.getTokenEnd())));
312           }
313         }
314         break;
315       }
316       lexer.advance();
317     }
318   }
319   
320   public interface CharCondition { boolean value(char ch); }
321 }