vendor tsgo
This commit is contained in:
104
tools/tsgo/internal/ls/lsutil/asi.go
Normal file
104
tools/tsgo/internal/ls/lsutil/asi.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
func PositionIsASICandidate(pos int, context *ast.Node, file *ast.SourceFile) bool {
|
||||
contextAncestor := ast.FindAncestorOrQuit(context, func(ancestor *ast.Node) ast.FindAncestorResult {
|
||||
if ancestor.End() != pos {
|
||||
return ast.FindAncestorQuit
|
||||
}
|
||||
|
||||
return ast.ToFindAncestorResult(SyntaxMayBeASICandidate(ancestor.Kind))
|
||||
})
|
||||
|
||||
return contextAncestor != nil && NodeIsASICandidate(contextAncestor, file)
|
||||
}
|
||||
|
||||
func SyntaxMayBeASICandidate(kind ast.Kind) bool {
|
||||
return SyntaxRequiresTrailingCommaOrSemicolonOrASI(kind) ||
|
||||
SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind) ||
|
||||
SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind) ||
|
||||
SyntaxRequiresTrailingSemicolonOrASI(kind)
|
||||
}
|
||||
|
||||
func SyntaxRequiresTrailingCommaOrSemicolonOrASI(kind ast.Kind) bool {
|
||||
return kind == ast.KindCallSignature ||
|
||||
kind == ast.KindConstructSignature ||
|
||||
kind == ast.KindIndexSignature ||
|
||||
kind == ast.KindPropertySignature ||
|
||||
kind == ast.KindMethodSignature
|
||||
}
|
||||
|
||||
func SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind ast.Kind) bool {
|
||||
return kind == ast.KindFunctionDeclaration ||
|
||||
kind == ast.KindConstructor ||
|
||||
kind == ast.KindMethodDeclaration ||
|
||||
kind == ast.KindGetAccessor ||
|
||||
kind == ast.KindSetAccessor
|
||||
}
|
||||
|
||||
func SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind ast.Kind) bool {
|
||||
return kind == ast.KindModuleDeclaration
|
||||
}
|
||||
|
||||
func SyntaxRequiresTrailingSemicolonOrASI(kind ast.Kind) bool {
|
||||
return kind == ast.KindVariableStatement ||
|
||||
kind == ast.KindExpressionStatement ||
|
||||
kind == ast.KindDoStatement ||
|
||||
kind == ast.KindContinueStatement ||
|
||||
kind == ast.KindBreakStatement ||
|
||||
kind == ast.KindReturnStatement ||
|
||||
kind == ast.KindThrowStatement ||
|
||||
kind == ast.KindDebuggerStatement ||
|
||||
kind == ast.KindPropertyDeclaration ||
|
||||
kind == ast.KindTypeAliasDeclaration ||
|
||||
kind == ast.KindImportDeclaration ||
|
||||
kind == ast.KindImportEqualsDeclaration ||
|
||||
kind == ast.KindExportDeclaration ||
|
||||
kind == ast.KindNamespaceExportDeclaration ||
|
||||
kind == ast.KindExportAssignment
|
||||
}
|
||||
|
||||
func NodeIsASICandidate(node *ast.Node, file *ast.SourceFile) bool {
|
||||
lastToken := GetLastToken(node, file)
|
||||
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
|
||||
return false
|
||||
}
|
||||
|
||||
if SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) {
|
||||
if lastToken != nil && lastToken.Kind == ast.KindCommaToken {
|
||||
return false
|
||||
}
|
||||
} else if SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(node.Kind) {
|
||||
lastChild := GetLastChild(node, file)
|
||||
if lastChild != nil && ast.IsModuleBlock(lastChild) {
|
||||
return false
|
||||
}
|
||||
} else if SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(node.Kind) {
|
||||
lastChild := GetLastChild(node, file)
|
||||
if lastChild != nil && ast.IsFunctionBlock(lastChild) {
|
||||
return false
|
||||
}
|
||||
} else if !SyntaxRequiresTrailingSemicolonOrASI(node.Kind) {
|
||||
return false
|
||||
}
|
||||
|
||||
// See comment in parser's `parseDoStatement`
|
||||
if node.Kind == ast.KindDoStatement {
|
||||
return true
|
||||
}
|
||||
|
||||
topNode := ast.FindAncestor(node, func(ancestor *ast.Node) bool { return ancestor.Parent == nil })
|
||||
nextToken := astnav.FindNextToken(node, topNode, file)
|
||||
if nextToken == nil || nextToken.Kind == ast.KindCloseBraceToken {
|
||||
return true
|
||||
}
|
||||
|
||||
startLine := scanner.GetECMALineOfPosition(file, node.End())
|
||||
endLine := scanner.GetECMALineOfPosition(file, astnav.GetStartOfNode(nextToken, file, false /*includeJSDoc*/))
|
||||
return startLine != endLine
|
||||
}
|
||||
130
tools/tsgo/internal/ls/lsutil/children.go
Normal file
130
tools/tsgo/internal/ls/lsutil/children.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"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/scanner"
|
||||
)
|
||||
|
||||
// Replaces last(node.getChildren(sourceFile))
|
||||
func GetLastChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
|
||||
lastChildNode := GetLastVisitedChild(node, sourceFile)
|
||||
if ast.IsJSDocSingleCommentNode(node) && lastChildNode == nil {
|
||||
return nil
|
||||
}
|
||||
var tokenStartPos int
|
||||
if lastChildNode != nil {
|
||||
tokenStartPos = lastChildNode.End()
|
||||
} else {
|
||||
tokenStartPos = node.Pos()
|
||||
}
|
||||
var lastToken *ast.Node
|
||||
scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos)
|
||||
for startPos := tokenStartPos; startPos < node.End(); {
|
||||
tokenKind := scanner.Token()
|
||||
tokenFullStart := scanner.TokenFullStart()
|
||||
tokenEnd := scanner.TokenEnd()
|
||||
lastToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node, scanner.TokenFlags())
|
||||
startPos = tokenEnd
|
||||
scanner.Scan()
|
||||
}
|
||||
return core.IfElse(lastToken != nil, lastToken, lastChildNode)
|
||||
}
|
||||
|
||||
func GetLastToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ast.IsTokenKind(node.Kind) || ast.IsIdentifier(node) {
|
||||
return nil
|
||||
}
|
||||
|
||||
AssertHasRealPosition(node)
|
||||
|
||||
lastChild := GetLastChild(node, sourceFile)
|
||||
if lastChild == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if lastChild.Kind < ast.KindFirstNode {
|
||||
return lastChild
|
||||
} else {
|
||||
return GetLastToken(lastChild, sourceFile)
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the last visited child of the given node.
|
||||
// NOTE: This doesn't include unvisited tokens; for this, use `getLastChild` or `getLastToken`.
|
||||
func GetLastVisitedChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
|
||||
var lastChild *ast.Node
|
||||
|
||||
visitNode := func(n *ast.Node, _ *ast.NodeVisitor) *ast.Node {
|
||||
if n != nil && n.Flags&ast.NodeFlagsReparsed == 0 {
|
||||
lastChild = n
|
||||
}
|
||||
return n
|
||||
}
|
||||
visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
|
||||
if nodeList != nil && len(nodeList.Nodes) > 0 {
|
||||
for i := len(nodeList.Nodes) - 1; i >= 0; i-- {
|
||||
if nodeList.Nodes[i].Flags&ast.NodeFlagsReparsed == 0 {
|
||||
lastChild = nodeList.Nodes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodeList
|
||||
}
|
||||
|
||||
astnav.VisitEachChildAndJSDoc(node, sourceFile, visitNode, visitNodeList)
|
||||
return lastChild
|
||||
}
|
||||
|
||||
func GetFirstToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
|
||||
if ast.IsIdentifier(node) || ast.IsTokenKind(node.Kind) {
|
||||
return nil
|
||||
}
|
||||
AssertHasRealPosition(node)
|
||||
var firstChild *ast.Node
|
||||
node.ForEachChild(func(n *ast.Node) bool {
|
||||
if n == nil || node.Flags&ast.NodeFlagsReparsed != 0 {
|
||||
return false
|
||||
}
|
||||
firstChild = n
|
||||
return true
|
||||
})
|
||||
|
||||
var tokenEndPosition int
|
||||
if firstChild != nil {
|
||||
tokenEndPosition = firstChild.Pos()
|
||||
} else {
|
||||
tokenEndPosition = node.End()
|
||||
}
|
||||
scanner := scanner.GetScannerForSourceFile(sourceFile, node.Pos())
|
||||
var firstToken *ast.Node
|
||||
if node.Pos() < tokenEndPosition {
|
||||
tokenKind := scanner.Token()
|
||||
tokenFullStart := scanner.TokenFullStart()
|
||||
tokenEnd := scanner.TokenEnd()
|
||||
firstToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node, scanner.TokenFlags())
|
||||
}
|
||||
|
||||
if firstToken != nil {
|
||||
return firstToken
|
||||
}
|
||||
if firstChild == nil {
|
||||
return nil
|
||||
}
|
||||
if firstChild.Kind < ast.KindFirstNode {
|
||||
return firstChild
|
||||
}
|
||||
return GetFirstToken(firstChild, sourceFile)
|
||||
}
|
||||
|
||||
func AssertHasRealPosition(node *ast.Node) {
|
||||
if ast.PositionIsSynthesized(node.Pos()) || ast.PositionIsSynthesized(node.End()) {
|
||||
panic("Node must have a real position for this operation.")
|
||||
}
|
||||
}
|
||||
196
tools/tsgo/internal/ls/lsutil/completednode.go
Normal file
196
tools/tsgo/internal/ls/lsutil/completednode.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"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/scanner"
|
||||
)
|
||||
|
||||
// PositionBelongsToNode returns true if the position belongs to the node.
|
||||
// Assumes `candidate.Pos() <= position` holds.
|
||||
func PositionBelongsToNode(candidate *ast.Node, position int, file *ast.SourceFile) bool {
|
||||
if candidate.Pos() > position {
|
||||
panic("Expected candidate.pos <= position")
|
||||
}
|
||||
return position < candidate.End() || !IsCompletedNode(candidate, file)
|
||||
}
|
||||
|
||||
func IsCompletedNode(n *ast.Node, sourceFile *ast.SourceFile) bool {
|
||||
if n == nil || ast.NodeIsMissing(n) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch n.Kind {
|
||||
case ast.KindClassDeclaration,
|
||||
ast.KindInterfaceDeclaration,
|
||||
ast.KindEnumDeclaration,
|
||||
ast.KindObjectLiteralExpression,
|
||||
ast.KindObjectBindingPattern,
|
||||
ast.KindTypeLiteral,
|
||||
ast.KindBlock,
|
||||
ast.KindModuleBlock,
|
||||
ast.KindCaseBlock,
|
||||
ast.KindNamedImports,
|
||||
ast.KindNamedExports:
|
||||
return nodeEndsWith(n, ast.KindCloseBraceToken, sourceFile)
|
||||
|
||||
case ast.KindCatchClause:
|
||||
return IsCompletedNode(n.AsCatchClause().Block, sourceFile)
|
||||
|
||||
case ast.KindNewExpression:
|
||||
if n.ArgumentList() == nil {
|
||||
return true
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case ast.KindCallExpression,
|
||||
ast.KindParenthesizedExpression,
|
||||
ast.KindParenthesizedType:
|
||||
return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile)
|
||||
|
||||
case ast.KindFunctionType,
|
||||
ast.KindConstructorType:
|
||||
return IsCompletedNode(n.Type(), sourceFile)
|
||||
|
||||
case ast.KindConstructor,
|
||||
ast.KindGetAccessor,
|
||||
ast.KindSetAccessor,
|
||||
ast.KindFunctionDeclaration,
|
||||
ast.KindFunctionExpression,
|
||||
ast.KindMethodDeclaration,
|
||||
ast.KindMethodSignature,
|
||||
ast.KindConstructSignature,
|
||||
ast.KindCallSignature,
|
||||
ast.KindArrowFunction:
|
||||
if n.Body() != nil {
|
||||
return IsCompletedNode(n.Body(), sourceFile)
|
||||
}
|
||||
if n.Type() != nil {
|
||||
return IsCompletedNode(n.Type(), sourceFile)
|
||||
}
|
||||
// Even though type parameters can be unclosed, we can get away with
|
||||
// having at least a closing paren.
|
||||
return hasChildOfKind(n, ast.KindCloseParenToken, sourceFile)
|
||||
|
||||
case ast.KindModuleDeclaration:
|
||||
return n.Body() != nil && IsCompletedNode(n.Body(), sourceFile)
|
||||
|
||||
case ast.KindIfStatement:
|
||||
if n.AsIfStatement().ElseStatement != nil {
|
||||
return IsCompletedNode(n.AsIfStatement().ElseStatement, sourceFile)
|
||||
}
|
||||
return IsCompletedNode(n.AsIfStatement().ThenStatement, sourceFile)
|
||||
|
||||
case ast.KindExpressionStatement:
|
||||
return IsCompletedNode(n.Expression(), sourceFile) ||
|
||||
hasChildOfKind(n, ast.KindSemicolonToken, sourceFile)
|
||||
|
||||
case ast.KindArrayLiteralExpression,
|
||||
ast.KindArrayBindingPattern,
|
||||
ast.KindElementAccessExpression,
|
||||
ast.KindComputedPropertyName,
|
||||
ast.KindTupleType:
|
||||
return nodeEndsWith(n, ast.KindCloseBracketToken, sourceFile)
|
||||
|
||||
case ast.KindIndexSignature:
|
||||
if n.AsIndexSignatureDeclaration().Type != nil {
|
||||
return IsCompletedNode(n.AsIndexSignatureDeclaration().Type, sourceFile)
|
||||
}
|
||||
return hasChildOfKind(n, ast.KindCloseBracketToken, sourceFile)
|
||||
|
||||
case ast.KindCaseClause,
|
||||
ast.KindDefaultClause:
|
||||
// there is no such thing as terminator token for CaseClause/DefaultClause so for simplicity always consider them non-completed
|
||||
return false
|
||||
|
||||
case ast.KindForStatement,
|
||||
ast.KindForInStatement,
|
||||
ast.KindForOfStatement,
|
||||
ast.KindWhileStatement:
|
||||
return IsCompletedNode(n.Statement(), sourceFile)
|
||||
case ast.KindDoStatement:
|
||||
// rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')';
|
||||
if hasChildOfKind(n, ast.KindWhileKeyword, sourceFile) {
|
||||
return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile)
|
||||
}
|
||||
return IsCompletedNode(n.Statement(), sourceFile)
|
||||
|
||||
case ast.KindTypeQuery:
|
||||
return IsCompletedNode(n.AsTypeQueryNode().ExprName, sourceFile)
|
||||
|
||||
case ast.KindTypeOfExpression,
|
||||
ast.KindDeleteExpression,
|
||||
ast.KindVoidExpression,
|
||||
ast.KindYieldExpression,
|
||||
ast.KindSpreadElement:
|
||||
return IsCompletedNode(n.Expression(), sourceFile)
|
||||
|
||||
case ast.KindTaggedTemplateExpression:
|
||||
return IsCompletedNode(n.AsTaggedTemplateExpression().Template, sourceFile)
|
||||
|
||||
case ast.KindTemplateExpression:
|
||||
if n.AsTemplateExpression().TemplateSpans == nil {
|
||||
return false
|
||||
}
|
||||
lastSpan := core.LastOrNil(n.AsTemplateExpression().TemplateSpans.Nodes)
|
||||
return IsCompletedNode(lastSpan, sourceFile)
|
||||
|
||||
case ast.KindTemplateSpan:
|
||||
return ast.NodeIsPresent(n.AsTemplateSpan().Literal)
|
||||
|
||||
case ast.KindExportDeclaration,
|
||||
ast.KindImportDeclaration:
|
||||
return ast.NodeIsPresent(n.ModuleSpecifier())
|
||||
|
||||
case ast.KindPrefixUnaryExpression:
|
||||
return IsCompletedNode(n.AsPrefixUnaryExpression().Operand, sourceFile)
|
||||
|
||||
case ast.KindBinaryExpression:
|
||||
return IsCompletedNode(n.AsBinaryExpression().Right, sourceFile)
|
||||
|
||||
case ast.KindConditionalExpression:
|
||||
return IsCompletedNode(n.AsConditionalExpression().WhenFalse, sourceFile)
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if node ends with 'expectedLastToken'.
|
||||
// If child at position 'length - 1' is 'SemicolonToken' it is skipped and 'expectedLastToken' is compared with child at position 'length - 2'.
|
||||
func nodeEndsWith(n *ast.Node, expectedLastToken ast.Kind, sourceFile *ast.SourceFile) bool {
|
||||
lastChildNode := GetLastVisitedChild(n, sourceFile)
|
||||
var lastNodeAndTokens []*ast.Node
|
||||
var tokenStartPos int
|
||||
if lastChildNode != nil {
|
||||
lastNodeAndTokens = []*ast.Node{lastChildNode}
|
||||
tokenStartPos = lastChildNode.End()
|
||||
} else {
|
||||
tokenStartPos = n.Pos()
|
||||
}
|
||||
scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos)
|
||||
for startPos := tokenStartPos; startPos < n.End(); {
|
||||
tokenKind := scanner.Token()
|
||||
tokenFullStart := scanner.TokenFullStart()
|
||||
tokenEnd := scanner.TokenEnd()
|
||||
token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, n, scanner.TokenFlags())
|
||||
lastNodeAndTokens = append(lastNodeAndTokens, token)
|
||||
startPos = tokenEnd
|
||||
scanner.Scan()
|
||||
}
|
||||
if len(lastNodeAndTokens) == 0 {
|
||||
return false
|
||||
}
|
||||
lastChild := lastNodeAndTokens[len(lastNodeAndTokens)-1]
|
||||
if lastChild.Kind == expectedLastToken {
|
||||
return true
|
||||
} else if lastChild.Kind == ast.KindSemicolonToken && len(lastNodeAndTokens) > 1 {
|
||||
return lastNodeAndTokens[len(lastNodeAndTokens)-2].Kind == expectedLastToken
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasChildOfKind(containingNode *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) bool {
|
||||
return astnav.FindChildOfKind(containingNode, kind, sourceFile) != nil
|
||||
}
|
||||
141
tools/tsgo/internal/ls/lsutil/formatcodeoptions.go
Normal file
141
tools/tsgo/internal/ls/lsutil/formatcodeoptions.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
)
|
||||
|
||||
type IndentStyle int
|
||||
|
||||
const (
|
||||
IndentStyleNone IndentStyle = iota
|
||||
IndentStyleBlock
|
||||
IndentStyleSmart
|
||||
)
|
||||
|
||||
func parseIndentStyle(v any) IndentStyle {
|
||||
switch s := v.(type) {
|
||||
case string:
|
||||
switch strings.ToLower(s) {
|
||||
case "none":
|
||||
return IndentStyleNone
|
||||
case "block":
|
||||
return IndentStyleBlock
|
||||
case "smart":
|
||||
return IndentStyleSmart
|
||||
}
|
||||
case float64:
|
||||
return IndentStyle(int(s))
|
||||
case int:
|
||||
return IndentStyle(s)
|
||||
}
|
||||
return IndentStyleSmart
|
||||
}
|
||||
|
||||
type SemicolonPreference string
|
||||
|
||||
const (
|
||||
SemicolonPreferenceIgnore SemicolonPreference = "ignore"
|
||||
SemicolonPreferenceInsert SemicolonPreference = "insert"
|
||||
SemicolonPreferenceRemove SemicolonPreference = "remove"
|
||||
)
|
||||
|
||||
func parseSemicolonPreference(v any) SemicolonPreference {
|
||||
if s, ok := v.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "ignore":
|
||||
return SemicolonPreferenceIgnore
|
||||
case "insert":
|
||||
return SemicolonPreferenceInsert
|
||||
case "remove":
|
||||
return SemicolonPreferenceRemove
|
||||
}
|
||||
}
|
||||
return SemicolonPreferenceIgnore
|
||||
}
|
||||
|
||||
type EditorSettings struct {
|
||||
BaseIndentSize int `raw:"baseIndentSize" config:"format.baseIndentSize"`
|
||||
IndentSize int `raw:"indentSize" config:"format.indentSize"`
|
||||
TabSize int `raw:"tabSize" config:"format.tabSize"`
|
||||
NewLineCharacter string `raw:"newLineCharacter" config:"format.newLineCharacter"`
|
||||
ConvertTabsToSpaces core.Tristate `raw:"convertTabsToSpaces" config:"format.convertTabsToSpaces"`
|
||||
IndentStyle IndentStyle `raw:"indentStyle" config:"format.indentStyle"`
|
||||
TrimTrailingWhitespace core.Tristate `raw:"trimTrailingWhitespace" config:"format.trimTrailingWhitespace"`
|
||||
}
|
||||
|
||||
type FormatCodeSettings struct {
|
||||
EditorSettings
|
||||
InsertSpaceAfterCommaDelimiter core.Tristate `raw:"insertSpaceAfterCommaDelimiter" config:"format.insertSpaceAfterCommaDelimiter"`
|
||||
InsertSpaceAfterSemicolonInForStatements core.Tristate `raw:"insertSpaceAfterSemicolonInForStatements" config:"format.insertSpaceAfterSemicolonInForStatements"`
|
||||
InsertSpaceBeforeAndAfterBinaryOperators core.Tristate `raw:"insertSpaceBeforeAndAfterBinaryOperators" config:"format.insertSpaceBeforeAndAfterBinaryOperators"`
|
||||
InsertSpaceAfterConstructor core.Tristate `raw:"insertSpaceAfterConstructor" config:"format.insertSpaceAfterConstructor"`
|
||||
InsertSpaceAfterKeywordsInControlFlowStatements core.Tristate `raw:"insertSpaceAfterKeywordsInControlFlowStatements" config:"format.insertSpaceAfterKeywordsInControlFlowStatements"`
|
||||
InsertSpaceAfterFunctionKeywordForAnonymousFunctions core.Tristate `raw:"insertSpaceAfterFunctionKeywordForAnonymousFunctions" config:"format.insertSpaceAfterFunctionKeywordForAnonymousFunctions"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingEmptyBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"`
|
||||
InsertSpaceAfterTypeAssertion core.Tristate `raw:"insertSpaceAfterTypeAssertion" config:"format.insertSpaceAfterTypeAssertion"`
|
||||
InsertSpaceBeforeFunctionParenthesis core.Tristate `raw:"insertSpaceBeforeFunctionParenthesis" config:"format.insertSpaceBeforeFunctionParenthesis"`
|
||||
PlaceOpenBraceOnNewLineForFunctions core.Tristate `raw:"placeOpenBraceOnNewLineForFunctions" config:"format.placeOpenBraceOnNewLineForFunctions"`
|
||||
PlaceOpenBraceOnNewLineForControlBlocks core.Tristate `raw:"placeOpenBraceOnNewLineForControlBlocks" config:"format.placeOpenBraceOnNewLineForControlBlocks"`
|
||||
InsertSpaceBeforeTypeAnnotation core.Tristate `raw:"insertSpaceBeforeTypeAnnotation" config:"format.insertSpaceBeforeTypeAnnotation"`
|
||||
IndentMultiLineObjectLiteralBeginningOnBlankLine core.Tristate `raw:"indentMultiLineObjectLiteralBeginningOnBlankLine" config:"format.indentMultiLineObjectLiteralBeginningOnBlankLine"`
|
||||
Semicolons SemicolonPreference `raw:"semicolons" config:"format.semicolons"`
|
||||
IndentSwitchCase core.Tristate `raw:"indentSwitchCase" config:"format.indentSwitchCase"`
|
||||
}
|
||||
|
||||
func FromLSFormatOptions(f FormatCodeSettings, opt *lsproto.FormattingOptions) FormatCodeSettings {
|
||||
updatedSettings := f
|
||||
updatedSettings.TabSize = int(opt.TabSize)
|
||||
updatedSettings.IndentSize = int(opt.TabSize)
|
||||
updatedSettings.ConvertTabsToSpaces = core.BoolToTristate(opt.InsertSpaces)
|
||||
if opt.TrimTrailingWhitespace != nil {
|
||||
updatedSettings.TrimTrailingWhitespace = core.BoolToTristate(*opt.TrimTrailingWhitespace)
|
||||
}
|
||||
return updatedSettings
|
||||
}
|
||||
|
||||
func (settings FormatCodeSettings) ToLSFormatOptions() *lsproto.FormattingOptions {
|
||||
trimTrailingWhitespace := settings.TrimTrailingWhitespace.IsTrue()
|
||||
return &lsproto.FormattingOptions{
|
||||
TabSize: uint32(settings.TabSize),
|
||||
InsertSpaces: settings.ConvertTabsToSpaces.IsTrue(),
|
||||
TrimTrailingWhitespace: &trimTrailingWhitespace,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDefaultFormatCodeSettings() FormatCodeSettings {
|
||||
return FormatCodeSettings{
|
||||
EditorSettings: EditorSettings{
|
||||
IndentSize: printer.GetDefaultIndentSize(),
|
||||
TabSize: printer.GetDefaultIndentSize(),
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSTrue,
|
||||
IndentStyle: IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
InsertSpaceAfterConstructor: core.TSFalse,
|
||||
InsertSpaceAfterCommaDelimiter: core.TSTrue,
|
||||
InsertSpaceAfterSemicolonInForStatements: core.TSTrue,
|
||||
InsertSpaceBeforeAndAfterBinaryOperators: core.TSTrue,
|
||||
InsertSpaceAfterKeywordsInControlFlowStatements: core.TSTrue,
|
||||
InsertSpaceAfterFunctionKeywordForAnonymousFunctions: core.TSFalse,
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: core.TSFalse,
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: core.TSFalse,
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: core.TSTrue,
|
||||
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: core.TSFalse,
|
||||
InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: core.TSFalse,
|
||||
InsertSpaceBeforeFunctionParenthesis: core.TSFalse,
|
||||
PlaceOpenBraceOnNewLineForFunctions: core.TSFalse,
|
||||
PlaceOpenBraceOnNewLineForControlBlocks: core.TSFalse,
|
||||
Semicolons: SemicolonPreferenceIgnore,
|
||||
IndentSwitchCase: core.TSTrue,
|
||||
}
|
||||
}
|
||||
695
tools/tsgo/internal/ls/lsutil/organizeimports.go
Normal file
695
tools/tsgo/internal/ls/lsutil/organizeimports.go
Normal file
@@ -0,0 +1,695 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"math"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// FilterImportDeclarations filters out non-import declarations from a list of statements.
|
||||
func FilterImportDeclarations(statements []*ast.Statement) []*ast.Statement {
|
||||
return core.Filter(statements, func(stmt *ast.Statement) bool {
|
||||
return stmt.Kind == ast.KindImportDeclaration
|
||||
})
|
||||
}
|
||||
|
||||
// GetDetectionLists returns the lists of comparers and type orders to test for organize imports detection.
|
||||
func GetDetectionLists(preferences UserPreferences) (comparersToTest []func(a, b string) int, typeOrdersToTest []OrganizeImportsTypeOrder) {
|
||||
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
|
||||
comparersToTest = []func(a, b string) int{getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort)}
|
||||
} else if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
|
||||
comparersToTest = []func(a, b string) int{getOrganizeImportsStringComparer(preferences, preferences.OrganizeImportsIgnoreCase.IsTrue())}
|
||||
} else {
|
||||
comparersToTest = []func(a, b string) int{
|
||||
getOrganizeImportsStringComparer(preferences, true),
|
||||
getOrganizeImportsStringComparer(preferences, false),
|
||||
}
|
||||
}
|
||||
|
||||
if preferences.OrganizeImportsTypeOrder != OrganizeImportsTypeOrderAuto {
|
||||
typeOrdersToTest = []OrganizeImportsTypeOrder{preferences.OrganizeImportsTypeOrder}
|
||||
} else {
|
||||
typeOrdersToTest = []OrganizeImportsTypeOrder{
|
||||
OrganizeImportsTypeOrderLast,
|
||||
OrganizeImportsTypeOrderInline,
|
||||
OrganizeImportsTypeOrderFirst,
|
||||
}
|
||||
}
|
||||
|
||||
return comparersToTest, typeOrdersToTest
|
||||
}
|
||||
|
||||
func ResolveOrganizeImportsSort(preferences UserPreferences) OrganizeImportsSort {
|
||||
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
|
||||
return preferences.OrganizeImportsSort
|
||||
}
|
||||
|
||||
if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode {
|
||||
switch preferences.OrganizeImportsIgnoreCase {
|
||||
case core.TSTrue:
|
||||
return OrganizeImportsSortNaturalIgnoreCase
|
||||
case core.TSFalse:
|
||||
return OrganizeImportsSortNatural
|
||||
default:
|
||||
return OrganizeImportsSortAuto
|
||||
}
|
||||
}
|
||||
|
||||
switch preferences.OrganizeImportsIgnoreCase {
|
||||
case core.TSTrue:
|
||||
return OrganizeImportsSortOrdinalIgnoreCase
|
||||
case core.TSFalse:
|
||||
return OrganizeImportsSortOrdinal
|
||||
default:
|
||||
return OrganizeImportsSortAuto
|
||||
}
|
||||
}
|
||||
|
||||
func getOrganizeImportsOrdinalStringComparer(ignoreCase bool) func(a, b string) int {
|
||||
if ignoreCase {
|
||||
return stringutil.CompareStringsCaseInsensitiveEslintCompatible
|
||||
}
|
||||
return stringutil.CompareStringsCaseSensitive
|
||||
}
|
||||
|
||||
func getOrganizeImportsNaturalStringComparer(caseSensitive bool) func(a, b string) int {
|
||||
return func(a, b string) int {
|
||||
return compareOrganizeImportsNaturalStrings(a, b, caseSensitive)
|
||||
}
|
||||
}
|
||||
|
||||
func getOrganizeImportsUnicodeStringComparer(ignoreCase bool, preferences UserPreferences) func(a, b string) int {
|
||||
caseFirst := preferences.OrganizeImportsCaseFirst
|
||||
numeric := preferences.OrganizeImportsNumericCollation.IsTrue()
|
||||
accents := !preferences.OrganizeImportsAccentCollation.IsFalse()
|
||||
|
||||
return func(a, b string) int {
|
||||
return compareOrganizeImportsUnicodeStrings(a, b, ignoreCase, caseFirst, numeric, accents)
|
||||
}
|
||||
}
|
||||
|
||||
func compareOrganizeImportsNaturalStrings(a string, b string, caseSensitive bool) int {
|
||||
if cmp := compareStringsNumeric(naturalCollationKey(a), naturalCollationKey(b)); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
|
||||
if caseSensitive {
|
||||
if cmp := compareOrganizeImportsCaseUpperFirst(a, b); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
func compareOrganizeImportsUnicodeStrings(a string, b string, ignoreCase bool, caseFirst OrganizeImportsCaseFirst, numeric bool, accents bool) int {
|
||||
if cmp := compareOrganizeImportsUnicodeKeys(naturalCollationKey(a), naturalCollationKey(b), numeric); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
|
||||
if accents {
|
||||
if cmp := compareOrganizeImportsUnicodeKeys(strings.ToLower(a), strings.ToLower(b), numeric); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
}
|
||||
|
||||
if !ignoreCase {
|
||||
if cmp := compareOrganizeImportsCase(a, b, caseFirst); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
func naturalCollationKey(s string) string {
|
||||
return strings.ToLower(removeDiacritics(s))
|
||||
}
|
||||
|
||||
func removeDiacritics(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if unicode.Is(unicode.Mn, r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, norm.NFD.String(s))
|
||||
}
|
||||
|
||||
func compareOrganizeImportsUnicodeKeys(a string, b string, numeric bool) int {
|
||||
if numeric {
|
||||
return compareStringsNumeric(a, b)
|
||||
}
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
func compareStringsNumeric(a string, b string) int {
|
||||
for len(a) > 0 && len(b) > 0 {
|
||||
if isASCIIDigit(a[0]) && isASCIIDigit(b[0]) {
|
||||
aRunEnd := asciiDigitRunEnd(a)
|
||||
bRunEnd := asciiDigitRunEnd(b)
|
||||
|
||||
if cmp := compareNumericText(a[:aRunEnd], b[:bRunEnd]); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
|
||||
a = a[aRunEnd:]
|
||||
b = b[bRunEnd:]
|
||||
continue
|
||||
}
|
||||
|
||||
aRune, aSize := utf8.DecodeRuneInString(a)
|
||||
bRune, bSize := utf8.DecodeRuneInString(b)
|
||||
if aRune != bRune {
|
||||
return cmp.Compare(aRune, bRune)
|
||||
}
|
||||
|
||||
a = a[aSize:]
|
||||
b = b[bSize:]
|
||||
}
|
||||
|
||||
return cmp.Compare(len(a), len(b))
|
||||
}
|
||||
|
||||
func isASCIIDigit(ch byte) bool {
|
||||
return ch >= '0' && ch <= '9'
|
||||
}
|
||||
|
||||
func asciiDigitRunEnd(s string) int {
|
||||
i := 0
|
||||
for i < len(s) && isASCIIDigit(s[i]) {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func compareNumericText(a string, b string) int {
|
||||
aDigits := strings.TrimLeft(a, "0")
|
||||
bDigits := strings.TrimLeft(b, "0")
|
||||
if aDigits == "" {
|
||||
aDigits = "0"
|
||||
}
|
||||
if bDigits == "" {
|
||||
bDigits = "0"
|
||||
}
|
||||
|
||||
if len(aDigits) != len(bDigits) {
|
||||
return cmp.Compare(len(aDigits), len(bDigits))
|
||||
}
|
||||
if cmp := strings.Compare(aDigits, bDigits); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
func compareOrganizeImportsCaseUpperFirst(a string, b string) int {
|
||||
return compareOrganizeImportsCase(a, b, OrganizeImportsCaseFirstUpper)
|
||||
}
|
||||
|
||||
func compareOrganizeImportsCase(a string, b string, caseFirst OrganizeImportsCaseFirst) int {
|
||||
aRunes := []rune(a)
|
||||
bRunes := []rune(b)
|
||||
minLen := min(len(aRunes), len(bRunes))
|
||||
|
||||
for i := range minLen {
|
||||
aUpper := unicode.IsUpper(aRunes[i])
|
||||
bUpper := unicode.IsUpper(bRunes[i])
|
||||
if aUpper != bUpper {
|
||||
switch caseFirst {
|
||||
case OrganizeImportsCaseFirstUpper:
|
||||
if aUpper {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
case OrganizeImportsCaseFirstLower:
|
||||
if !aUpper {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
default:
|
||||
if aUpper {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cmp.Compare(len(aRunes), len(bRunes))
|
||||
}
|
||||
|
||||
func getOrganizeImportsPresetStringComparer(sort OrganizeImportsSort) func(a, b string) int {
|
||||
switch sort {
|
||||
case OrganizeImportsSortOrdinalIgnoreCase:
|
||||
return getOrganizeImportsOrdinalStringComparer(true)
|
||||
case OrganizeImportsSortNatural:
|
||||
return getOrganizeImportsNaturalStringComparer(true)
|
||||
case OrganizeImportsSortNaturalIgnoreCase:
|
||||
return getOrganizeImportsNaturalStringComparer(false)
|
||||
default:
|
||||
return getOrganizeImportsOrdinalStringComparer(false)
|
||||
}
|
||||
}
|
||||
|
||||
func getOrganizeImportsStringComparer(preferences UserPreferences, ignoreCase bool) func(a, b string) int {
|
||||
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
|
||||
return getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort)
|
||||
}
|
||||
if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode {
|
||||
return getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences)
|
||||
}
|
||||
return getOrganizeImportsOrdinalStringComparer(ignoreCase)
|
||||
}
|
||||
|
||||
func getModuleSpecifierExpression(declaration *ast.Statement) *ast.Expression {
|
||||
switch declaration.Kind {
|
||||
case ast.KindImportEqualsDeclaration:
|
||||
importEquals := declaration.AsImportEqualsDeclaration()
|
||||
if importEquals.ModuleReference.Kind == ast.KindExternalModuleReference {
|
||||
return importEquals.ModuleReference.Expression()
|
||||
}
|
||||
return nil
|
||||
case ast.KindImportDeclaration:
|
||||
return declaration.ModuleSpecifier()
|
||||
case ast.KindVariableStatement:
|
||||
declarations := declaration.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes
|
||||
if len(declarations) > 0 {
|
||||
initializer := declarations[0].Initializer()
|
||||
if initializer != nil && initializer.Kind == ast.KindCallExpression {
|
||||
callExpr := initializer.AsCallExpression()
|
||||
if len(callExpr.Arguments.Nodes) > 0 {
|
||||
return callExpr.Arguments.Nodes[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetExternalModuleName returns the module name from a module specifier expression.
|
||||
func GetExternalModuleName(specifier *ast.Expression) string {
|
||||
if specifier != nil && ast.IsStringLiteralLike(specifier.AsNode()) {
|
||||
return specifier.Text()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CompareModuleSpecifiers compares two module specifiers using the given comparer.
|
||||
func CompareModuleSpecifiers(m1 *ast.Expression, m2 *ast.Expression, comparer func(a, b string) int) int {
|
||||
name1 := GetExternalModuleName(m1)
|
||||
name2 := GetExternalModuleName(m2)
|
||||
if cmp := core.CompareBooleans(name1 == "", name2 == ""); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
if cmp := core.CompareBooleans(tspath.IsExternalModuleNameRelative(name1), tspath.IsExternalModuleNameRelative(name2)); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return comparer(name1, name2)
|
||||
}
|
||||
|
||||
func compareImportKind(s1 *ast.Statement, s2 *ast.Statement) int {
|
||||
return cmp.Compare(getImportKindOrder(s1), getImportKindOrder(s2))
|
||||
}
|
||||
|
||||
// getImportKindOrder returns the sort order for different import kinds:
|
||||
// 1. Side-effect imports
|
||||
// 2. Type-only imports
|
||||
// 3. Namespace imports
|
||||
// 4. Default imports
|
||||
// 5. Named imports
|
||||
// 6. ImportEqualsDeclarations
|
||||
// 7. Require variable statements
|
||||
const (
|
||||
importKindOrderSideEffect = 0
|
||||
importKindOrderTypeOnly = 1
|
||||
importKindOrderNamespace = 2
|
||||
importKindOrderDefault = 3
|
||||
importKindOrderNamed = 4
|
||||
importKindOrderImportEquals = 5
|
||||
importKindOrderRequire = 6
|
||||
importKindOrderUnknown = 7
|
||||
)
|
||||
|
||||
func getImportKindOrder(s1 *ast.Statement) int {
|
||||
switch s1.Kind {
|
||||
case ast.KindImportDeclaration:
|
||||
importDecl := s1.AsImportDeclaration()
|
||||
if importDecl.ImportClause == nil {
|
||||
return importKindOrderSideEffect
|
||||
}
|
||||
importClause := importDecl.ImportClause.AsImportClause()
|
||||
if importClause.IsTypeOnly() {
|
||||
return importKindOrderTypeOnly
|
||||
}
|
||||
if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamespaceImport {
|
||||
return importKindOrderNamespace
|
||||
}
|
||||
if importClause.Name() != nil {
|
||||
return importKindOrderDefault
|
||||
}
|
||||
return importKindOrderNamed
|
||||
case ast.KindImportEqualsDeclaration:
|
||||
return importKindOrderImportEquals
|
||||
case ast.KindVariableStatement:
|
||||
return importKindOrderRequire
|
||||
default:
|
||||
return importKindOrderUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// CompareImportsOrRequireStatements compares two import or require statements.
|
||||
func CompareImportsOrRequireStatements(s1 *ast.Statement, s2 *ast.Statement, comparer func(a, b string) int) int {
|
||||
if cmp := CompareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return compareImportKind(s1, s2)
|
||||
}
|
||||
|
||||
func compareImportOrExportSpecifiers(s1 *ast.Node, s2 *ast.Node, comparer func(a, b string) int, preferences UserPreferences) int {
|
||||
typeOrder := preferences.OrganizeImportsTypeOrder
|
||||
|
||||
s1Name := s1.Name().Text()
|
||||
s2Name := s2.Name().Text()
|
||||
|
||||
switch typeOrder {
|
||||
case OrganizeImportsTypeOrderFirst:
|
||||
if cmp := core.CompareBooleans(s2.IsTypeOnly(), s1.IsTypeOnly()); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return comparer(s1Name, s2Name)
|
||||
case OrganizeImportsTypeOrderInline:
|
||||
return comparer(s1Name, s2Name)
|
||||
default: // OrganizeImportsTypeOrderLast
|
||||
if cmp := core.CompareBooleans(s1.IsTypeOnly(), s2.IsTypeOnly()); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return comparer(s1Name, s2Name)
|
||||
}
|
||||
}
|
||||
|
||||
// GetNamedImportSpecifierComparer returns a comparer function for sorting import specifiers.
|
||||
func GetNamedImportSpecifierComparer(preferences UserPreferences, comparer func(a, b string) int) func(s1, s2 *ast.Node) int {
|
||||
if comparer == nil {
|
||||
ignoreCase := false
|
||||
if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
|
||||
ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue()
|
||||
}
|
||||
comparer = getOrganizeImportsStringComparer(preferences, ignoreCase)
|
||||
}
|
||||
return func(s1, s2 *ast.Node) int {
|
||||
return compareImportOrExportSpecifiers(s1, s2, comparer, preferences)
|
||||
}
|
||||
}
|
||||
|
||||
// GetImportSpecifierInsertionIndex returns the index at which to insert a new import specifier.
|
||||
func GetImportSpecifierInsertionIndex(sortedImports []*ast.Node, newImport *ast.Node, comparer func(s1, s2 *ast.Node) int) int {
|
||||
return core.FirstResult(core.BinarySearchUniqueFunc(sortedImports, func(mid int, value *ast.Node) int {
|
||||
return comparer(value, newImport)
|
||||
}))
|
||||
}
|
||||
|
||||
// GetImportDeclarationInsertIndex returns the index at which to insert a new import declaration.
|
||||
func GetImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int {
|
||||
return core.FirstResult(core.BinarySearchUniqueFunc(sortedImports, func(mid int, value *ast.Statement) int {
|
||||
return comparer(value, newImport)
|
||||
}))
|
||||
}
|
||||
|
||||
// GetOrganizeImportsStringComparerWithDetection returns a string comparer based on detecting the order of import statements by the module specifier
|
||||
func GetOrganizeImportsStringComparerWithDetection(originalImportDecls []*ast.Statement, preferences UserPreferences) (comparer func(a, b string) int, isSorted bool) {
|
||||
result, sorted := DetectModuleSpecifierCaseBySort([][]*ast.Statement{originalImportDecls}, getComparers(preferences))
|
||||
return result, sorted
|
||||
}
|
||||
|
||||
func getComparers(preferences UserPreferences) []func(a string, b string) int {
|
||||
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto || !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
|
||||
ignoreCase := false
|
||||
if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
|
||||
ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue()
|
||||
}
|
||||
return []func(a, b string) int{getOrganizeImportsStringComparer(preferences, ignoreCase)}
|
||||
}
|
||||
return []func(a, b string) int{
|
||||
getOrganizeImportsStringComparer(preferences, true),
|
||||
getOrganizeImportsStringComparer(preferences, false),
|
||||
}
|
||||
}
|
||||
|
||||
type namedImportSortResult struct {
|
||||
namedImportComparer func(a, b string) int
|
||||
typeOrder OrganizeImportsTypeOrder
|
||||
isSorted bool
|
||||
}
|
||||
|
||||
// DetectNamedImportOrganizationBySort detects the order of named imports throughout the file by considering the named imports in each statement as a group
|
||||
func DetectNamedImportOrganizationBySort(
|
||||
originalGroups []*ast.Statement,
|
||||
comparersToTest []func(a, b string) int,
|
||||
typesToTest []OrganizeImportsTypeOrder,
|
||||
) (comparer func(a, b string) int, typeOrder OrganizeImportsTypeOrder, found bool) {
|
||||
result := detectNamedImportOrganizationBySort(originalGroups, comparersToTest, typesToTest)
|
||||
if result == nil {
|
||||
return nil, OrganizeImportsTypeOrderLast, false
|
||||
}
|
||||
return result.namedImportComparer, result.typeOrder, true
|
||||
}
|
||||
|
||||
func detectNamedImportOrganizationBySort(
|
||||
originalGroups []*ast.Statement,
|
||||
comparersToTest []func(a, b string) int,
|
||||
typesToTest []OrganizeImportsTypeOrder,
|
||||
) *namedImportSortResult {
|
||||
var bothNamedImports bool
|
||||
var importDeclsWithNamed []*ast.Statement
|
||||
|
||||
for _, imp := range originalGroups {
|
||||
if imp.AsImportDeclaration().ImportClause == nil {
|
||||
continue
|
||||
}
|
||||
clause := imp.AsImportDeclaration().ImportClause.AsImportClause()
|
||||
if clause.NamedBindings == nil || clause.NamedBindings.Kind != ast.KindNamedImports {
|
||||
continue
|
||||
}
|
||||
namedImports := clause.NamedBindings.AsNamedImports()
|
||||
if len(namedImports.Elements.Nodes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !bothNamedImports {
|
||||
hasTypeOnly := false
|
||||
hasRegular := false
|
||||
for _, elem := range namedImports.Elements.Nodes {
|
||||
if elem.IsTypeOnly() {
|
||||
hasTypeOnly = true
|
||||
} else {
|
||||
hasRegular = true
|
||||
}
|
||||
}
|
||||
if hasTypeOnly && hasRegular {
|
||||
bothNamedImports = true
|
||||
}
|
||||
}
|
||||
|
||||
importDeclsWithNamed = append(importDeclsWithNamed, imp)
|
||||
}
|
||||
|
||||
if len(importDeclsWithNamed) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
namedImportsByDecl := make([][]*ast.Statement, 0, len(importDeclsWithNamed))
|
||||
for _, imp := range importDeclsWithNamed {
|
||||
clause := imp.AsImportDeclaration().ImportClause.AsImportClause()
|
||||
namedImports := clause.NamedBindings.AsNamedImports()
|
||||
namedImportsByDecl = append(namedImportsByDecl, namedImports.Elements.Nodes)
|
||||
}
|
||||
|
||||
if !bothNamedImports || len(typesToTest) == 0 {
|
||||
namesList := make([][]string, len(namedImportsByDecl))
|
||||
for i, imports := range namedImportsByDecl {
|
||||
names := make([]string, len(imports))
|
||||
for j, imp := range imports {
|
||||
names[j] = imp.Name().Text()
|
||||
}
|
||||
namesList[i] = names
|
||||
}
|
||||
sortState := detectCaseSensitivityBySort(namesList, comparersToTest)
|
||||
typeOrder := OrganizeImportsTypeOrderLast
|
||||
if len(typesToTest) == 1 {
|
||||
typeOrder = typesToTest[0]
|
||||
}
|
||||
return &namedImportSortResult{
|
||||
namedImportComparer: sortState.comparer,
|
||||
typeOrder: typeOrder,
|
||||
isSorted: sortState.isSorted,
|
||||
}
|
||||
}
|
||||
|
||||
bestDiff := map[OrganizeImportsTypeOrder]int{
|
||||
OrganizeImportsTypeOrderFirst: math.MaxInt,
|
||||
OrganizeImportsTypeOrderLast: math.MaxInt,
|
||||
OrganizeImportsTypeOrderInline: math.MaxInt,
|
||||
}
|
||||
bestComparer := map[OrganizeImportsTypeOrder]func(a, b string) int{
|
||||
OrganizeImportsTypeOrderFirst: comparersToTest[0],
|
||||
OrganizeImportsTypeOrderLast: comparersToTest[0],
|
||||
OrganizeImportsTypeOrderInline: comparersToTest[0],
|
||||
}
|
||||
|
||||
for _, curComparer := range comparersToTest {
|
||||
currDiff := map[OrganizeImportsTypeOrder]int{
|
||||
OrganizeImportsTypeOrderFirst: 0,
|
||||
OrganizeImportsTypeOrderLast: 0,
|
||||
OrganizeImportsTypeOrderInline: 0,
|
||||
}
|
||||
|
||||
for _, importDecl := range namedImportsByDecl {
|
||||
for _, typeOrder := range typesToTest {
|
||||
prefs := UserPreferences{OrganizeImportsTypeOrder: typeOrder}
|
||||
diff := measureSortedness(importDecl, func(n1, n2 *ast.Node) int {
|
||||
return compareImportOrExportSpecifiers(n1, n2, curComparer, prefs)
|
||||
})
|
||||
currDiff[typeOrder] = currDiff[typeOrder] + diff
|
||||
}
|
||||
}
|
||||
|
||||
for _, typeOrder := range typesToTest {
|
||||
if currDiff[typeOrder] < bestDiff[typeOrder] {
|
||||
bestDiff[typeOrder] = currDiff[typeOrder]
|
||||
bestComparer[typeOrder] = curComparer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, bestTypeOrder := range typesToTest {
|
||||
isBest := true
|
||||
for _, testTypeOrder := range typesToTest {
|
||||
if bestDiff[testTypeOrder] < bestDiff[bestTypeOrder] {
|
||||
isBest = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isBest {
|
||||
return &namedImportSortResult{
|
||||
namedImportComparer: bestComparer[bestTypeOrder],
|
||||
typeOrder: bestTypeOrder,
|
||||
isSorted: bestDiff[bestTypeOrder] == 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &namedImportSortResult{
|
||||
namedImportComparer: bestComparer[OrganizeImportsTypeOrderLast],
|
||||
typeOrder: OrganizeImportsTypeOrderLast,
|
||||
isSorted: bestDiff[OrganizeImportsTypeOrderLast] == 0,
|
||||
}
|
||||
}
|
||||
|
||||
type caseSensitivityDetectionResult struct {
|
||||
comparer func(a, b string) int
|
||||
isSorted bool
|
||||
}
|
||||
|
||||
// DetectModuleSpecifierCaseBySort detects the order of module specifiers based on import statements throughout the module/file
|
||||
func DetectModuleSpecifierCaseBySort(importDeclsByGroup [][]*ast.Statement, comparersToTest []func(a, b string) int) (comparer func(a, b string) int, isSorted bool) {
|
||||
moduleSpecifiersByGroup := make([][]string, 0, len(importDeclsByGroup))
|
||||
for _, importGroup := range importDeclsByGroup {
|
||||
moduleNames := make([]string, 0, len(importGroup))
|
||||
for _, decl := range importGroup {
|
||||
if expr := getModuleSpecifierExpression(decl); expr != nil {
|
||||
moduleNames = append(moduleNames, GetExternalModuleName(expr))
|
||||
} else {
|
||||
moduleNames = append(moduleNames, "")
|
||||
}
|
||||
}
|
||||
moduleSpecifiersByGroup = append(moduleSpecifiersByGroup, moduleNames)
|
||||
}
|
||||
result := detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest)
|
||||
return result.comparer, result.isSorted
|
||||
}
|
||||
|
||||
func detectCaseSensitivityBySort(originalGroups [][]string, comparersToTest []func(a, b string) int) caseSensitivityDetectionResult {
|
||||
var bestComparer func(a, b string) int
|
||||
bestDiff := math.MaxInt
|
||||
|
||||
for _, curComparer := range comparersToTest {
|
||||
diffOfCurrentComparer := 0
|
||||
|
||||
for _, listToSort := range originalGroups {
|
||||
if len(listToSort) <= 1 {
|
||||
continue
|
||||
}
|
||||
diff := measureSortedness(listToSort, curComparer)
|
||||
diffOfCurrentComparer += diff
|
||||
}
|
||||
|
||||
if diffOfCurrentComparer < bestDiff {
|
||||
bestDiff = diffOfCurrentComparer
|
||||
bestComparer = curComparer
|
||||
}
|
||||
}
|
||||
|
||||
if bestComparer == nil && len(comparersToTest) > 0 {
|
||||
bestComparer = comparersToTest[0]
|
||||
}
|
||||
|
||||
return caseSensitivityDetectionResult{
|
||||
comparer: bestComparer,
|
||||
isSorted: bestDiff == 0,
|
||||
}
|
||||
}
|
||||
|
||||
func measureSortedness[T any](arr []T, comparer func(a, b T) int) int {
|
||||
i := 0
|
||||
for j := range len(arr) - 1 {
|
||||
if comparer(arr[j], arr[j+1]) > 0 {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// GetNamedImportSpecifierComparerWithDetection returns a specifier comparer based on detecting the existing sort order within a single import statement
|
||||
func GetNamedImportSpecifierComparerWithDetection(importDecl *ast.Node, sourceFile *ast.SourceFile, preferences UserPreferences) (specifierComparer func(s1, s2 *ast.Node) int, isSorted core.Tristate) {
|
||||
comparersToTest, typeOrdersToTest := GetDetectionLists(preferences)
|
||||
|
||||
var importStmt *ast.Statement
|
||||
if importDecl.Kind == ast.KindImportDeclaration {
|
||||
importStmt = importDecl
|
||||
}
|
||||
|
||||
specifierComparer = GetNamedImportSpecifierComparer(preferences, comparersToTest[0])
|
||||
isSorted = core.TSUnknown
|
||||
|
||||
if (ResolveOrganizeImportsSort(preferences) == OrganizeImportsSortAuto || preferences.OrganizeImportsTypeOrder == OrganizeImportsTypeOrderAuto) && importStmt != nil {
|
||||
detectFromDecl := detectNamedImportOrganizationBySort([]*ast.Statement{importStmt}, comparersToTest, typeOrdersToTest)
|
||||
if detectFromDecl != nil {
|
||||
isSorted = core.BoolToTristate(detectFromDecl.isSorted)
|
||||
specifierComparer = GetNamedImportSpecifierComparer(
|
||||
UserPreferences{OrganizeImportsTypeOrder: detectFromDecl.typeOrder},
|
||||
detectFromDecl.namedImportComparer,
|
||||
)
|
||||
} else if sourceFile != nil {
|
||||
allImports := FilterImportDeclarations(sourceFile.Statements.Nodes)
|
||||
detectFromFile := detectNamedImportOrganizationBySort(allImports, comparersToTest, typeOrdersToTest)
|
||||
if detectFromFile != nil {
|
||||
isSorted = core.BoolToTristate(detectFromFile.isSorted)
|
||||
specifierComparer = GetNamedImportSpecifierComparer(
|
||||
UserPreferences{OrganizeImportsTypeOrder: detectFromFile.typeOrder},
|
||||
detectFromFile.namedImportComparer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return specifierComparer, isSorted
|
||||
}
|
||||
438
tools/tsgo/internal/ls/lsutil/symbol_display.go
Normal file
438
tools/tsgo/internal/ls/lsutil/symbol_display.go
Normal file
@@ -0,0 +1,438 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
)
|
||||
|
||||
type ScriptElementKind int
|
||||
|
||||
const (
|
||||
ScriptElementKindUnknown ScriptElementKind = iota
|
||||
ScriptElementKindWarning
|
||||
// predefined type (void) or keyword (class)
|
||||
ScriptElementKindKeyword
|
||||
// top level script node
|
||||
ScriptElementKindScriptElement
|
||||
// module foo {}
|
||||
ScriptElementKindModuleElement
|
||||
// class X {}
|
||||
ScriptElementKindClassElement
|
||||
// var x = class X {}
|
||||
ScriptElementKindLocalClassElement
|
||||
// interface Y {}
|
||||
ScriptElementKindInterfaceElement
|
||||
// type T = ...
|
||||
ScriptElementKindTypeElement
|
||||
// enum E {}
|
||||
ScriptElementKindEnumElement
|
||||
ScriptElementKindEnumMemberElement
|
||||
// Inside module and script only.
|
||||
// const v = ...
|
||||
ScriptElementKindVariableElement
|
||||
// Inside function.
|
||||
ScriptElementKindLocalVariableElement
|
||||
// using foo = ...
|
||||
ScriptElementKindVariableUsingElement
|
||||
// await using foo = ...
|
||||
ScriptElementKindVariableAwaitUsingElement
|
||||
// Inside module and script only.
|
||||
// function f() {}
|
||||
ScriptElementKindFunctionElement
|
||||
// Inside function.
|
||||
ScriptElementKindLocalFunctionElement
|
||||
// class X { [public|private]* foo() {} }
|
||||
ScriptElementKindMemberFunctionElement
|
||||
// class X { [public|private]* [get|set] foo:number; }
|
||||
ScriptElementKindMemberGetAccessorElement
|
||||
ScriptElementKindMemberSetAccessorElement
|
||||
// class X { [public|private]* foo:number; }
|
||||
// interface Y { foo:number; }
|
||||
ScriptElementKindMemberVariableElement
|
||||
// class X { [public|private]* accessor foo: number; }
|
||||
ScriptElementKindMemberAccessorVariableElement
|
||||
// class X { constructor() { } }
|
||||
// class X { static { } }
|
||||
ScriptElementKindConstructorImplementationElement
|
||||
// interface Y { ():number; }
|
||||
ScriptElementKindCallSignatureElement
|
||||
// interface Y { []:number; }
|
||||
ScriptElementKindIndexSignatureElement
|
||||
// interface Y { new():Y; }
|
||||
ScriptElementKindConstructSignatureElement
|
||||
// function foo(*Y*: string)
|
||||
ScriptElementKindParameterElement
|
||||
ScriptElementKindTypeParameterElement
|
||||
ScriptElementKindPrimitiveType
|
||||
ScriptElementKindLabel
|
||||
ScriptElementKindAlias
|
||||
ScriptElementKindConstElement
|
||||
ScriptElementKindLetElement
|
||||
ScriptElementKindDirectory
|
||||
ScriptElementKindExternalModuleName
|
||||
// String literal
|
||||
ScriptElementKindString
|
||||
// Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}"
|
||||
ScriptElementKindLink
|
||||
// Jsdoc @link: in `{@link C link text}`, the entity name "C"
|
||||
ScriptElementKindLinkName
|
||||
// Jsdoc @link: in `{@link C link text}`, the link text "link text"
|
||||
ScriptElementKindLinkText
|
||||
)
|
||||
|
||||
type ScriptElementKindModifier uint32
|
||||
|
||||
const (
|
||||
ScriptElementKindModifierNone ScriptElementKindModifier = 0
|
||||
ScriptElementKindModifierPublic ScriptElementKindModifier = 1 << iota
|
||||
ScriptElementKindModifierPrivate
|
||||
ScriptElementKindModifierProtected
|
||||
ScriptElementKindModifierExported
|
||||
ScriptElementKindModifierAmbient
|
||||
ScriptElementKindModifierStatic
|
||||
ScriptElementKindModifierAbstract
|
||||
ScriptElementKindModifierOptional
|
||||
ScriptElementKindModifierDeprecated
|
||||
ScriptElementKindModifierDts
|
||||
ScriptElementKindModifierTs
|
||||
ScriptElementKindModifierTsx
|
||||
ScriptElementKindModifierJs
|
||||
ScriptElementKindModifierJsx
|
||||
ScriptElementKindModifierJson
|
||||
ScriptElementKindModifierDmts
|
||||
ScriptElementKindModifierMts
|
||||
ScriptElementKindModifierMjs
|
||||
ScriptElementKindModifierDcts
|
||||
ScriptElementKindModifierCts
|
||||
ScriptElementKindModifierCjs
|
||||
)
|
||||
|
||||
var scriptElementKindModifierNames = []struct {
|
||||
flag ScriptElementKindModifier
|
||||
name string
|
||||
}{
|
||||
{ScriptElementKindModifierPublic, "public"},
|
||||
{ScriptElementKindModifierPrivate, "private"},
|
||||
{ScriptElementKindModifierProtected, "protected"},
|
||||
{ScriptElementKindModifierExported, "export"},
|
||||
{ScriptElementKindModifierAmbient, "declare"},
|
||||
{ScriptElementKindModifierStatic, "static"},
|
||||
{ScriptElementKindModifierAbstract, "abstract"},
|
||||
{ScriptElementKindModifierOptional, "optional"},
|
||||
{ScriptElementKindModifierDeprecated, "deprecated"},
|
||||
{ScriptElementKindModifierDts, ".d.ts"},
|
||||
{ScriptElementKindModifierTs, ".ts"},
|
||||
{ScriptElementKindModifierTsx, ".tsx"},
|
||||
{ScriptElementKindModifierJs, ".js"},
|
||||
{ScriptElementKindModifierJsx, ".jsx"},
|
||||
{ScriptElementKindModifierJson, ".json"},
|
||||
{ScriptElementKindModifierDmts, ".d.mts"},
|
||||
{ScriptElementKindModifierMts, ".mts"},
|
||||
{ScriptElementKindModifierMjs, ".mjs"},
|
||||
{ScriptElementKindModifierDcts, ".d.cts"},
|
||||
{ScriptElementKindModifierCts, ".cts"},
|
||||
{ScriptElementKindModifierCjs, ".cjs"},
|
||||
}
|
||||
|
||||
func (m ScriptElementKindModifier) Strings() collections.Set[string] {
|
||||
result := collections.Set[string]{}
|
||||
for _, entry := range scriptElementKindModifierNames {
|
||||
if m&entry.flag != 0 {
|
||||
result.Add(entry.name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var FileExtensionKindModifiers = ScriptElementKindModifierDts |
|
||||
ScriptElementKindModifierTs |
|
||||
ScriptElementKindModifierTsx |
|
||||
ScriptElementKindModifierJs |
|
||||
ScriptElementKindModifierJsx |
|
||||
ScriptElementKindModifierJson |
|
||||
ScriptElementKindModifierDmts |
|
||||
ScriptElementKindModifierMts |
|
||||
ScriptElementKindModifierMjs |
|
||||
ScriptElementKindModifierDcts |
|
||||
ScriptElementKindModifierCts |
|
||||
ScriptElementKindModifierCjs
|
||||
|
||||
func GetSymbolKind(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind {
|
||||
result := getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location)
|
||||
if result != ScriptElementKindUnknown {
|
||||
return result
|
||||
}
|
||||
flags := symbol.CombinedLocalAndExportSymbolFlags()
|
||||
if flags&ast.SymbolFlagsClass != 0 {
|
||||
decl := ast.GetDeclarationOfKind(symbol, ast.KindClassExpression)
|
||||
if decl != nil {
|
||||
return ScriptElementKindLocalClassElement
|
||||
}
|
||||
return ScriptElementKindClassElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsEnum != 0 {
|
||||
return ScriptElementKindEnumElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsTypeAlias != 0 {
|
||||
return ScriptElementKindTypeElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsInterface != 0 {
|
||||
return ScriptElementKindInterfaceElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsTypeParameter != 0 {
|
||||
return ScriptElementKindTypeParameterElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsEnumMember != 0 {
|
||||
return ScriptElementKindEnumMemberElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsAlias != 0 {
|
||||
return ScriptElementKindAlias
|
||||
}
|
||||
if flags&ast.SymbolFlagsModule != 0 {
|
||||
return ScriptElementKindModuleElement
|
||||
}
|
||||
|
||||
return ScriptElementKindUnknown
|
||||
}
|
||||
|
||||
func getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind {
|
||||
var roots []*ast.Symbol
|
||||
if typeChecker != nil {
|
||||
roots = typeChecker.GetRootSymbols(symbol)
|
||||
} else {
|
||||
roots = []*ast.Symbol{symbol}
|
||||
}
|
||||
|
||||
// If this is a method from a mapped type, leave as a method so long as it still has a call signature, as opposed to e.g.
|
||||
// `{ [K in keyof I]: number }`.
|
||||
if len(roots) == 1 &&
|
||||
roots[0].Flags&ast.SymbolFlagsMethod != 0 &&
|
||||
(typeChecker == nil || len(typeChecker.GetCallSignatures(typeChecker.GetNonNullableType(typeChecker.GetTypeOfSymbolAtLocation(symbol, location)))) > 0) {
|
||||
return ScriptElementKindMemberFunctionElement
|
||||
}
|
||||
|
||||
if typeChecker != nil {
|
||||
if typeChecker.IsUndefinedSymbol(symbol) {
|
||||
return ScriptElementKindVariableElement
|
||||
}
|
||||
if typeChecker.IsArgumentsSymbol(symbol) {
|
||||
return ScriptElementKindLocalVariableElement
|
||||
}
|
||||
if location.Kind == ast.KindThisKeyword && ast.IsExpression(location) ||
|
||||
ast.IsThisInTypeQuery(location) {
|
||||
return ScriptElementKindParameterElement
|
||||
}
|
||||
}
|
||||
|
||||
flags := symbol.CombinedLocalAndExportSymbolFlags()
|
||||
if flags&ast.SymbolFlagsVariable != 0 {
|
||||
if isFirstDeclarationOfSymbolParameter(symbol) {
|
||||
return ScriptElementKindParameterElement
|
||||
} else if symbol.ValueDeclaration != nil && ast.IsVarConst(symbol.ValueDeclaration) {
|
||||
return ScriptElementKindConstElement
|
||||
} else if symbol.ValueDeclaration != nil && ast.IsVarUsing(symbol.ValueDeclaration) {
|
||||
return ScriptElementKindVariableUsingElement
|
||||
} else if symbol.ValueDeclaration != nil && ast.IsVarAwaitUsing(symbol.ValueDeclaration) {
|
||||
return ScriptElementKindVariableAwaitUsingElement
|
||||
} else if core.Some(symbol.Declarations, ast.IsLet) {
|
||||
return ScriptElementKindLetElement
|
||||
}
|
||||
if isLocalVariableOrFunction(symbol) {
|
||||
return ScriptElementKindLocalVariableElement
|
||||
}
|
||||
return ScriptElementKindVariableElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsFunction != 0 {
|
||||
if isLocalVariableOrFunction(symbol) {
|
||||
return ScriptElementKindLocalFunctionElement
|
||||
}
|
||||
return ScriptElementKindFunctionElement
|
||||
}
|
||||
// FIXME: getter and setter use the same symbol. And it is rare to use only setter without getter, so in most cases the symbol always has getter flag.
|
||||
// So, even when the location is just on the declaration of setter, this function returns getter.
|
||||
if flags&ast.SymbolFlagsGetAccessor != 0 {
|
||||
return ScriptElementKindMemberGetAccessorElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsSetAccessor != 0 {
|
||||
return ScriptElementKindMemberSetAccessorElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsMethod != 0 {
|
||||
return ScriptElementKindMemberFunctionElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsConstructor != 0 {
|
||||
return ScriptElementKindConstructorImplementationElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsSignature != 0 {
|
||||
return ScriptElementKindIndexSignatureElement
|
||||
}
|
||||
|
||||
if flags&ast.SymbolFlagsProperty != 0 {
|
||||
if typeChecker != nil && flags&ast.SymbolFlagsTransient != 0 && symbol.CheckFlags&ast.CheckFlagsSynthetic != 0 {
|
||||
// If union property is result of union of non method (property/accessors/variables), it is labeled as property
|
||||
var unionPropertyKind ScriptElementKind
|
||||
for _, rootSymbol := range roots {
|
||||
if rootSymbol.Flags&(ast.SymbolFlagsPropertyOrAccessor|ast.SymbolFlagsVariable) != 0 {
|
||||
unionPropertyKind = ScriptElementKindMemberVariableElement
|
||||
break
|
||||
}
|
||||
}
|
||||
if unionPropertyKind == ScriptElementKindUnknown {
|
||||
// If this was union of all methods,
|
||||
// make sure it has call signatures before we can label it as method.
|
||||
typeOfUnionProperty := typeChecker.GetTypeOfSymbolAtLocation(symbol, location)
|
||||
if len(typeChecker.GetCallSignatures(typeOfUnionProperty)) > 0 {
|
||||
return ScriptElementKindMemberFunctionElement
|
||||
}
|
||||
return ScriptElementKindMemberVariableElement
|
||||
}
|
||||
return unionPropertyKind
|
||||
}
|
||||
|
||||
return ScriptElementKindMemberVariableElement
|
||||
}
|
||||
|
||||
return ScriptElementKindUnknown
|
||||
}
|
||||
|
||||
func isFirstDeclarationOfSymbolParameter(symbol *ast.Symbol) bool {
|
||||
var declaration *ast.Node
|
||||
if len(symbol.Declarations) > 0 {
|
||||
declaration = symbol.Declarations[0]
|
||||
}
|
||||
result := ast.FindAncestorOrQuit(declaration, func(n *ast.Node) ast.FindAncestorResult {
|
||||
if ast.IsParameterDeclaration(n) {
|
||||
return ast.FindAncestorTrue
|
||||
}
|
||||
if ast.IsBindingElement(n) || ast.IsObjectBindingPattern(n) || ast.IsArrayBindingPattern(n) {
|
||||
return ast.FindAncestorFalse
|
||||
}
|
||||
return ast.FindAncestorQuit
|
||||
})
|
||||
|
||||
return result != nil
|
||||
}
|
||||
|
||||
func isLocalVariableOrFunction(symbol *ast.Symbol) bool {
|
||||
if symbol.Parent != nil {
|
||||
return false // This is exported symbol
|
||||
}
|
||||
|
||||
for _, decl := range symbol.Declarations {
|
||||
// Function expressions are local
|
||||
if decl.Kind == ast.KindFunctionExpression {
|
||||
return true
|
||||
}
|
||||
|
||||
if decl.Kind != ast.KindVariableDeclaration && decl.Kind != ast.KindFunctionDeclaration {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the parent is not source file or module block, it is a local variable.
|
||||
parent := decl.Parent
|
||||
for ; !ast.IsFunctionBlock(parent); parent = parent.Parent {
|
||||
// Reached source file or module block
|
||||
if parent.Kind == ast.KindSourceFile || parent.Kind == ast.KindModuleBlock {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ast.IsFunctionBlock(parent) {
|
||||
// Parent is in function block.
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) ScriptElementKindModifier {
|
||||
if symbol == nil {
|
||||
return ScriptElementKindModifierNone
|
||||
}
|
||||
|
||||
modifiers := getNormalizedSymbolModifiers(typeChecker, symbol)
|
||||
if symbol.Flags&ast.SymbolFlagsAlias != 0 && typeChecker != nil {
|
||||
resolvedSymbol := typeChecker.GetAliasedSymbol(symbol)
|
||||
if resolvedSymbol != symbol {
|
||||
modifiers |= getNormalizedSymbolModifiers(typeChecker, resolvedSymbol)
|
||||
}
|
||||
}
|
||||
if symbol.Flags&ast.SymbolFlagsOptional != 0 {
|
||||
modifiers |= ScriptElementKindModifierOptional
|
||||
}
|
||||
|
||||
return modifiers
|
||||
}
|
||||
|
||||
func getNormalizedSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) ScriptElementKindModifier {
|
||||
var modifierSet ScriptElementKindModifier
|
||||
if len(symbol.Declarations) > 0 {
|
||||
declaration := symbol.Declarations[0]
|
||||
declarations := symbol.Declarations[1:]
|
||||
// omit deprecated flag if some declarations are not deprecated
|
||||
var excludeFlags ast.ModifierFlags
|
||||
if len(declarations) > 0 &&
|
||||
isDeprecatedDeclaration(typeChecker, declaration) && // !!! include jsdoc node flags
|
||||
core.Some(declarations, func(d *ast.Node) bool { return !isDeprecatedDeclaration(typeChecker, d) }) {
|
||||
excludeFlags = ast.ModifierFlagsDeprecated
|
||||
} else {
|
||||
excludeFlags = ast.ModifierFlagsNone
|
||||
}
|
||||
modifierSet = getNodeModifiers(typeChecker, declaration, excludeFlags)
|
||||
}
|
||||
|
||||
return modifierSet
|
||||
}
|
||||
|
||||
func isDeprecatedDeclaration(typeChecker *checker.Checker, declaration *ast.Node) bool {
|
||||
if typeChecker != nil {
|
||||
return typeChecker.IsDeprecatedDeclaration(declaration)
|
||||
}
|
||||
return ast.IsDeprecatedDeclaration(declaration)
|
||||
}
|
||||
|
||||
func getNodeModifiers(typeChecker *checker.Checker, node *ast.Node, excludeFlags ast.ModifierFlags) ScriptElementKindModifier {
|
||||
var result ScriptElementKindModifier
|
||||
var flags ast.ModifierFlags
|
||||
if ast.IsDeclaration(node) {
|
||||
flags = ast.GetCombinedModifierFlags(node)
|
||||
if isDeprecatedDeclaration(typeChecker, node) {
|
||||
flags |= ast.ModifierFlagsDeprecated
|
||||
}
|
||||
flags &^= excludeFlags
|
||||
}
|
||||
|
||||
if flags&ast.ModifierFlagsPrivate != 0 {
|
||||
result |= ScriptElementKindModifierPrivate
|
||||
}
|
||||
if flags&ast.ModifierFlagsProtected != 0 {
|
||||
result |= ScriptElementKindModifierProtected
|
||||
}
|
||||
if flags&ast.ModifierFlagsPublic != 0 {
|
||||
result |= ScriptElementKindModifierPublic
|
||||
}
|
||||
if flags&ast.ModifierFlagsStatic != 0 {
|
||||
result |= ScriptElementKindModifierStatic
|
||||
}
|
||||
if flags&ast.ModifierFlagsAbstract != 0 {
|
||||
result |= ScriptElementKindModifierAbstract
|
||||
}
|
||||
if flags&ast.ModifierFlagsExport != 0 {
|
||||
result |= ScriptElementKindModifierExported
|
||||
}
|
||||
if flags&ast.ModifierFlagsDeprecated != 0 {
|
||||
result |= ScriptElementKindModifierDeprecated
|
||||
}
|
||||
if flags&ast.ModifierFlagsAmbient != 0 {
|
||||
result |= ScriptElementKindModifierAmbient
|
||||
}
|
||||
if node.Flags&ast.NodeFlagsAmbient != 0 {
|
||||
result |= ScriptElementKindModifierAmbient
|
||||
}
|
||||
if node.Kind == ast.KindExportAssignment {
|
||||
result |= ScriptElementKindModifierExported
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
901
tools/tsgo/internal/ls/lsutil/userpreferences.go
Normal file
901
tools/tsgo/internal/ls/lsutil/userpreferences.go
Normal file
@@ -0,0 +1,901 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/modulespecifiers"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
|
||||
)
|
||||
|
||||
func NewDefaultUserPreferences() UserPreferences {
|
||||
return UserPreferences{
|
||||
FormatCodeSettings: GetDefaultFormatCodeSettings(),
|
||||
|
||||
IncludeCompletionsForModuleExports: core.TSTrue,
|
||||
IncludeCompletionsForImportStatements: core.TSTrue,
|
||||
EnableAutoClosingTags: core.TSTrue,
|
||||
EnableJSDocCompletions: core.TSTrue,
|
||||
GenerateReturnInDocTemplate: core.TSTrue,
|
||||
|
||||
AllowRenameOfImportPath: core.TSTrue,
|
||||
ProvideRefactorNotApplicableReason: core.TSTrue,
|
||||
EnableFormatting: core.TSTrue,
|
||||
EnableValidation: core.TSTrue,
|
||||
DisplayPartsForJSDoc: core.TSTrue,
|
||||
DisableLineTextInReferences: core.TSTrue,
|
||||
ReportStyleChecksAsWarnings: core.TSTrue,
|
||||
|
||||
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
|
||||
}
|
||||
}
|
||||
|
||||
// UserPreferences represents TypeScript language service preferences.
|
||||
//
|
||||
// Fields are populated using two tags:
|
||||
// - `raw:"name"` or `raw:"name,invert"` - TypeScript/raw name for unstable section lookup
|
||||
// - `config:"path.to.setting"` or `config:"path.to.setting,invert"` - VS Code nested config path
|
||||
//
|
||||
// At least one tag must be present on each preference field.
|
||||
// The `,invert` modifier inverts boolean values (e.g., VS Code's "suppress" -> our "include").
|
||||
type UserPreferences struct {
|
||||
FormatCodeSettings FormatCodeSettings
|
||||
|
||||
QuotePreference QuotePreference `raw:"quotePreference" config:"preferences.quoteStyle"`
|
||||
LazyConfiguredProjectsFromExternalProject core.Tristate `raw:"lazyConfiguredProjectsFromExternalProject"` // !!!
|
||||
|
||||
// A positive integer indicating the maximum length of a hover text before it is truncated.
|
||||
//
|
||||
// Default: `500`
|
||||
MaximumHoverLength int `raw:"maximumHoverLength"` // !!!
|
||||
|
||||
// ------- Completions -------
|
||||
|
||||
// If enabled, TypeScript will search through all external modules' exports and add them to the completions list.
|
||||
// This affects lone identifier completions but not completions on the right hand side of `obj.`.
|
||||
IncludeCompletionsForModuleExports core.Tristate `raw:"includeCompletionsForModuleExports" config:"suggest.autoImports"`
|
||||
// Enables auto-import-style completions on partially-typed import statements. E.g., allows
|
||||
// `import write|` to be completed to `import { writeFile } from "fs"`.
|
||||
IncludeCompletionsForImportStatements core.Tristate `raw:"includeCompletionsForImportStatements" config:"suggest.includeCompletionsForImportStatements"`
|
||||
// Unless this option is `false`, member completion lists triggered with `.` will include entries
|
||||
// on potentially-null and potentially-undefined values, with insertion text to replace
|
||||
// preceding `.` tokens with `?.`.
|
||||
IncludeAutomaticOptionalChainCompletions core.Tristate `raw:"includeAutomaticOptionalChainCompletions" config:"suggest.includeAutomaticOptionalChainCompletions"`
|
||||
// If enabled, completions for class members (e.g. methods and properties) will include
|
||||
// a whole declaration for the member.
|
||||
// E.g., `class A { f| }` could be completed to `class A { foo(): number {} }`, instead of
|
||||
// `class A { foo }`.
|
||||
IncludeCompletionsWithClassMemberSnippets core.Tristate `raw:"includeCompletionsWithClassMemberSnippets" config:"suggest.classMemberSnippets.enabled"` // !!!
|
||||
// If enabled, object literal methods will have a method declaration completion entry in addition
|
||||
// to the regular completion entry containing just the method name.
|
||||
// E.g., `const objectLiteral: T = { f| }` could be completed to `const objectLiteral: T = { foo(): void {} }`,
|
||||
// in addition to `const objectLiteral: T = { foo }`.
|
||||
IncludeCompletionsWithObjectLiteralMethodSnippets core.Tristate `raw:"includeCompletionsWithObjectLiteralMethodSnippets" config:"suggest.objectLiteralMethodSnippets.enabled"` // !!!
|
||||
JsxAttributeCompletionStyle JsxAttributeCompletionStyle `raw:"jsxAttributeCompletionStyle" config:"preferences.jsxAttributeCompletionStyle"`
|
||||
EnableAutoClosingTags core.Tristate `raw:"autoClosingTags" config:"autoClosingTags.enabled" fallbackConfig:"autoClosingTags"`
|
||||
EnableJSDocCompletions core.Tristate `raw:"completeJSDocs" config:"suggest.jsdoc.enabled" fallbackConfig:"suggest.completeJSDocs"`
|
||||
GenerateReturnInDocTemplate core.Tristate `raw:"generateReturnInDocTemplate" config:"suggest.jsdoc.generateReturns"`
|
||||
|
||||
// ------- AutoImports --------
|
||||
|
||||
ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference `raw:"importModuleSpecifierPreference" config:"preferences.importModuleSpecifier"` // !!!
|
||||
// Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js"
|
||||
ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `raw:"importModuleSpecifierEnding" config:"preferences.importModuleSpecifierEnding"` // !!!
|
||||
AutoImportSpecifierExcludeRegexes []string `raw:"autoImportSpecifierExcludeRegexes" config:"preferences.autoImportSpecifierExcludeRegexes"` // !!!
|
||||
AutoImportFileExcludePatterns []string `raw:"autoImportFileExcludePatterns" config:"preferences.autoImportFileExcludePatterns"`
|
||||
AutoImportEntrypointDirectorySearch core.Tristate `raw:"autoImportEntrypointDirectorySearch" config:"preferences.autoImportEntrypointDirectorySearch"`
|
||||
PreferTypeOnlyAutoImports core.Tristate `raw:"preferTypeOnlyAutoImports" config:"preferences.preferTypeOnlyAutoImports"`
|
||||
|
||||
// ------- OrganizeImports -------
|
||||
|
||||
// Indicates which deterministic preset should be used to sort imports.
|
||||
// "auto" detects the existing ordinal case sensitivity where possible.
|
||||
OrganizeImportsSort OrganizeImportsSort `raw:"organizeImportsSort" config:"preferences.organizeImports.sort"` // !!!
|
||||
// Indicates whether imports should be organized in a case-insensitive manner.
|
||||
//
|
||||
// Default: TSUnknown ("auto" in strada), will perform detection
|
||||
OrganizeImportsIgnoreCase core.Tristate `raw:"organizeImportsIgnoreCase" config:"preferences.organizeImports.caseSensitivity"` // !!!
|
||||
// Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value of their
|
||||
// code points, or via "unicode" natural sorting. This implementation is locale-agnostic and approximates the practical
|
||||
// import-sorting behavior rather than the full Unicode Collation Algorithm.
|
||||
//
|
||||
// Default: Ordinal
|
||||
OrganizeImportsCollation OrganizeImportsCollation `raw:"organizeImportsCollation" config:"preferences.organizeImports.unicodeCollation"` // !!!
|
||||
// Indicates the locale to use for "unicode" collation in legacy clients. This is accepted for compatibility, but
|
||||
// currently ignored because organize-import sorting is deterministic and locale-agnostic.
|
||||
//
|
||||
// This preference is ignored if organizeImportsCollation is not `unicode`.
|
||||
//
|
||||
// Default: `"en"`
|
||||
OrganizeImportsLocale string `raw:"organizeImportsLocale" config:"preferences.organizeImports.locale"` // !!!
|
||||
// Indicates whether numeric collation should be used for digit sequences in strings. When `true`, will collate
|
||||
// strings such that `a1z < a2z < a100z`. When `false`, will collate strings such that `a1z < a100z < a2z`.
|
||||
//
|
||||
// This preference is ignored if organizeImportsCollation is not `unicode`.
|
||||
//
|
||||
// Default: `false`
|
||||
OrganizeImportsNumericCollation core.Tristate `raw:"organizeImportsNumericCollation" config:"preferences.organizeImports.numericCollation"` // !!!
|
||||
// Indicates whether accents and other diacritic marks are considered unequal for the purpose of sorting.
|
||||
//
|
||||
// This preference is ignored if organizeImportsCollation is not `unicode`.
|
||||
//
|
||||
// Default: `true`
|
||||
OrganizeImportsAccentCollation core.Tristate `raw:"organizeImportsAccentCollation" config:"preferences.organizeImports.accentCollation"` // !!!
|
||||
// Indicates whether upper case or lower case should sort first.
|
||||
//
|
||||
// This permission is ignored if:
|
||||
// - organizeImportsCollation is not `unicode`
|
||||
// - organizeImportsIgnoreCase is `true`
|
||||
// - organizeImportsIgnoreCase is `auto` and the auto-detected case sensitivity is case-insensitive.
|
||||
//
|
||||
// Default: `false`
|
||||
OrganizeImportsCaseFirst OrganizeImportsCaseFirst `raw:"organizeImportsCaseFirst" config:"preferences.organizeImports.caseFirst"` // !!!
|
||||
// Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is type-only.
|
||||
//
|
||||
// Default: `auto`, which defaults to `last`
|
||||
OrganizeImportsTypeOrder OrganizeImportsTypeOrder `raw:"organizeImportsTypeOrder" config:"preferences.organizeImports.typeOrder"` // !!!
|
||||
|
||||
// ------- MoveToFile -------
|
||||
|
||||
AllowTextChangesInNewFiles core.Tristate `raw:"allowTextChangesInNewFiles"` // !!!
|
||||
|
||||
// ------- Rename -------
|
||||
|
||||
UseAliasesForRename core.Tristate `raw:"providePrefixAndSuffixTextForRename" config:"preferences.useAliasesForRenames"`
|
||||
AllowRenameOfImportPath core.Tristate `raw:"allowRenameOfImportPath"`
|
||||
|
||||
// ------- CodeFixes/Refactors -------
|
||||
|
||||
ProvideRefactorNotApplicableReason core.Tristate `raw:"provideRefactorNotApplicableReason"` // !!!
|
||||
|
||||
// ------- InlayHints -------
|
||||
|
||||
InlayHints InlayHintsPreferences
|
||||
|
||||
// ------- CodeLens -------
|
||||
|
||||
CodeLens CodeLensUserPreferences
|
||||
|
||||
// ------- Definition -------
|
||||
|
||||
PreferGoToSourceDefinition bool `raw:"preferGoToSourceDefinition"`
|
||||
|
||||
// ------- Symbols -------
|
||||
|
||||
ExcludeLibrarySymbolsInNavTo core.Tristate `raw:"excludeLibrarySymbolsInNavTo" config:"workspaceSymbols.excludeLibrarySymbols"`
|
||||
|
||||
// ------- Misc -------
|
||||
|
||||
EnableFormatting core.Tristate `raw:"formatEnabled" config:"format.enabled" fallbackConfig:"format.enable"`
|
||||
EnableValidation core.Tristate `raw:"validateEnabled" config:"validate.enabled" fallbackConfig:"validate.enable"`
|
||||
DisableSuggestions core.Tristate `raw:"disableSuggestions"` // !!!
|
||||
DisableLineTextInReferences core.Tristate `raw:"disableLineTextInReferences"` // !!!
|
||||
DisplayPartsForJSDoc core.Tristate `raw:"displayPartsForJSDoc"` // !!!
|
||||
ReportStyleChecksAsWarnings core.Tristate `raw:"reportStyleChecksAsWarnings" config:"reportStyleChecksAsWarnings"`
|
||||
|
||||
// ------- ATA -------
|
||||
|
||||
// DisableAutomaticTypeAcquisition is the deprecated setting from typescript.disableAutomaticTypeAcquisition.
|
||||
DisableAutomaticTypeAcquisition core.Tristate `raw:"disableAutomaticTypeAcquisition" config:"disableAutomaticTypeAcquisition"`
|
||||
// AutomaticTypeAcquisitionEnabled is the unified setting from tsserver.automaticTypeAcquisition.enabled under the js/ts section.
|
||||
// When set, it takes precedence over DisableAutomaticTypeAcquisition.
|
||||
AutomaticTypeAcquisitionEnabled core.Tristate `raw:"automaticTypeAcquisitionEnabled" config:"tsserver.automaticTypeAcquisition.enabled"`
|
||||
// TODO: add tsserver.web.typeAcquisition.enabled under the js/ts section for the web variant when web support is implemented.
|
||||
|
||||
// ------- Project Configuration -------
|
||||
|
||||
// CustomConfigFileName specifies a custom config file name to use before defaulting to tsconfig.json/jsconfig.json.
|
||||
CustomConfigFileName string `raw:"customConfigFileName" config:"customConfigFileName"`
|
||||
}
|
||||
|
||||
// IsATADisabled returns whether Automatic Type Acquisition is disabled based on user preferences.
|
||||
// It checks the unified setting (tsserver.automaticTypeAcquisition.enabled) first,
|
||||
// then falls back to the deprecated setting (disableAutomaticTypeAcquisition).
|
||||
func (p UserPreferences) IsATADisabled() bool {
|
||||
if !p.AutomaticTypeAcquisitionEnabled.IsUnknown() {
|
||||
return !p.AutomaticTypeAcquisitionEnabled.IsTrue()
|
||||
}
|
||||
return p.DisableAutomaticTypeAcquisition.IsTrue()
|
||||
}
|
||||
|
||||
type InlayHintsPreferences struct {
|
||||
IncludeInlayParameterNameHints IncludeInlayParameterNameHints `raw:"includeInlayParameterNameHints" config:"inlayHints.parameterNames.enabled"`
|
||||
IncludeInlayParameterNameHintsWhenArgumentMatchesName core.Tristate `raw:"includeInlayParameterNameHintsWhenArgumentMatchesName" config:"inlayHints.parameterNames.suppressWhenArgumentMatchesName,invert"`
|
||||
IncludeInlayFunctionParameterTypeHints core.Tristate `raw:"includeInlayFunctionParameterTypeHints" config:"inlayHints.parameterTypes.enabled"`
|
||||
IncludeInlayVariableTypeHints core.Tristate `raw:"includeInlayVariableTypeHints" config:"inlayHints.variableTypes.enabled"`
|
||||
IncludeInlayVariableTypeHintsWhenTypeMatchesName core.Tristate `raw:"includeInlayVariableTypeHintsWhenTypeMatchesName" config:"inlayHints.variableTypes.suppressWhenTypeMatchesName,invert"`
|
||||
IncludeInlayPropertyDeclarationTypeHints core.Tristate `raw:"includeInlayPropertyDeclarationTypeHints" config:"inlayHints.propertyDeclarationTypes.enabled"`
|
||||
IncludeInlayFunctionLikeReturnTypeHints core.Tristate `raw:"includeInlayFunctionLikeReturnTypeHints" config:"inlayHints.functionLikeReturnTypes.enabled"`
|
||||
IncludeInlayEnumMemberValueHints core.Tristate `raw:"includeInlayEnumMemberValueHints" config:"inlayHints.enumMemberValues.enabled"`
|
||||
}
|
||||
|
||||
type CodeLensUserPreferences struct {
|
||||
ReferencesCodeLensEnabled core.Tristate `raw:"referencesCodeLensEnabled" config:"referencesCodeLens.enabled"`
|
||||
ImplementationsCodeLensEnabled core.Tristate `raw:"implementationsCodeLensEnabled" config:"implementationsCodeLens.enabled"`
|
||||
ReferencesCodeLensShowOnAllFunctions core.Tristate `raw:"referencesCodeLensShowOnAllFunctions" config:"referencesCodeLens.showOnAllFunctions"`
|
||||
ImplementationsCodeLensShowOnInterfaceMethods core.Tristate `raw:"implementationsCodeLensShowOnInterfaceMethods" config:"implementationsCodeLens.showOnInterfaceMethods"`
|
||||
ImplementationsCodeLensShowOnAllClassMethods core.Tristate `raw:"implementationsCodeLensShowOnAllClassMethods" config:"implementationsCodeLens.showOnAllClassMethods"`
|
||||
}
|
||||
|
||||
// --- Enum Types ---
|
||||
|
||||
type QuotePreference string
|
||||
|
||||
const (
|
||||
QuotePreferenceUnknown QuotePreference = ""
|
||||
QuotePreferenceAuto QuotePreference = "auto"
|
||||
QuotePreferenceDouble QuotePreference = "double"
|
||||
QuotePreferenceSingle QuotePreference = "single"
|
||||
)
|
||||
|
||||
type JsxAttributeCompletionStyle string
|
||||
|
||||
const (
|
||||
JsxAttributeCompletionStyleUnknown JsxAttributeCompletionStyle = ""
|
||||
JsxAttributeCompletionStyleAuto JsxAttributeCompletionStyle = "auto"
|
||||
JsxAttributeCompletionStyleBraces JsxAttributeCompletionStyle = "braces"
|
||||
JsxAttributeCompletionStyleNone JsxAttributeCompletionStyle = "none"
|
||||
)
|
||||
|
||||
type IncludeInlayParameterNameHints string
|
||||
|
||||
const (
|
||||
IncludeInlayParameterNameHintsNone IncludeInlayParameterNameHints = ""
|
||||
IncludeInlayParameterNameHintsAll IncludeInlayParameterNameHints = "all"
|
||||
IncludeInlayParameterNameHintsLiterals IncludeInlayParameterNameHints = "literals"
|
||||
)
|
||||
|
||||
type OrganizeImportsSort int
|
||||
|
||||
const (
|
||||
OrganizeImportsSortAuto OrganizeImportsSort = iota
|
||||
OrganizeImportsSortOrdinal
|
||||
OrganizeImportsSortOrdinalIgnoreCase
|
||||
OrganizeImportsSortNatural
|
||||
OrganizeImportsSortNaturalIgnoreCase
|
||||
)
|
||||
|
||||
type OrganizeImportsCollation bool
|
||||
|
||||
const (
|
||||
OrganizeImportsCollationOrdinal OrganizeImportsCollation = false
|
||||
OrganizeImportsCollationUnicode OrganizeImportsCollation = true
|
||||
)
|
||||
|
||||
type OrganizeImportsCaseFirst int
|
||||
|
||||
const (
|
||||
OrganizeImportsCaseFirstFalse OrganizeImportsCaseFirst = 0
|
||||
OrganizeImportsCaseFirstLower OrganizeImportsCaseFirst = 1
|
||||
OrganizeImportsCaseFirstUpper OrganizeImportsCaseFirst = 2
|
||||
)
|
||||
|
||||
type OrganizeImportsTypeOrder int
|
||||
|
||||
const (
|
||||
OrganizeImportsTypeOrderAuto OrganizeImportsTypeOrder = 0
|
||||
OrganizeImportsTypeOrderLast OrganizeImportsTypeOrder = 1
|
||||
OrganizeImportsTypeOrderInline OrganizeImportsTypeOrder = 2
|
||||
OrganizeImportsTypeOrderFirst OrganizeImportsTypeOrder = 3
|
||||
)
|
||||
|
||||
// --- Reflection-based parsing infrastructure ---
|
||||
|
||||
// typeParsers maps reflect.Type to a function that parses a value into that type.
|
||||
var typeParsers = map[reflect.Type]func(any) any{
|
||||
reflect.TypeFor[core.Tristate](): func(val any) any {
|
||||
if b, ok := val.(bool); ok {
|
||||
if b {
|
||||
return core.TSTrue
|
||||
}
|
||||
return core.TSFalse
|
||||
}
|
||||
return core.TSUnknown
|
||||
},
|
||||
reflect.TypeFor[IndentStyle](): func(val any) any {
|
||||
return parseIndentStyle(val)
|
||||
},
|
||||
reflect.TypeFor[SemicolonPreference](): func(val any) any {
|
||||
return parseSemicolonPreference(val)
|
||||
},
|
||||
reflect.TypeFor[QuotePreference](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "auto":
|
||||
return QuotePreferenceAuto
|
||||
case "double":
|
||||
return QuotePreferenceDouble
|
||||
case "single":
|
||||
return QuotePreferenceSingle
|
||||
}
|
||||
}
|
||||
return QuotePreferenceUnknown
|
||||
},
|
||||
reflect.TypeFor[JsxAttributeCompletionStyle](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "braces":
|
||||
return JsxAttributeCompletionStyleBraces
|
||||
case "none":
|
||||
return JsxAttributeCompletionStyleNone
|
||||
}
|
||||
}
|
||||
return JsxAttributeCompletionStyleAuto
|
||||
},
|
||||
reflect.TypeFor[IncludeInlayParameterNameHints](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch s {
|
||||
case "all":
|
||||
return IncludeInlayParameterNameHintsAll
|
||||
case "literals":
|
||||
return IncludeInlayParameterNameHintsLiterals
|
||||
}
|
||||
}
|
||||
return IncludeInlayParameterNameHintsNone
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsSort](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "ordinal":
|
||||
return OrganizeImportsSortOrdinal
|
||||
case "ordinalignorecase":
|
||||
return OrganizeImportsSortOrdinalIgnoreCase
|
||||
case "natural":
|
||||
return OrganizeImportsSortNatural
|
||||
case "naturalignorecase":
|
||||
return OrganizeImportsSortNaturalIgnoreCase
|
||||
}
|
||||
}
|
||||
return OrganizeImportsSortAuto
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsCollation](): func(val any) any {
|
||||
if s, ok := val.(string); ok && strings.ToLower(s) == "unicode" {
|
||||
return OrganizeImportsCollationUnicode
|
||||
}
|
||||
return OrganizeImportsCollationOrdinal
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsCaseFirst](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch s {
|
||||
case "lower":
|
||||
return OrganizeImportsCaseFirstLower
|
||||
case "upper":
|
||||
return OrganizeImportsCaseFirstUpper
|
||||
}
|
||||
}
|
||||
return OrganizeImportsCaseFirstFalse
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsTypeOrder](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch s {
|
||||
case "last":
|
||||
return OrganizeImportsTypeOrderLast
|
||||
case "inline":
|
||||
return OrganizeImportsTypeOrderInline
|
||||
case "first":
|
||||
return OrganizeImportsTypeOrderFirst
|
||||
}
|
||||
}
|
||||
return OrganizeImportsTypeOrderAuto
|
||||
},
|
||||
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierPreference](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "project-relative":
|
||||
return modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative
|
||||
case "relative":
|
||||
return modulespecifiers.ImportModuleSpecifierPreferenceRelative
|
||||
case "non-relative":
|
||||
return modulespecifiers.ImportModuleSpecifierPreferenceNonRelative
|
||||
}
|
||||
}
|
||||
return modulespecifiers.ImportModuleSpecifierPreferenceShortest
|
||||
},
|
||||
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierEndingPreference](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "minimal":
|
||||
return modulespecifiers.ImportModuleSpecifierEndingPreferenceMinimal
|
||||
case "index":
|
||||
return modulespecifiers.ImportModuleSpecifierEndingPreferenceIndex
|
||||
case "js":
|
||||
return modulespecifiers.ImportModuleSpecifierEndingPreferenceJs
|
||||
}
|
||||
}
|
||||
return modulespecifiers.ImportModuleSpecifierEndingPreferenceAuto
|
||||
},
|
||||
}
|
||||
|
||||
// typeSerializers maps reflect.Type to a function that serializes a value of that type.
|
||||
// For types which do not serialize as-is (tristate, enums, etc).
|
||||
var typeSerializers = map[reflect.Type]func(any) any{
|
||||
reflect.TypeFor[core.Tristate](): func(val any) any {
|
||||
switch val.(core.Tristate) {
|
||||
case core.TSTrue:
|
||||
return true
|
||||
case core.TSFalse:
|
||||
return false
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsSort](): func(val any) any {
|
||||
switch val.(OrganizeImportsSort) {
|
||||
case OrganizeImportsSortOrdinal:
|
||||
return "ordinal"
|
||||
case OrganizeImportsSortOrdinalIgnoreCase:
|
||||
return "ordinalIgnoreCase"
|
||||
case OrganizeImportsSortNatural:
|
||||
return "natural"
|
||||
case OrganizeImportsSortNaturalIgnoreCase:
|
||||
return "naturalIgnoreCase"
|
||||
default:
|
||||
return "auto"
|
||||
}
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsCollation](): func(val any) any {
|
||||
if val.(OrganizeImportsCollation) == OrganizeImportsCollationUnicode {
|
||||
return "unicode"
|
||||
}
|
||||
return "ordinal"
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsCaseFirst](): func(val any) any {
|
||||
switch val.(OrganizeImportsCaseFirst) {
|
||||
case OrganizeImportsCaseFirstLower:
|
||||
return "lower"
|
||||
case OrganizeImportsCaseFirstUpper:
|
||||
return "upper"
|
||||
default:
|
||||
return "default"
|
||||
}
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsTypeOrder](): func(val any) any {
|
||||
switch val.(OrganizeImportsTypeOrder) {
|
||||
case OrganizeImportsTypeOrderLast:
|
||||
return "last"
|
||||
case OrganizeImportsTypeOrderInline:
|
||||
return "inline"
|
||||
case OrganizeImportsTypeOrderFirst:
|
||||
return "first"
|
||||
default:
|
||||
return "auto"
|
||||
}
|
||||
},
|
||||
// These enums distinguish an unset zero value (e.g. "") from their effective
|
||||
// default (e.g. "auto"): the parser promotes unset/unknown input to the
|
||||
// non-zero default. Plain string serialization would therefore write "" for
|
||||
// an unset field and the parser would read it back as the non-zero default,
|
||||
// breaking round-tripping. Mirror the core.Tristate serializer above and omit
|
||||
// the unset value (return nil) so it decodes back to the zero value. (Enums
|
||||
// whose default already is their zero value, like the OrganizeImports* ones,
|
||||
// round-trip without this.)
|
||||
//
|
||||
// TODO: These three are the only parsers whose fallback is a non-zero value;
|
||||
// every other parser returns its zero value as the fallback. They should be
|
||||
// made consistent: change the parser fallback to return the zero value and
|
||||
// remove this serializer (relying on the default string serialization, which
|
||||
// already omits ""). The consumer must then treat the zero value as the
|
||||
// effective default. The two module-specifier enums are safe to convert (all
|
||||
// read sites already treat the "" zero identically to the promoted default).
|
||||
reflect.TypeFor[JsxAttributeCompletionStyle](): func(val any) any {
|
||||
// TODO: make consistent with other enums (see note above). Unlike the
|
||||
// module-specifier enums, the consumer in completions.go distinguishes
|
||||
// JsxAttributeCompletionStyleUnknown from ...Auto, so converting this one
|
||||
// requires updating that consumer to treat the zero value as "auto".
|
||||
if v := val.(JsxAttributeCompletionStyle); v != JsxAttributeCompletionStyleUnknown {
|
||||
return string(v)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierPreference](): func(val any) any {
|
||||
// TODO: make consistent with other enums (see note above): have the parser
|
||||
// return the zero value (None) as its fallback and drop this serializer.
|
||||
if v := val.(modulespecifiers.ImportModuleSpecifierPreference); v != "" {
|
||||
return string(v)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierEndingPreference](): func(val any) any {
|
||||
// TODO: make consistent with other enums (see note above): have the parser
|
||||
// return the zero value (None) as its fallback and drop this serializer.
|
||||
if v := val.(modulespecifiers.ImportModuleSpecifierEndingPreference); v != "" {
|
||||
return string(v)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// configPathParsers provides field-specific config value parsers that override the default
|
||||
// type-based parser when the VS Code config value format differs from the Go field type.
|
||||
var configPathParsers = map[string]func(any) any{
|
||||
// VS Code sends caseSensitivity as a string ("auto"/"caseSensitive"/"caseInsensitive"),
|
||||
// but OrganizeImportsIgnoreCase is a core.Tristate.
|
||||
"preferences.organizeImports.caseSensitivity": func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "caseinsensitive":
|
||||
return core.TSTrue
|
||||
case "casesensitive":
|
||||
return core.TSFalse
|
||||
}
|
||||
}
|
||||
if b, ok := val.(bool); ok {
|
||||
if b {
|
||||
return core.TSTrue
|
||||
}
|
||||
return core.TSFalse
|
||||
}
|
||||
return core.TSUnknown
|
||||
},
|
||||
}
|
||||
|
||||
type fieldInfo struct {
|
||||
rawName string // raw name for unstable section lookup (e.g., "quotePreference")
|
||||
configPath string // dotted path for config (e.g., "preferences.quoteStyle")
|
||||
fallbackConfigPaths []configPathInfo
|
||||
fieldPath []int // index path to field in struct
|
||||
rawInvert bool // whether to invert boolean values for raw name
|
||||
configInvert bool // whether to invert boolean values for config path
|
||||
}
|
||||
|
||||
type configPathInfo struct {
|
||||
path string
|
||||
invert bool
|
||||
}
|
||||
|
||||
var fieldInfoCache = sync.OnceValue(func() []fieldInfo {
|
||||
return collectFieldInfos(reflect.TypeFor[UserPreferences](), nil)
|
||||
})
|
||||
|
||||
// unstableNameIndex maps raw names to fieldInfo index for unstable section lookup.
|
||||
var unstableNameIndex = sync.OnceValue(func() map[string]int {
|
||||
infos := fieldInfoCache()
|
||||
index := make(map[string]int, len(infos))
|
||||
for i, info := range infos {
|
||||
if info.rawName != "" {
|
||||
index[info.rawName] = i
|
||||
}
|
||||
}
|
||||
return index
|
||||
})
|
||||
|
||||
func collectFieldInfos(t reflect.Type, indexPath []int) []fieldInfo {
|
||||
var infos []fieldInfo
|
||||
for i := range t.NumField() {
|
||||
field := t.Field(i)
|
||||
currentPath := append(slices.Clone(indexPath), i)
|
||||
|
||||
rawTag := field.Tag.Get("raw")
|
||||
configTag := field.Tag.Get("config")
|
||||
fallbackConfigTag := field.Tag.Get("fallbackConfig")
|
||||
|
||||
if rawTag == "" && configTag == "" {
|
||||
// Embedded struct without tags - recurse into it
|
||||
if field.Type.Kind() == reflect.Struct {
|
||||
infos = append(infos, collectFieldInfos(field.Type, currentPath)...)
|
||||
continue
|
||||
}
|
||||
panic("raw or config tag required for field " + field.Name)
|
||||
}
|
||||
|
||||
info := fieldInfo{
|
||||
fieldPath: currentPath,
|
||||
}
|
||||
|
||||
// Parse raw tag: "name" or "name,invert"
|
||||
if rawTag != "" {
|
||||
parts := strings.Split(rawTag, ",")
|
||||
info.rawName = parts[0]
|
||||
for _, part := range parts[1:] {
|
||||
if part == "invert" {
|
||||
info.rawInvert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse config tag: "path.to.setting" or "path.to.setting,invert"
|
||||
if configTag != "" {
|
||||
configPath := parseConfigPathTag(configTag)
|
||||
info.configPath = configPath.path
|
||||
info.configInvert = configPath.invert
|
||||
}
|
||||
if fallbackConfigTag != "" {
|
||||
for tag := range strings.SplitSeq(fallbackConfigTag, ";") {
|
||||
info.fallbackConfigPaths = append(info.fallbackConfigPaths, parseConfigPathTag(tag))
|
||||
}
|
||||
}
|
||||
|
||||
infos = append(infos, info)
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
func parseConfigPathTag(tag string) configPathInfo {
|
||||
parts := strings.Split(tag, ",")
|
||||
info := configPathInfo{path: parts[0]}
|
||||
for _, part := range parts[1:] {
|
||||
if part == "invert" {
|
||||
info.invert = true
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func getNestedValue(config map[string]any, path string) (any, bool) {
|
||||
parts := strings.Split(path, ".")
|
||||
current := any(config)
|
||||
for _, part := range parts {
|
||||
m, ok := current.(map[string]any)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
current, ok = m[part]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return current, true
|
||||
}
|
||||
|
||||
func setNestedValue(config map[string]any, path string, value any) {
|
||||
parts := strings.Split(path, ".")
|
||||
current := config
|
||||
for _, part := range parts[:len(parts)-1] {
|
||||
next, ok := current[part].(map[string]any)
|
||||
if !ok {
|
||||
next = make(map[string]any)
|
||||
current[part] = next
|
||||
}
|
||||
current = next
|
||||
}
|
||||
current[parts[len(parts)-1]] = value
|
||||
}
|
||||
|
||||
func setRawFieldsFromConfig(v reflect.Value, infos []fieldInfo, settings map[string]any) {
|
||||
index := unstableNameIndex()
|
||||
for name, value := range settings {
|
||||
if idx, found := index[name]; found {
|
||||
info := infos[idx]
|
||||
field := getFieldByPath(v, info.fieldPath)
|
||||
if info.rawInvert {
|
||||
if b, ok := value.(bool); ok {
|
||||
value = !b
|
||||
}
|
||||
}
|
||||
setFieldFromValue(field, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p UserPreferences) withConfig(config map[string]any) UserPreferences {
|
||||
v := reflect.ValueOf(&p).Elem()
|
||||
infos := fieldInfoCache()
|
||||
|
||||
// Raw UserPreferences can be provided directly, notably via LSP initializationOptions.
|
||||
setRawFieldsFromConfig(v, infos, config)
|
||||
|
||||
// Process "unstable" section first - allows any field to be set by raw name.
|
||||
// This mirrors VS Code's behavior: { ...config.get('unstable'), ...stableOptions }
|
||||
// where stable options are spread after and take precedence.
|
||||
if unstable, ok := config["unstable"].(map[string]any); ok {
|
||||
setRawFieldsFromConfig(v, infos, unstable)
|
||||
}
|
||||
|
||||
// Process path-based config (VS Code style nested paths).
|
||||
// These run after unstable, so stable config values take precedence.
|
||||
for _, info := range infos {
|
||||
if info.configPath == "" {
|
||||
continue
|
||||
}
|
||||
configPath := configPathInfo{path: info.configPath, invert: info.configInvert}
|
||||
val, ok := getNestedValue(config, configPath.path)
|
||||
if !ok {
|
||||
for _, fallbackConfigPath := range info.fallbackConfigPaths {
|
||||
val, ok = getNestedValue(config, fallbackConfigPath.path)
|
||||
if ok {
|
||||
configPath = fallbackConfigPath
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
field := getFieldByPath(v, info.fieldPath)
|
||||
if configPath.invert {
|
||||
if b, ok := val.(bool); ok {
|
||||
val = !b
|
||||
}
|
||||
}
|
||||
if parser, ok := configPathParsers[configPath.path]; ok {
|
||||
field.Set(reflect.ValueOf(parser(val)))
|
||||
continue
|
||||
}
|
||||
setFieldFromValue(field, val)
|
||||
}
|
||||
|
||||
// Validate CustomConfigFileName for path traversal
|
||||
if p.CustomConfigFileName != "" {
|
||||
name := strings.TrimSpace(p.CustomConfigFileName)
|
||||
if strings.ContainsAny(name, "/\\") || name == ".." || name == "." {
|
||||
p.CustomConfigFileName = ""
|
||||
} else {
|
||||
p.CustomConfigFileName = name
|
||||
}
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func getFieldByPath(v reflect.Value, path []int) reflect.Value {
|
||||
for _, idx := range path {
|
||||
v = v.Field(idx)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func setFieldFromValue(field reflect.Value, val any) {
|
||||
if val == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check custom parsers first (for types like Tristate, enums, etc.)
|
||||
if parser, ok := typeParsers[field.Type()]; ok {
|
||||
field.Set(reflect.ValueOf(parser(val)))
|
||||
return
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.Bool:
|
||||
if b, ok := val.(bool); ok {
|
||||
field.SetBool(b)
|
||||
}
|
||||
case reflect.Int:
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
field.SetInt(int64(v))
|
||||
case float64:
|
||||
field.SetInt(int64(v))
|
||||
}
|
||||
case reflect.String:
|
||||
if s, ok := val.(string); ok {
|
||||
field.SetString(s)
|
||||
}
|
||||
case reflect.Slice:
|
||||
if arr, ok := val.([]any); ok {
|
||||
result := reflect.MakeSlice(field.Type(), 0, len(arr))
|
||||
for _, item := range arr {
|
||||
if s, ok := item.(string); ok {
|
||||
result = reflect.Append(result, reflect.ValueOf(s))
|
||||
}
|
||||
}
|
||||
field.Set(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserPreferences) MarshalJSONTo(enc *json.Encoder) error {
|
||||
config := make(map[string]any)
|
||||
v := reflect.ValueOf(p).Elem()
|
||||
|
||||
for _, info := range fieldInfoCache() {
|
||||
field := getFieldByPath(v, info.fieldPath)
|
||||
|
||||
val := serializeField(field)
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Prefer config path if available, otherwise use unstable section
|
||||
if info.configPath != "" {
|
||||
if info.configInvert {
|
||||
if b, ok := val.(bool); ok {
|
||||
val = !b
|
||||
}
|
||||
}
|
||||
setNestedValue(config, info.configPath, val)
|
||||
} else if info.rawName != "" {
|
||||
if info.rawInvert {
|
||||
if b, ok := val.(bool); ok {
|
||||
val = !b
|
||||
}
|
||||
}
|
||||
setNestedValue(config, "unstable."+info.rawName, val)
|
||||
}
|
||||
}
|
||||
|
||||
return json.MarshalEncode(enc, config, json.Deterministic(true))
|
||||
}
|
||||
|
||||
func serializeField(field reflect.Value) any {
|
||||
// Check custom serializers first (for types like Tristate, enums, etc.)
|
||||
if serializer, ok := typeSerializers[field.Type()]; ok {
|
||||
return serializer(field.Interface())
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.Bool:
|
||||
return field.Bool()
|
||||
case reflect.Int:
|
||||
// Zero means "unset" for these preference fields. Omit it so a partial
|
||||
// config does not clobber defaults with zeros when round-tripped through
|
||||
// withConfig.
|
||||
i := field.Int()
|
||||
if i == 0 {
|
||||
return nil
|
||||
}
|
||||
return int(i)
|
||||
case reflect.String:
|
||||
// Zero ("") means "unset"; omit it for the same reason as int above.
|
||||
s := field.String()
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
case reflect.Slice:
|
||||
if field.IsNil() {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, field.Len())
|
||||
for i := range field.Len() {
|
||||
result[i] = field.Index(i).String()
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return field.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserPreferences) UnmarshalJSONFrom(dec *json.Decoder) error {
|
||||
var config map[string]any
|
||||
if err := json.UnmarshalDecode(dec, &config); err != nil {
|
||||
return err
|
||||
}
|
||||
// Start with defaults, then overlay parsed values
|
||||
*p = NewDefaultUserPreferences().withConfig(config)
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Helper methods ---
|
||||
|
||||
func (p UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences {
|
||||
return modulespecifiers.UserPreferences{
|
||||
ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference,
|
||||
ImportModuleSpecifierEnding: p.ImportModuleSpecifierEnding,
|
||||
AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes,
|
||||
}
|
||||
}
|
||||
|
||||
func (p UserPreferences) ParsedAutoImportFileExcludePatterns(useCaseSensitiveFileNames bool) *vfsmatch.SpecMatcher {
|
||||
return vfsmatch.NewSpecMatcher(p.AutoImportFileExcludePatterns, "", vfsmatch.UsageExclude, useCaseSensitiveFileNames)
|
||||
}
|
||||
|
||||
func (p UserPreferences) IsModuleSpecifierExcluded(moduleSpecifier string) bool {
|
||||
return modulespecifiers.IsExcludedByRegex(moduleSpecifier, p.AutoImportSpecifierExcludeRegexes)
|
||||
}
|
||||
|
||||
func ParseUserPreferences(items map[string]any) UserPreferences {
|
||||
prefs := NewDefaultUserPreferences()
|
||||
// Apply editor settings first (tabSize, indentSize, etc.) as raw-name defaults,
|
||||
// then overlay language-specific settings with increasing precedence:
|
||||
// editor < javascript < typescript < js/ts
|
||||
if editorItem, ok := items["editor"]; ok && editorItem != nil {
|
||||
if editorSettings, ok := editorItem.(map[string]any); ok {
|
||||
prefs = prefs.withConfig(map[string]any{"unstable": editorSettings})
|
||||
}
|
||||
}
|
||||
// Apply javascript, then typescript, then js/ts (highest precedence).
|
||||
for _, section := range []string{"javascript", "typescript", "js/ts"} {
|
||||
if item, ok := items[section]; ok && item != nil {
|
||||
if settings, ok := item.(map[string]any); ok {
|
||||
prefs = prefs.withConfig(settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
return prefs
|
||||
}
|
||||
644
tools/tsgo/internal/ls/lsutil/userpreferences_test.go
Normal file
644
tools/tsgo/internal/ls/lsutil/userpreferences_test.go
Normal file
@@ -0,0 +1,644 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/modulespecifiers"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func fillNonZeroValues(v reflect.Value) {
|
||||
t := v.Type()
|
||||
for i := range t.NumField() {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
switch field.Kind() {
|
||||
case reflect.Bool:
|
||||
field.SetBool(true)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
field.SetInt(1)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
field.SetUint(1)
|
||||
case reflect.String:
|
||||
val := getValidStringValue(field.Type())
|
||||
field.SetString(val)
|
||||
case reflect.Slice:
|
||||
if field.Type().Elem().Kind() == reflect.String {
|
||||
field.Set(reflect.ValueOf([]string{"test"}))
|
||||
}
|
||||
case reflect.Struct:
|
||||
fillNonZeroValues(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getValidStringValue(t reflect.Type) string {
|
||||
typeName := t.String()
|
||||
switch typeName {
|
||||
case "lsutil.QuotePreference":
|
||||
return string(QuotePreferenceSingle)
|
||||
case "lsutil.JsxAttributeCompletionStyle":
|
||||
return string(JsxAttributeCompletionStyleBraces)
|
||||
case "lsutil.IncludeInlayParameterNameHints":
|
||||
return string(IncludeInlayParameterNameHintsAll)
|
||||
case "lsutil.SemicolonPreference":
|
||||
return string(SemicolonPreferenceInsert)
|
||||
case "modulespecifiers.ImportModuleSpecifierPreference":
|
||||
return string(modulespecifiers.ImportModuleSpecifierPreferenceRelative)
|
||||
case "modulespecifiers.ImportModuleSpecifierEndingPreference":
|
||||
return string(modulespecifiers.ImportModuleSpecifierEndingPreferenceJs)
|
||||
default:
|
||||
return "test"
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserPreferencesRoundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var original UserPreferences
|
||||
fillNonZeroValues(reflect.ValueOf(&original).Elem())
|
||||
|
||||
jsonBytes, err := json.Marshal(&original)
|
||||
assert.NilError(t, err)
|
||||
|
||||
t.Run("UnmarshalJSONFrom", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var parsed UserPreferences
|
||||
err2 := json.Unmarshal(jsonBytes, &parsed)
|
||||
assert.NilError(t, err2)
|
||||
assert.DeepEqual(t, original, parsed)
|
||||
})
|
||||
|
||||
t.Run("withConfig", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var config map[string]any
|
||||
err2 := json.Unmarshal(jsonBytes, &config)
|
||||
assert.NilError(t, err2)
|
||||
parsed := UserPreferences{}.withConfig(config)
|
||||
assert.DeepEqual(t, original, parsed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPreferencesSerialize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("config path field serializes to nested path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := &UserPreferences{
|
||||
QuotePreference: QuotePreferenceSingle,
|
||||
}
|
||||
jsonBytes, err := json.Marshal(prefs)
|
||||
assert.NilError(t, err)
|
||||
|
||||
var actual map[string]any
|
||||
err = json.Unmarshal(jsonBytes, &actual)
|
||||
assert.NilError(t, err)
|
||||
|
||||
preferences := actual["preferences"].(map[string]any)
|
||||
assert.Equal(t, "single", preferences["quoteStyle"])
|
||||
})
|
||||
|
||||
t.Run("raw-only field serializes to unstable section", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := &UserPreferences{
|
||||
DisableSuggestions: core.TSTrue,
|
||||
}
|
||||
jsonBytes, err := json.Marshal(prefs)
|
||||
assert.NilError(t, err)
|
||||
|
||||
var actual map[string]any
|
||||
err = json.Unmarshal(jsonBytes, &actual)
|
||||
assert.NilError(t, err)
|
||||
|
||||
unstable := actual["unstable"].(map[string]any)
|
||||
assert.Equal(t, true, unstable["disableSuggestions"])
|
||||
})
|
||||
|
||||
t.Run("inlay hint inversion on serialize", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := &UserPreferences{
|
||||
InlayHints: InlayHintsPreferences{
|
||||
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
|
||||
IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSTrue,
|
||||
},
|
||||
}
|
||||
jsonBytes, err := json.Marshal(prefs)
|
||||
assert.NilError(t, err)
|
||||
|
||||
var actual map[string]any
|
||||
err = json.Unmarshal(jsonBytes, &actual)
|
||||
assert.NilError(t, err)
|
||||
|
||||
inlayHints := actual["inlayHints"].(map[string]any)
|
||||
parameterNames := inlayHints["parameterNames"].(map[string]any)
|
||||
assert.Equal(t, "all", parameterNames["enabled"])
|
||||
assert.Equal(t, false, parameterNames["suppressWhenArgumentMatchesName"]) // inverted
|
||||
})
|
||||
|
||||
t.Run("mixed config and unstable fields", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := &UserPreferences{
|
||||
QuotePreference: QuotePreferenceSingle,
|
||||
DisableSuggestions: core.TSTrue,
|
||||
DisplayPartsForJSDoc: core.TSTrue,
|
||||
}
|
||||
jsonBytes, err := json.Marshal(prefs)
|
||||
assert.NilError(t, err)
|
||||
|
||||
var actual map[string]any
|
||||
err = json.Unmarshal(jsonBytes, &actual)
|
||||
assert.NilError(t, err)
|
||||
|
||||
preferences := actual["preferences"].(map[string]any)
|
||||
assert.Equal(t, "single", preferences["quoteStyle"])
|
||||
|
||||
unstable := actual["unstable"].(map[string]any)
|
||||
assert.Equal(t, true, unstable["disableSuggestions"])
|
||||
assert.Equal(t, true, unstable["displayPartsForJSDoc"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPreferencesParseUnstable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
expected UserPreferences
|
||||
}{
|
||||
{
|
||||
name: "unstable fields with correct casing",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"disableSuggestions": true,
|
||||
"maximumHoverLength": 100,
|
||||
"allowRenameOfImportPath": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
DisableSuggestions: core.TSTrue,
|
||||
MaximumHoverLength: 100,
|
||||
AllowRenameOfImportPath: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested preferences path",
|
||||
json: `{
|
||||
"preferences": {
|
||||
"quoteStyle": "single",
|
||||
"useAliasesForRenames": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
QuotePreference: QuotePreferenceSingle,
|
||||
UseAliasesForRename: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "suggest section",
|
||||
json: `{
|
||||
"suggest": {
|
||||
"autoImports": false,
|
||||
"includeCompletionsForImportStatements": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
IncludeCompletionsForModuleExports: core.TSFalse,
|
||||
IncludeCompletionsForImportStatements: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inlayHints with invert",
|
||||
json: `{
|
||||
"inlayHints": {
|
||||
"parameterNames": {
|
||||
"enabled": "all",
|
||||
"suppressWhenArgumentMatchesName": true
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
InlayHints: InlayHintsPreferences{
|
||||
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
|
||||
IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSFalse, // inverted
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed config",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"displayPartsForJSDoc": true
|
||||
},
|
||||
"preferences": {
|
||||
"importModuleSpecifier": "relative"
|
||||
},
|
||||
"workspaceSymbols": {
|
||||
"excludeLibrarySymbols": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
DisplayPartsForJSDoc: core.TSTrue,
|
||||
ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceRelative,
|
||||
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stable config overrides unstable",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"quotePreference": "double"
|
||||
},
|
||||
"preferences": {
|
||||
"quoteStyle": "single"
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
QuotePreference: QuotePreferenceSingle, // stable wins
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unstable sets value when no stable config",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"includeAutomaticOptionalChainCompletions": false
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
IncludeAutomaticOptionalChainCompletions: core.TSFalse,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "any field can be passed via unstable by its raw name",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"quotePreference": "double",
|
||||
"includeCompletionsForModuleExports": true,
|
||||
"excludeLibrarySymbolsInNavTo": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
QuotePreference: QuotePreferenceDouble,
|
||||
IncludeCompletionsForModuleExports: core.TSTrue,
|
||||
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TypeScript raw names work in unstable section",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"includeCompletionsForModuleExports": true,
|
||||
"quotePreference": "single",
|
||||
"providePrefixAndSuffixTextForRename": true,
|
||||
"includeInlayParameterNameHints": "all",
|
||||
"organizeImportsLocale": "en"
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
IncludeCompletionsForModuleExports: core.TSTrue,
|
||||
QuotePreference: QuotePreferenceSingle,
|
||||
UseAliasesForRename: core.TSTrue,
|
||||
OrganizeImportsLocale: "en",
|
||||
InlayHints: InlayHintsPreferences{
|
||||
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "old raw organize imports unicode preferences load as raw state",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"organizeImportsCollation": "unicode",
|
||||
"organizeImportsCaseFirst": "upper",
|
||||
"organizeImportsIgnoreCase": false,
|
||||
"organizeImportsNumericCollation": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper,
|
||||
OrganizeImportsIgnoreCase: core.TSFalse,
|
||||
OrganizeImportsNumericCollation: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "old top-level raw organize imports unicode preferences load as raw state",
|
||||
json: `{
|
||||
"organizeImportsCollation": "unicode",
|
||||
"organizeImportsIgnoreCase": true
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "new top-level raw organize imports sort is accepted",
|
||||
json: `{
|
||||
"organizeImportsSort": "natural"
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsSort: OrganizeImportsSortNatural,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "old raw organize imports ignore case loads as raw state",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"organizeImportsIgnoreCase": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "new raw organize imports sort loads alongside old raw preferences",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"organizeImportsSort": "ordinal",
|
||||
"organizeImportsCollation": "unicode",
|
||||
"organizeImportsIgnoreCase": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsSort: OrganizeImportsSortOrdinal,
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "old nested organize imports unicode preferences load as raw state",
|
||||
json: `{
|
||||
"preferences": {
|
||||
"organizeImports": {
|
||||
"unicodeCollation": "unicode",
|
||||
"caseSensitivity": "caseSensitive",
|
||||
"numericCollation": true,
|
||||
"caseFirst": "upper"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSFalse,
|
||||
OrganizeImportsNumericCollation: core.TSTrue,
|
||||
OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "new nested organize imports sort loads alongside old nested preferences",
|
||||
json: `{
|
||||
"preferences": {
|
||||
"organizeImports": {
|
||||
"sort": "ordinalIgnoreCase",
|
||||
"unicodeCollation": "unicode",
|
||||
"caseSensitivity": "caseSensitive"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsSort: OrganizeImportsSortOrdinalIgnoreCase,
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSFalse,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var config map[string]any
|
||||
err := json.Unmarshal([]byte(tt.json), &config)
|
||||
assert.NilError(t, err)
|
||||
|
||||
parsed := UserPreferences{}.withConfig(config)
|
||||
|
||||
assert.DeepEqual(t, tt.expected, parsed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserPreferencesReportStyleChecksAsWarnings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("reportStyleChecksAsWarnings via config path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"reportStyleChecksAsWarnings": false,
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("reportStyleChecksAsWarnings defaults to true", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := NewDefaultUserPreferences()
|
||||
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSTrue)
|
||||
})
|
||||
|
||||
t.Run("reportStyleChecksAsWarnings via unstable section", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"unstable": map[string]any{
|
||||
"reportStyleChecksAsWarnings": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPreferencesParseServerFeaturePreferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("preferred server feature settings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"validate": map[string]any{"enabled": false},
|
||||
"format": map[string]any{"enabled": false},
|
||||
"autoClosingTags": map[string]any{
|
||||
"enabled": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableValidation, core.TSFalse)
|
||||
assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
|
||||
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("legacy server feature fallbacks", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"validate": map[string]any{"enable": false},
|
||||
"format": map[string]any{"enable": false},
|
||||
"autoClosingTags": false,
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableValidation, core.TSFalse)
|
||||
assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
|
||||
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("preferred settings take precedence over fallbacks", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"validate": map[string]any{"enable": false},
|
||||
"format": map[string]any{"enable": false},
|
||||
"autoClosingTags": false,
|
||||
},
|
||||
"js/ts": map[string]any{
|
||||
"validate": map[string]any{"enabled": true},
|
||||
"format": map[string]any{"enabled": true},
|
||||
"autoClosingTags": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableValidation, core.TSTrue)
|
||||
assert.Equal(t, prefs.EnableFormatting, core.TSTrue)
|
||||
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSTrue)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPreferencesParseJSDocCompletionPreferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("unified jsdoc enabled setting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"jsdoc": map[string]any{
|
||||
"enabled": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("language fallback completeJSDocs setting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"completeJSDocs": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("unified jsdoc enabled takes precedence over language fallback", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"completeJSDocs": false,
|
||||
},
|
||||
},
|
||||
"js/ts": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"jsdoc": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSTrue)
|
||||
})
|
||||
|
||||
t.Run("unified jsdoc generateReturns setting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"jsdoc": map[string]any{
|
||||
"generateReturns": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("language jsdoc generateReturns setting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"jsdoc": map[string]any{
|
||||
"generateReturns": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPreferencesParseATA(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("ParseUserPreferences with unified ATA setting in js/ts section", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"tsserver": map[string]any{
|
||||
"automaticTypeAcquisition": map[string]any{
|
||||
"enabled": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Assert(t, prefs.IsATADisabled())
|
||||
assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("ParseUserPreferences with deprecated disableAutomaticTypeAcquisition in typescript section", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"disableAutomaticTypeAcquisition": true,
|
||||
},
|
||||
})
|
||||
assert.Assert(t, prefs.IsATADisabled())
|
||||
assert.Equal(t, prefs.DisableAutomaticTypeAcquisition, core.TSTrue)
|
||||
})
|
||||
|
||||
t.Run("unified setting takes precedence over deprecated setting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Both settings set: unified (js/ts) should take precedence
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"disableAutomaticTypeAcquisition": true,
|
||||
},
|
||||
"js/ts": map[string]any{
|
||||
"tsserver": map[string]any{
|
||||
"automaticTypeAcquisition": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Assert(t, !prefs.IsATADisabled())
|
||||
assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSTrue)
|
||||
})
|
||||
|
||||
t.Run("IsATADisabled returns false when neither setting is configured", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := NewDefaultUserPreferences()
|
||||
assert.Assert(t, !prefs.IsATADisabled())
|
||||
})
|
||||
}
|
||||
157
tools/tsgo/internal/ls/lsutil/utilities.go
Normal file
157
tools/tsgo/internal/ls/lsutil/utilities.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
func ProbablyUsesSemicolons(file *ast.SourceFile) bool {
|
||||
withSemicolon := 0
|
||||
withoutSemicolon := 0
|
||||
nStatementsToObserve := 5
|
||||
|
||||
var visit func(node *ast.Node) bool
|
||||
visit = func(node *ast.Node) bool {
|
||||
if node.Flags&ast.NodeFlagsReparsed != 0 {
|
||||
return false
|
||||
}
|
||||
if SyntaxRequiresTrailingSemicolonOrASI(node.Kind) {
|
||||
lastToken := GetLastToken(node, file)
|
||||
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
|
||||
withSemicolon++
|
||||
} else {
|
||||
withoutSemicolon++
|
||||
}
|
||||
} else if SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) {
|
||||
lastToken := GetLastToken(node, file)
|
||||
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
|
||||
withSemicolon++
|
||||
} else if lastToken != nil && lastToken.Kind != ast.KindCommaToken {
|
||||
lastTokenLine := scanner.GetECMALineOfPosition(
|
||||
file,
|
||||
astnav.GetStartOfNode(lastToken, file, false /*includeJSDoc*/),
|
||||
)
|
||||
nextTokenLine := scanner.GetECMALineOfPosition(
|
||||
file,
|
||||
scanner.SkipTrivia(file.Text(), lastToken.End()),
|
||||
)
|
||||
// Avoid counting missing semicolon in single-line objects:
|
||||
// `function f(p: { x: string /*no semicolon here is insignificant*/ }) {`
|
||||
if lastTokenLine != nextTokenLine {
|
||||
withoutSemicolon++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if withSemicolon+withoutSemicolon >= nStatementsToObserve {
|
||||
return true
|
||||
}
|
||||
|
||||
return node.ForEachChild(visit)
|
||||
}
|
||||
|
||||
file.ForEachChild(visit)
|
||||
|
||||
// One statement missing a semicolon isn't sufficient evidence to say the user
|
||||
// doesn't want semicolons, because they may not even be done writing that statement.
|
||||
if withSemicolon == 0 && withoutSemicolon <= 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
// When both kinds of observation exist, treat the file as using semicolons when the
|
||||
// ratio withSemicolon/withoutSemicolon exceeds 1/nStatementsToObserve (real arithmetic),
|
||||
// implemented as an integer inequality to avoid truncation.
|
||||
if withoutSemicolon == 0 {
|
||||
return true
|
||||
}
|
||||
return withSemicolon*nStatementsToObserve > withoutSemicolon
|
||||
}
|
||||
|
||||
func ShouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) core.Tristate {
|
||||
for _, node := range file.Imports() {
|
||||
if core.NodeCoreModules()[node.Text()] && !core.ExclusivelyPrefixedNodeCoreModules[node.Text()] {
|
||||
if strings.HasPrefix(node.Text(), "node:") {
|
||||
return core.TSTrue
|
||||
} else {
|
||||
return core.TSFalse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return program.UsesUriStyleNodeCoreModules()
|
||||
}
|
||||
|
||||
func QuotePreferenceFromString(str *ast.StringLiteral) QuotePreference {
|
||||
if str.TokenFlags&ast.TokenFlagsSingleQuote != 0 {
|
||||
return QuotePreferenceSingle
|
||||
}
|
||||
return QuotePreferenceDouble
|
||||
}
|
||||
|
||||
func GetQuotePreference(sourceFile *ast.SourceFile, preferences UserPreferences) QuotePreference {
|
||||
if preferences.QuotePreference != "" && preferences.QuotePreference != "auto" {
|
||||
if preferences.QuotePreference == "single" {
|
||||
return QuotePreferenceSingle
|
||||
}
|
||||
return QuotePreferenceDouble
|
||||
}
|
||||
// ignore synthetic import added when importHelpers: true
|
||||
firstModuleSpecifier := core.Find(sourceFile.Imports(), func(n *ast.Node) bool {
|
||||
return ast.IsStringLiteral(n) && !ast.NodeIsSynthesized(n.Parent)
|
||||
})
|
||||
if firstModuleSpecifier != nil {
|
||||
return QuotePreferenceFromString(firstModuleSpecifier.AsStringLiteral())
|
||||
}
|
||||
return QuotePreferenceDouble
|
||||
}
|
||||
|
||||
func ModuleSymbolToValidIdentifier(moduleSymbol *ast.Symbol, forceCapitalize bool) string {
|
||||
return ModuleSpecifierToValidIdentifier(stringutil.StripQuotes(moduleSymbol.Name), forceCapitalize)
|
||||
}
|
||||
|
||||
func ModuleSpecifierToValidIdentifier(moduleSpecifier string, forceCapitalize bool) string {
|
||||
baseName := tspath.GetBaseFileName(strings.TrimSuffix(tspath.RemoveFileExtension(moduleSpecifier), "/index"))
|
||||
res := []rune{}
|
||||
lastCharWasValid := true
|
||||
baseNameRunes := []rune(baseName)
|
||||
if len(baseNameRunes) > 0 && scanner.IsIdentifierStart(baseNameRunes[0]) {
|
||||
if forceCapitalize {
|
||||
res = append(res, unicode.ToUpper(baseNameRunes[0]))
|
||||
} else {
|
||||
res = append(res, baseNameRunes[0])
|
||||
}
|
||||
} else {
|
||||
lastCharWasValid = false
|
||||
}
|
||||
|
||||
for i := 1; i < len(baseNameRunes); i++ {
|
||||
isValid := scanner.IsIdentifierPart(baseNameRunes[i])
|
||||
if isValid {
|
||||
if !lastCharWasValid {
|
||||
res = append(res, unicode.ToUpper(baseNameRunes[i]))
|
||||
} else {
|
||||
res = append(res, baseNameRunes[i])
|
||||
}
|
||||
}
|
||||
lastCharWasValid = isValid
|
||||
}
|
||||
|
||||
// Need `"_"` to ensure result isn't empty.
|
||||
resString := string(res)
|
||||
if resString != "" && !IsNonContextualKeyword(scanner.StringToToken(resString)) {
|
||||
return resString
|
||||
}
|
||||
return "_" + resString
|
||||
}
|
||||
|
||||
func IsNonContextualKeyword(token ast.Kind) bool {
|
||||
return ast.IsKeywordKind(token) && !ast.IsContextualKeyword(token)
|
||||
}
|
||||
200
tools/tsgo/internal/ls/lsutil/utilities_test.go
Normal file
200
tools/tsgo/internal/ls/lsutil/utilities_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
)
|
||||
|
||||
func parseTS(t *testing.T, text string) *ast.SourceFile {
|
||||
t.Helper()
|
||||
return parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, text, core.ScriptKindTS)
|
||||
}
|
||||
|
||||
func TestProbablyUsesSemicolons(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "mixed semicolons and ASI favors semicolons when ratio exceeds one fifth",
|
||||
// First five observations: 2 with semicolon, 3 without. Real ratio 2/3 > 1/5.
|
||||
// Integer division bug compared against 1/5==0 and used with/without as ints,
|
||||
// so the old check was effectively (with/without) > 0, which failed here.
|
||||
src: `let a = 1;
|
||||
let b = 2;
|
||||
let c = 3
|
||||
let d = 4
|
||||
let e = 5
|
||||
`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "consistent ASI with no semicolons",
|
||||
src: `let a = 1
|
||||
let b = 2
|
||||
let c = 3
|
||||
`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "consistent semicolons",
|
||||
src: `let a = 1;
|
||||
let b = 2;
|
||||
let c = 3;
|
||||
`,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
file := parseTS(t, tt.src)
|
||||
if got := ProbablyUsesSemicolons(file); got != tt.want {
|
||||
t.Errorf("ProbablyUsesSemicolons() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOrganizeImportsSort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
preferences UserPreferences
|
||||
want OrganizeImportsSort
|
||||
}{
|
||||
{
|
||||
name: "explicit sort wins",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsSort: OrganizeImportsSortOrdinal,
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
want: OrganizeImportsSortOrdinal,
|
||||
},
|
||||
{
|
||||
name: "unicode case-sensitive maps to natural",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSFalse,
|
||||
},
|
||||
want: OrganizeImportsSortNatural,
|
||||
},
|
||||
{
|
||||
name: "unicode ignore case maps to natural ignore case",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
want: OrganizeImportsSortNaturalIgnoreCase,
|
||||
},
|
||||
{
|
||||
name: "unicode unknown case sensitivity stays auto for detection",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
},
|
||||
want: OrganizeImportsSortAuto,
|
||||
},
|
||||
{
|
||||
name: "ordinal ignore case maps to ordinal ignore case",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
want: OrganizeImportsSortOrdinalIgnoreCase,
|
||||
},
|
||||
{
|
||||
name: "ordinal case sensitive maps to ordinal",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsIgnoreCase: core.TSFalse,
|
||||
},
|
||||
want: OrganizeImportsSortOrdinal,
|
||||
},
|
||||
{
|
||||
name: "unknown ordinal stays auto",
|
||||
want: OrganizeImportsSortAuto,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := ResolveOrganizeImportsSort(tt.preferences); got != tt.want {
|
||||
t.Fatalf("ResolveOrganizeImportsSort() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareOrganizeImportsNaturalStrings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
comparer := getOrganizeImportsPresetStringComparer(OrganizeImportsSortNaturalIgnoreCase)
|
||||
tests := []struct {
|
||||
name string
|
||||
a string
|
||||
b string
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "numeric runs sort by numeric value",
|
||||
a: "a2",
|
||||
b: "a100",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "numeric runs with equal value use raw tie break",
|
||||
a: "a02",
|
||||
b: "a2",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "accents are folded for primary comparison",
|
||||
a: "À",
|
||||
b: "B",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "raw comparison breaks accent ties",
|
||||
a: "A",
|
||||
b: "À",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "hyphen sorts before slash like Intl.Collator fallback",
|
||||
a: "app-init",
|
||||
b: "app/app",
|
||||
want: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := cmpSign(comparer(tt.a, tt.b)); got != tt.want {
|
||||
t.Fatalf("comparer(%q, %q) = %v, want sign %v", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func cmpSign(value int) int {
|
||||
switch {
|
||||
case value < 0:
|
||||
return -1
|
||||
case value > 0:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user