// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Text { public static partial class Extensions { /// /// ITextSnapshot implementation of SourceText /// private class SnapshotSourceText : SourceText { /// /// Use a separate class for closed files to simplify memory leak investigations /// internal sealed class ClosedSnapshotSourceText : SnapshotSourceText { public ClosedSnapshotSourceText(ITextSnapshot roslynSnapshot, Encoding encodingOpt) : base(roslynSnapshot, encodingOpt, containerOpt: null) { } } private static readonly Func s_textLog = (v1, v2) => string.Format("FullRange : from {0} to {1}", v1, v2); /// /// The ITextSnapshot backing the SourceText instance /// protected readonly ITextSnapshot RoslynSnapshot; private readonly Encoding _encodingOpt; private readonly TextBufferContainer _containerOpt; private readonly int _reiteratedVersion; private SnapshotSourceText(ITextSnapshot editorSnapshot, Encoding encodingOpt) { Contract.ThrowIfNull(editorSnapshot); this.RoslynSnapshot = TextBufferMapper.ToRoslyn(editorSnapshot); _containerOpt = TextBufferContainer.From(editorSnapshot.TextBuffer); _reiteratedVersion = editorSnapshot.Version.ReiteratedVersionNumber; _encodingOpt = encodingOpt; } public SnapshotSourceText(ITextSnapshot roslynSnapshot, Encoding encodingOpt, TextBufferContainer containerOpt) { Contract.ThrowIfNull(roslynSnapshot); this.RoslynSnapshot = roslynSnapshot; _encodingOpt = encodingOpt; _containerOpt = containerOpt; } /// /// A weak map of all Editor ITextSnapshots and their associated SourceText /// private static readonly ConditionalWeakTable s_textSnapshotMap = new ConditionalWeakTable(); private static readonly ConditionalWeakTable.CreateValueCallback s_createTextCallback = CreateText; public static SourceText From(ITextSnapshot editorSnapshot) { if (editorSnapshot == null) { throw new ArgumentNullException("textSnapshot"); } return s_textSnapshotMap.GetValue(editorSnapshot, s_createTextCallback); } // Use this as a secondary cache to catch ITextSnapshots that have the same ReiteratedVersionNumber as a previously created SnapshotSourceText private static readonly ConditionalWeakTable> s_textBufferLatestSnapshotMap = new ConditionalWeakTable>(); private static SnapshotSourceText CreateText(ITextSnapshot editorSnapshot) { var strongBox = s_textBufferLatestSnapshotMap.GetOrCreateValue(editorSnapshot.TextBuffer); var text = strongBox.Value; if (text != null && text._reiteratedVersion == editorSnapshot.Version.ReiteratedVersionNumber) { return text; } text = new SnapshotSourceText(editorSnapshot, editorSnapshot.TextBuffer.GetEncodingOrUTF8()); strongBox.Value = text; return text; } public override Encoding Encoding { get { return _encodingOpt; } } public ITextSnapshot EditorSnapshot { get { return TextBufferMapper.ToEditor(this.RoslynSnapshot); } } protected static ITextBufferCloneService TextBufferFactory { get { // simplest way to get text factory var ws = PrimaryWorkspace.Workspace; if (ws != null) { return ws.Services.GetService(); } return null; } } public override SourceTextContainer Container { get { return _containerOpt ?? base.Container; } } public override int Length { get { var res = this.RoslynSnapshot.Length; return res; } } public override char this[int position] { get { return this.RoslynSnapshot[position]; } } #region Lines protected override TextLineCollection GetLinesCore() { return new LineInfo(this); } private class LineInfo : TextLineCollection { private readonly SnapshotSourceText _text; public LineInfo(SnapshotSourceText text) { _text = text; } public override int Count { get { return _text.RoslynSnapshot.LineCount; } } public override TextLine this[int index] { get { var line = _text.RoslynSnapshot.GetLineFromLineNumber(index); return TextLine.FromSpan(_text, TextSpan.FromBounds(line.Start, line.End)); } } public override int IndexOf(int position) { return _text.RoslynSnapshot.GetLineNumberFromPosition(position); } public override TextLine GetLineFromPosition(int position) { return this[this.IndexOf(position)]; } public override LinePosition GetLinePosition(int position) { ITextSnapshotLine textLine = _text.RoslynSnapshot.GetLineFromPosition(position); return new LinePosition(textLine.LineNumber, position - textLine.Start); } } #endregion public override string ToString() { return this.RoslynSnapshot.GetText(); } public override string ToString(TextSpan textSpan) { var editorSpan = new Span(textSpan.Start, textSpan.Length); var res = this.RoslynSnapshot.GetText(editorSpan); return res; } public override SourceText WithChanges(IEnumerable changes) { if (changes == null) { throw new ArgumentNullException("changes"); } if (!changes.Any()) { return this; } // check whether we can use text buffer factory var factory = TextBufferFactory; if (factory == null) { // if we can't get the factory, use the default implementation return base.WithChanges(changes); } // otherwise, create a new cloned snapshot var buffer = factory.Clone(RoslynSnapshot.GetFullSpan()); var baseSnapshot = buffer.CurrentSnapshot; // apply the change to the buffer using (var edit = buffer.CreateEdit()) { foreach (var change in changes) { edit.Replace(change.Span.ToSpan(), change.NewText); } edit.Apply(); } return new ChangedSourceText(this, baseSnapshot, buffer.CurrentSnapshot); } /// /// Perf: Optimize calls to GetChangeRanges after WithChanges by using editor snapshots /// private class ChangedSourceText : SnapshotSourceText { private readonly SnapshotSourceText _baseText; private readonly ITextSnapshot _baseSnapshot; public ChangedSourceText(SnapshotSourceText baseText, ITextSnapshot baseSnapshot, ITextSnapshot currentSnapshot) : base(currentSnapshot, baseText.Encoding, containerOpt: null) { _baseText = baseText; _baseSnapshot = baseSnapshot; } public override IReadOnlyList GetChangeRanges(SourceText oldText) { if (oldText == null) { throw new ArgumentNullException("oldText"); } // if they are the same text there is no change. if (oldText == this) { return TextChangeRange.NoChanges; } if (oldText != _baseText) { return new[] { new TextChangeRange(new TextSpan(0, oldText.Length), this.Length) }; } return GetChangeRanges(_baseSnapshot, _baseSnapshot.Length, this.RoslynSnapshot); } } public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) { this.RoslynSnapshot.CopyTo(sourceIndex, destination, destinationIndex, count); } public override void Write(TextWriter textWriter, TextSpan span, CancellationToken cancellationToken) { this.RoslynSnapshot.Write(textWriter, span.ToSpan()); } #region GetChangeRangesImplementation public override IReadOnlyList GetChangeRanges(SourceText oldText) { if (oldText == null) { throw new ArgumentNullException("oldText"); } // if they are the same text there is no change. if (oldText == this) { return TextChangeRange.NoChanges; } // first, check whether the text buffer is still alive. var container = this.Container as TextBufferContainer; if (container != null) { var lastEventArgs = container.LastEventArgs; if (lastEventArgs != null && lastEventArgs.OldText == oldText && lastEventArgs.NewText == this) { return lastEventArgs.Changes; } } var oldSnapshot = oldText.FindCorrespondingEditorTextSnapshot(); var newSnapshot = this.FindCorrespondingEditorTextSnapshot(); return GetChangeRanges(oldSnapshot, oldText.Length, newSnapshot); } private IReadOnlyList GetChangeRanges(ITextSnapshot oldSnapshot, int oldTextLength, ITextSnapshot newSnapshot) { if (oldSnapshot == null || newSnapshot == null || oldSnapshot.TextBuffer != newSnapshot.TextBuffer) { // Claim its all changed Logger.Log(FunctionId.Workspace_SourceText_GetChangeRanges, "Invalid Snapshots"); return ImmutableArray.Create(new TextChangeRange(new TextSpan(0, oldTextLength), this.Length)); } else if (oldSnapshot.Version.ReiteratedVersionNumber == newSnapshot.Version.ReiteratedVersionNumber) { // content of two snapshot must be same even if versions are different return TextChangeRange.NoChanges; } else { return GetChangeRanges(oldSnapshot, newSnapshot, forward: oldSnapshot.Version.VersionNumber <= newSnapshot.Version.VersionNumber); } } private static readonly Func s_forwardTextChangeRange = c => CreateTextChangeRange(c, forward: true); private static readonly Func s_backwardTextChangeRange = c => CreateTextChangeRange(c, forward: false); private IReadOnlyList GetChangeRanges(ITextSnapshot snapshot1, ITextSnapshot snapshot2, bool forward) { var oldSnapshot = forward ? snapshot1 : snapshot2; var newSnapshot = forward ? snapshot2 : snapshot1; INormalizedTextChangeCollection changes = null; for (var oldVersion = oldSnapshot.Version; oldVersion != newSnapshot.Version; oldVersion = oldVersion.Next) { if (oldVersion.Changes.Count != 0) { if (changes != null) { // Oops - more than one "textual" change between these snapshots, bail and try to find smallest changes span Logger.Log(FunctionId.Workspace_SourceText_GetChangeRanges, s_textLog, snapshot1.Version.VersionNumber, snapshot2.Version.VersionNumber); return new[] { GetChangeRanges(oldSnapshot.Version, newSnapshot.Version, forward) }; } else { changes = oldVersion.Changes; } } } if (changes == null) { return ImmutableArray.Create(); } else { return ImmutableArray.CreateRange(changes.Select(forward ? s_forwardTextChangeRange : s_backwardTextChangeRange)); } } private TextChangeRange GetChangeRanges(ITextVersion oldVersion, ITextVersion newVersion, bool forward) { TextChangeRange? range = null; var iterator = GetMultipleVersionTextChanges(oldVersion, newVersion, forward); foreach (var changes in forward ? iterator : iterator.Reverse()) { range = range.Accumulate(changes); } Contract.Requires(range.HasValue); return range.Value; } private static IEnumerable> GetMultipleVersionTextChanges(ITextVersion oldVersion, ITextVersion newVersion, bool forward) { for (var version = oldVersion; version != newVersion; version = version.Next) { yield return version.Changes.Select(forward ? s_forwardTextChangeRange : s_backwardTextChangeRange); } } private static TextChangeRange CreateTextChangeRange(ITextChange change, bool forward) { if (forward) { return new TextChangeRange(new TextSpan(change.OldSpan.Start, change.OldSpan.Length), change.NewLength); } else { return new TextChangeRange(new TextSpan(change.NewSpan.Start, change.NewSpan.Length), change.OldLength); } } #endregion } } }