Example #1
0
        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;
        }
Example #2
0
 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);
        }
Example #4
0
        // 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;
        }
Example #5
0
        public string BuildExpressionPadding(Span target, int generatedStart)
        {
            if (target == null)
            {
                throw new ArgumentNullException(nameof(target));
            }

            var padding = CalculatePadding(target, generatedStart);

            return BuildPaddingInternal(padding);
        }
Example #6
0
 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;
        }
Example #9
0
        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();
        }
Example #10
0
 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);
        }
Example #12
0
        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));
        }
Example #13
0
 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));
 }
Example #24
0
        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());
                }
            }
        }
Example #25
0
        // 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;
 }
Example #27
0
        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);
        }
Example #29
0
 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 == ".";
        }