replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / editor / impl / view / EditorView.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.impl.view;
17
18 import com.intellij.diagnostic.Dumpable;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.editor.FoldRegion;
22 import com.intellij.openapi.editor.LogicalPosition;
23 import com.intellij.openapi.editor.VisualPosition;
24 import com.intellij.openapi.editor.colors.EditorFontType;
25 import com.intellij.openapi.editor.event.VisibleAreaEvent;
26 import com.intellij.openapi.editor.event.VisibleAreaListener;
27 import com.intellij.openapi.editor.ex.DocumentEx;
28 import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
29 import com.intellij.openapi.editor.ex.util.EditorUtil;
30 import com.intellij.openapi.editor.impl.DocumentImpl;
31 import com.intellij.openapi.editor.impl.EditorImpl;
32 import com.intellij.openapi.editor.impl.FontInfo;
33 import com.intellij.openapi.editor.impl.TextDrawingCallback;
34 import com.intellij.openapi.editor.markup.TextAttributes;
35 import com.intellij.openapi.util.Disposer;
36 import com.intellij.openapi.util.Key;
37 import com.intellij.openapi.util.text.StringUtil;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.TestOnly;
40
41 import java.awt.*;
42 import java.awt.event.HierarchyEvent;
43 import java.awt.event.HierarchyListener;
44 import java.awt.font.FontRenderContext;
45 import java.awt.geom.Point2D;
46 import java.text.Bidi;
47
48 /**
49  * A facade for components responsible for drawing editor contents, managing editor size 
50  * and coordinate conversions (offset <-> logical position <-> visual position <-> x,y).
51  * 
52  * Also contains a cache of several font-related quantities (line height, space width, etc).
53  */
54 public class EditorView implements TextDrawingCallback, Disposable, Dumpable, HierarchyListener, VisibleAreaListener {
55   private static Key<LineLayout> FOLD_REGION_TEXT_LAYOUT = Key.create("text.layout");
56
57   private final EditorImpl myEditor;
58   private final DocumentEx myDocument;
59   private final EditorPainter myPainter;
60   private final EditorCoordinateMapper myMapper;
61   private final EditorSizeManager mySizeManager;
62   private final TextLayoutCache myTextLayoutCache;
63   private final LogicalPositionCache myLogicalPositionCache;
64   private final TabFragment myTabFragment;
65
66   private FontRenderContext myFontRenderContext;
67   private String myPrefixText; // accessed only in EDT
68   private LineLayout myPrefixLayout; // guarded by myLock
69   private TextAttributes myPrefixAttributes; // accessed only in EDT
70   private int myBidiFlags; // accessed only in EDT
71   
72   private int myPlainSpaceWidth; // guarded by myLock
73   private int myLineHeight; // guarded by myLock
74   private int myDescent; // guarded by myLock
75   private int myCharHeight; // guarded by myLock
76   private int myMaxCharWidth; // guarded by myLock
77   private int myTabSize; // guarded by myLock
78   private int myTopOverhang; //guarded by myLock
79   private int myBottomOverhang; //guarded by myLock
80
81   private final Object myLock = new Object();
82   
83   public EditorView(EditorImpl editor) {
84     myEditor = editor;
85     myDocument = editor.getDocument();
86     
87     myPainter = new EditorPainter(this);
88     myMapper = new EditorCoordinateMapper(this);
89     mySizeManager = new EditorSizeManager(this);
90     myTextLayoutCache = new TextLayoutCache(this);
91     myLogicalPositionCache = new LogicalPositionCache(this);
92     myTabFragment = new TabFragment(this);
93
94     myEditor.getContentComponent().addHierarchyListener(this);
95     myEditor.getScrollingModel().addVisibleAreaListener(this);
96
97     Disposer.register(this, myLogicalPositionCache);
98     Disposer.register(this, myTextLayoutCache);
99     Disposer.register(this, mySizeManager);
100   }
101
102   EditorImpl getEditor() {
103     return myEditor;
104   }
105
106   FontRenderContext getFontRenderContext() {
107     return myFontRenderContext;
108   }
109
110   EditorSizeManager getSizeManager() {
111     return mySizeManager;
112   }
113   
114   TextLayoutCache getTextLayoutCache() {
115     return myTextLayoutCache;
116   }
117   
118   EditorPainter getPainter() {
119     return myPainter;
120   }
121   
122   TabFragment getTabFragment() {
123     return myTabFragment;
124   }
125   
126   LogicalPositionCache getLogicalPositionCache() {
127     return myLogicalPositionCache;
128   }
129
130   @Override
131   public void dispose() {
132     myEditor.getScrollingModel().removeVisibleAreaListener(this);
133     myEditor.getContentComponent().removeHierarchyListener(this);
134   }
135
136   @Override
137   public void hierarchyChanged(HierarchyEvent e) {
138     if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && e.getComponent().isShowing()) {
139       checkFontRenderContext(null);
140     }
141   }
142
143   @Override
144   public void visibleAreaChanged(VisibleAreaEvent e) {
145     checkFontRenderContext(null);
146   }
147   public int yToVisualLine(int y) {
148     return myMapper.yToVisualLine(y);
149   }
150
151   public int visualLineToY(int line) {
152     return myMapper.visualLineToY(line);
153   }
154
155   @NotNull
156   public LogicalPosition offsetToLogicalPosition(int offset) {
157     assertIsReadAccess();
158     return myMapper.offsetToLogicalPosition(offset);
159   }
160
161   public int logicalPositionToOffset(@NotNull LogicalPosition pos) {
162     assertIsReadAccess();
163     return myMapper.logicalPositionToOffset(pos);
164   }
165
166   @NotNull
167   public VisualPosition logicalToVisualPosition(@NotNull LogicalPosition pos, boolean beforeSoftWrap) {
168     assertIsDispatchThread();
169     assertNotInBulkMode();
170     myEditor.getSoftWrapModel().prepareToMapping();
171     return myMapper.logicalToVisualPosition(pos, beforeSoftWrap);
172   }
173
174   @NotNull
175   public LogicalPosition visualToLogicalPosition(@NotNull VisualPosition pos) {
176     assertIsDispatchThread();
177     assertNotInBulkMode();
178     myEditor.getSoftWrapModel().prepareToMapping();
179     return myMapper.visualToLogicalPosition(pos);
180   }
181
182   @NotNull
183   public VisualPosition offsetToVisualPosition(int offset, boolean leanTowardsLargerOffsets, boolean beforeSoftWrap) {
184     assertIsDispatchThread();
185     assertNotInBulkMode();
186     myEditor.getSoftWrapModel().prepareToMapping();
187     return myMapper.offsetToVisualPosition(offset, leanTowardsLargerOffsets, beforeSoftWrap);
188   }
189
190   public int visualPositionToOffset(VisualPosition visualPosition) {
191     assertIsDispatchThread();
192     assertNotInBulkMode();
193     myEditor.getSoftWrapModel().prepareToMapping();
194     return myMapper.visualPositionToOffset(visualPosition);
195   }
196
197   public int offsetToVisualLine(int offset, boolean beforeSoftWrap) {
198     assertIsDispatchThread();
199     assertNotInBulkMode();
200     myEditor.getSoftWrapModel().prepareToMapping();
201     return myMapper.offsetToVisualLine(offset, beforeSoftWrap);
202   }
203   
204   public int visualLineToOffset(int visualLine) {
205     assertIsDispatchThread();
206     assertNotInBulkMode();
207     myEditor.getSoftWrapModel().prepareToMapping();
208     return myMapper.visualLineToOffset(visualLine);
209   }
210   
211   @NotNull
212   public VisualPosition xyToVisualPosition(@NotNull Point2D p) {
213     assertIsDispatchThread();
214     assertNotInBulkMode();
215     myEditor.getSoftWrapModel().prepareToMapping();
216     return myMapper.xyToVisualPosition(p);
217   }
218
219   @NotNull
220   public Point2D visualPositionToXY(@NotNull VisualPosition pos) {
221     assertIsDispatchThread();
222     assertNotInBulkMode();
223     myEditor.getSoftWrapModel().prepareToMapping();
224     return myMapper.visualPositionToXY(pos);
225   }
226
227   @NotNull
228   public Point2D offsetToXY(int offset, boolean leanTowardsLargerOffsets, boolean beforeSoftWrap) {
229     assertIsDispatchThread();
230     assertNotInBulkMode();
231     myEditor.getSoftWrapModel().prepareToMapping();
232     return myMapper.offsetToXY(offset, leanTowardsLargerOffsets, beforeSoftWrap);
233   }
234
235   public void setPrefix(String prefixText, TextAttributes attributes) {
236     assertIsDispatchThread();
237     myPrefixText = prefixText;
238     synchronized (myLock) {
239       myPrefixLayout = prefixText == null || prefixText.isEmpty() ? null :
240                        LineLayout.create(this, prefixText, attributes.getFontType());
241     }
242     myPrefixAttributes = attributes;
243     mySizeManager.invalidateRange(0, 0);
244   }
245
246   public float getPrefixTextWidthInPixels() {
247     synchronized (myLock) {
248       return myPrefixLayout == null ? 0 : myPrefixLayout.getWidth();
249     }
250   }
251
252   LineLayout getPrefixLayout() {
253     synchronized (myLock) {
254       return myPrefixLayout;
255     }
256   }
257
258   TextAttributes getPrefixAttributes() {
259     return myPrefixAttributes;
260   }
261
262   public void paint(Graphics2D g) {
263     assertIsDispatchThread();
264     myEditor.getSoftWrapModel().prepareToMapping();
265     checkFontRenderContext(g.getFontRenderContext());
266     myPainter.paint(g);
267   }
268
269   public void repaintCarets() {
270     assertIsDispatchThread();
271     myPainter.repaintCarets();
272   }
273
274   public Dimension getPreferredSize() {
275     assertIsDispatchThread();
276     assert !myEditor.isPurePaintingMode();
277     myEditor.getSoftWrapModel().prepareToMapping();
278     return mySizeManager.getPreferredSize();
279   }
280
281   /**
282    * Returns preferred pixel width of the lines in range.
283    * <p>
284    * This method is currently used only with "idea.true.smooth.scrolling" experimental option.
285    *
286    * @param beginLine begin visual line (inclusive)
287    * @param endLine   end visual line (exclusive), may be greater than the actual number of lines
288    * @return preferred pixel width
289    */
290   public int getPreferredWidth(int beginLine, int endLine) {
291     assertIsDispatchThread();
292     assert !myEditor.isPurePaintingMode();
293     myEditor.getSoftWrapModel().prepareToMapping();
294     return mySizeManager.getPreferredWidth(beginLine, endLine);
295   }
296
297   public int getPreferredHeight() {
298     assertIsDispatchThread();
299     assert !myEditor.isPurePaintingMode();
300     myEditor.getSoftWrapModel().prepareToMapping();
301     return mySizeManager.getPreferredHeight();
302   }
303
304   public int getMaxWidthInRange(int startOffset, int endOffset) {
305     assertIsDispatchThread();
306     return getMaxWidthInLineRange(offsetToVisualLine(startOffset, false), offsetToVisualLine(endOffset, true));
307   }
308
309   /**
310    * If {@code quickEvaluationListener} is provided, quick approximate size evaluation becomes enabled, listener will be invoked
311    * if approximation will in fact be used during width calculation.
312    */
313   int getMaxWidthInLineRange(int startVisualLine, int endVisualLine) {
314     myEditor.getSoftWrapModel().prepareToMapping();
315     int maxWidth = 0;
316     VisualLinesIterator iterator = new VisualLinesIterator(myEditor, startVisualLine);
317     while (!iterator.atEnd() && iterator.getVisualLine() <= endVisualLine) {
318       int width = mySizeManager.getVisualLineWidth(iterator, null);
319       maxWidth = Math.max(maxWidth, width);
320       iterator.advance();
321     }
322     return maxWidth;
323   }
324
325   public void reinitSettings() {
326     assertIsDispatchThread();
327     synchronized (myLock) {
328       myPlainSpaceWidth = -1;
329       myTabSize = -1;
330     }
331     switch (EditorSettingsExternalizable.getInstance().getBidiTextDirection()) {
332       case LTR:
333         myBidiFlags = Bidi.DIRECTION_LEFT_TO_RIGHT;
334         break;
335       case RTL:
336         myBidiFlags = Bidi.DIRECTION_RIGHT_TO_LEFT;
337         break;
338       default:
339         myBidiFlags = Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
340     }
341     setFontRenderContext(null);
342     myLogicalPositionCache.reset(false);
343     myTextLayoutCache.resetToDocumentSize(false);
344     invalidateFoldRegionLayouts();
345     setPrefix(myPrefixText, myPrefixAttributes); // recreate prefix layout
346     mySizeManager.reset();
347   }
348   
349   public void invalidateRange(int startOffset, int endOffset) {
350     assertIsDispatchThread();
351     int textLength = myDocument.getTextLength();
352     if (startOffset > endOffset || startOffset >= textLength || endOffset < 0) {
353       return;
354     }
355     int startLine = myDocument.getLineNumber(Math.max(0, startOffset));
356     int endLine = myDocument.getLineNumber(Math.min(textLength, endOffset));
357     myTextLayoutCache.invalidateLines(startLine, endLine);
358     mySizeManager.invalidateRange(startOffset, endOffset);
359   }
360
361   /**
362    * Invoked when document might have changed, but no notifications were sent (for a hacky document in EditorTextFieldCellRenderer)
363    */
364   public void reset() {
365     assertIsDispatchThread();
366     myLogicalPositionCache.reset(true);
367     myTextLayoutCache.resetToDocumentSize(true);
368     mySizeManager.reset();
369   }
370   
371   public boolean isRtlLocation(@NotNull VisualPosition visualPosition) {
372     assertIsDispatchThread();
373     if (myDocument.getTextLength() == 0) return false;
374     LogicalPosition logicalPosition = visualToLogicalPosition(visualPosition);
375     int offset = logicalPositionToOffset(logicalPosition);
376     if (myEditor.getSoftWrapModel().getSoftWrap(offset) != null) {
377       VisualPosition beforeWrapPosition = offsetToVisualPosition(offset, true, true);
378       if (visualPosition.line == beforeWrapPosition.line && 
379           (visualPosition.column > beforeWrapPosition.column || 
380            visualPosition.column == beforeWrapPosition.column && visualPosition.leansRight)) {
381         return false;
382       }
383       VisualPosition afterWrapPosition = offsetToVisualPosition(offset, false, false);
384       if (visualPosition.line == afterWrapPosition.line &&
385           (visualPosition.column < afterWrapPosition.column ||
386            visualPosition.column == afterWrapPosition.column && !visualPosition.leansRight)) {
387         return false;
388       }
389     } 
390     int line = myDocument.getLineNumber(offset);
391     LineLayout layout = myTextLayoutCache.getLineLayout(line);
392     return layout.isRtlLocation(offset - myDocument.getLineStartOffset(line), logicalPosition.leansForward);
393   }
394
395   public boolean isAtBidiRunBoundary(@NotNull VisualPosition visualPosition) {
396     assertIsDispatchThread();
397     int offset = visualPositionToOffset(visualPosition);
398     int otherSideOffset = visualPositionToOffset(visualPosition.leanRight(!visualPosition.leansRight));
399     return offset != otherSideOffset;
400   }
401
402   /**
403    * Offset of nearest boundary (not equal to {@code offset}) on the same line is returned. {@code -1} is returned if
404    * corresponding boundary is not found.
405    */
406   public int findNearestDirectionBoundary(int offset, boolean lookForward) {
407     assertIsDispatchThread();
408     int textLength = myDocument.getTextLength();
409     if (textLength == 0 || offset < 0 || offset > textLength) return -1;
410     int line = myDocument.getLineNumber(offset);
411     LineLayout layout = myTextLayoutCache.getLineLayout(line);
412     int lineStartOffset = myDocument.getLineStartOffset(line);
413     int relativeOffset = layout.findNearestDirectionBoundary(offset - lineStartOffset, lookForward);
414     return relativeOffset < 0 ? -1 : lineStartOffset + relativeOffset;
415   }
416
417   public int getPlainSpaceWidth() {
418     synchronized (myLock) {
419       initMetricsIfNeeded();
420       return myPlainSpaceWidth;
421     }
422   }
423
424   public int getNominalLineHeight() {
425     synchronized (myLock) {
426       initMetricsIfNeeded();
427       return myLineHeight + myTopOverhang + myBottomOverhang;
428     }
429   }
430
431   public int getLineHeight() {
432     synchronized (myLock) {
433       initMetricsIfNeeded();
434       return myLineHeight;
435     }
436   }
437
438   private float getVerticalScalingFactor() {
439     if (myEditor.isOneLineMode()) return 1;
440     float lineSpacing = myEditor.getColorsScheme().getLineSpacing();
441     return lineSpacing > 0 ? lineSpacing : 1;
442   }
443
444   public int getDescent() {
445     synchronized (myLock) {
446       return myDescent;
447     }
448   }
449
450   public int getCharHeight() {
451     synchronized (myLock) {
452       initMetricsIfNeeded();
453       return myCharHeight;
454     }
455   }
456
457   int getMaxCharWidth() {
458     synchronized (myLock) {
459       initMetricsIfNeeded();
460       return myMaxCharWidth;
461     }
462   }
463
464   public int getAscent() {
465     synchronized (myLock) {
466       initMetricsIfNeeded();
467       return myLineHeight - myDescent;
468     }
469   }
470
471   public int getTopOverhang() {
472     synchronized (myLock) {
473       initMetricsIfNeeded();
474       return myTopOverhang;
475     }
476   }
477
478   public int getBottomOverhang() {
479     synchronized (myLock) {
480       initMetricsIfNeeded();
481       return myBottomOverhang;
482     }
483   }
484
485   // guarded by myLock
486   private void initMetricsIfNeeded() {
487     if (myPlainSpaceWidth >= 0) return;
488
489     FontMetrics fm = myEditor.getContentComponent().getFontMetrics(myEditor.getColorsScheme().getFont(EditorFontType.PLAIN));
490
491     int width = FontLayoutService.getInstance().charWidth(fm, ' ');
492     myPlainSpaceWidth = width > 0 ? width : 1;
493
494     myCharHeight = FontLayoutService.getInstance().charWidth(fm, 'a');
495
496     float verticalScalingFactor = getVerticalScalingFactor();
497
498     int fontMetricsHeight = FontLayoutService.getInstance().getHeight(fm);
499     myLineHeight = (int)Math.ceil(fontMetricsHeight * verticalScalingFactor);
500
501     int descent = FontLayoutService.getInstance().getDescent(fm);
502     myDescent = (int)Math.floor(descent * verticalScalingFactor);
503     myTopOverhang = fontMetricsHeight - myLineHeight + myDescent - descent;
504     myBottomOverhang = descent - myDescent;
505
506     // assuming that bold italic 'W' gives a good approximation of font's widest character
507     FontMetrics fmBI = myEditor.getContentComponent().getFontMetrics(myEditor.getColorsScheme().getFont(EditorFontType.BOLD_ITALIC));
508     myMaxCharWidth = FontLayoutService.getInstance().charWidth(fmBI, 'W');
509   }
510   
511   public int getTabSize() {
512     synchronized (myLock) {
513       if (myTabSize < 0) {
514         myTabSize = EditorUtil.getTabSize(myEditor);
515       }
516       return myTabSize;
517     }
518   }
519
520   private void setFontRenderContext(FontRenderContext context) {
521     myFontRenderContext = context == null ? FontInfo.getFontRenderContext(myEditor.getContentComponent()) : context;
522   }
523
524   private void checkFontRenderContext(FontRenderContext context) {
525     FontRenderContext oldContext = myFontRenderContext;
526     setFontRenderContext(context);
527     if (!myFontRenderContext.equals(oldContext)) {
528       myTextLayoutCache.resetToDocumentSize(false);
529       invalidateFoldRegionLayouts();
530     }
531   }
532
533   LineLayout getFoldRegionLayout(FoldRegion foldRegion) {
534     LineLayout layout = foldRegion.getUserData(FOLD_REGION_TEXT_LAYOUT);
535     if (layout == null) {
536       TextAttributes placeholderAttributes = myEditor.getFoldingModel().getPlaceholderAttributes();
537       layout = LineLayout.create(this, StringUtil.replace(foldRegion.getPlaceholderText(), "\n", " "),
538                               placeholderAttributes == null ? Font.PLAIN : placeholderAttributes.getFontType());
539       foldRegion.putUserData(FOLD_REGION_TEXT_LAYOUT, layout);
540     }
541     return layout;
542   }
543
544   void invalidateFoldRegionLayouts() {
545     for (FoldRegion region : myEditor.getFoldingModel().getAllFoldRegions()) {
546       region.putUserData(FOLD_REGION_TEXT_LAYOUT, null);
547     }
548   }
549   
550   Insets getInsets() {
551     return myEditor.getContentComponent().getInsets();
552   }
553
554   int getBidiFlags() {
555     return myBidiFlags;
556   }
557
558   private static void assertIsDispatchThread() {
559     ApplicationManager.getApplication().assertIsDispatchThread();
560   }
561   
562   private static void assertIsReadAccess() {
563     ApplicationManager.getApplication().assertReadAccessAllowed();
564   }
565
566   @Override
567   public void drawChars(@NotNull Graphics g, @NotNull char[] data, int start, int end, int x, int y, Color color, FontInfo fontInfo) {
568     myPainter.drawChars(g, data, start, end, x, y, color, fontInfo);
569   }
570
571   @NotNull
572   @Override
573   public String dumpState() {
574     String prefixText = myPrefixText;
575     TextAttributes prefixAttributes = myPrefixAttributes;
576     synchronized (myLock) {
577       return "[prefix text: " + prefixText +
578              ", prefix attributes: " + prefixAttributes +
579              ", space width: " + myPlainSpaceWidth +
580              ", line height: " + myLineHeight +
581              ", descent: " + myDescent +
582              ", char height: " + myCharHeight +
583              ", max char width: " + myMaxCharWidth +
584              ", tab size: " + myTabSize +
585              " ,size manager: " + mySizeManager.dumpState() +
586              " ,logical position cache: " + myLogicalPositionCache.dumpState() +
587              "]";
588     }
589   }
590
591   @TestOnly
592   public void validateState() {
593     myLogicalPositionCache.validateState();
594     mySizeManager.validateState();
595   }
596
597   private void assertNotInBulkMode() {
598     if (myDocument instanceof DocumentImpl) ((DocumentImpl)myDocument).assertNotInBulkUpdate();
599     else if (myDocument.isInBulkUpdate()) throw new IllegalStateException("Current operation is not available in bulk mode");
600   }
601 }