package ls import ( "context" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" ) func (l *LanguageService) ProvideOnAutoInsert(ctx context.Context, params *lsproto.VSOnAutoInsertParams) (lsproto.VSOnAutoInsertResponse, error) { if l.UserPreferences().EnableAutoClosingTags.IsFalse() { return lsproto.VSOnAutoInsertResponse{}, nil } if params.VSCh != ">" { return lsproto.VSOnAutoInsertResponse{}, nil } _, sourceFile := l.getProgramAndFile(params.VSTextDocument.Uri) position := l.converters.LineAndCharacterToPosition(sourceFile, params.VSPosition) token := astnav.FindPrecedingToken(sourceFile, int(position)) if token == nil { return lsproto.VSOnAutoInsertResponse{}, nil } var closingText string var element *ast.Node if token.Kind == ast.KindGreaterThanToken && ast.IsJsxOpeningElement(token.Parent) { element = token.Parent.Parent } else if ast.IsJsxText(token) && ast.IsJsxElement(token.Parent) { element = token.Parent } if element != nil && isUnclosedTag(element.AsJsxElement()) { tagNameNode := element.AsJsxElement().OpeningElement.TagName() // Slight divergence from Strada - we don't use the verbatim text from the opening tag. closingText = "" } else { var fragment *ast.Node if token.Kind == ast.KindGreaterThanToken && ast.IsJsxOpeningFragment(token.Parent) { fragment = token.Parent.Parent } else if ast.IsJsxText(token) && ast.IsJsxFragment(token.Parent) { fragment = token.Parent } if fragment != nil && isUnclosedFragment(fragment.AsJsxFragment()) { closingText = "" } } if closingText == "" { return lsproto.VSOnAutoInsertResponse{}, nil } return lsproto.VSOnAutoInsertResponse{ VSOnAutoInsertResponseItem: &lsproto.VSOnAutoInsertResponseItem{ VSTextEditFormat: lsproto.InsertTextFormatSnippet, VSTextEdit: &lsproto.TextEdit{ Range: lsproto.Range{Start: params.VSPosition, End: params.VSPosition}, // Tag names can contain `$` (valid JSX identifier characters), so // escape the closing text to avoid being interpreted as a snippet // placeholder/variable. NewText: "$0" + escapeSnippetText(closingText), }, }, }, nil } func isUnclosedTag(node *ast.JsxElement) bool { openingElement := node.OpeningElement closingElement := node.ClosingElement if !ast.TagNamesAreEquivalent(openingElement.TagName(), closingElement.TagName()) { return true } parent := node.Parent if ast.IsJsxElement(parent) { parent := parent.AsJsxElement() return ast.TagNamesAreEquivalent(openingElement.TagName(), parent.OpeningElement.TagName()) && isUnclosedTag(parent) } return false } func isUnclosedFragment(node *ast.JsxFragment) bool { closingFragment := node.ClosingFragment if closingFragment.Flags&ast.NodeFlagsThisNodeHasError != 0 { return true } parent := node.Parent if ast.IsJsxFragment(parent) && isUnclosedFragment(parent.AsJsxFragment()) { return true } return false }