using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using Unity.CodeEditor.Document; using Unity.CodeEditor.Text; using Unity.CodeEditor.Utils; using Unity.PlasticSCM.Editor.UI; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; namespace Unity.CodeEditor.Rendering { internal class TextView : VisualElement { bool _isMeasureValid; private EventHandler _scrollInvalidated; private readonly CurrentLineHighlightRenderer _currentLineHighlightRenderer; private readonly ColumnRulerRenderer _columnRulerRenderer; private TextDocument _document; private HeightTree _heightTree; private TextEditorOptions _options; private bool _defaultTextMetricsValid; private float _wideSpaceWidth; // Width of an 'x'. Used as basis for the tab width, and for scrolling. private float _defaultLineHeight; // Height of a line containing 'x'. Used for scrolling. private float _defaultBaseline; // Baseline of a line containing 'x'. Used for TextTop/TextBottom calculation. private List _allVisualLines = new List(); private ReadOnlyCollection _visibleVisualLines; private float _clippedPixelsOnTop; private List _newVisualLines; private Vector2 _lastAvailableSize; private bool _inMeasure; private bool _canVerticallyScroll = true; private bool _canHorizontallyScroll = true; /// /// Offset of the scroll position. /// private Vector2 _scrollOffset; /// /// Size of the viewport. /// private Vector2 _scrollViewport; private Vector2 _scrollExtent; private Font _font; private int _fontSize = 13; private Color _foregroundColor = TextEditorColors.DefaultText; private Color _lineNumbersForegroundColor = TextEditorColors.LineNumbersForeground; private Color _lineNumbersSeparatorColor = TextEditorColors.LineNumbersSeparator; private Color _columnRulerColor = TextEditorColors.ColumnRuler; /// /// Gets the currently visible visual lines. /// /// /// Gets thrown if there are invalid visual lines when this property is accessed. /// You can use the property to check for this case, /// or use the method to force creating the visual lines /// when they are invalid. /// [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] internal ReadOnlyCollection VisualLines { get { if (_visibleVisualLines == null) throw new VisualLinesInvalidException(); return _visibleVisualLines; } } internal Rect Bounds => new Rect(0, 0, resolvedStyle.width, resolvedStyle.height); internal Vector2 ScrollViewport { get { return _scrollViewport; } } internal Vector2 ScrollExtent { get { return _scrollExtent; } } /// /// Gets the horizontal scroll offset. /// internal float HorizontalOffset => _scrollOffset.x; /// /// Gets the vertical scroll offset. /// internal float VerticalOffset => _scrollOffset.y; /// /// Gets the scroll offset; /// internal Vector2 ScrollOffset { get { return _scrollOffset; } set { value = new Vector2(ValidateVisualOffset(value.x), ValidateVisualOffset(value.y)); var isX = !_scrollOffset.x.IsClose(value.x); var isY = !_scrollOffset.y.IsClose(value.y); if (isX || isY) { SetScrollOffset(value); if (isX) { InvalidateVisual(); } InvalidateMeasure(); } } } #region colors internal Color LineNumbersForegroundColor { get { return _lineNumbersForegroundColor; } set { if (_lineNumbersForegroundColor != value) { _lineNumbersForegroundColor = value; Redraw(); } } } internal Color LineNumbersSeparatorColor { get { return _lineNumbersSeparatorColor; } set { if (_lineNumbersSeparatorColor != value) { _lineNumbersSeparatorColor = value; Redraw(); } } } /// /// Gets/sets the Color used for displaying non-printable characters. /// internal Color NonPrintableCharacterColor { get; set; } = TextEditorColors.NonPrintableCharacter; /// /// Gets/Sets the pen used to draw the column ruler. /// /// internal Color ColumnRulerColor { get { return _columnRulerColor;} set { if (_columnRulerColor != value) { _columnRulerColor = value; _columnRulerRenderer.SetRuler(Options.ColumnRulerPositions, value); InvalidateVisual(); } } } #endregion internal void InvalidateVisual() { MarkDirtyRepaint(); } internal void InvalidateMeasure() { _isMeasureValid = false; ScheduleVisualLineUpdate(); } private bool _updateScheduled; private void ScheduleVisualLineUpdate() { if (_updateScheduled) return; _updateScheduled = true; schedule.Execute(() => { _updateScheduled = false; UpdateVisualLines(); }); } /// /// Gets/Sets highlighted line number. /// internal int HighlightedLine { get => _currentLineHighlightRenderer.Line; set => _currentLineHighlightRenderer.Line = value; } internal bool CanHorizontallyScroll { get => _canHorizontallyScroll; set { if (_canHorizontallyScroll != value) { _canHorizontallyScroll = value; ClearVisualLines(); MarkDirtyRepaint(); } } } internal Font Font { get { return _font; } set { if (_font != value) { _font = value; // changing font properties requires recreating cached elements // and we need to re-measure the font metrics: InvalidateDefaultTextMetrics(); Redraw(); } } } internal int FontSize { get { return _fontSize; } set { if (_fontSize != value) { _fontSize = value; // changing font properties requires recreating cached elements // and we need to re-measure the font metrics: InvalidateDefaultTextMetrics(); Redraw(); } } } internal Color ForegroundColor { get { return _foregroundColor; } set { if (_foregroundColor != value) { _foregroundColor = value; // changing brushes requires recreating the cached elements Redraw(); } } } /// /// Additonal amount that allows horizontal scrolling past the end of the longest line. /// This is necessary to ensure the caret always is visible, even when it is at the end of the longest line. /// private const float AdditionalHorizontalScrollAmount = 25; private readonly ObserveAddRemoveCollection _backgroundRenderers; /// /// Gets the list of background renderers. /// internal IList BackgroundRenderers => _backgroundRenderers; private TextFormatter _formatter; private TextParagraphProperties CreateParagraphProperties() { if (_font == null) _font = Fonts.GetCascadiaFont(); return new TextParagraphProperties( _canHorizontallyScroll == false, Options.IndentationSize * WideSpaceWidth, Font, FontSize, ForegroundColor); } internal TextView() { style.flexGrow = 1; style.overflow = Overflow.Hidden; style.position = Position.Relative; style.backgroundColor = TextEditorColors.Background; Services.AddService(this); TextLayer = new TextLayer(this); _elementGenerators = new ObserveAddRemoveCollection(ElementGenerator_Added, ElementGenerator_Removed); _lineTransformers = new ObserveAddRemoveCollection(LineTransformer_Added, LineTransformer_Removed); _backgroundRenderers = new ObserveAddRemoveCollection(BackgroundRenderer_Added, BackgroundRenderer_Removed); _currentLineHighlightRenderer = new CurrentLineHighlightRenderer(this); _columnRulerRenderer = new ColumnRulerRenderer(this); Options = new TextEditorOptions(); Debug.Assert(_singleCharacterElementGenerator != null); // assert that the option change created the builtin element generators Layers = new LayerCollection(this); InsertLayer(TextLayer, KnownLayer.Text, LayerInsertionPosition.Replace); this.SetMouseCursor(MouseCursor.Text); } #region Layers internal readonly TextLayer TextLayer; /// /// Gets the list of layers displayed in the text view. /// internal LayerCollection Layers { get; } internal sealed class LayerCollection : Collection { private readonly TextView _textView; internal LayerCollection(TextView textView) { _textView = textView; } protected override void ClearItems() { foreach (var control in Items) { control.RemoveFromHierarchy(); } base.ClearItems(); _textView.LayersChanged(); } protected override void InsertItem(int index, Layer item) { base.InsertItem(index, item); _textView.Insert(index, item); _textView.LayersChanged(); } protected override void RemoveItem(int index) { Items[index].RemoveFromHierarchy(); base.RemoveItem(index); _textView.LayersChanged(); } protected override void SetItem(int index, Layer item) { Items[index].RemoveFromHierarchy(); base.SetItem(index, item); _textView.Insert(index, item); _textView.LayersChanged(); } } private void LayersChanged() { TextLayer.Index = Layers.IndexOf(TextLayer); } /// /// Inserts a new layer at a position specified relative to an existing layer. /// /// The new layer to insert. /// The existing layer /// Specifies whether the layer is inserted above,below, or replaces the referenced layer internal void InsertLayer(Layer layer, KnownLayer referencedLayer, LayerInsertionPosition position) { if (layer == null) throw new ArgumentNullException(nameof(layer)); if (!Enum.IsDefined(typeof(KnownLayer), referencedLayer)) throw new ArgumentOutOfRangeException(nameof(referencedLayer), (int)referencedLayer, nameof(KnownLayer)); if (!Enum.IsDefined(typeof(LayerInsertionPosition), position)) throw new ArgumentOutOfRangeException(nameof(position), (int)position, nameof(LayerInsertionPosition)); if (referencedLayer == KnownLayer.Background && position != LayerInsertionPosition.Above) throw new InvalidOperationException("Cannot replace or insert below the background layer."); var newPosition = new LayerPosition(referencedLayer, position); if (Layers == null) return; layer.LayerPosition = newPosition; for (var i = 0; i < Layers.Count; i++) { var p = Layers[i].LayerPosition; if (p != null) { if (p.KnownLayer == referencedLayer && p.Position == LayerInsertionPosition.Replace) { // found the referenced layer switch (position) { case LayerInsertionPosition.Below: Layers.Insert(i, layer); return; case LayerInsertionPosition.Above: Layers.Insert(i + 1, layer); return; case LayerInsertionPosition.Replace: Layers[i] = layer; return; } } else if (p.KnownLayer == referencedLayer && p.Position == LayerInsertionPosition.Above || p.KnownLayer > referencedLayer) { // we skipped the insertion position (referenced layer does not exist?) Layers.Insert(i, layer); return; } } } // inserting after all existing layers: Layers.Add(layer); } #endregion void Measure(Vector2 availableSize) { if (!_canHorizontallyScroll && !availableSize.x.IsClose(_lastAvailableSize.x)) { ClearVisualLines(); } _lastAvailableSize = availableSize; float maxWidth; if (_document == null) { // no document -> create empty list of lines _allVisualLines = new List(); _visibleVisualLines = new ReadOnlyCollection(_allVisualLines.ToArray()); maxWidth = 0; } else { _inMeasure = true; try { maxWidth = CreateAndMeasureVisualLines(availableSize); } finally { _inMeasure = false; } } //RemoveInlineObjectsNow(); maxWidth += AdditionalHorizontalScrollAmount; var heightTreeHeight = DocumentHeight; var options = Options; float desiredHeight = Math.Min(availableSize.y, heightTreeHeight); float extraHeightToAllowScrollBelowDocument = 0; if (options.AllowScrollBelowDocument) { if (!double.IsInfinity(_scrollViewport.y)) { // HACK: we need to keep at least Caret.MinimumDistanceToViewBorder visible so that we don't scroll back up when the user types after // scrolling to the very bottom. var minVisibleDocumentHeight = DefaultLineHeight; // increase the extend height to allow scrolling below the document extraHeightToAllowScrollBelowDocument = desiredHeight - minVisibleDocumentHeight; } } TextLayer.SetVisualLines(_visibleVisualLines); SetScrollData(availableSize, new Vector2(maxWidth, heightTreeHeight + extraHeightToAllowScrollBelowDocument), _scrollOffset); VisualLinesChanged?.Invoke(this, EventArgs.Empty); } void Arrange() { } /// /// Build all VisualLines in the visible range. /// /// Width the longest line private float CreateAndMeasureVisualLines(Vector2 availableSize) { //Debug.Log("Measure availableSize=" + availableSize + ", scrollOffset=" + _scrollOffset); var firstLineInView = _heightTree.GetLineByVisualPosition(_scrollOffset.y); // number of pixels clipped from the first visual line(s) _clippedPixelsOnTop = _scrollOffset.y - _heightTree.GetVisualPosition(firstLineInView); // clippedPixelsOnTop should be >= 0, except for floating point inaccurracy. Debug.Assert(_clippedPixelsOnTop >= -ExtensionMethods.Epsilon); _newVisualLines = new List(); VisualLineConstructionStarting?.Invoke(this, new VisualLineConstructionStartEventArgs(firstLineInView)); var elementGeneratorsArray = _elementGenerators.ToArray(); var lineTransformersArray = _lineTransformers.ToArray(); var nextLine = firstLineInView; float maxWidth = 0; var yPos = -_clippedPixelsOnTop; while (yPos < availableSize.y && nextLine != null) { var visualLine = GetVisualLine(nextLine.LineNumber) ?? BuildVisualLine(nextLine, CreateParagraphProperties(), elementGeneratorsArray, lineTransformersArray, availableSize); visualLine.VisualTop = _scrollOffset.y + yPos; nextLine = visualLine.LastDocumentLine.NextLine; yPos += visualLine.Height; foreach (var textLine in visualLine.TextLines) { if (textLine.WidthIncludingTrailingWhitespace > maxWidth) maxWidth = textLine.WidthIncludingTrailingWhitespace; } _newVisualLines.Add(visualLine); } foreach (var line in _allVisualLines) { Debug.Assert(line.IsDisposed == false); if (!_newVisualLines.Contains(line)) DisposeVisualLine(line); } _allVisualLines = _newVisualLines; // visibleVisualLines = readonly copy of visual lines _visibleVisualLines = new ReadOnlyCollection(_newVisualLines.ToArray()); _newVisualLines = null; if (_allVisualLines.Any(line => line.IsDisposed)) { throw new InvalidOperationException("A visual line was disposed even though it is still in use.\n" + "This can happen when Redraw() is called during measure for lines " + "that are already constructed."); } return maxWidth; } #region BuildVisualLine private VisualLine BuildVisualLine(DocumentLine documentLine, TextParagraphProperties paragraphProperties, IReadOnlyList elementGeneratorsArray, IReadOnlyList lineTransformersArray, Vector2 availableSize) { if (_heightTree.GetIsCollapsed(documentLine.LineNumber)) throw new InvalidOperationException("Trying to build visual line from collapsed line"); //Debug.Log("Building line " + documentLine.LineNumber); VisualLine visualLine = new VisualLine(this, documentLine); VisualLineTextSource textSource = new VisualLineTextSource(visualLine) { Document = _document, TextParagraphProperties = paragraphProperties, TextView = this }; visualLine.ConstructVisualElements(textSource, elementGeneratorsArray); if (visualLine.FirstDocumentLine != visualLine.LastDocumentLine) { // Check whether the lines are collapsed correctly: double firstLinePos = _heightTree.GetVisualPosition(visualLine.FirstDocumentLine.NextLine); double lastLinePos = _heightTree.GetVisualPosition(visualLine.LastDocumentLine.NextLine ?? visualLine.LastDocumentLine); if (!firstLinePos.IsClose(lastLinePos)) { for (int i = visualLine.FirstDocumentLine.LineNumber + 1; i <= visualLine.LastDocumentLine.LineNumber; i++) { if (!_heightTree.GetIsCollapsed(i)) throw new InvalidOperationException("Line " + i + " was skipped by a VisualLineElementGenerator, but it is not collapsed."); } throw new InvalidOperationException("All lines collapsed but visual pos different - height tree inconsistency?"); } } visualLine.RunTransformers(textSource, lineTransformersArray); // now construct textLines: var textOffset = 0; var textLines = new List(); while (textOffset <= visualLine.VisualLengthWithEndOfLineMarker) { var textLine = _formatter.FormatLine( textSource, textOffset, availableSize.x, paragraphProperties); textLines.Add(textLine); textOffset += textLine.Length; // exit loop so that we don't do the indentation calculation if there's only a single line if (textOffset >= visualLine.VisualLengthWithEndOfLineMarker) break; if (paragraphProperties.FirstLineInParagraph) { paragraphProperties.FirstLineInParagraph = false; TextEditorOptions options = this.Options; double indentation = 0; if (options.InheritWordWrapIndentation) { // determine indentation for next line: int indentVisualColumn = GetIndentationVisualColumn(visualLine); if (indentVisualColumn > 0 && indentVisualColumn < textOffset) { indentation = textLine.GetDistanceFromCharacterHit(new CharacterHit(indentVisualColumn, 0)); } } indentation += options.WordWrapIndentation; // apply the calculated indentation unless it's more than half of the text editor size: if (indentation > 0 && indentation * 2 < availableSize.x) paragraphProperties.Indent = indentation; } } visualLine.SetTextLines(textLines); _heightTree.SetHeight(visualLine.FirstDocumentLine, visualLine.Height); return visualLine; } private static int GetIndentationVisualColumn(VisualLine visualLine) { if (visualLine.Elements.Count == 0) return 0; var column = 0; var elementIndex = 0; var element = visualLine.Elements[elementIndex]; while (element.IsWhitespace(column)) { column++; if (column == element.VisualColumn + element.VisualLength) { elementIndex++; if (elementIndex == visualLine.Elements.Count) break; element = visualLine.Elements[elementIndex]; } } return column; } #endregion private void BackgroundRenderer_Added(IBackgroundRenderer renderer) { ConnectToTextView(renderer); InvalidateLayer(renderer.Layer); } private void BackgroundRenderer_Removed(IBackgroundRenderer renderer) { DisconnectFromTextView(renderer); InvalidateLayer(renderer.Layer); } /// /// Updates visual lines for the current viewport. Called when scroll offset /// changes or when the element size changes. /// internal void UpdateVisualLines() { var availableSize = new Vector2(resolvedStyle.width, resolvedStyle.height); if (float.IsNaN(availableSize.x) || float.IsNaN(availableSize.y)) return; if (availableSize.x <= 0 || availableSize.y <= 0) return; if (_isMeasureValid && _lastAvailableSize.IsClose(availableSize)) return; Measure(availableSize); Arrange(); _isMeasureValid = true; } internal void RenderBackground(KnownLayer layer) { foreach (var bg in _backgroundRenderers) { if (bg.Layer == layer) { bg.Draw(this); } } } internal void ArrangeTextLayer(IList visuals) { var pos = new Vector2(-_scrollOffset.x, -_clippedPixelsOnTop); foreach (var visual in visuals) { var t = visual.RenderTransform; if (t == null || t.x != pos.x || t.y != pos.y) { visual.RenderTransform = new Vector2(pos.x, pos.y); } pos = new Vector2(pos.x, pos.y + visual.LineHeight); } } #region Document Property /// /// Gets/Sets the document displayed by the text editor. /// internal TextDocument Document { get { return _document; } set { TextDocument oldValue = _document; _document = value; OnDocumentChanged(oldValue, value); } } /// /// Occurs when the document property has changed. /// internal event EventHandler DocumentChanged; private void OnDocumentChanged(TextDocument oldValue, TextDocument newValue) { if (oldValue != null) { _heightTree.Dispose(); _heightTree = null; _formatter = null; TextDocumentWeakEventManager.Changing.RemoveHandler(oldValue, OnChanging); } _document = newValue; ClearScrollData(); ClearVisualLines(); if (newValue != null) { TextDocumentWeakEventManager.Changing.AddHandler(newValue, OnChanging); _formatter = new TextFormatter(); InvalidateDefaultTextMetrics(); // measuring DefaultLineHeight depends on formatter _heightTree = new HeightTree(newValue, DefaultLineHeight); } MarkDirtyRepaint(); DocumentChanged?.Invoke(this, new DocumentChangedEventArgs(oldValue, newValue)); } private void OnChanging(object sender, DocumentChangeEventArgs e) { Redraw(e.Offset, e.RemovalLength); } #endregion #region Options property /// /// Gets/Sets the options used by the text editor. /// internal TextEditorOptions Options { get { return _options; } set { TextEditorOptions oldValue = _options; _options = value; OnOptionsChanged(oldValue, value); } } /// /// Occurs when a text editor option has changed. /// internal event PropertyChangedEventHandler OptionChanged; /// /// Raises the event. /// protected virtual void OnOptionChanged(PropertyChangedEventArgs e) { OptionChanged?.Invoke(this, e); if (Options.ShowColumnRulers) _columnRulerRenderer.SetRuler(Options.ColumnRulerPositions, ColumnRulerColor); else _columnRulerRenderer.SetRuler(null, ColumnRulerColor); UpdateBuiltinElementGeneratorsFromOptions(); Redraw(); } private void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue) { if (oldValue != null) { PropertyChangedWeakEventManager.RemoveHandler(oldValue, OnPropertyChanged); } if (newValue != null) { PropertyChangedWeakEventManager.AddHandler(newValue, OnPropertyChanged); } OnOptionChanged(new PropertyChangedEventArgs(null)); } private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { OnOptionChanged(e); } #endregion #region Service Provider /// /// Gets a service container used to associate services with the text view. /// internal IServiceContainer Services { get; } = new ServiceContainer(); #endregion #region ElementGenerators+LineTransformers Properties private readonly ObserveAddRemoveCollection _elementGenerators; /// /// Gets a collection where element generators can be registered. /// internal IList ElementGenerators => _elementGenerators; private void ElementGenerator_Added(VisualLineElementGenerator generator) { ConnectToTextView(generator); Redraw(); } private void ElementGenerator_Removed(VisualLineElementGenerator generator) { DisconnectFromTextView(generator); Redraw(); } private readonly ObserveAddRemoveCollection _lineTransformers; /// /// Gets a collection where line transformers can be registered. /// internal IList LineTransformers => _lineTransformers; private void LineTransformer_Added(IVisualLineTransformer lineTransformer) { ConnectToTextView(lineTransformer); Redraw(); } private void LineTransformer_Removed(IVisualLineTransformer lineTransformer) { DisconnectFromTextView(lineTransformer); Redraw(); } #endregion #region Builtin ElementGenerators // NewLineElementGenerator newLineElementGenerator; private SingleCharacterElementGenerator _singleCharacterElementGenerator; private LinkElementGenerator _linkElementGenerator; private MailLinkElementGenerator _mailLinkElementGenerator; TextFormatter mFormatter; private void UpdateBuiltinElementGeneratorsFromOptions() { var options = Options; // AddRemoveDefaultElementGeneratorOnDemand(ref newLineElementGenerator, options.ShowEndOfLine); AddRemoveDefaultElementGeneratorOnDemand(ref _singleCharacterElementGenerator, true); AddRemoveDefaultElementGeneratorOnDemand(ref _linkElementGenerator, options.EnableHyperlinks); AddRemoveDefaultElementGeneratorOnDemand(ref _mailLinkElementGenerator, options.EnableEmailHyperlinks); } private void AddRemoveDefaultElementGeneratorOnDemand(ref T generator, bool demand) where T : VisualLineElementGenerator, IBuiltinElementGenerator, new() { var hasGenerator = generator != null; if (hasGenerator != demand) { if (demand) { generator = new T(); ElementGenerators.Add(generator); } else { ElementGenerators.Remove(generator); generator = null; } } generator?.FetchOptions(Options); } #endregion /// /// Gets the visual line that contains the document line with the specified number. /// If that line is outside the visible range, a new VisualLine for that document line is constructed. /// internal VisualLine GetOrConstructVisualLine(DocumentLine documentLine) { if (documentLine == null) throw new ArgumentNullException("documentLine"); if (!this.Document.Lines.Contains(documentLine)) throw new InvalidOperationException("Line belongs to wrong document"); EditorDispatcher.VerifyMainThreadAccess(); VisualLine l = GetVisualLine(documentLine.LineNumber); if (l == null) { TextParagraphProperties paragraphProperties = CreateParagraphProperties(); while (_heightTree.GetIsCollapsed(documentLine.LineNumber)) { documentLine = documentLine.PreviousLine; } l = BuildVisualLine(documentLine, paragraphProperties, _elementGenerators.ToArray(), _lineTransformers.ToArray(), _lastAvailableSize); _allVisualLines.Add(l); // update all visual top values (building the line might have changed visual top of other lines due to word wrapping) foreach (var line in _allVisualLines) { line.VisualTop = _heightTree.GetVisualPosition(line.FirstDocumentLine); } } return l; } #region Redraw methods / VisualLine invalidation /// /// Causes the text editor to regenerate all visual lines. /// internal void Redraw() { EditorDispatcher.VerifyMainThreadAccess(); ClearVisualLines(); InvalidateMeasure(); } /// /// Causes the text editor to regenerate the specified visual line. /// internal void Redraw(VisualLine visualLine) { EditorDispatcher.VerifyMainThreadAccess(); if (_allVisualLines.Remove(visualLine)) { DisposeVisualLine(visualLine); MarkDirtyRepaint(); } } /// /// Causes the text editor to redraw all lines overlapping with the specified segment. /// internal void Redraw(int offset, int length) { EditorDispatcher.VerifyMainThreadAccess(); var changedSomethingBeforeOrInLine = false; for (var i = 0; i < _allVisualLines.Count; i++) { try { var visualLine = _allVisualLines[i]; var lineStart = visualLine.FirstDocumentLine.Offset; var lineEnd = visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength; if (offset <= lineEnd) { changedSomethingBeforeOrInLine = true; if (offset + length >= lineStart) { _allVisualLines.RemoveAt(i--); DisposeVisualLine(visualLine); } } } catch (Exception ex) { Debug.LogError(ex); } } if (changedSomethingBeforeOrInLine) { // Repaint not only when something in visible area was changed, but also when anything in front of it // was changed. We might have to redraw the line number margin. Or the highlighting changed. // However, we'll try to reuse the existing VisualLines. InvalidateMeasure(); } } /// /// Causes a known layer to redraw. /// This method does not invalidate visual lines; /// use the method to do that. /// internal void InvalidateLayer(KnownLayer knownLayer) { MarkDirtyRepaint(); } /// /// Causes the text editor to redraw all lines overlapping with the specified segment. /// Does nothing if segment is null. /// internal void Redraw(ISegment segment) { if (segment != null) { Redraw(segment.Offset, segment.Length); } } /// /// Invalidates all visual lines. /// The caller of ClearVisualLines() must also call InvalidateMeasure() to ensure /// that the visual lines will be recreated. /// private void ClearVisualLines() { if (_allVisualLines.Count != 0) { foreach (var visualLine in _allVisualLines) { DisposeVisualLine(visualLine); } _allVisualLines.Clear(); _visibleVisualLines = new ReadOnlyCollection(_allVisualLines.ToArray()); } } private void DisposeVisualLine(VisualLine visualLine) { if (_newVisualLines != null && _newVisualLines.Contains(visualLine)) { throw new ArgumentException("Cannot dispose visual line because it is in construction!"); } visualLine.Dispose(); //RemoveInlineObjects(visualLine); } #endregion #region Get(OrConstruct)VisualLine /// /// Gets the visual line that contains the document line with the specified number. /// Returns null if the document line is outside the visible range. /// internal VisualLine GetVisualLine(int documentLineNumber) { // TODO: EnsureVisualLines() ? foreach (var visualLine in _allVisualLines) { Debug.Assert(visualLine.IsDisposed == false); var start = visualLine.FirstDocumentLine.LineNumber; var end = visualLine.LastDocumentLine.LineNumber; if (documentLineNumber >= start && documentLineNumber <= end) return visualLine; } return null; } #endregion // Kept for backward compatibility with code that references visual line styling internal GUIStyle VisualLineStyle { get; set; } = new GUIStyle(); /// /// Retrieves a service from the text view. /// If the service is not found in the container, /// this method will also look for it in the current document's service provider. /// internal virtual object GetService(Type serviceType) { var instance = Services.GetService(serviceType); if (instance == null && _document != null) { instance = _document.ServiceProvider.GetService(serviceType); } return instance; } private void ConnectToTextView(object obj) { var c = obj as ITextViewConnect; c?.AddToTextView(this); } private void DisconnectFromTextView(object obj) { var c = obj as ITextViewConnect; c?.RemoveFromTextView(this); } /// /// Gets the width of a 'wide space' (the space width used for calculating the tab size). /// /// /// This is the width of an 'x' in the current font. /// We do not measure the width of an actual space as that would lead to tiny tabs in /// some proportional fonts. /// For monospaced fonts, this property will return the expected value, as 'x' and ' ' have the same width. /// internal float WideSpaceWidth { get { CalculateDefaultTextMetrics(); return _wideSpaceWidth; } } /// /// Gets the default line height. This is the height of an empty line or a line containing regular text. /// Lines that include formatted text or custom UI elements may have a different line height. /// internal float DefaultLineHeight { get { CalculateDefaultTextMetrics(); return _defaultLineHeight; } } private void InvalidateDefaultTextMetrics() { _defaultTextMetricsValid = false; if (_heightTree != null) { // calculate immediately so that height tree gets updated CalculateDefaultTextMetrics(); } } private void CalculateDefaultTextMetrics() { if (_defaultTextMetricsValid) return; _defaultTextMetricsValid = true; if (_formatter != null) { var paragraphProperties = CreateParagraphProperties(); var size = _formatter.CalcSize( "x", paragraphProperties); _wideSpaceWidth = Math.Max(1, size.x); _defaultLineHeight = Math.Max(1, Mathf.Round(size.y)); } // Update heightTree.DefaultLineHeight, if a document is loaded. if (_heightTree != null) _heightTree.DefaultLineHeight = _defaultLineHeight; } private static float ValidateVisualOffset(float offset) { if (float.IsNaN(offset)) throw new ArgumentException("offset must not be NaN"); if (offset < 0) return 0; return offset; } /// /// Scrolls the text view so that the specified rectangle gets visible. /// internal virtual void MakeVisible(Rect rectangle) { var visibleRectangle = new Rect(_scrollOffset.x, _scrollOffset.y, _scrollViewport.x, _scrollViewport.y); var newScrollOffsetX = _scrollOffset.x; var newScrollOffsetY = _scrollOffset.y; if (rectangle.x < visibleRectangle.x) { if (rectangle.xMax > visibleRectangle.xMax) { newScrollOffsetX = rectangle.x + rectangle.width / 2; } else { newScrollOffsetX = rectangle.x; } } else if (rectangle.xMax > visibleRectangle.xMax) { newScrollOffsetX = rectangle.xMax - _scrollViewport.x; } if (rectangle.y < visibleRectangle.y) { if (rectangle.yMax > visibleRectangle.yMax) { newScrollOffsetY = rectangle.y + rectangle.height / 2; } else { newScrollOffsetY = rectangle.y; } } else if (rectangle.yMax > visibleRectangle.yMax) { newScrollOffsetY = rectangle.yMax - _scrollViewport.y; } newScrollOffsetX = ValidateVisualOffset(newScrollOffsetX); newScrollOffsetY = ValidateVisualOffset(newScrollOffsetY); var newScrollOffset = new Vector2(newScrollOffsetX, newScrollOffsetY); if (!_scrollOffset.IsClose(newScrollOffset)) { SetScrollOffset(newScrollOffset); OnScrollChange(); InvalidateMeasure(); } } /// /// Gets the height of the document. /// internal float DocumentHeight => _heightTree?.TotalHeight ?? 0; /// /// Occurs when the TextView was measured and changed its visual lines. /// internal event EventHandler VisualLinesChanged; /// /// Gets the default baseline position. This is the difference between /// and for a line containing regular text. /// Lines that include formatted text or custom UI elements may have a different baseline. /// internal float DefaultBaseline { get { CalculateDefaultTextMetrics(); return _defaultBaseline; } } /// /// Gets whether the visual lines are valid. /// Will return false after a call to Redraw(). /// Accessing the visual lines property will cause a /// if this property is false. /// internal bool VisualLinesValid => _visibleVisualLines != null; /// /// Occurs when the TextView is about to be measured and will regenerate its visual lines. /// This event may be used to mark visual lines as invalid that would otherwise be reused. /// internal event EventHandler VisualLineConstructionStarting; /// /// Occurs when the scroll offset has changed. /// internal event EventHandler ScrollOffsetChanged; /// /// If the visual lines are invalid, creates new visual lines for the visible part /// of the document. /// If all visual lines are valid, this method does nothing. /// /// The visual line build process is already running. /// It is not allowed to call this method during the construction of a visual line. internal void EnsureVisualLines() { EditorDispatcher.VerifyMainThreadAccess(); if (_inMeasure) throw new InvalidOperationException("The visual line build process is already running! Cannot EnsureVisualLines() during Measure!"); if (!VisualLinesValid) { // force immediate re-measure UpdateVisualLines(); } // Sometimes we still have invalid lines after UpdateLayout - work around the problem // by calling MeasureOverride directly. if (!VisualLinesValid) { Debug.Log("UpdateLayout() failed in EnsureVisualLines"); Measure(_lastAvailableSize); } if (!VisualLinesValid) throw new VisualLinesInvalidException("Internal error: visual lines invalid after EnsureVisualLines call"); } #region Visual element pointer handling internal void OnPointerPressed(Vector2 localPosition) { EnsureVisualLines(); var element = GetVisualLineElementFromPosition(localPosition + _scrollOffset); element?.OnPointerPressed(localPosition); } internal void OnPointerMoved() { if (_lastAvailableSize.x > 0 && _lastAvailableSize.y > 0) Measure(_lastAvailableSize); } internal void OnPointerReleased(Vector2 localPosition) { EnsureVisualLines(); var element = GetVisualLineElementFromPosition(localPosition + _scrollOffset); element?.OnPointerReleased(localPosition); } #endregion #region Getting elements from Visual Position /// /// Gets the visual line at the specified document position (relative to start of document). /// Returns null if there is no visual line for the position (e.g. the position is outside the visible /// text area). /// internal VisualLine GetVisualLineFromVisualTop(double visualTop) { // TODO: change this method to also work outside the visible range - // required to make GetPosition work as expected! EnsureVisualLines(); foreach (var vl in VisualLines) { if (visualTop < vl.VisualTop) continue; if (visualTop < vl.VisualTop + vl.Height) return vl; } return null; } /// /// Gets the visual top position (relative to start of document) from a document line number. /// internal double GetVisualTopByDocumentLine(int line) { EditorDispatcher.VerifyMainThreadAccess(); if (_heightTree == null) throw ThrowUtil.NoDocumentAssigned(); return _heightTree.GetVisualPosition(_heightTree.GetLineByNumber(line)); } private VisualLineElement GetVisualLineElementFromPosition(Vector2 visualPosition) { var vl = GetVisualLineFromVisualTop(visualPosition.y); if (vl != null) { var column = vl.GetVisualColumnFloor(visualPosition); foreach (var element in vl.Elements) { if (element.VisualColumn + element.VisualLength <= column) continue; return element; } } return null; } #endregion /// /// Empty line selection width. /// internal virtual float EmptyLineSelectionWidth => 1; internal TextFormatter Formatter => _formatter; #region ScrollInfo implementation private void ClearScrollData() { SetScrollData(new Vector2(), new Vector2(), new Vector2()); } private bool SetScrollData(Vector2 viewport, Vector2 extent, Vector2 offset) { if (!(viewport.IsClose(_scrollViewport) && extent.IsClose(_scrollExtent) && offset.IsClose(_scrollOffset))) { _scrollViewport = viewport; _scrollExtent = extent; SetScrollOffset(offset); OnScrollChange(); return true; } return false; } private void SetScrollOffset(Vector2 vector) { if (!_canHorizontallyScroll) { vector = new Vector2(0, vector.y); } if (!_canVerticallyScroll) { vector = new Vector2(vector.x, 0); } if (!_scrollOffset.IsClose(vector)) { _scrollOffset = vector; ScrollOffsetChanged?.Invoke(this, EventArgs.Empty); } } private void OnScrollChange() { //_repaintAction(); } internal DocumentLine GetDocumentLineByVisualTop(double visualTop) { EditorDispatcher.VerifyMainThreadAccess(); if (_heightTree == null) throw ThrowUtil.NoDocumentAssigned(); return _heightTree.GetLineByVisualPosition(visualTop); } #endregion } }