|
' Licensed to the .NET Foundation under one or more agreements.
' The .NET Foundation licenses this file to you under the MIT license.
' See the LICENSE file in the project root for more information.
Imports System.Composition
Imports System.Diagnostics.CodeAnalysis
Imports System.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.Editor.Implementation.EndConstructGeneration
Imports Microsoft.CodeAnalysis.Editor.Shared.Utilities
Imports Microsoft.CodeAnalysis.Host.Mef
Imports Microsoft.CodeAnalysis.Internal.Log
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
Imports Microsoft.VisualStudio.Text
Imports Microsoft.VisualStudio.Text.Editor
Imports Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods
Imports Microsoft.VisualStudio.Text.Operations
Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.EndConstructGeneration
<ExportLanguageService(GetType(IEndConstructGenerationService), LanguageNames.VisualBasic), [Shared]>
Friend Class VisualBasicEndConstructService
Implements IEndConstructGenerationService
Private ReadOnly _smartIndentationService As ISmartIndentationService
Private ReadOnly _undoHistoryRegistry As ITextUndoHistoryRegistry
Private ReadOnly _editorOperationsFactoryService As IEditorOperationsFactoryService
Private ReadOnly _editorOptionsFactoryService As IEditorOptionsFactoryService
<ImportingConstructor()>
<SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification:="Used in test code: https://github.com/dotnet/roslyn/issues/42814")>
Public Sub New(
smartIndentationService As ISmartIndentationService,
undoHistoryRegistry As ITextUndoHistoryRegistry,
editorOperationsFactoryService As IEditorOperationsFactoryService,
editorOptionsFactoryService As IEditorOptionsFactoryService)
ThrowIfNull(smartIndentationService)
ThrowIfNull(undoHistoryRegistry)
ThrowIfNull(editorOperationsFactoryService)
ThrowIfNull(editorOptionsFactoryService)
_smartIndentationService = smartIndentationService
_undoHistoryRegistry = undoHistoryRegistry
_editorOperationsFactoryService = editorOperationsFactoryService
_editorOptionsFactoryService = editorOptionsFactoryService
End Sub
Private Shared Function IsMissingStatementError(statement As SyntaxNode, [error] As String) As Boolean
Select Case [error]
' TODO(jasonmal): get rid of this. It is an open design goal to move missing end errors from the
' statement to the block. Besides making incremental parsing easier, it will also clean this mess up.
' Until then, I'm content with this hack.
Case "BC30012" ' Missing #End If
Return True
Case "BC30481" ' Missing End Class
Return True
Case "BC30625" ' Missing End Module
Return True
Case "BC30185" ' Missing End Enum
Return True
Case "BC30253" ' Missing End Interface
Return True
Case "BC30624" ' Missing End Structure
Return True
Case "BC30626" ' Missing End Namespace
Return True
Case "BC30026" ' Missing End Sub
Return True
Case "BC30027" ' Missing End Function
Return True
Case "BC30631" ' Missing End Get
Return True
Case "BC30633" ' Missing End Set
Return True
Case "BC30025" ' Missing End Property
Return True
Case "BC30081" ' Missing End If
Return True
Case "BC30082" ' Missing End While
Return True
Case "BC30083" ' Missing Loop
Return True
Case "BC30084" ' Missing Next
Return True
Case "BC30085" ' Missing End With
Return True
Case "BC30095" ' Missing End Select
Return True
Case "BC36008" ' Missing End Using
Return True
Case "BC30675" ' Missing End SyncLock
Return True
Case "BC30681" ' Missing #End Region
Return True
Case "BC33005" ' Missing End Operator
Return True
Case "BC36759" ' Auto-implemented properties cannot have parameters
Return True
Case "BC36673" ' Missing End Sub for Lambda
Return True
Case "BC36674" ' Missing End Function for Lambda
Return True
Case "BC30384" ' Missing End Try
Return True
Case "BC30198", "BC30199"
' These happen if I type Dim x = Function without parenthesis, so as long as we are in that content,
' count this as an acceptable error.
Return TypeOf statement Is LambdaHeaderSyntax
Case "BC31114" ' Missing End Event
Return True
End Select
Return False
End Function
Private Shared Function IsExpectedXmlNameError([error] As String) As Boolean
Return [error] = "BC31146"
End Function
Private Shared Function IsMissingXmlEndTagError([error] As String) As Boolean
Return [error] = "BC31151"
End Function
Private Shared Function IsExpectedXmlEndEmbeddedError([error] As String) As Boolean
Return [error] = "BC31159"
End Function
Private Shared Function IsExpectedXmlEndPIError([error] As String) As Boolean
Return [error] = "BC31160"
End Function
Private Shared Function IsExpectedXmlEndCommentError([error] As String) As Boolean
Return [error] = "BC31161"
End Function
Private Shared Function IsExpectedXmlEndCDataError([error] As String) As Boolean
Return [error] = "BC31162"
End Function
Private Function GetEndConstructState(textView As ITextView,
subjectBuffer As ITextBuffer,
cancellationToken As CancellationToken) As EndConstructState
Dim caretPosition = textView.GetCaretPoint(subjectBuffer)
If caretPosition Is Nothing Then
Return Nothing
End If
Dim document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges()
If document Is Nothing Then
Return Nothing
End If
Dim tree = document.GetSyntaxTreeSynchronously(cancellationToken)
Dim tokenToLeft = tree.FindTokenOnLeftOfPosition(caretPosition.Value, cancellationToken, includeDirectives:=True, includeDocumentationComments:=True)
If tokenToLeft.Kind = SyntaxKind.None Then
Return Nothing
End If
Dim bufferOptions = _editorOptionsFactoryService.GetOptions(subjectBuffer)
Return New EndConstructState(
caretPosition.Value, New Lazy(Of SemanticModel)(Function() document.GetSemanticModelAsync(cancellationToken).WaitAndGetResult(cancellationToken)), tree, tokenToLeft, bufferOptions.GetNewLineCharacter())
End Function
Friend Overridable Function TryDoEndConstructForEnterKey(textView As ITextView,
subjectBuffer As ITextBuffer,
cancellationToken As CancellationToken) As Boolean
Using Logger.LogBlock(FunctionId.EndConstruct_DoStatement, cancellationToken)
Using transaction = New CaretPreservingEditTransaction(VBEditorResources.End_Construct, textView, _undoHistoryRegistry, _editorOperationsFactoryService)
transaction.MergePolicy = AutomaticCodeChangeMergePolicy.Instance
' The user may have some text selected. In this scenario, we want to guarantee
' two things:
'
' 1) the text that was selected is deleted, as a normal pressing of an enter key
' would do. Since we're not letting the editor do it's own thing during end
' construct generation, we need to make sure the selection is deleted.
' 2) that we compute what statements we should spit assuming the selected text
' is no longer there. Consider a scenario where the user has something like:
'
' If True Then ~~~~
'
' and the completely invalid "~~~~" is selected. In VS2010, if you pressed
' enter, we would still spit enter, since we effectively view that code as
' "no longer there."
'
' The fix is simple: as a part of our transaction, we'll just delete anything
' under our selection. As long as our transaction goes through, the user won't
' suspect anything was fishy. If we don't spit, we'll cancel the transaction
' which will roll back this edit.
_editorOperationsFactoryService.GetEditorOperations(textView).ReplaceSelection("")
Dim state = GetEndConstructState(textView, subjectBuffer, cancellationToken)
If state Is Nothing Then
Return False
End If
' Are we in the middle of XML tags?
If state.TokenToLeft.Kind = SyntaxKind.GreaterThanToken Then
Dim element = state.TokenToLeft.GetAncestor(Of XmlElementSyntax)
If element IsNot Nothing Then
If element.StartTag IsNot Nothing AndAlso element.StartTag.Span.End = state.CaretPosition AndAlso
element.EndTag IsNot Nothing AndAlso element.EndTag.SpanStart = state.CaretPosition Then
InsertBlankLineBetweenXmlTags(state, textView, subjectBuffer)
transaction.Complete()
Return True
End If
End If
End If
' Figure out which statement that is to the left of us
Dim statement = state.TokenToLeft.FirstAncestorOrSelf(Function(n) TypeOf n Is StatementSyntax OrElse TypeOf n Is DirectiveTriviaSyntax)
' Make sure we are after the last token of the statement or
' if the statement is a single-line If statement that
' we're after the "Then" or "Else" token.
If statement Is Nothing Then
Return False
ElseIf statement.Kind = SyntaxKind.SingleLineIfStatement Then
Dim asSingleLine = DirectCast(statement, SingleLineIfStatementSyntax)
If state.TokenToLeft <> asSingleLine.ThenKeyword AndAlso
(asSingleLine.ElseClause Is Nothing OrElse
state.TokenToLeft <> asSingleLine.ElseClause.ElseKeyword) Then
Return False
End If
ElseIf statement.GetLastToken() <> state.TokenToLeft Then
Return False
End If
' Make sure we were on the same line as the last token.
Dim caretLine = subjectBuffer.CurrentSnapshot.GetLineNumberFromPosition(state.CaretPosition)
Dim lineOfLastToken = subjectBuffer.CurrentSnapshot.GetLineNumberFromPosition(state.TokenToLeft.SpanStart)
If caretLine <> lineOfLastToken Then
Return False
End If
' Make sure that we don't have any skipped trivia between our target token and
' the end of the line
Dim nextToken = state.TokenToLeft.GetNextTokenOrEndOfFile()
Dim nextTokenLine = subjectBuffer.CurrentSnapshot.GetLineNumberFromPosition(nextToken.SpanStart)
If nextToken.IsKind(SyntaxKind.EndOfFileToken) AndAlso nextTokenLine = caretLine Then
If nextToken.LeadingTrivia.Any(Function(trivia) trivia.IsKind(SyntaxKind.SkippedTokensTrivia)) Then
Return False
End If
End If
' If this is an Imports or Implements declaration, we should use the enclosing type declaration.
If TypeOf statement Is InheritsOrImplementsStatementSyntax Then
Dim baseDeclaration = DirectCast(statement, InheritsOrImplementsStatementSyntax)
Dim typeBlock = baseDeclaration.GetAncestor(Of TypeBlockSyntax)()
If typeBlock Is Nothing Then
Return False
End If
statement = typeBlock.BlockStatement
End If
If statement Is Nothing Then
Return False
End If
Dim errors = state.SyntaxTree.GetDiagnostics(statement)
If errors.Any(Function(e) Not IsMissingStatementError(statement, e.Id)) Then
If statement.Kind = SyntaxKind.SingleLineIfStatement Then
Dim asSingleLine = DirectCast(statement, SingleLineIfStatementSyntax)
Dim span = TextSpan.FromBounds(asSingleLine.IfKeyword.SpanStart, asSingleLine.ThenKeyword.Span.End)
If errors.Any(Function(e) span.Contains(e.Location.SourceSpan)) Then
Return False
End If
Else
Return False
End If
End If
' Make sure this statement does not end with the line continuation character
If statement.GetLastToken(includeZeroWidth:=True).TrailingTrivia.Any(Function(t) t.Kind = SyntaxKind.LineContinuationTrivia) Then
Return False
End If
Dim visitor = New EndConstructStatementVisitor(textView, subjectBuffer, state, cancellationToken)
Dim result = visitor.Visit(statement)
If result Is Nothing Then
Return False
End If
result.Apply(textView, subjectBuffer, state.CaretPosition, _smartIndentationService, _undoHistoryRegistry, _editorOperationsFactoryService)
transaction.Complete()
End Using
End Using
Return True
End Function
Private Sub InsertBlankLineBetweenXmlTags(state As EndConstructState, textView As ITextView, subjectBuffer As ITextBuffer)
' Add an extra newline first
Using edit = subjectBuffer.CreateEdit()
Dim aligningWhitespace = subjectBuffer.CurrentSnapshot.GetAligningWhitespace(state.TokenToLeft.Parent.Span.Start)
edit.Insert(state.CaretPosition, state.NewLineCharacter + aligningWhitespace)
edit.ApplyAndLogExceptions()
End Using
' And now just send down a normal enter
textView.TryMoveCaretToAndEnsureVisible(New SnapshotPoint(subjectBuffer.CurrentSnapshot, state.CaretPosition))
_editorOperationsFactoryService.GetEditorOperations(textView).InsertNewLine()
End Sub
Private Shared Function GetNodeFromToken(Of T As SyntaxNode)(token As SyntaxToken, expectedKind As SyntaxKind) As T
If token.Kind <> expectedKind Then
Return Nothing
End If
Return TryCast(token.Parent, T)
End Function
Private Shared Function InsertEndTextAndUpdateCaretPosition(
view As ITextView,
subjectBuffer As ITextBuffer,
insertPosition As Integer,
caretPosition As Integer,
endText As String
) As Boolean
Dim document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges()
If document Is Nothing Then
Return False
End If
subjectBuffer.ApplyChange(New TextChange(New TextSpan(insertPosition, 0), endText))
Dim caretPosAfterEdit = New SnapshotPoint(subjectBuffer.CurrentSnapshot, caretPosition)
view.TryMoveCaretToAndEnsureVisible(caretPosAfterEdit)
Return True
End Function
Friend Function TryDoXmlCDataEndConstruct(textView As ITextView, subjectBuffer As ITextBuffer, cancellationToken As CancellationToken) As Boolean
Using Logger.LogBlock(FunctionId.EndConstruct_XmlCData, cancellationToken)
Dim state = GetEndConstructState(textView, subjectBuffer, cancellationToken)
If state Is Nothing Then
Return False
End If
Dim xmlCData = GetNodeFromToken(Of XmlCDataSectionSyntax)(state.TokenToLeft, expectedKind:=SyntaxKind.BeginCDataToken)
If xmlCData Is Nothing Then
Return False
End If
Dim errors = state.SyntaxTree.GetDiagnostics(xmlCData)
' Exactly one error is expected: ERRID_ExpectedXmlEndCData
If errors.Count <> 1 Then
Return False
End If
If Not IsExpectedXmlEndCDataError(errors(0).Id) Then
Return False
End If
Dim endText = "]]>"
Return InsertEndTextAndUpdateCaretPosition(textView, subjectBuffer, state.CaretPosition, state.TokenToLeft.Span.End, endText)
End Using
End Function
Friend Function TryDoXmlCommentEndConstruct(textView As ITextView, subjectBuffer As ITextBuffer, cancellationToken As CancellationToken) As Boolean
Using Logger.LogBlock(FunctionId.EndConstruct_XmlComment, cancellationToken)
Dim state = GetEndConstructState(textView, subjectBuffer, cancellationToken)
If state Is Nothing Then
Return False
End If
Dim xmlComment = GetNodeFromToken(Of XmlCommentSyntax)(state.TokenToLeft, expectedKind:=SyntaxKind.LessThanExclamationMinusMinusToken)
If xmlComment Is Nothing Then
Return False
End If
Dim errors = state.SyntaxTree.GetDiagnostics(xmlComment)
' Exactly one error is expected: ERRID_ExpectedXmlEndComment
If errors.Count <> 1 Then
Return False
End If
If Not IsExpectedXmlEndCommentError(errors(0).Id) Then
Return False
End If
Dim endText = "-->"
Return InsertEndTextAndUpdateCaretPosition(textView, subjectBuffer, state.CaretPosition, state.TokenToLeft.Span.End, endText)
End Using
End Function
Friend Function TryDoXmlElementEndConstruct(textView As ITextView, subjectBuffer As ITextBuffer, cancellationToken As CancellationToken) As Boolean
Using Logger.LogBlock(FunctionId.EndConstruct_XmlElement, cancellationToken)
Dim state = GetEndConstructState(textView, subjectBuffer, cancellationToken)
If state Is Nothing Then
Return False
End If
Dim xmlStartElement = GetNodeFromToken(Of XmlElementStartTagSyntax)(state.TokenToLeft, expectedKind:=SyntaxKind.GreaterThanToken)
If xmlStartElement Is Nothing Then
Return False
End If
Dim errors = state.SyntaxTree.GetDiagnostics(xmlStartElement)
' Exactly one error is expected: ERRID_MissingXmlEndTag
If errors.Count <> 1 Then
Return False
End If
If Not IsMissingXmlEndTagError(errors(0).Id) Then
Return False
End If
Dim endTagText = "</" & xmlStartElement.Name.ToString & ">"
Return InsertEndTextAndUpdateCaretPosition(textView, subjectBuffer, state.CaretPosition, state.TokenToLeft.Span.End, endTagText)
End Using
End Function
Friend Function TryDoXmlEmbeddedExpressionEndConstruct(textView As ITextView, subjectBuffer As ITextBuffer, cancellationToken As CancellationToken) As Boolean
Using Logger.LogBlock(FunctionId.EndConstruct_XmlEmbeddedExpression, cancellationToken)
Dim state = GetEndConstructState(textView, subjectBuffer, cancellationToken)
If state Is Nothing Then
Return False
End If
Dim xmlEmbeddedExpression = GetNodeFromToken(Of XmlEmbeddedExpressionSyntax)(state.TokenToLeft, expectedKind:=SyntaxKind.LessThanPercentEqualsToken)
If xmlEmbeddedExpression Is Nothing Then
Return False
End If
Dim errors = state.SyntaxTree.GetDiagnostics(xmlEmbeddedExpression)
' Errors should contain ERRID_ExpectedXmlEndEmbedded
If Not errors.Any(Function(e) IsExpectedXmlEndEmbeddedError(e.Id)) Then
Return False
End If
Dim endText = " %>" ' NOTE: two spaces are inserted. The caret will be moved between them
Return InsertEndTextAndUpdateCaretPosition(textView, subjectBuffer, state.CaretPosition, state.TokenToLeft.Span.End + 1, endText)
End Using
End Function
Friend Function TryDoXmlProcessingInstructionEndConstruct(textView As ITextView, subjectBuffer As ITextBuffer, cancellationToken As CancellationToken) As Boolean
Using Logger.LogBlock(FunctionId.EndConstruct_XmlProcessingInstruction, cancellationToken)
Dim state = GetEndConstructState(textView, subjectBuffer, cancellationToken)
If state Is Nothing Then
Return False
End If
Dim xmlProcessingInstruction = GetNodeFromToken(Of XmlProcessingInstructionSyntax)(state.TokenToLeft, expectedKind:=SyntaxKind.LessThanQuestionToken)
If xmlProcessingInstruction Is Nothing Then
Return False
End If
Dim errors = state.SyntaxTree.GetDiagnostics(xmlProcessingInstruction)
' Exactly two errors are expected: ERRID_ExpectedXmlName and ERRID_ExpectedXmlEndPI
If errors.Count <> 2 Then
Return False
End If
If Not (errors.Any(Function(e) IsExpectedXmlNameError(e.Id)) AndAlso
errors.Any(Function(e) IsExpectedXmlEndPIError(e.Id))) Then
Return False
End If
Dim endText = "?>"
Return InsertEndTextAndUpdateCaretPosition(textView, subjectBuffer, state.CaretPosition, state.TokenToLeft.Span.End, endText)
End Using
End Function
Public Function TryDo(textView As ITextView, subjectBuffer As ITextBuffer, typedChar As Char, cancellationToken As CancellationToken) As Boolean Implements IEndConstructGenerationService.TryDo
Select Case typedChar
Case vbLf(0)
Return Me.TryDoEndConstructForEnterKey(textView, subjectBuffer, cancellationToken)
Case ">"c
Return Me.TryDoXmlElementEndConstruct(textView, subjectBuffer, cancellationToken)
Case "-"c
Return Me.TryDoXmlCommentEndConstruct(textView, subjectBuffer, cancellationToken)
Case "="c
Return Me.TryDoXmlEmbeddedExpressionEndConstruct(textView, subjectBuffer, cancellationToken)
Case "["c
Return Me.TryDoXmlCDataEndConstruct(textView, subjectBuffer, cancellationToken)
Case "?"c
Return Me.TryDoXmlProcessingInstructionEndConstruct(textView, subjectBuffer, cancellationToken)
End Select
Return False
End Function
End Class
End Namespace
|