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:
commit
ef0eff7089
|
|
@ -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.
|
// We always consider a document to have at least a 0th line, even if it's empty.
|
||||||
starts.Add(0);
|
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;
|
var length = _document.Length;
|
||||||
for (var i = 0; i < length - 1; i++)
|
for (var i = 0; i < length; i++)
|
||||||
{
|
{
|
||||||
var c = _document[i];
|
var c = _document[i];
|
||||||
var isLineBreak = false;
|
|
||||||
|
|
||||||
switch (c)
|
switch (c)
|
||||||
{
|
{
|
||||||
case '\r':
|
case '\r':
|
||||||
unprocessedCR = true;
|
if (i + 1 < length && _document[i + 1] == '\n')
|
||||||
continue;
|
{
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
starts.Add(i + 1);
|
||||||
|
break;
|
||||||
|
|
||||||
case '\n':
|
case '\n':
|
||||||
unprocessedCR = false;
|
starts.Add(i + 1);
|
||||||
isLineBreak = true;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '\u0085':
|
case '\u0085':
|
||||||
case '\u2028':
|
case '\u2028':
|
||||||
case '\u2029':
|
case '\u2029':
|
||||||
isLineBreak = true;
|
starts.Add(i + 1);
|
||||||
break;
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,10 +18,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
|
||||||
}
|
}
|
||||||
|
|
||||||
SourceDocument = source;
|
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;
|
DesignTimeMode = options.DesignTime;
|
||||||
FeatureFlags = options.FeatureFlags;
|
FeatureFlags = options.FeatureFlags;
|
||||||
ParseLeadingDirectives = options.ParseLeadingDirectives;
|
ParseLeadingDirectives = options.ParseLeadingDirectives;
|
||||||
|
|
|
||||||
|
|
@ -3,32 +3,36 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language.Syntax;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Language.Legacy
|
namespace Microsoft.AspNetCore.Razor.Language.Legacy
|
||||||
{
|
{
|
||||||
internal class SeekableTextReader : TextReader, ITextDocument
|
internal class SeekableTextReader : TextReader, ITextDocument
|
||||||
{
|
{
|
||||||
private readonly LineTrackingStringBuffer _buffer;
|
private readonly RazorSourceDocument _sourceDocument;
|
||||||
private int _position = 0;
|
private int _position = 0;
|
||||||
private int _current;
|
private int _current;
|
||||||
private SourceLocation _location;
|
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)
|
if (source == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(source));
|
throw new ArgumentNullException(nameof(source));
|
||||||
}
|
}
|
||||||
|
|
||||||
_buffer = new LineTrackingStringBuffer(source, filePath);
|
_sourceDocument = source;
|
||||||
|
_cachedLineInfo = (new TextSpan(0, _sourceDocument.Lines.GetLineLength(0)), 0);
|
||||||
UpdateState();
|
UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SourceLocation Location => _location;
|
public SourceLocation Location => _location;
|
||||||
|
|
||||||
public int Length => _buffer.Length;
|
public int Length => _sourceDocument.Length;
|
||||||
|
|
||||||
public int Position
|
public int Position
|
||||||
{
|
{
|
||||||
|
|
@ -55,22 +59,73 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
|
||||||
|
|
||||||
private void UpdateState()
|
private void UpdateState()
|
||||||
{
|
{
|
||||||
if (_position < _buffer.Length)
|
if (_cachedLineInfo.Span.Contains(_position))
|
||||||
{
|
{
|
||||||
var chr = _buffer.CharAt(_position);
|
_location = new SourceLocation(_sourceDocument.FilePath, _position, _cachedLineInfo.LineIndex, _position - _cachedLineInfo.Span.Start);
|
||||||
_current = chr.Character;
|
_current = _sourceDocument[_location.AbsoluteIndex];
|
||||||
_location = chr.Location;
|
|
||||||
|
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;
|
_location = SourceLocation.Zero;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_current = -1;
|
_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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue