diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LineTrackingStringBuffer.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LineTrackingStringBuffer.cs deleted file mode 100644 index a235bb482b..0000000000 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LineTrackingStringBuffer.cs +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) .NET Foundation. 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.Diagnostics; -using System.Text; - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - internal class LineTrackingStringBuffer - { - private readonly IList _lines; - private readonly string _filePath; - private TextLine _currentLine; - - public LineTrackingStringBuffer(string content, string filePath) - : this(content.ToCharArray(), filePath) - { - } - - public LineTrackingStringBuffer(char[] content, string filePath) - { - _lines = new List(); - - BuildTextLines(content); - - _filePath = filePath; - } - - public int Length - { - get { return _lines[_lines.Count - 1].End; } - } - - public SourceLocation EndLocation - { - get { return new SourceLocation(_filePath, Length, _lines.Count - 1, _lines[_lines.Count - 1].Length); } - } - - public CharacterReference CharAt(int absoluteIndex) - { - var line = FindLine(absoluteIndex); - if (line.IsDefault) - { - throw new ArgumentOutOfRangeException(nameof(absoluteIndex)); - } - var idx = absoluteIndex - line.Start; - return new CharacterReference(line.Content[idx], new SourceLocation(_filePath, absoluteIndex, line.Index, idx)); - } - - private void BuildTextLines(char[] content) - { - string lineText; - var lineStart = 0; - - for (int i = 0; i < content.Length; i++) - { - if (ParserHelpers.IsNewLine(content[i])) - { - // \r on it's own: Start a new line, otherwise wait for \n - // Other Newline: Start a new line - if (content[i] == '\r' && i + 1 < content.Length && content[i + 1] == '\n') - { - i++; - } - - lineText = new string(content, lineStart, (i - lineStart) + 1); // +1 to include the current char - _lines.Add(new TextLine(lineStart, _lines.Count, lineText)); - - lineStart = i + 1; - } - } - - lineText = new string(content, lineStart, content.Length - lineStart); // no +1 as content.Length points past the last char already - _lines.Add(new TextLine(lineStart, _lines.Count, lineText)); - } - - private TextLine FindLine(int absoluteIndex) - { - TextLine selected; - - if (_currentLine.IsDefault) - { - // Scan from line 0 - selected = ScanLines(absoluteIndex, 0, _lines.Count); - } - else if (absoluteIndex >= _currentLine.End) - { - if (_currentLine.Index + 1 < _lines.Count) - { - // This index is after the last read line - var nextLine = _lines[_currentLine.Index + 1]; - - // Optimization to not search if it's the common case where the line after _currentLine is being requested. - if (nextLine.Contains(absoluteIndex)) - { - selected = nextLine; - } - else - { - selected = ScanLines(absoluteIndex, _currentLine.Index, _lines.Count); - } - } - else - { - selected = default; - } - } - else if (absoluteIndex < _currentLine.Start) - { - if (_currentLine.Index > 0) - { - // This index is before the last read line - var prevLine = _lines[_currentLine.Index - 1]; - - // Optimization to not search if it's the common case where the line before _currentLine is being requested. - if (prevLine.Contains(absoluteIndex)) - { - selected = prevLine; - } - else - { - selected = ScanLines(absoluteIndex, 0, _currentLine.Index); - } - } - else - { - selected = default; - } - } - else - { - // This index is on the last read line - selected = _currentLine; - } - - Debug.Assert(selected.IsDefault || selected.Contains(absoluteIndex)); - _currentLine = selected; - return selected; - } - - private TextLine ScanLines(int absoluteIndex, int startLineIndex, int endLineIndex) - { - // binary search for the line containing absoluteIndex - var lowIndex = startLineIndex; - var highIndex = endLineIndex; - - while (lowIndex != highIndex) - { - var midIndex = (lowIndex + highIndex) / 2; - var midLine = _lines[midIndex]; - - if (absoluteIndex >= midLine.End) - { - lowIndex = midIndex + 1; - } - else if (absoluteIndex < midLine.Start) - { - highIndex = midIndex; - } - else - { - return midLine; - } - } - - return default; - } - - internal struct CharacterReference - { - public CharacterReference(char character, SourceLocation location) - { - Character = character; - Location = location; - } - - public char Character { get; } - - public SourceLocation Location { get; } - } - - private struct TextLine - { - public TextLine(int start, int index, string content) - { - Start = start; - Index = index; - Content = content; - } - - public string Content { get; } - - public bool IsDefault => Content == null; - - public int Length - { - get { return Content.Length; } - } - - public int Start { get; } - public int Index { get; } - - public int End - { - get { return Start + Length; } - } - - public bool Contains(int index) - { - return index < End && index >= Start; - } - } - } -} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/ParserContext.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/ParserContext.cs index e976191f92..5b35faa1d6 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/ParserContext.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/ParserContext.cs @@ -18,10 +18,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } SourceDocument = source; - var chars = new char[source.Length]; - source.CopyTo(0, chars, 0, source.Length); - Source = new SeekableTextReader(chars, source.FilePath); + Source = new SeekableTextReader(SourceDocument); DesignTimeMode = options.DesignTime; FeatureFlags = options.FeatureFlags; ParseLeadingDirectives = options.ParseLeadingDirectives; diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/SeekableTextReader.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/SeekableTextReader.cs index 85057cf9fb..d50a6efd4a 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/SeekableTextReader.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/SeekableTextReader.cs @@ -3,32 +3,36 @@ using System; using System.IO; +using System.Text; +using Microsoft.AspNetCore.Razor.Language.Syntax; namespace Microsoft.AspNetCore.Razor.Language.Legacy { internal class SeekableTextReader : TextReader, ITextDocument { - private readonly LineTrackingStringBuffer _buffer; + private readonly RazorSourceDocument _sourceDocument; private int _position = 0; private int _current; private SourceLocation _location; + private (TextSpan Span, int LineIndex) _cachedLineInfo; - public SeekableTextReader(string source, string filePath) : this(source.ToCharArray(), filePath) { } + public SeekableTextReader(string source, string filePath) : this(new StringSourceDocument(source, Encoding.UTF8, new RazorSourceDocumentProperties(filePath, relativePath: null))) { } - public SeekableTextReader(char[] source, string filePath) + public SeekableTextReader(RazorSourceDocument source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } - _buffer = new LineTrackingStringBuffer(source, filePath); + _sourceDocument = source; + _cachedLineInfo = (new TextSpan(0, _sourceDocument.Lines.GetLineLength(0)), 0); UpdateState(); } public SourceLocation Location => _location; - public int Length => _buffer.Length; + public int Length => _sourceDocument.Length; public int Position { @@ -55,22 +59,73 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy private void UpdateState() { - if (_position < _buffer.Length) + if (_cachedLineInfo.Span.Contains(_position)) { - var chr = _buffer.CharAt(_position); - _current = chr.Character; - _location = chr.Location; + _location = new SourceLocation(_sourceDocument.FilePath, _position, _cachedLineInfo.LineIndex, _position - _cachedLineInfo.Span.Start); + _current = _sourceDocument[_location.AbsoluteIndex]; + + return; } - else if (_buffer.Length == 0) + + if (_position < _sourceDocument.Length) + { + if (_position >= _cachedLineInfo.Span.End) + { + // Try to avoid the GetLocation call by checking if the next line contains the position + int nextLineIndex = _cachedLineInfo.LineIndex + 1; + int nextLineLength = _sourceDocument.Lines.GetLineLength(nextLineIndex); + TextSpan nextLineSpan = new TextSpan(_cachedLineInfo.Span.End, nextLineLength); + + if (nextLineSpan.Contains(_position)) + { + _cachedLineInfo = (nextLineSpan, nextLineIndex); + _location = new SourceLocation(_sourceDocument.FilePath, _position, nextLineIndex, _position - nextLineSpan.Start); + _current = _sourceDocument[_location.AbsoluteIndex]; + + return; + } + } + else + { + // Try to avoid the GetLocation call by checking if the previous line contains the position + int prevLineIndex = _cachedLineInfo.LineIndex - 1; + int prevLineLength = _sourceDocument.Lines.GetLineLength(prevLineIndex); + TextSpan prevLineSpan = new TextSpan(_cachedLineInfo.Span.Start - prevLineLength, prevLineLength); + + if (prevLineSpan.Contains(_position)) + { + _cachedLineInfo = (prevLineSpan, prevLineIndex); + _location = new SourceLocation(_sourceDocument.FilePath, _position, prevLineIndex, _position - prevLineSpan.Start); + _current = _sourceDocument[_location.AbsoluteIndex]; + + return; + } + } + + // The call to GetLocation is expensive + _location = _sourceDocument.Lines.GetLocation(_position); + + int lineLength = _sourceDocument.Lines.GetLineLength(_location.LineIndex); + TextSpan lineSpan = new TextSpan(_position - _location.CharacterIndex, lineLength); + _cachedLineInfo = (lineSpan, _location.LineIndex); + + _current = _sourceDocument[_location.AbsoluteIndex]; + + return; + } + + if (_sourceDocument.Length == 0) { - _current = -1; _location = SourceLocation.Zero; - } - else - { _current = -1; - _location = _buffer.EndLocation; + + return; } + + var lineNumber = _sourceDocument.Lines.Count - 1; + _location = new SourceLocation(_sourceDocument.FilePath, Length, lineNumber, _sourceDocument.Lines.GetLineLength(lineNumber)); + + _current = -1; } } } diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Legacy/LineTrackingStringBufferTest.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Legacy/LineTrackingStringBufferTest.cs deleted file mode 100644 index 27179a9f6d..0000000000 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Legacy/LineTrackingStringBufferTest.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Xunit; - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - public class LineTrackingStringBufferTest - { - [Fact] - public void CtorInitializesProperties() - { - var buffer = new LineTrackingStringBuffer(string.Empty, "test.cshtml"); - Assert.Equal(0, buffer.Length); - } - - [Fact] - public void CharAtCorrectlyReturnsLocation() - { - var buffer = new LineTrackingStringBuffer("foo\rbar\nbaz\r\nbiz", "test.cshtml"); - var chr = buffer.CharAt(14); - Assert.Equal('i', chr.Character); - Assert.Equal(new SourceLocation("test.cshtml", 14, 3, 1), chr.Location); - } - } -}