cleanup (inspection "Java | Class structure | Utility class is not 'final'")
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / fileChooser / ex / FileTextFieldUtil.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.fileChooser.ex;
3
4 import com.intellij.openapi.application.ReadAction;
5 import com.intellij.openapi.diagnostic.Logger;
6 import com.intellij.openapi.fileChooser.ex.FileLookup.Finder;
7 import com.intellij.openapi.util.SystemInfo;
8 import com.intellij.openapi.util.text.StringUtil;
9 import com.intellij.psi.codeStyle.MinusculeMatcher;
10 import com.intellij.psi.codeStyle.NameUtil;
11 import com.intellij.util.ThrowableRunnable;
12 import org.jetbrains.annotations.ApiStatus;
13 import org.jetbrains.annotations.NotNull;
14 import org.jetbrains.annotations.Nullable;
15
16 import javax.swing.*;
17 import javax.swing.text.BadLocationException;
18 import javax.swing.text.Document;
19 import java.io.File;
20 import java.util.ArrayList;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.function.Consumer;
24
25 @ApiStatus.Experimental
26 public final class FileTextFieldUtil {
27
28   private static final Logger LOG = Logger.getInstance(FileTextFieldUtil.class);
29
30   public static void processCompletion(final FileTextFieldImpl.CompletionResult result,
31                                        @NotNull Finder finder,
32                                        @NotNull FileLookup.LookupFilter filter,
33                                        @NotNull String fileSpitRegExp,
34                                        @NotNull Map<String, String> macroMap) {
35     result.myToComplete = new ArrayList<>();
36     result.mySiblings = new ArrayList<>();
37     result.myKidsAfterSeparator = new ArrayList<>();
38     final String typed = result.myCompletionBase;
39
40     if (typed == null) return;
41
42     FileTextFieldImpl.addMacroPaths(result, typed, finder, macroMap);
43
44     final String typedText = finder.normalize(typed);
45
46
47     result.current = getClosestParent(typed, finder, fileSpitRegExp);
48     result.myClosestParent = result.current;
49
50     if (result.current != null) {
51       result.currentParentMatch = SystemInfo.isFileSystemCaseSensitive
52                                   ? typedText.equals(result.current.getAbsolutePath())
53                                   : typedText.equalsIgnoreCase(result.current.getAbsolutePath());
54
55       result.closedPath = typed.endsWith(finder.getSeparator()) && typedText.length() > finder.getSeparator().length();
56       final String currentParentText = result.current.getAbsolutePath();
57
58       if (!StringUtil.toUpperCase(typedText).startsWith(StringUtil.toUpperCase(currentParentText))) return;
59
60       String prefix = typedText.substring(currentParentText.length());
61       if (prefix.startsWith(finder.getSeparator())) {
62         prefix = prefix.substring(finder.getSeparator().length());
63       }
64       else if (typed.endsWith(finder.getSeparator())) {
65         prefix = "";
66       }
67
68       result.effectivePrefix = prefix;
69
70       result.currentGrandparent = result.current.getParent();
71       if (result.currentGrandparent != null && result.currentParentMatch && !result.closedPath) {
72         final String currentGrandparentText = result.currentGrandparent.getAbsolutePath();
73         if (StringUtil.startsWithConcatenation(typedText, currentGrandparentText, finder.getSeparator())) {
74           result.grandparentPrefix = currentParentText.substring(currentGrandparentText.length() + finder.getSeparator().length());
75         }
76       }
77     }
78     else {
79       result.effectivePrefix = typedText;
80     }
81
82
83     ReadAction.run(new ThrowableRunnable<RuntimeException>() {
84       @Override
85       public void run() {
86         if (result.current != null) {
87           result.myToComplete.addAll(getMatchingChildren(result.effectivePrefix, result.current));
88
89           if (result.currentParentMatch && !result.closedPath && !typed.isEmpty()) {
90             result.myKidsAfterSeparator.addAll(result.myToComplete);
91           }
92
93           if (result.grandparentPrefix != null) {
94             final List<FileLookup.LookupFile> siblings = getMatchingChildren(result.grandparentPrefix, result.currentGrandparent);
95             result.myToComplete.addAll(0, siblings);
96             result.mySiblings.addAll(siblings);
97           }
98         }
99
100         int currentDiff = Integer.MIN_VALUE;
101         FileLookup.LookupFile toPreselect = result.myPreselected;
102
103         if (toPreselect == null || !result.myToComplete.contains(toPreselect)) {
104           boolean toPreselectFixed = false;
105           if (result.effectivePrefix.length() > 0) {
106             for (FileLookup.LookupFile each : result.myToComplete) {
107               String eachName = StringUtil.toUpperCase(each.getName());
108               if (!eachName.startsWith(result.effectivePrefix)) continue;
109               int diff = result.effectivePrefix.compareTo(eachName);
110               currentDiff = Math.max(diff, currentDiff);
111               if (currentDiff == diff) {
112                 toPreselect = each;
113                 toPreselectFixed = true;
114                 break;
115               }
116             }
117
118             if (!toPreselectFixed) {
119               toPreselect = null;
120             }
121           }
122           else {
123             toPreselect = null;
124           }
125
126           if (toPreselect == null) {
127             if (result.myToComplete.size() == 1) {
128               toPreselect = result.myToComplete.get(0);
129             }
130             else if (result.effectivePrefix.length() == 0) {
131               if (result.mySiblings.size() > 0) {
132                 toPreselect = result.mySiblings.get(0);
133               }
134             }
135
136             if (toPreselect == null && !result.myToComplete.contains(toPreselect) && result.myToComplete.size() > 0) {
137               toPreselect = result.myToComplete.get(0);
138             }
139           }
140         }
141
142         if (result.currentParentMatch && result.mySiblings.size() > 0) {
143           toPreselect = null;
144         }
145
146         result.myPreselected = toPreselect;
147       }
148
149       private List<FileLookup.LookupFile> getMatchingChildren(String prefix, FileLookup.LookupFile parent) {
150         final MinusculeMatcher matcher = createMatcher(prefix);
151         return parent.getChildren(new FileLookup.LookupFilter() {
152           @Override
153           public boolean isAccepted(final FileLookup.LookupFile file) {
154             return !file.equals(result.current) && filter.isAccepted(file) && matcher.matches(file.getName());
155           }
156         });
157       }
158     });
159   }
160
161   static MinusculeMatcher createMatcher(String prefix) {
162     return NameUtil.buildMatcher("*" + prefix, NameUtil.MatchingCaseSensitivity.NONE);
163   }
164
165   @Nullable
166   private static FileLookup.LookupFile getClosestParent(final String typed, Finder finder, String fileSpitRegExp) {
167     if (typed == null) return null;
168     FileLookup.LookupFile lastFound = finder.find(typed);
169     if (lastFound == null) return null;
170     if (typed.isEmpty()) return lastFound;
171     if (lastFound.exists()) {
172       if (typed.charAt(typed.length() - 1) != File.separatorChar) return lastFound.getParent();
173       return lastFound;
174     }
175
176     final String[] splits = finder.normalize(typed).split(fileSpitRegExp);
177     StringBuilder fullPath = new StringBuilder();
178     for (int i = 0; i < splits.length; i++) {
179       String each = splits[i];
180       fullPath.append(each);
181       if (i < splits.length - 1) {
182         fullPath.append(finder.getSeparator());
183       }
184       final FileLookup.LookupFile file = finder.find(fullPath.toString());
185       if (file == null || !file.exists()) return lastFound;
186       lastFound = file;
187     }
188
189     return lastFound;
190   }
191
192   @NotNull
193   public static String getLookupString(@NotNull FileLookup.LookupFile file, @NotNull Finder finder, @Nullable FileTextFieldImpl.CompletionResult result) {
194     if (file.getMacro() != null) {
195       return file.getMacro();
196     }
197     String prefix = result != null && result.myKidsAfterSeparator.contains(file) ? finder.getSeparator() : "";
198     return prefix + file.getName();
199   }
200
201   public interface DocumentOwner {
202     String getText(int offset, int length) throws BadLocationException;
203
204     void remove(int offs, int len) throws BadLocationException;
205
206     void insertString(int offset, String str) throws BadLocationException;
207
208     int getLength();
209
210     void removeSelection();
211
212     void setCaretPosition(int position);
213
214     int getCaretPosition();
215
216     void setText(@NotNull String text);
217
218     void setTextToFile(@NotNull FileLookup.LookupFile file);
219   }
220
221   public static class TextFieldDocumentOwner implements DocumentOwner {
222
223     private final JTextField myField;
224     private final Document myDocument;
225     private final Consumer<FileLookup.LookupFile> mySetText;
226
227     public TextFieldDocumentOwner(@NotNull JTextField field, @NotNull Consumer<FileLookup.LookupFile> setText) {
228       myField = field;
229       myDocument = field.getDocument();
230       mySetText = setText;
231     }
232
233     @Override
234     public String getText(int offset, int length) throws BadLocationException {
235       return myDocument.getText(offset, length);
236     }
237
238     @Override
239     public void remove(int offset, int length) throws BadLocationException {
240       myDocument.remove(offset, length);
241     }
242
243     @Override
244     public void insertString(int offset, String str) throws BadLocationException {
245       myDocument.insertString(offset, str, myDocument.getDefaultRootElement().getAttributes());
246     }
247
248     @Override
249     public int getLength() {
250       return myDocument.getLength();
251     }
252
253     @Override
254     public void removeSelection() {
255       myField.setSelectionStart(0);
256       myField.setSelectionEnd(0);
257     }
258
259     @Override
260     public void setCaretPosition(int position) {
261       myField.setCaretPosition(position);
262     }
263
264     @Override
265     public int getCaretPosition() {
266       return myField.getCaretPosition();
267     }
268
269     @Override
270     public void setText(@NotNull String text) {
271       myField.setText(text);
272     }
273
274     @Override
275     public void setTextToFile(FileLookup.@NotNull LookupFile file) {
276       mySetText.accept(file);
277     }
278   }
279
280   /**
281    * Replace the path component under the caret with the file selected from the completion list.
282    *
283    * @param file     the selected file.
284    * @param caretPos
285    * @param start    the start offset of the path component under the caret.
286    * @param end      the end offset of the path component under the caret.
287    * @throws BadLocationException
288    */
289   private static void replacePathComponent(@NotNull FileLookup.LookupFile file,
290                                            @NotNull DocumentOwner doc,
291                                            @NotNull Finder finder,
292                                            int caretPos,
293                                            int start,
294                                            int end) throws BadLocationException {
295
296     doc.removeSelection();
297
298     final String name = file.getName();
299     boolean toRemoveExistingName;
300
301     if (caretPos >= start) {
302       String prefix = doc.getText(start, caretPos - start);
303       if (prefix.length() == 0) {
304         prefix = doc.getText(start, end - start);
305       }
306       if (SystemInfo.isFileSystemCaseSensitive) {
307         toRemoveExistingName = name.startsWith(prefix) && prefix.length() > 0;
308       }
309       else {
310         toRemoveExistingName = StringUtil.toUpperCase(name).startsWith(StringUtil.toUpperCase(prefix)) && prefix.length() > 0;
311       }
312     }
313     else {
314       toRemoveExistingName = true;
315     }
316
317     int newPos;
318     if (toRemoveExistingName) {
319       doc.remove(start, end - start);
320       doc.insertString(start, name);
321       newPos = start + name.length();
322     }
323     else {
324       doc.insertString(caretPos, name);
325       newPos = caretPos + name.length();
326     }
327
328     if (file.isDirectory()) {
329       if (!finder.getSeparator().equals(doc.getText(newPos, 1))) {
330         doc.insertString(newPos, finder.getSeparator());
331         newPos++;
332       }
333     }
334
335     if (newPos < doc.getLength()) {
336       if (finder.getSeparator().equals(doc.getText(newPos, 1))) {
337         newPos++;
338       }
339     }
340     doc.setCaretPosition(newPos);
341   }
342
343   public static void processChosenFromCompletion(FileLookup.LookupFile file,
344                                                  DocumentOwner doc,
345                                                  Finder finder,
346                                                  boolean nameOnly) {
347     if (file == null) return;
348
349     if (nameOnly) {
350       try {
351         int caretPos = doc.getCaretPosition();
352         if (finder.getSeparator().equals(doc.getText(caretPos, 1))) {
353           for (; caretPos < doc.getLength(); caretPos++) {
354             final String eachChar = doc.getText(caretPos, 1);
355             if (!finder.getSeparator().equals(eachChar)) break;
356           }
357         }
358
359         int start = caretPos > 0 ? caretPos - 1 : caretPos;
360         while (start >= 0) {
361           final String each = doc.getText(start, 1);
362           if (finder.getSeparator().equals(each)) {
363             start++;
364             break;
365           }
366           start--;
367         }
368
369         int end = Math.max(start, caretPos);
370         while (end <= doc.getLength()) {
371           final String each = doc.getText(end, 1);
372           if (finder.getSeparator().equals(each)) {
373             break;
374           }
375           end++;
376         }
377
378         if (end > doc.getLength()) {
379           end = doc.getLength();
380         }
381
382         if (start > end || start < 0 || end > doc.getLength()) {
383           doc.setText(file.getAbsolutePath());
384         }
385         else {
386           replacePathComponent(file, doc, finder, caretPos, start, end);
387         }
388       }
389       catch (BadLocationException e) {
390         LOG.error(e);
391       }
392     }
393     else {
394       doc.setTextToFile(file);
395     }
396   }
397
398   public static void setTextToFile(@NotNull FileLookup.LookupFile file, Finder finder, @NotNull DocumentOwner doc) {
399     String text = file.getAbsolutePath();
400     if (file.isDirectory() && !text.endsWith(finder.getSeparator())) {
401       text += finder.getSeparator();
402     }
403     doc.setText(text);
404   }
405 }