595 lines
18 KiB
Go
595 lines
18 KiB
Go
package ls
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/microsoft/typescript-go/internal/ast"
|
|
"github.com/microsoft/typescript-go/internal/astnav"
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/diagnostics"
|
|
"github.com/microsoft/typescript-go/internal/format"
|
|
"github.com/microsoft/typescript-go/internal/locale"
|
|
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
|
"github.com/microsoft/typescript-go/internal/parser"
|
|
"github.com/microsoft/typescript-go/internal/stringutil"
|
|
)
|
|
|
|
type docCommentTemplate struct {
|
|
newText string
|
|
}
|
|
|
|
type commentOwnerInfo struct {
|
|
commentOwner *ast.Node
|
|
parameters []*ast.ParameterDeclarationNode
|
|
hasReturn bool
|
|
}
|
|
|
|
func (l *LanguageService) getJSDocSnippetCompletion(ctx context.Context, file *ast.SourceFile, position int) *CompletionList {
|
|
if l.UserPreferences().EnableJSDocCompletions.IsFalse() {
|
|
return nil
|
|
}
|
|
if !isPotentiallyValidJSDocSnippetCompletionPosition(file, position) {
|
|
return nil
|
|
}
|
|
newLine := l.FormatOptions().NewLineCharacter
|
|
if newLine == "" {
|
|
newLine = "\n"
|
|
}
|
|
template := getDocCommentTemplateAtPosition(file, position, l.UserPreferences().GenerateReturnInDocTemplate.IsTrue(), newLine)
|
|
if template == nil {
|
|
return nil
|
|
}
|
|
|
|
insertText := template.newText
|
|
var insertTextFormat *lsproto.InsertTextFormat
|
|
if clientSupportsItemSnippet(ctx) {
|
|
insertText = templateToSnippet(insertText, newLine)
|
|
insertTextFormat = new(lsproto.InsertTextFormatSnippet)
|
|
}
|
|
|
|
editRange := l.getJSDocSnippetCompletionRange(ctx, file, position, insertText)
|
|
var commitCharacters *[]string
|
|
if clientSupportsItemCommitCharacters(ctx) {
|
|
commitCharacters = &[]string{}
|
|
}
|
|
item := &CompletionItem{
|
|
CompletionItem: &lsproto.CompletionItem{
|
|
Label: "/** */",
|
|
Kind: new(lsproto.CompletionItemKindText),
|
|
Detail: new(diagnostics.JSDoc_comment.Localize(locale.FromContext(ctx))),
|
|
SortText: new("\x00"),
|
|
InsertTextFormat: insertTextFormat,
|
|
TextEdit: editRange,
|
|
CommitCharacters: commitCharacters,
|
|
},
|
|
}
|
|
return &CompletionList{
|
|
IsIncomplete: false,
|
|
Items: []*CompletionItem{item},
|
|
}
|
|
}
|
|
|
|
func isPotentiallyValidJSDocSnippetCompletionPosition(file *ast.SourceFile, position int) bool {
|
|
text := file.Text()
|
|
lineStart := format.GetLineStartPositionForPosition(position, file)
|
|
prefix := text[lineStart:position]
|
|
if !isJSDocSnippetPrefix(prefix) {
|
|
return false
|
|
}
|
|
|
|
lineEnd := getLineEndOfPosition(file, position)
|
|
suffix := text[position:lineEnd]
|
|
return isJSDocSnippetSuffix(suffix)
|
|
}
|
|
|
|
func (l *LanguageService) getJSDocSnippetCompletionRange(ctx context.Context, file *ast.SourceFile, position int, newText string) *lsproto.TextEditOrInsertReplaceEdit {
|
|
text := file.Text()
|
|
lineStart := format.GetLineStartPositionForPosition(position, file)
|
|
prefix := text[lineStart:position]
|
|
start := position
|
|
if prefixStart, ok := getJSDocSnippetPrefixStart(prefix); ok {
|
|
start = lineStart + prefixStart
|
|
}
|
|
|
|
lineEnd := getLineEndOfPosition(file, position)
|
|
suffix := text[position:lineEnd]
|
|
end := position
|
|
if suffixEnd, ok := getJSDocSnippetSuffixEnd(suffix); ok {
|
|
end += suffixEnd
|
|
}
|
|
|
|
replacementRange := l.createLspRangeFromBounds(start, end, file)
|
|
if clientSupportsItemInsertReplace(ctx) {
|
|
return &lsproto.TextEditOrInsertReplaceEdit{
|
|
InsertReplaceEdit: &lsproto.InsertReplaceEdit{
|
|
NewText: newText,
|
|
Insert: replacementRange,
|
|
Replace: replacementRange,
|
|
},
|
|
}
|
|
}
|
|
return &lsproto.TextEditOrInsertReplaceEdit{
|
|
TextEdit: &lsproto.TextEdit{
|
|
NewText: newText,
|
|
Range: replacementRange,
|
|
},
|
|
}
|
|
}
|
|
|
|
func getDocCommentTemplateAtPosition(sourceFile *ast.SourceFile, position int, generateReturnInDocTemplate bool, newLine string) *docCommentTemplate {
|
|
tokenAtPos := astnav.GetTokenAtPosition(sourceFile, position)
|
|
if tokenAtPos == nil {
|
|
return nil
|
|
}
|
|
|
|
existingDocComment := ast.FindAncestor(tokenAtPos, ast.IsJSDoc)
|
|
docCommentEnd, hasDocCommentAtPosition, hasClosingDocCommentAtPosition := getDocCommentEndAtPosition(sourceFile, position)
|
|
isInEmptyDocComment := existingDocComment != nil || hasDocCommentAtPosition
|
|
if isNonEmptyJSDoc(existingDocComment) && hasDocCommentAtPosition && !hasClosingDocCommentAtPosition {
|
|
reparseText := sourceFile.Text()[:position] + " */" + sourceFile.Text()[position:]
|
|
reparse := parser.ParseSourceFile(sourceFile.ParseOptions(), reparseText, sourceFile.ScriptKind)
|
|
return getDocCommentTemplateAtPosition(reparse, position, generateReturnInDocTemplate, newLine)
|
|
}
|
|
if isNonEmptyJSDoc(existingDocComment) {
|
|
return nil
|
|
}
|
|
if existingDocComment == nil && hasDocCommentAtPosition {
|
|
tokenAtPos = astnav.GetTokenAtPosition(sourceFile, skipWhitespace(sourceFile.Text(), docCommentEnd))
|
|
if tokenAtPos == nil {
|
|
return nil
|
|
}
|
|
}
|
|
tokenStart := astnav.GetStartOfNode(tokenAtPos, sourceFile, false /*includeJSDoc*/)
|
|
if !isInEmptyDocComment && tokenStart < position {
|
|
return nil
|
|
}
|
|
|
|
commentOwnerInfo := getCommentOwnerInfo(tokenAtPos, generateReturnInDocTemplate)
|
|
if commentOwnerInfo == nil {
|
|
return nil
|
|
}
|
|
|
|
commentOwner := commentOwnerInfo.commentOwner
|
|
lastJSDoc := core.LastOrNil(commentOwner.JSDoc(sourceFile))
|
|
if commentOwnerStart := astnav.GetStartOfNode(commentOwner, sourceFile, false /*includeJSDoc*/); commentOwnerStart < position ||
|
|
lastJSDoc != nil && existingDocComment != nil && lastJSDoc != existingDocComment {
|
|
return nil
|
|
}
|
|
|
|
indentation := getIndentationStringAtPosition(sourceFile, position)
|
|
tags := parameterDocComments(commentOwnerInfo.parameters, ast.IsSourceFileJS(sourceFile), indentation, newLine)
|
|
if commentOwnerInfo.hasReturn {
|
|
tags += returnsDocComment(indentation, newLine)
|
|
}
|
|
|
|
if tags != "" && !hasJSDocTags(commentOwner, sourceFile) {
|
|
preamble := "/**" + newLine + indentation + " * "
|
|
endLine := ""
|
|
if tokenStart == position {
|
|
endLine = newLine + indentation
|
|
}
|
|
return &docCommentTemplate{newText: preamble + newLine + tags + indentation + " */" + endLine}
|
|
}
|
|
return &docCommentTemplate{newText: "/** */"}
|
|
}
|
|
|
|
func getDocCommentEndAtPosition(file *ast.SourceFile, position int) (end int, ok bool, hasClosing bool) {
|
|
text := file.Text()
|
|
lineStart := format.GetLineStartPositionForPosition(position, file)
|
|
lineEnd := getLineEndOfPosition(file, position)
|
|
prefix := text[lineStart:position]
|
|
suffix := text[position:lineEnd]
|
|
if !strings.HasSuffix(trimRightSingleLineWhitespace(prefix), "/**") {
|
|
return 0, false, false
|
|
}
|
|
suffixEnd, hasClosing := getJSDocSnippetSuffixEnd(suffix)
|
|
return position + suffixEnd, true, hasClosing
|
|
}
|
|
|
|
func skipWhitespace(text string, position int) int {
|
|
for position < len(text) {
|
|
ch, size := stringutil.DecodeJSStringRune(text[position:])
|
|
if size == 0 {
|
|
break
|
|
}
|
|
if !stringutil.IsWhiteSpaceLike(ch) {
|
|
break
|
|
}
|
|
position += size
|
|
}
|
|
return position
|
|
}
|
|
|
|
func getCommentOwnerInfo(tokenAtPos *ast.Node, generateReturnInDocTemplate bool) *commentOwnerInfo {
|
|
for node := tokenAtPos; node != nil; node = node.Parent {
|
|
info, quit := getCommentOwnerInfoWorker(node, generateReturnInDocTemplate)
|
|
if info != nil || quit {
|
|
return info
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getCommentOwnerInfoWorker(commentOwner *ast.Node, generateReturnInDocTemplate bool) (*commentOwnerInfo, bool) {
|
|
if commentOwner == nil {
|
|
return nil, false
|
|
}
|
|
switch commentOwner.Kind {
|
|
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindMethodDeclaration, ast.KindConstructor, ast.KindMethodSignature, ast.KindArrowFunction:
|
|
return &commentOwnerInfo{commentOwner: commentOwner, parameters: commentOwner.Parameters(), hasReturn: hasReturn(commentOwner, generateReturnInDocTemplate)}, false
|
|
case ast.KindPropertyAssignment:
|
|
return getCommentOwnerInfoWorker(commentOwner.AsPropertyAssignment().Initializer, generateReturnInDocTemplate)
|
|
case ast.KindClassDeclaration, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration, ast.KindEnumMember, ast.KindTypeAliasDeclaration:
|
|
return &commentOwnerInfo{commentOwner: commentOwner}, false
|
|
case ast.KindPropertySignature:
|
|
if typeNode := commentOwner.AsPropertySignatureDeclaration().Type; typeNode != nil && ast.IsFunctionTypeNode(typeNode) {
|
|
return &commentOwnerInfo{commentOwner: commentOwner, parameters: typeNode.Parameters(), hasReturn: hasReturn(typeNode, generateReturnInDocTemplate)}, false
|
|
}
|
|
return &commentOwnerInfo{commentOwner: commentOwner}, false
|
|
case ast.KindVariableStatement:
|
|
declarations := commentOwner.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes
|
|
if len(declarations) == 1 {
|
|
if initializer := declarations[0].AsVariableDeclaration().Initializer; initializer != nil {
|
|
if host := getRightHandSideOfAssignment(initializer); host != nil {
|
|
return &commentOwnerInfo{commentOwner: commentOwner, parameters: host.Parameters(), hasReturn: hasReturn(host, generateReturnInDocTemplate)}, false
|
|
}
|
|
}
|
|
}
|
|
return &commentOwnerInfo{commentOwner: commentOwner}, false
|
|
case ast.KindSourceFile:
|
|
return nil, true
|
|
case ast.KindModuleDeclaration:
|
|
if commentOwner.Parent.Kind == ast.KindModuleDeclaration {
|
|
return nil, false
|
|
}
|
|
return &commentOwnerInfo{commentOwner: commentOwner}, false
|
|
case ast.KindExpressionStatement:
|
|
return getCommentOwnerInfoWorker(commentOwner.AsExpressionStatement().Expression, generateReturnInDocTemplate)
|
|
case ast.KindBinaryExpression:
|
|
binaryExpression := commentOwner.AsBinaryExpression()
|
|
if ast.GetAssignmentDeclarationKind(commentOwner) == ast.JSDeclarationKindNone {
|
|
return nil, true
|
|
}
|
|
if ast.IsFunctionLike(binaryExpression.Right) {
|
|
return &commentOwnerInfo{commentOwner: commentOwner, parameters: binaryExpression.Right.Parameters(), hasReturn: hasReturn(binaryExpression.Right, generateReturnInDocTemplate)}, false
|
|
}
|
|
return &commentOwnerInfo{commentOwner: commentOwner}, false
|
|
case ast.KindPropertyDeclaration:
|
|
if initializer := commentOwner.AsPropertyDeclaration().Initializer; initializer != nil && ast.IsFunctionExpressionOrArrowFunction(initializer) {
|
|
return &commentOwnerInfo{commentOwner: commentOwner, parameters: initializer.Parameters(), hasReturn: hasReturn(initializer, generateReturnInDocTemplate)}, false
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func hasReturn(node *ast.Node, generateReturnInDocTemplate bool) bool {
|
|
if !generateReturnInDocTemplate {
|
|
return false
|
|
}
|
|
if ast.IsFunctionTypeNode(node) {
|
|
return true
|
|
}
|
|
if ast.IsArrowFunction(node) {
|
|
if body := node.Body(); body != nil && ast.IsExpression(body) {
|
|
return true
|
|
}
|
|
}
|
|
return ast.IsFunctionLikeDeclaration(node) && node.Body() != nil && ast.IsBlock(node.Body()) && ast.ForEachReturnStatement(node.Body(), func(*ast.Node) bool {
|
|
return true
|
|
})
|
|
}
|
|
|
|
func getRightHandSideOfAssignment(rightHandSide *ast.Node) *ast.Node {
|
|
if rightHandSide == nil {
|
|
return nil
|
|
}
|
|
for rightHandSide.Kind == ast.KindParenthesizedExpression {
|
|
rightHandSide = rightHandSide.AsParenthesizedExpression().Expression
|
|
}
|
|
switch rightHandSide.Kind {
|
|
case ast.KindFunctionExpression, ast.KindArrowFunction:
|
|
return rightHandSide
|
|
case ast.KindClassExpression:
|
|
return core.Find(rightHandSide.Members(), ast.IsConstructorDeclaration)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func parameterDocComments(parameters []*ast.ParameterDeclarationNode, isJavaScriptFile bool, indentation, newLine string) string {
|
|
var b strings.Builder
|
|
for i, parameter := range parameters {
|
|
paramName := fmt.Sprintf("param%d", i)
|
|
if ast.IsIdentifier(parameter.Name()) {
|
|
paramName = parameter.Name().Text()
|
|
}
|
|
paramType := ""
|
|
if isJavaScriptFile {
|
|
if parameter.AsParameterDeclaration().DotDotDotToken != nil {
|
|
paramType = "{...any} "
|
|
} else {
|
|
paramType = "{any} "
|
|
}
|
|
}
|
|
b.WriteString(indentation)
|
|
b.WriteString(" * @param ")
|
|
b.WriteString(paramType)
|
|
b.WriteString(paramName)
|
|
b.WriteString(newLine)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func returnsDocComment(indentation, newLine string) string {
|
|
return indentation + " * @returns" + newLine
|
|
}
|
|
|
|
func getIndentationStringAtPosition(sourceFile *ast.SourceFile, position int) string {
|
|
text := sourceFile.Text()
|
|
lineStart := format.GetLineStartPositionForPosition(position, sourceFile)
|
|
pos := lineStart
|
|
for pos < position {
|
|
ch, size := stringutil.DecodeJSStringRune(text[pos:])
|
|
if size == 0 {
|
|
break
|
|
}
|
|
if !stringutil.IsWhiteSpaceSingleLine(ch) {
|
|
break
|
|
}
|
|
pos += size
|
|
}
|
|
return text[lineStart:pos]
|
|
}
|
|
|
|
func isNonEmptyJSDoc(jsdoc *ast.Node) bool {
|
|
if jsdoc == nil {
|
|
return false
|
|
}
|
|
data := jsdoc.AsJSDoc()
|
|
return data.Comment != nil && len(data.Comment.Nodes) > 0 || data.Tags != nil && len(data.Tags.Nodes) > 0
|
|
}
|
|
|
|
func hasJSDocTags(node *ast.Node, file *ast.SourceFile) bool {
|
|
jsdocs := node.JSDoc(file)
|
|
if len(jsdocs) == 0 {
|
|
return false
|
|
}
|
|
tags := jsdocs[len(jsdocs)-1].AsJSDoc().Tags
|
|
return tags != nil && len(tags.Nodes) > 0
|
|
}
|
|
|
|
func templateToSnippet(template string, newLine string) string {
|
|
if template == "/** */" {
|
|
return "/**" + newLine + " * $0" + newLine + " */"
|
|
}
|
|
|
|
snippetIndex := 1
|
|
template = escapeSnippetText(template)
|
|
template = stripJSDocTemplateIndentation(template, newLine)
|
|
return transformJSDocTemplateLines(template, newLine, &snippetIndex)
|
|
}
|
|
|
|
func stripJSDocTemplateIndentation(template string, newLine string) string {
|
|
lines := strings.Split(template, newLine)
|
|
for i, line := range lines {
|
|
trimmed := strings.TrimLeft(line, " \t")
|
|
if strings.HasPrefix(trimmed, "/") {
|
|
lines[i] = trimmed
|
|
} else if strings.HasPrefix(trimmed, "*") {
|
|
lines[i] = " " + trimmed
|
|
}
|
|
}
|
|
return strings.Join(lines, newLine)
|
|
}
|
|
|
|
func transformJSDocTemplateLines(template string, newLine string, snippetIndex *int) string {
|
|
lines := strings.Split(template, newLine)
|
|
for i, line := range lines {
|
|
if i > 0 && strings.HasPrefix(lines[i-1], "/**") && lineHasOnlyJSDocAsterisk(line) {
|
|
lines[i] = line + "$0"
|
|
continue
|
|
}
|
|
if transformed, ok := transformJSDocParamLine(line, snippetIndex); ok {
|
|
lines[i] = transformed
|
|
continue
|
|
}
|
|
if transformed, ok := transformJSDocReturnsLine(line, snippetIndex); ok {
|
|
lines[i] = transformed
|
|
}
|
|
}
|
|
return strings.Join(lines, newLine)
|
|
}
|
|
|
|
func lineHasOnlyJSDocAsterisk(line string) bool {
|
|
line = strings.TrimLeft(line, " \t")
|
|
return strings.HasPrefix(line, "*") && isOnlySpacesOrTabs(line[1:])
|
|
}
|
|
|
|
func transformJSDocParamLine(line string, snippetIndex *int) (string, bool) {
|
|
prefix := ""
|
|
rest := line
|
|
if strings.HasPrefix(rest, " ") {
|
|
prefix = " "
|
|
rest = rest[1:]
|
|
}
|
|
if !strings.HasPrefix(rest, "* @param") {
|
|
return "", false
|
|
}
|
|
rest = rest[len("* @param"):]
|
|
if !startsWithSingleLineWhitespace(rest) {
|
|
return "", false
|
|
}
|
|
rest = strings.TrimLeft(rest, " \t")
|
|
|
|
var typeText string
|
|
if strings.HasPrefix(rest, "{") {
|
|
closeBrace := strings.IndexByte(rest, '}')
|
|
if closeBrace < 0 {
|
|
return "", false
|
|
}
|
|
typeText = " " + rest[:closeBrace+1]
|
|
rest = rest[closeBrace+1:]
|
|
if !startsWithSingleLineWhitespace(rest) {
|
|
return "", false
|
|
}
|
|
rest = strings.TrimLeft(rest, " \t")
|
|
}
|
|
|
|
paramName, rest, ok := scanNonWhitespace(rest)
|
|
if !ok || !isOnlySpacesOrTabs(rest) {
|
|
return "", false
|
|
}
|
|
|
|
out := prefix + "* @param "
|
|
if typeText == " {any}" || typeText == " {*}" {
|
|
out += fmt.Sprintf("{${%d:*}} ", *snippetIndex)
|
|
*snippetIndex++
|
|
} else if typeText != "" {
|
|
out += typeText + " "
|
|
}
|
|
out += fmt.Sprintf("%s ${%d}", paramName, *snippetIndex)
|
|
*snippetIndex++
|
|
return out, true
|
|
}
|
|
|
|
func transformJSDocReturnsLine(line string, snippetIndex *int) (string, bool) {
|
|
prefix := ""
|
|
rest := line
|
|
if strings.HasPrefix(rest, " ") {
|
|
prefix = " "
|
|
rest = rest[1:]
|
|
}
|
|
if !strings.HasPrefix(rest, "* @returns") || !isOnlySpacesOrTabs(rest[len("* @returns"):]) {
|
|
return "", false
|
|
}
|
|
text := fmt.Sprintf("%s* @returns ${%d}", prefix, *snippetIndex)
|
|
*snippetIndex++
|
|
return text, true
|
|
}
|
|
|
|
func scanNonWhitespace(text string) (word string, rest string, ok bool) {
|
|
if text == "" {
|
|
return "", "", false
|
|
}
|
|
for i := 0; i < len(text); {
|
|
ch, size := stringutil.DecodeJSStringRune(text[i:])
|
|
if size == 0 || stringutil.IsWhiteSpaceLike(ch) {
|
|
if i == 0 {
|
|
return "", "", false
|
|
}
|
|
return text[:i], text[i:], true
|
|
}
|
|
i += size
|
|
}
|
|
return text, "", true
|
|
}
|
|
|
|
func isJSDocSnippetPrefix(prefix string) bool {
|
|
trimmed := trimRightSingleLineWhitespace(prefix)
|
|
if strings.HasSuffix(trimmed, "/**") {
|
|
return true
|
|
}
|
|
start := skipSingleLineWhitespace(prefix, 0)
|
|
if start >= len(trimmed) || trimmed[start] != '/' {
|
|
return false
|
|
}
|
|
if start+3 > len(trimmed) {
|
|
return false
|
|
}
|
|
for i := start + 1; i < len(trimmed); i++ {
|
|
if trimmed[i] != '*' {
|
|
return false
|
|
}
|
|
}
|
|
return len(trimmed)-start >= 3
|
|
}
|
|
|
|
func getJSDocSnippetPrefixStart(prefix string) (int, bool) {
|
|
trimmed := trimRightSingleLineWhitespace(prefix)
|
|
for i := len(trimmed) - 1; i >= 0 && trimmed[i] == '*'; i-- {
|
|
if i > 0 && trimmed[i-1] == '/' {
|
|
return i - 1, true
|
|
}
|
|
}
|
|
if strings.HasSuffix(trimmed, "/") {
|
|
return len(trimmed) - 1, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func isJSDocSnippetSuffix(suffix string) bool {
|
|
trimmed := trimRightSingleLineWhitespace(suffix[skipSingleLineWhitespace(suffix, 0):])
|
|
if trimmed == "" {
|
|
return true
|
|
}
|
|
if !strings.HasSuffix(trimmed, "/") {
|
|
return false
|
|
}
|
|
for i := range len(trimmed) - 1 {
|
|
if trimmed[i] != '*' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func getJSDocSnippetSuffixEnd(suffix string) (int, bool) {
|
|
pos := skipSingleLineWhitespace(suffix, 0)
|
|
for pos < len(suffix) && suffix[pos] == '*' {
|
|
pos++
|
|
}
|
|
if pos < len(suffix) && suffix[pos] == '/' {
|
|
return pos + 1, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func trimRightSingleLineWhitespace(text string) string {
|
|
end := 0
|
|
for pos := 0; pos < len(text); {
|
|
ch, size := stringutil.DecodeJSStringRune(text[pos:])
|
|
if size == 0 {
|
|
break
|
|
}
|
|
pos += size
|
|
if !stringutil.IsWhiteSpaceSingleLine(ch) {
|
|
end = pos
|
|
}
|
|
}
|
|
return text[:end]
|
|
}
|
|
|
|
func skipSingleLineWhitespace(text string, pos int) int {
|
|
for pos < len(text) {
|
|
ch, size := stringutil.DecodeJSStringRune(text[pos:])
|
|
if size == 0 || !stringutil.IsWhiteSpaceSingleLine(ch) {
|
|
break
|
|
}
|
|
pos += size
|
|
}
|
|
return pos
|
|
}
|
|
|
|
func isOnlySingleLineWhitespace(text string) bool {
|
|
return skipSingleLineWhitespace(text, 0) == len(text)
|
|
}
|
|
|
|
func startsWithSingleLineWhitespace(text string) bool {
|
|
if text == "" {
|
|
return false
|
|
}
|
|
ch, size := stringutil.DecodeJSStringRune(text)
|
|
return size != 0 && stringutil.IsWhiteSpaceSingleLine(ch)
|
|
}
|
|
|
|
func isOnlySpacesOrTabs(text string) bool {
|
|
for i := range len(text) {
|
|
if text[i] != ' ' && text[i] != '\t' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|