internal int CalculatePadding(Span target, int generatedStart) { if (target == null) { throw new ArgumentNullException(nameof(target)); } int padding; padding = CollectSpacesAndTabs(target, _host.TabSize) - generatedStart; // if we add generated text that is longer than the padding we wanted to insert we have no recourse and we have to skip padding // example: // Razor code at column zero: @somecode() // Generated code will be: // In design time: __o = somecode(); // In Run time: Write(somecode()); // // In both cases the padding would have been 1 space to remote the space the @ symbol takes, which will be smaller than the 6 // chars the hidden generated code takes. if (padding < 0) { padding = 0; } return padding; }
public virtual bool OwnsChange(Span target, TextChange change) { var end = target.Start.AbsoluteIndex + target.Length; var changeOldEnd = change.OldPosition + change.OldLength; return change.OldPosition >= target.Start.AbsoluteIndex && (changeOldEnd < end || (changeOldEnd == end && AcceptedCharacters != AcceptedCharacters.None)); }
public void ParseInjectKeyword_AllowsOptionalTrailingSemicolon( string injectStatement, string expectedService, string expectedPropertyName) { // Arrange var documentContent = "@inject " + injectStatement; var factory = SpanFactory.CreateCsHtml(); var errors = new List<RazorError>(); var expectedSpans = new Span[] { factory.EmptyHtml(), factory.CodeTransition(SyntaxConstants.TransitionString) .Accepts(AcceptedCharacters.None), factory.MetaCode("inject ") .Accepts(AcceptedCharacters.None), factory.Code(injectStatement) .As(new InjectParameterGenerator(expectedService, expectedPropertyName)) .Accepts(AcceptedCharacters.AnyExceptNewline), factory.EmptyHtml() }; // Act var spans = ParseDocument(documentContent, errors); // Assert Assert.Equal(expectedSpans, spans); Assert.Empty(errors); }
// Special case for statement padding to account for brace positioning in the editor. public string BuildStatementPadding(Span target) { if (target == null) { throw new ArgumentNullException(nameof(target)); } var padding = CalculatePadding(target, generatedStart: 0); // We treat statement padding specially so for brace positioning, so that in the following example: // @if (foo > 0) // { // } // // the braces shows up under the @ rather than under the if. if (_host.DesignTimeMode && padding > 0 && target.Previous.Kind == SpanKind.Transition && // target.Previous is guaranteed to not be null if you have padding. string.Equals(target.Previous.Content, SyntaxConstants.TransitionString, StringComparison.Ordinal)) { padding--; } var generatedCode = BuildPaddingInternal(padding); return generatedCode; }
public string BuildExpressionPadding(Span target, int generatedStart) { if (target == null) { throw new ArgumentNullException(nameof(target)); } var padding = CalculatePadding(target, generatedStart); return BuildPaddingInternal(padding); }
internal void CalculateStart(Span prev) { if (prev == null) { Start = SourceLocation.Zero; } else { Start = new SourceLocationTracker(prev.Start).UpdateLocation(prev.Content).CurrentLocation; } }
protected override PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange) { if (((AutoCompleteAtEndOfSpan && IsAtEndOfSpan(target, normalizedChange)) || IsAtEndOfFirstLine(target, normalizedChange)) && normalizedChange.IsInsert && ParserHelpers.IsNewLine(normalizedChange.NewText) && AutoCompleteString != null) { return PartialParseResult.Rejected | PartialParseResult.AutoCompleteBlock; } return PartialParseResult.Rejected; }
protected override PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange) { if (AcceptedCharacters == AcceptedCharacters.Any) { return PartialParseResult.Rejected; } // In some editors intellisense insertions are handled as "dotless commits". If an intellisense selection is confirmed // via something like '.' a dotless commit will append a '.' and then insert the remaining intellisense selection prior // to the appended '.'. This 'if' statement attempts to accept the intermediate steps of a dotless commit via // intellisense. It will accept two cases: // 1. '@foo.' -> '@foobaz.'. // 2. '@foobaz..' -> '@foobaz.bar.'. Includes Sub-cases '@foobaz()..' -> '@foobaz().bar.' etc. // The key distinction being the double '.' in the second case. if (IsDotlessCommitInsertion(target, normalizedChange)) { return HandleDotlessCommitInsertion(target); } if (IsAcceptableReplace(target, normalizedChange)) { return HandleReplacement(target, normalizedChange); } var changeRelativePosition = normalizedChange.OldPosition - target.Start.AbsoluteIndex; // Get the edit context char? lastChar = null; if (changeRelativePosition > 0 && target.Content.Length > 0) { lastChar = target.Content[changeRelativePosition - 1]; } // Don't support 0->1 length edits if (lastChar == null) { return PartialParseResult.Rejected; } // Accepts cases when insertions are made at the end of a span or '.' is inserted within a span. if (IsAcceptableInsertion(target, normalizedChange)) { // Handle the insertion return HandleInsertion(target, lastChar.Value, normalizedChange); } if (IsAcceptableDeletion(target, normalizedChange)) { return HandleDeletion(target, lastChar.Value, normalizedChange); } return PartialParseResult.Rejected; }
protected override SyntaxTreeNode RewriteSpan(BlockBuilder parent, Span span) { // Only rewrite if we have a previous that is also markup (CanRewrite does this check for us!) var previous = parent.Children.LastOrDefault() as Span; if (previous == null || !CanRewrite(previous)) { return span; } // Merge spans parent.Children.Remove(previous); var merged = new SpanBuilder(); FillSpan(merged, previous.Start, previous.Content + span.Content); return merged.Build(); }
public override void VisitSpan(Span span) { if (CanRewrite(span)) { var newNode = RewriteSpan(_blocks.Peek(), span); if (newNode != null) { _blocks.Peek().Children.Add(newNode); } } else { _blocks.Peek().Children.Add(span); } }
public void CalculatePaddingForEmptySpanReturnsZero() { // Arrange var host = CreateHost(designTime: true); var span = new Span(new SpanBuilder()); var paddingBuilder = new CSharpPaddingBuilder(host); // Act var padding = paddingBuilder.CalculatePadding(span, 0); // Assert Assert.Equal(0, padding); }
public virtual EditResult ApplyChange(Span target, TextChange change, bool force) { var result = PartialParseResult.Accepted; var normalized = change.Normalize(); if (!force) { result = CanAcceptChange(target, normalized); } // If the change is accepted then apply the change if ((result & PartialParseResult.Accepted) == PartialParseResult.Accepted) { return new EditResult(result, UpdateSpan(target, normalized)); } return new EditResult(result, new SpanBuilder(target)); }
protected virtual SpanBuilder UpdateSpan(Span target, TextChange normalizedChange) { var newContent = normalizedChange.ApplyChange(target); var newSpan = new SpanBuilder(target); newSpan.ClearSymbols(); foreach (ISymbol sym in Tokenizer(newContent)) { sym.OffsetStart(target.Start); newSpan.Accept(sym); } if (target.Next != null) { var newEnd = SourceLocationTracker.CalculateNewLocation(target.Start, newContent); target.Next.ChangeStart(newEnd); } return newSpan; }
public override void VisitSpan(Span span) { if (span == null) { throw new ArgumentNullException(nameof(span)); } TagHelperDirectiveType directiveType; if (span.ChunkGenerator is AddTagHelperChunkGenerator) { directiveType = TagHelperDirectiveType.AddTagHelper; } else if (span.ChunkGenerator is RemoveTagHelperChunkGenerator) { directiveType = TagHelperDirectiveType.RemoveTagHelper; } else if (span.ChunkGenerator is TagHelperPrefixDirectiveChunkGenerator) { directiveType = TagHelperDirectiveType.TagHelperPrefix; } else { // Not a chunk generator that we're interested in. return; } var directiveText = span.Content.Trim(); var startOffset = span.Content.IndexOf(directiveText, StringComparison.Ordinal); var offsetContent = span.Content.Substring(0, startOffset); var offsetTextLocation = SourceLocation.Advance(span.Start, offsetContent); var directiveDescriptor = new TagHelperDirectiveDescriptor { DirectiveText = directiveText, Location = offsetTextLocation, DirectiveType = directiveType }; _directiveDescriptors.Add(directiveDescriptor); }
public void ParseModelKeyword_HandlesSingleInstance() { // Arrange + Act var document = "@model Foo"; var spans = ParseDocument(document); // Assert var factory = SpanFactory.CreateCsHtml(); var expectedSpans = new Span[] { factory.EmptyHtml(), factory.CodeTransition(SyntaxConstants.TransitionString) .Accepts(AcceptedCharacters.None), factory.MetaCode("model ") .Accepts(AcceptedCharacters.None), factory.Code(" Foo") .As(new ModelChunkGenerator("Foo")) .Accepts(AcceptedCharacters.AnyExceptNewline), factory.EmptyHtml() }; Assert.Equal(expectedSpans, spans.ToArray()); }
public void ParseModelKeyword_HandlesNullableTypes() { // Arrange + Act var document = $"@model Foo?{Environment.NewLine}Bar"; var spans = ParseDocument(document); // Assert var factory = SpanFactory.CreateCsHtml(); var expectedSpans = new Span[] { factory.EmptyHtml(), factory.CodeTransition(SyntaxConstants.TransitionString) .Accepts(AcceptedCharacters.None), factory.MetaCode("model ") .Accepts(AcceptedCharacters.None), factory.Code("Foo?" + Environment.NewLine) .As(new ModelChunkGenerator("Foo?")) .Accepts(AcceptedCharacters.AnyExceptNewline), factory.Markup("Bar") .With(new MarkupChunkGenerator()) }; Assert.Equal(expectedSpans, spans.ToArray()); }
private PartialParseResult TryAcceptChange(Span target, TextChange change, PartialParseResult acceptResult = PartialParseResult.Accepted) { var content = change.ApplyChange(target); if (StartsWithKeyword(content)) { return PartialParseResult.Rejected | PartialParseResult.SpanContextChanged; } return acceptResult; }
private PartialParseResult HandleInsertionAfterDot(Span target, TextChange change) { // If the insertion is a full identifier or another dot, accept it if (ParserHelpers.IsIdentifier(change.NewText) || change.NewText == ".") { return TryAcceptChange(target, change); } return PartialParseResult.Rejected; }
private PartialParseResult HandleInsertionAfterIdPart(Span target, TextChange change) { // If the insertion is a full identifier part, accept it if (ParserHelpers.IsIdentifier(change.NewText, requireIdentifierStart: false)) { return TryAcceptChange(target, change); } else if (EndsWithDot(change.NewText)) { // Accept it, possibly provisionally var result = PartialParseResult.Accepted; if (!AcceptTrailingDot) { result |= PartialParseResult.Provisional; } return TryAcceptChange(target, change, result); } else { return PartialParseResult.Rejected; } }
private PartialParseResult HandleInsertion(Span target, char previousChar, TextChange change) { // What are we inserting after? if (previousChar == '.') { return HandleInsertionAfterDot(target, change); } else if (ParserHelpers.IsIdentifierPart(previousChar) || previousChar == ')' || previousChar == ']') { return HandleInsertionAfterIdPart(target, change); } else { return PartialParseResult.Rejected; } }
private PartialParseResult HandleDeletion(Span target, char previousChar, TextChange change) { // What's left after deleting? if (previousChar == '.') { return TryAcceptChange(target, change, PartialParseResult.Accepted | PartialParseResult.Provisional); } else if (ParserHelpers.IsIdentifierPart(previousChar)) { return TryAcceptChange(target, change); } else { return PartialParseResult.Rejected; } }
private PartialParseResult HandleReplacement(Span target, TextChange change) { // Special Case for IntelliSense commits. // When IntelliSense commits, we get two changes (for example user typed "Date", then committed "DateTime" by pressing ".") // 1. Insert "." at the end of this span // 2. Replace the "Date." at the end of the span with "DateTime." // We need partial parsing to accept case #2. var oldText = GetOldText(target, change); var result = PartialParseResult.Rejected; if (EndsWithDot(oldText) && EndsWithDot(change.NewText)) { result = PartialParseResult.Accepted; if (!AcceptTrailingDot) { result |= PartialParseResult.Provisional; } } return result; }
private static bool RemainingIsWhitespace(Span target, TextChange change) { var offset = (change.OldPosition - target.Start.AbsoluteIndex) + change.OldLength; return string.IsNullOrWhiteSpace(target.Content.Substring(offset)); }
private void OnDocumentParseComplete(DocumentParseCompleteEventArgs args) { using (_parser.SynchronizeMainThreadState()) { _currentParseTree = args.GeneratorResults.Document; _lastChangeOwner = null; } Debug.Assert(args != null, "Event arguments cannot be null"); EventHandler<DocumentParseCompleteEventArgs> handler = DocumentParseComplete; if (handler != null) { try { handler(this, args); } catch (Exception ex) { Debug.WriteLine("[RzEd] Document Parse Complete Handler Threw: " + ex.ToString()); } } }
// This method handles cases when the attribute is a simple span attribute such as // class="something moresomething". This does not handle complex attributes such as // class="@myclass". Therefore the span.Content is equivalent to the entire attribute. private static TryParseResult TryParseSpan( Span span, IEnumerable<TagHelperDescriptor> descriptors, ErrorSink errorSink) { var afterEquals = false; var builder = new SpanBuilder { ChunkGenerator = span.ChunkGenerator, EditHandler = span.EditHandler, Kind = span.Kind }; // Will contain symbols that represent a single attribute value: <input| class="btn"| /> var htmlSymbols = span.Symbols.OfType<HtmlSymbol>().ToArray(); var capturedAttributeValueStart = false; var attributeValueStartLocation = span.Start; // Default to DoubleQuotes. We purposefully do not persist NoQuotes ValueStyle to stay consistent with the // TryParseBlock() variation of attribute parsing. var attributeValueStyle = HtmlAttributeValueStyle.DoubleQuotes; // The symbolOffset is initialized to 0 to expect worst case: "class=". If a quote is found later on for // the attribute value the symbolOffset is adjusted accordingly. var symbolOffset = 0; string name = null; // Iterate down through the symbols to find the name and the start of the value. // We subtract the symbolOffset so we don't accept an ending quote of a span. for (var i = 0; i < htmlSymbols.Length - symbolOffset; i++) { var symbol = htmlSymbols[i]; if (afterEquals) { // We've captured all leading whitespace, the attribute name, and an equals with an optional // quote/double quote. We're now at: " asp-for='|...'" or " asp-for=|..." // The goal here is to capture all symbols until the end of the attribute. Note this will not // consume an ending quote due to the symbolOffset. // When symbols are accepted into SpanBuilders, their locations get altered to be offset by the // parent which is why we need to mark our start location prior to adding the symbol. // This is needed to know the location of the attribute value start within the document. if (!capturedAttributeValueStart) { capturedAttributeValueStart = true; attributeValueStartLocation = span.Start + symbol.Start; } builder.Accept(symbol); } else if (name == null && HtmlMarkupParser.IsValidAttributeNameSymbol(symbol)) { // We've captured all leading whitespace prior to the attribute name. // We're now at: " |asp-for='...'" or " |asp-for=..." // The goal here is to capture the attribute name. var symbolContents = htmlSymbols .Skip(i) // Skip prefix .TakeWhile(nameSymbol => HtmlMarkupParser.IsValidAttributeNameSymbol(nameSymbol)) .Select(nameSymbol => nameSymbol.Content); // Move the indexer past the attribute name symbols. i += symbolContents.Count() - 1; name = string.Concat(symbolContents); attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, name); } else if (symbol.Type == HtmlSymbolType.Equals) { // We've captured all leading whitespace and the attribute name. // We're now at: " asp-for|='...'" or " asp-for|=..." // The goal here is to consume the equal sign and the optional single/double-quote. // The coming symbols will either be a quote or value (in the case that the value is unquoted). SourceLocation symbolStartLocation; // Skip the whitespace preceding the start of the attribute value. do { i++; // Start from the symbol after '='. } while (i < htmlSymbols.Length && (htmlSymbols[i].Type == HtmlSymbolType.WhiteSpace || htmlSymbols[i].Type == HtmlSymbolType.NewLine)); // Check for attribute start values, aka single or double quote if (i < htmlSymbols.Length && IsQuote(htmlSymbols[i])) { if (htmlSymbols[i].Type == HtmlSymbolType.SingleQuote) { attributeValueStyle = HtmlAttributeValueStyle.SingleQuotes; } symbolStartLocation = htmlSymbols[i].Start; // If there's a start quote then there must be an end quote to be valid, skip it. symbolOffset = 1; } else { // We are at the symbol after equals. Go back to equals to ensure we don't skip past that symbol. i--; symbolStartLocation = symbol.Start; } attributeValueStartLocation = span.Start + symbolStartLocation + new SourceLocation(absoluteIndex: 1, lineIndex: 0, characterIndex: 1); afterEquals = true; } else if (symbol.Type == HtmlSymbolType.WhiteSpace) { // We're at the start of the attribute, this branch may be hit on the first iterations of // the loop since the parser separates attributes with their spaces included as symbols. // We're at: "| asp-for='...'" or "| asp-for=..." // Note: This will not be hit even for situations like asp-for ="..." because the core Razor // parser currently does not know how to handle attributes in that format. This will be addressed // by https://github.com/aspnet/Razor/issues/123. attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, symbol.Content); } } // After all symbols have been added we need to set the builders start position so we do not indirectly // modify each symbol's Start location. builder.Start = attributeValueStartLocation; if (name == null) { // We couldn't find a name, if the original span content was whitespace it ultimately means the tag // that owns this "attribute" is malformed and is expecting a user to type a new attribute. // ex: <myTH class="btn"| | if (!string.IsNullOrWhiteSpace(span.Content)) { errorSink.OnError( span.Start, RazorResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed, span.Content.Length); } return null; } var result = CreateTryParseResult(name, descriptors); // If we're not after an equal then we should treat the value as if it were a minimized attribute. Span attributeValue = null; if (afterEquals) { attributeValue = CreateMarkupAttribute(builder, result.IsBoundNonStringAttribute); } else { attributeValueStyle = HtmlAttributeValueStyle.Minimized; } result.AttributeValueNode = attributeValue; result.AttributeValueStyle = attributeValueStyle; return result; }
private PartialParseResult HandleDotlessCommitInsertion(Span target) { var result = PartialParseResult.Accepted; if (!AcceptTrailingDot && target.Content.LastOrDefault() == '.') { result |= PartialParseResult.Provisional; } return result; }
private PartialParseResult TryPartialParse(TextChange change) { var result = PartialParseResult.Rejected; // Try the last change owner if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change)) { var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change); result = editResult.Result; if ((editResult.Result & PartialParseResult.Rejected) != PartialParseResult.Rejected) { _lastChangeOwner.ReplaceWith(editResult.EditedSpan); } return result; } // Locate the span responsible for this change _lastChangeOwner = CurrentParseTree.LocateOwner(change); if (LastResultProvisional) { // Last change owner couldn't accept this, so we must do a full reparse result = PartialParseResult.Rejected; } else if (_lastChangeOwner != null) { var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change); result = editResult.Result; if ((editResult.Result & PartialParseResult.Rejected) != PartialParseResult.Rejected) { _lastChangeOwner.ReplaceWith(editResult.EditedSpan); } if ((result & PartialParseResult.AutoCompleteBlock) == PartialParseResult.AutoCompleteBlock) { _lastAutoCompleteSpan = _lastChangeOwner; } else { _lastAutoCompleteSpan = null; } } return result; }
// Accepts character insertions at the end of spans. AKA: '@foo' -> '@fooo' or '@foo' -> '@foo ' etc. private static bool IsAcceptableEndInsertion(Span target, TextChange change) { Debug.Assert(change.IsInsert); return IsAtEndOfSpan(target, change) || RemainingIsWhitespace(target, change); }
private static void EvaluateSpan(ErrorCollector collector, Span actual, Span expected) { if (!Equals(expected, actual)) { AddMismatchError(collector, actual, expected); } else { AddPassedMessage(collector, expected); } }
// Accepts '.' insertions in the middle of spans. Ex: '@foo.baz.bar' -> '@foo..baz.bar' // This is meant to allow intellisense when editing a span. private static bool IsAcceptableInnerInsertion(Span target, TextChange change) { Debug.Assert(change.IsInsert); // Ensure that we're actually inserting in the middle of a span and not at the end. // This case will fail if the IsAcceptableEndInsertion does not capture an end insertion correctly. Debug.Assert(!IsAtEndOfSpan(target, change)); return change.NewPosition > 0 && change.NewText == "."; }