Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions tsc/internal/format/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,45 @@ func TestFormatNoTrailingSpace(t *testing.T) {
})
}
}

func TestFormatIdempotentWhenLineAddedBeforeNestedNodes(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
text string
expected string
}{
// The callee `0!` and the call both start at `0`. When formatting moves `0` to
// a new line, the call's arguments must be indented as if `0` were already there.
{"call with non-null callee", "{{0!\n(\"\")}}", "{\n {\n 0!\n (\"\")\n }\n}"},
// The binary expression and its statement both start at `x`. When formatting
// moves `x` to a new line, the operator keeps the statement's continuation indent.
{"binary expression statement", "{{x\n+ y}}", "{\n {\n x\n +y\n }\n}"},
}

format1 := func(t *testing.T, text string) string {
ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{
TabSize: 4,
IndentSize: 4,
NewLineCharacter: "\n",
ConvertTabsToSpaces: core.TSTrue,
IndentStyle: lsutil.IndentStyleSmart,
TrimTrailingWhitespace: core.TSTrue,
}, "\n")
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/test.ts",
PathKey: "/test.ts",
}, text, core.ScriptKindTS)
return applyBulkEdits(text, format.FormatDocument(ctx, sourceFile))
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
first := format1(t, tc.text)
assert.Equal(t, first, tc.expected)
assert.Equal(t, format1(t, first), first)
})
}
}
35 changes: 24 additions & 11 deletions tsc/internal/format/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ func (w *formatSpanWorker) execute(s *formattingScanner) []core.TextChange {
undecoratedStartLine = scanner.GetECMALineOfPosition(w.sourceFile, getNonDecoratorTokenPosOfNode(w.enclosingNode, w.sourceFile))
}

w.processNode(w.enclosingNode, w.enclosingNode, startLine, undecoratedStartLine, w.initialIndentation, w.delta)
w.processNode(w.enclosingNode, w.enclosingNode, startLine, undecoratedStartLine, w.initialIndentation, w.delta, nil)
}

// Leading trivia items get attached to and processed with the token that proceeds them. If the
Expand Down Expand Up @@ -425,7 +425,7 @@ func (w *formatSpanWorker) processChildNode(
childIndentation, delta = w.computeIndentation(child, childStartLine, childIndentationAmount, node, parentDynamicIndentation, effectiveParentStartLine)
}

w.processNode(child, w.childContextNode, childStartLine, undecoratedChildStartLine, childIndentation, delta)
w.processNode(child, w.childContextNode, childStartLine, undecoratedChildStartLine, childIndentation, delta, parentDynamicIndentation)

w.childContextNode = node

Expand Down Expand Up @@ -612,12 +612,13 @@ func (w *formatSpanWorker) tryComputeIndentationForListItem(startPos int, endPos
return -1
}

func (w *formatSpanWorker) processNode(node *ast.Node, contextNode *ast.Node, nodeStartLine int, undecoratedNodeStartLine int, indentation int, delta int) {
func (w *formatSpanWorker) processNode(node *ast.Node, contextNode *ast.Node, nodeStartLine int, undecoratedNodeStartLine int, indentation int, delta int, parentDynamicIndentation *dynamicIndenter) {
if !w.originalRange.Overlaps(withTokenStart(node, w.sourceFile)) {
return
}

nodeDynamicIndentation := w.getDynamicIndentation(node, nodeStartLine, indentation, delta)
nodeDynamicIndentation.parent = parentDynamicIndentation

// a useful observations when tracking context node
// /
Expand Down Expand Up @@ -1123,6 +1124,7 @@ type dynamicIndenter struct {
nodeStartLine int
indentation int
delta int
parent *dynamicIndenter

options lsutil.FormatCodeSettings
sourceFile *ast.SourceFile
Expand Down Expand Up @@ -1177,15 +1179,26 @@ func (i *dynamicIndenter) getDelta(child *ast.Node) int {

func (i *dynamicIndenter) recomputeIndentation(lineAdded bool, parent *ast.Node) {
if ShouldIndentChildNode(i.options, parent, i.node, i.sourceFile) {
if lineAdded {
i.indentation += i.options.IndentSize // !!! no nil check???
} else {
i.indentation -= i.options.IndentSize // !!! no nil check???
shift := i.options.IndentSize // !!! no nil check???
if !lineAdded {
shift = -shift
}
if ShouldIndentChildNode(i.options, i.node, nil, nil) {
i.delta = i.options.IndentSize
} else {
i.delta = 0
i.indentation += shift
start := scanner.GetTokenPosOfNode(i.node, i.sourceFile, false)
// A node that starts at the same token as its parent, like `x + y` in the
// statement `x + y`, stays on its parent's line and keeps its inherited delta.
if i.parent == nil || scanner.GetTokenPosOfNode(i.parent.node, i.sourceFile, false) != start {
if ShouldIndentChildNode(i.options, i.node, nil, nil) {
i.delta = i.options.IndentSize
} else {
i.delta = 0
}
}
// Enclosing nodes that start at the same token, like the call in `0!("")`,
// moved with this node, so shift them too. Keep their deltas: resetting one
// would drop the delta a binary expression inherits from its statement.
for p := i.parent; p != nil && scanner.GetTokenPosOfNode(p.node, i.sourceFile, false) == start; p = p.parent {
p.indentation += shift
}
}
}
Expand Down