Merge pull request #22903 from dotnet/dev/toddgrun/NoMoreLineTrackingStringBuffer

Get rid of LineTrackingStringBuffer class and instead use the line in…
This commit is contained in:
Todd Grunke 2020-06-15 14:38:37 -07:00 committed by GitHub
commit ef0eff7089
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 81 additions and 284 deletions

View File

@ -75,45 +75,31 @@ namespace Microsoft.AspNetCore.Razor.Language
// We always consider a document to have at least a 0th line, even if it's empty.
starts.Add(0);
var unprocessedCR = false;
// Length - 1 because we don't care if there was a linebreak as the last character.
var length = _document.Length;
for (var i = 0; i < length - 1; i++)
for (var i = 0; i < length; i++)
{
var c = _document[i];
var isLineBreak = false;
switch (c)
{
case '\r':
unprocessedCR = true;
continue;
if (i + 1 < length && _document[i + 1] == '\n')
{
i++;
}
starts.Add(i + 1);
break;
case '\n':
unprocessedCR = false;
isLineBreak = true;
starts.Add(i + 1);
break;
case '\u0085':
case '\u2028':
case '\u2029':
isLineBreak = true;
starts.Add(i + 1);
break;
}
if (unprocessedCR)
{
// If we get here it means that we had a CR followed by something other than an LF.
// Add the CR as a line break.
starts.Add(i);
unprocessedCR = false;
}
if (isLineBreak)
{
starts.Add(i + 1);
}
}

View File

@ -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<TextLine> _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<TextLine>();
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;
}
}
}
}

View File

@ -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;

View File

@ -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
var nextLineIndex = _cachedLineInfo.LineIndex + 1;
var nextLineLength = _sourceDocument.Lines.GetLineLength(nextLineIndex);
var 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
var prevLineIndex = _cachedLineInfo.LineIndex - 1;
var prevLineLength = _sourceDocument.Lines.GetLineLength(prevLineIndex);
var 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);
var lineLength = _sourceDocument.Lines.GetLineLength(_location.LineIndex);
var 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;
}
}
}

View File

@ -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);
}
}
}