replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-api / src / com / intellij / openapi / editor / EditorModificationUtil.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.editor;
17
18 import com.intellij.codeInsight.hint.HintManager;
19 import com.intellij.codeStyle.CodeStyleFacade;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.editor.textarea.TextComponentEditor;
22 import com.intellij.openapi.fileEditor.FileDocumentManager;
23 import com.intellij.openapi.ide.CopyPasteManager;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.util.Key;
26 import com.intellij.openapi.util.text.LineTokenizer;
27 import com.intellij.psi.PsiDocumentManager;
28 import com.intellij.util.Producer;
29 import org.jetbrains.annotations.NotNull;
30 import org.jetbrains.annotations.Nullable;
31
32 import java.awt.datatransfer.DataFlavor;
33 import java.awt.datatransfer.Transferable;
34 import java.awt.datatransfer.UnsupportedFlavorException;
35 import java.io.IOException;
36 import java.util.Iterator;
37 import java.util.LinkedList;
38 import java.util.List;
39
40 public class EditorModificationUtil {
41   public static final Key<String> READ_ONLY_VIEW_MESSAGE_KEY = Key.create("READ_ONLY_VIEW_MESSAGE_KEY");
42
43   private EditorModificationUtil() { }
44
45   public static void deleteSelectedText(Editor editor) {
46     SelectionModel selectionModel = editor.getSelectionModel();
47     if(!selectionModel.hasSelection()) return;
48
49     int selectionStart = selectionModel.getSelectionStart();
50     int selectionEnd = selectionModel.getSelectionEnd();
51
52     VisualPosition selectionStartPosition = selectionModel.getSelectionStartPosition();
53     if (editor.isColumnMode() && editor.getCaretModel().supportsMultipleCarets() && selectionStartPosition != null) {
54       editor.getCaretModel().moveToVisualPosition(selectionStartPosition);
55     }
56     else {
57       editor.getCaretModel().moveToOffset(selectionStart);
58     }
59     selectionModel.removeSelection();
60     editor.getDocument().deleteString(selectionStart, selectionEnd);
61     scrollToCaret(editor);
62   }
63
64   public static void deleteSelectedTextForAllCarets(@NotNull final Editor editor) {
65     editor.getCaretModel().runForEachCaret(new CaretAction() {
66       @Override
67       public void perform(Caret caret) {
68         deleteSelectedText(editor);
69       }
70     });
71   }
72
73   public static void zeroWidthBlockSelectionAtCaretColumn(final Editor editor, final int startLine, final int endLine) {
74     int caretColumn = editor.getCaretModel().getLogicalPosition().column;
75     editor.getSelectionModel().setBlockSelection(new LogicalPosition(startLine, caretColumn), new LogicalPosition(endLine, caretColumn));
76   }
77
78   public static void insertStringAtCaret(Editor editor, @NotNull String s) {
79     insertStringAtCaret(editor, s, false, true);
80   }
81
82   public static int insertStringAtCaret(Editor editor, @NotNull String s, boolean toProcessOverwriteMode) {
83     return insertStringAtCaret(editor, s, toProcessOverwriteMode, s.length());
84   }
85
86   public static int insertStringAtCaret(Editor editor, @NotNull String s, boolean toProcessOverwriteMode, boolean toMoveCaret) {
87     return insertStringAtCaret(editor, s, toProcessOverwriteMode, toMoveCaret, s.length());
88   }
89
90   public static int insertStringAtCaret(Editor editor, @NotNull String s, boolean toProcessOverwriteMode, int caretShift) {
91     return insertStringAtCaret(editor, s, toProcessOverwriteMode, true, caretShift);
92   }
93
94   public static int insertStringAtCaret(Editor editor, @NotNull String s, boolean toProcessOverwriteMode, boolean toMoveCaret, int caretShift) {
95     int result = insertStringAtCaretNoScrolling(editor, s, toProcessOverwriteMode, toMoveCaret, caretShift);
96     if (toMoveCaret) {
97       scrollToCaret(editor);
98     }
99     return result;
100   }
101
102   private static int insertStringAtCaretNoScrolling(Editor editor, @NotNull String s, boolean toProcessOverwriteMode, boolean toMoveCaret, int caretShift) {
103     // There is a possible case that particular soft wraps become hard wraps if the caret is located at soft wrap-introduced virtual
104     // space, hence, we need to give editor a chance to react accordingly.
105     editor.getSoftWrapModel().beforeDocumentChangeAtCaret();
106     int oldOffset = editor.getCaretModel().getOffset();
107
108     String filler = calcStringToFillVirtualSpace(editor);
109     if (filler.length() > 0) {
110       s = filler + s;
111     }
112
113     Document document = editor.getDocument();
114     SelectionModel selectionModel = editor.getSelectionModel();
115     if (editor.isInsertMode() || !toProcessOverwriteMode) {
116       if (selectionModel.hasSelection()) {
117         oldOffset = selectionModel.getSelectionStart();
118         document.replaceString(selectionModel.getSelectionStart(), selectionModel.getSelectionEnd(), s);
119       } else {
120         document.insertString(oldOffset, s);
121       }
122     } else {
123       deleteSelectedText(editor);
124       int lineNumber = editor.getCaretModel().getLogicalPosition().line;
125       if (lineNumber >= document.getLineCount()){
126         return insertStringAtCaretNoScrolling(editor, s, false, toMoveCaret, s.length());
127       }
128
129       int endOffset = document.getLineEndOffset(lineNumber);
130       document.replaceString(oldOffset, Math.min(endOffset, oldOffset + s.length()), s);
131     }
132
133     int offset = oldOffset + filler.length() + caretShift;
134     if (toMoveCaret){
135       editor.getCaretModel().moveToOffset(offset, true);
136       selectionModel.removeSelection();
137     }
138     else if (editor.getCaretModel().getOffset() != oldOffset) { // handling the case when caret model tracks document changes
139       editor.getCaretModel().moveToOffset(oldOffset);
140     }
141
142     return offset;
143   }
144
145   public static void pasteTransferableAsBlock(Editor editor, @Nullable Producer<Transferable> producer) {
146     Transferable content = getTransferable(producer);
147     if (content == null) return;
148     String text = getStringContent(content);
149     if (text == null) return;
150
151     int caretLine = editor.getCaretModel().getLogicalPosition().line;
152
153     LogicalPosition caretToRestore = editor.getCaretModel().getLogicalPosition();
154
155     String[] lines = LineTokenizer.tokenize(text.toCharArray(), false);
156     int longestLineLength = 0;
157     for (int i = 0; i < lines.length; i++) {
158       String line = lines[i];
159       longestLineLength = Math.max(longestLineLength, line.length());
160       editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(caretLine + i, caretToRestore.column));
161       insertStringAtCaret(editor, line, false, true);
162     }
163     caretToRestore = new LogicalPosition(caretLine, caretToRestore.column + longestLineLength);
164
165     editor.getCaretModel().moveToLogicalPosition(caretToRestore);
166     zeroWidthBlockSelectionAtCaretColumn(editor, caretLine, caretLine);
167   }
168
169   @Nullable
170   public static Transferable getContentsToPasteToEditor(@Nullable Producer<Transferable> producer) {
171     if (producer == null) {
172       CopyPasteManager manager = CopyPasteManager.getInstance();
173       return manager.areDataFlavorsAvailable(DataFlavor.stringFlavor) ? manager.getContents() : null;
174     }
175     else {
176       return producer.produce();
177     }
178   } 
179
180   @Nullable
181   public static String getStringContent(@NotNull Transferable content) {
182     RawText raw = RawText.fromTransferable(content);
183     if (raw != null) return raw.rawText;
184
185     try {
186       return (String)content.getTransferData(DataFlavor.stringFlavor);
187     }
188     catch (UnsupportedFlavorException | IOException ignore) { }
189
190     return null;
191   }
192
193   private static Transferable getTransferable(Producer<Transferable> producer) {
194     Transferable content = null;
195     if (producer != null) {
196       content = producer.produce();
197     }
198     else {
199       CopyPasteManager manager = CopyPasteManager.getInstance();
200       if (manager.areDataFlavorsAvailable(DataFlavor.stringFlavor)) {
201         content = manager.getContents();
202       }
203     }
204     return content;
205   }
206   /**
207    * Calculates difference in columns between current editor caret position and end of the logical line fragment displayed
208    * on a current visual line.
209    *
210    * @param editor    target editor
211    * @return          difference in columns between current editor caret position and end of the logical line fragment displayed
212    *                  on a current visual line
213    */
214   public static int calcAfterLineEnd(Editor editor) {
215     Document document = editor.getDocument();
216     CaretModel caretModel = editor.getCaretModel();
217     LogicalPosition logicalPosition = caretModel.getLogicalPosition();
218     int lineNumber = logicalPosition.line;
219     int columnNumber = logicalPosition.column;
220     if (lineNumber >= document.getLineCount()) {
221       return columnNumber;
222     }
223
224     int caretOffset = caretModel.getOffset();
225     int anchorLineEndOffset = document.getLineEndOffset(lineNumber);
226     List<? extends SoftWrap> softWraps = editor.getSoftWrapModel().getSoftWrapsForLine(logicalPosition.line);
227     for (SoftWrap softWrap : softWraps) {
228       if (!editor.getSoftWrapModel().isVisible(softWrap)) {
229         continue;
230       }
231
232       int softWrapOffset = softWrap.getStart();
233       if (softWrapOffset == caretOffset) {
234         // There are two possible situations:
235         //     *) caret is located on a visual line before soft wrap-introduced line feed;
236         //     *) caret is located on a visual line after soft wrap-introduced line feed;
237         VisualPosition position = editor.offsetToVisualPosition(caretOffset - 1);
238         VisualPosition visualCaret = caretModel.getVisualPosition();
239         if (position.line == visualCaret.line) {
240           return visualCaret.column - position.column - 1;
241         }
242       }
243       if (softWrapOffset > caretOffset) {
244         anchorLineEndOffset = softWrapOffset;
245         break;
246       }
247
248       // Same offset corresponds to all soft wrap-introduced symbols, however, current method should behave differently in
249       // situations when the caret is located just before the soft wrap and at the next visual line.
250       if (softWrapOffset == caretOffset) {
251         boolean visuallyBeforeSoftWrap = caretModel.getVisualPosition().line < editor.offsetToVisualPosition(caretOffset).line;
252         if (visuallyBeforeSoftWrap) {
253           anchorLineEndOffset = softWrapOffset;
254           break;
255         }
256       }
257     }
258
259     int lineEndColumnNumber = editor.offsetToLogicalPosition(anchorLineEndOffset).column;
260     return columnNumber - lineEndColumnNumber;
261   }
262
263   public static String calcStringToFillVirtualSpace(Editor editor) {
264     int afterLineEnd = calcAfterLineEnd(editor);
265     if (afterLineEnd > 0) {
266       return calcStringToFillVirtualSpace(editor, afterLineEnd);
267     }
268
269     return "";
270   }
271
272   public static String calcStringToFillVirtualSpace(Editor editor, int afterLineEnd) {
273     final Project project = editor.getProject();
274     StringBuilder buf = new StringBuilder();
275     final Document doc = editor.getDocument();
276     final int caretOffset = editor.getCaretModel().getOffset();
277     boolean atLineStart = caretOffset >= doc.getTextLength() || doc.getLineStartOffset(doc.getLineNumber(caretOffset)) == caretOffset;
278     if (atLineStart && project != null) {
279       int offset = editor.getCaretModel().getOffset();
280       PsiDocumentManager.getInstance(project).commitDocument(doc); // Sync document and PSI before formatting.
281       String properIndent = offset >= doc.getTextLength() ? "" : CodeStyleFacade.getInstance(project).getLineIndent(doc, offset);
282       if (properIndent != null) {
283         int tabSize = editor.getSettings().getTabSize(project);
284         for (int i = 0; i < properIndent.length(); i++) {
285           if (properIndent.charAt(i) == ' ') {
286             afterLineEnd--;
287           }
288           else if (properIndent.charAt(i) == '\t') {
289             if (afterLineEnd < tabSize) {
290               break;
291             }
292             afterLineEnd -= tabSize;
293           }
294           buf.append(properIndent.charAt(i));
295           if (afterLineEnd == 0) break;
296         }
297       }
298     }
299
300     for (int i = 0; i < afterLineEnd; i++) {
301       buf.append(' ');
302     }
303
304     return buf.toString();
305   }
306
307   public static void typeInStringAtCaretHonorMultipleCarets(final Editor editor, @NotNull final String str) {
308     typeInStringAtCaretHonorMultipleCarets(editor, str, true, str.length());
309   }
310
311   public static void typeInStringAtCaretHonorMultipleCarets(final Editor editor, @NotNull final String str, final int caretShift) {
312     typeInStringAtCaretHonorMultipleCarets(editor, str, true, caretShift);
313   }
314
315   public static void typeInStringAtCaretHonorMultipleCarets(final Editor editor, @NotNull final String str, final boolean toProcessOverwriteMode) {
316     typeInStringAtCaretHonorMultipleCarets(editor, str, toProcessOverwriteMode, str.length());
317   }
318
319   /**
320    * Inserts given string at each caret's position. Effective caret shift will be equal to {@code caretShift} for each caret.
321    */
322   public static void typeInStringAtCaretHonorMultipleCarets(final Editor editor, @NotNull final String str, final boolean toProcessOverwriteMode, final int caretShift)
323     throws ReadOnlyFragmentModificationException
324   {
325     editor.getCaretModel().runForEachCaret(new CaretAction() {
326       @Override
327       public void perform(Caret caret) {
328         insertStringAtCaretNoScrolling(editor, str, toProcessOverwriteMode, true, caretShift);
329       }
330     });
331     editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
332   }
333
334   public static void moveAllCaretsRelatively(@NotNull Editor editor, final int caretShift) {
335     editor.getCaretModel().runForEachCaret(new CaretAction() {
336       @Override
337       public void perform(Caret caret) {
338         caret.moveToOffset(caret.getOffset() + caretShift);
339       }
340     });
341   }
342
343   public static void moveCaretRelatively(@NotNull Editor editor, final int caretShift) {
344     CaretModel caretModel = editor.getCaretModel();
345     caretModel.moveToOffset(caretModel.getOffset() + caretShift);
346   }
347
348   /**
349    * This method is safe to run both in and out of {@link com.intellij.openapi.editor.CaretModel#runForEachCaret(CaretAction)} context.
350    * It scrolls to primary caret in both cases, and, in the former case, avoids performing excessive scrolling in case of large number
351    * of carets.
352    */
353   public static void scrollToCaret(@NotNull Editor editor) {
354     if (editor.getCaretModel().getCurrentCaret() == editor.getCaretModel().getPrimaryCaret()) {
355       editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
356     }
357   }
358   
359   @NotNull
360   public static List<CaretState> calcBlockSelectionState(@NotNull Editor editor, 
361                                                          @NotNull LogicalPosition blockStart, @NotNull LogicalPosition blockEnd) {
362     int startLine = Math.max(Math.min(blockStart.line, editor.getDocument().getLineCount() - 1), 0);
363     int endLine = Math.max(Math.min(blockEnd.line, editor.getDocument().getLineCount() - 1), 0);
364     int step = endLine < startLine ? -1 : 1;
365     int count = 1 + Math.abs(endLine - startLine);
366     List<CaretState> caretStates = new LinkedList<>();
367     boolean hasSelection = false;
368     for (int line = startLine, i = 0; i < count; i++, line += step) {
369       int startColumn = blockStart.column;
370       int endColumn = blockEnd.column;
371       int lineEndOffset = editor.getDocument().getLineEndOffset(line);
372       LogicalPosition lineEndPosition = editor.offsetToLogicalPosition(lineEndOffset);
373       int lineWidth = lineEndPosition.column;
374       if (startColumn > lineWidth && endColumn > lineWidth && !editor.isColumnMode()) {
375         LogicalPosition caretPos = new LogicalPosition(line, Math.min(startColumn, endColumn));
376         caretStates.add(new CaretState(caretPos,
377                                        lineEndPosition,
378                                        lineEndPosition));
379       }
380       else {
381         LogicalPosition startPos = new LogicalPosition(line, editor.isColumnMode() ? startColumn : Math.min(startColumn, lineWidth));
382         LogicalPosition endPos = new LogicalPosition(line, editor.isColumnMode() ? endColumn : Math.min(endColumn, lineWidth));
383         int startOffset = editor.logicalPositionToOffset(startPos);
384         int endOffset = editor.logicalPositionToOffset(endPos);
385         caretStates.add(new CaretState(endPos, startPos, endPos));
386         hasSelection |= startOffset != endOffset;
387       }
388     }
389     if (hasSelection && !editor.isColumnMode()) { // filtering out lines without selection
390       Iterator<CaretState> caretStateIterator = caretStates.iterator();
391       while(caretStateIterator.hasNext()) {
392         CaretState state = caretStateIterator.next();
393         //noinspection ConstantConditions
394         if (state.getSelectionStart().equals(state.getSelectionEnd())) {
395           caretStateIterator.remove();
396         }
397       }
398     }
399     return caretStates;
400   }
401
402   public static boolean requestWriting(@NotNull Editor editor) {
403     if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), editor.getProject())) {
404       HintManager.getInstance().showInformationHint(editor, EditorBundle.message("editing.read.only.file.hint"));
405       return false;
406     }
407     return true;
408   }
409
410   /**
411    * @return true when not viewer
412    *         false otherwise, additionally information hint with warning would be shown
413    */
414   public static boolean checkModificationAllowed(Editor editor) {
415     if (!editor.isViewer()) return true;
416     if (ApplicationManager.getApplication().isHeadlessEnvironment() || editor instanceof TextComponentEditor) return false;
417
418     String data = READ_ONLY_VIEW_MESSAGE_KEY.get(editor);
419     HintManager.getInstance().showInformationHint(editor, data == null ? EditorBundle.message("editing.viewer.hint") : data);
420     return false;
421   }
422 }