Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cf06ca3
Add refactoring code action `Infer function return type`
xeho91 Jul 26, 2026
37ef7ab
Add support `fourslash` for refactor code actions
xeho91 Jul 26, 2026
ef5db47
Add integration tests for infering return type code action refactor
xeho91 Jul 26, 2026
be8c7b8
Strenghten testing strategy with post-refactor comparison
xeho91 Jul 26, 2026
776d88d
Fix paren-less arrow handling case
xeho91 Jul 26, 2026
7db11ee
Swap `codeActionKindContains` arguments
xeho91 Jul 26, 2026
71a15fe
Fix handling type predicates
xeho91 Jul 26, 2026
8263c3c
Stop ancestor walk at blocks and arrow function boundaries
xeho91 Jul 26, 2026
4c11c49
Defer checker acquisition until after cheap applicability checks
xeho91 Jul 26, 2026
31f98ba
Pass `AllowUnresolvedNames` to type-node builder - fixes computed pro…
xeho91 Jul 26, 2026
c14941b
Filter refactor providers by per-action Kinds against `Context.Only`
xeho91 Jul 26, 2026
ba248b0
Use `GetTouchingPropertyName` to exclude leading trivia from token lo…
xeho91 Jul 26, 2026
cc7f34a
Use stable sort for equal-position edits in `fourslash` verifier
xeho91 Jul 26, 2026
2c5437c
Remove initializer shortcuts from `findConvertibleAncestor`
xeho91 Jul 26, 2026
1587095
Add coverage for `Context.Only` filtering on refactor providers
xeho91 Jul 26, 2026
e959063
Add `TypePredicateToTypePredicateNodeEx` with configurable internal f…
xeho91 Jul 26, 2026
41e2c6a
Treat empty kind in `Context.Only` as matching all refactor actions
xeho91 Jul 26, 2026
7eebf25
Return disabled code actions with not-applicable reasons
xeho91 Jul 26, 2026
c2810de
Gate disabled refactor on `DisabledSupport` client capability
xeho91 Jul 26, 2026
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
5 changes: 5 additions & 0 deletions internal/checker/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,3 +484,8 @@ func (c *Checker) TypePredicateToTypePredicateNode(t *TypePredicate, enclosingDe
nodeBuilder := c.getNodeBuilderEx(idToSymbol)
return nodeBuilder.TypePredicateToTypePredicateNode(t, enclosingDeclaration, flags, nodebuilder.InternalFlagsNone, nil)
}

func (c *Checker) TypePredicateToTypePredicateNodeEx(t *TypePredicate, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) *ast.TypePredicateNodeNode {
nodeBuilder := c.getNodeBuilderEx(idToSymbol)
return nodeBuilder.TypePredicateToTypePredicateNode(t, enclosingDeclaration, flags, internalFlags, nil)
}
187 changes: 186 additions & 1 deletion internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -1980,6 +1980,191 @@ func (f *FourslashTest) getAllQuickFixActions(t *testing.T, errorCode ...int) []
return actions
}

// getRefactorActions gets all refactoring code actions at the current cursor position.
func (f *FourslashTest) getRefactorActions(t *testing.T) []*lsproto.CodeAction {
t.Helper()
return f.getRefactorActionsWithOnly(t, nil)
}

func (f *FourslashTest) getRefactorActionsWithOnly(t *testing.T, only *[]lsproto.CodeActionKind) []*lsproto.CodeAction {
t.Helper()

params := &lsproto.CodeActionParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
},
Range: lsproto.Range{
Start: f.currentCaretPosition,
End: f.currentCaretPosition,
},
Context: &lsproto.CodeActionContext{
Diagnostics: []*lsproto.Diagnostic{},
Only: only,
},
}

result := sendRequest(t, f, lsproto.TextDocumentCodeActionInfo, params)

var actions []*lsproto.CodeAction
if result.CommandOrCodeActionArray != nil {
for _, item := range *result.CommandOrCodeActionArray {
if item.CodeAction != nil && item.CodeAction.Kind != nil &&
isRefactoringKind(*item.CodeAction.Kind) && item.CodeAction.Disabled == nil {
actions = append(actions, item.CodeAction)
}
}
}

return actions
}

// VerifyRefactorOptions contains options for VerifyRefactor.
type VerifyRefactorOptions struct {
Title string
NewFileContent string
}

// VerifyRefactor verifies that a refactoring code action matching the given title is available,
// and optionally verifies the file content after applying the edit.
func (f *FourslashTest) VerifyRefactor(t *testing.T, options VerifyRefactorOptions) {
t.Helper()
actions := f.getRefactorActions(t)

var matchingAction *lsproto.CodeAction
for _, action := range actions {
if action.Title == options.Title {
matchingAction = action
break
}
}
if matchingAction == nil {
t.Fatalf("Expected refactoring %q to be available, but it was not. Got: %v", options.Title, actionTitles(actions))
}

if options.NewFileContent != "" {
actual := f.getScriptInfo(f.activeFilename).content
if matchingAction.Edit != nil && matchingAction.Edit.Changes != nil {
expectedURI := lsconv.FileNameToDocumentURI(f.activeFilename)

for uri, edits := range *matchingAction.Edit.Changes {
if uri != expectedURI {
t.Fatalf("Refactoring returned edits for unexpected URI %q (expected %q)", uri, expectedURI)
}

actual = f.applyEditsToContent(actual, edits)
Comment thread
xeho91 marked this conversation as resolved.
}
}
assert.Equal(t, options.NewFileContent, actual, "File content after applying refactoring did not match expected content.")
}
}

// VerifyRefactorAvailable verifies that a refactoring code action with the given title is available.
func (f *FourslashTest) VerifyRefactorAvailable(t *testing.T, title string) {
t.Helper()
f.VerifyRefactor(t, VerifyRefactorOptions{Title: title})
}

// VerifyRefactorNotAvailable verifies that a refactoring code action with the given title is NOT available.
func (f *FourslashTest) VerifyRefactorNotAvailable(t *testing.T, title string) {
t.Helper()
actions := f.getRefactorActions(t)
for _, action := range actions {
if action.Title == title {
t.Fatalf("Expected refactoring %q to not be available, but it was", title)
}
}
}

// getRefactorActionsIncludingDisabled returns all refactoring code actions at the current cursor position,
// including disabled ones. This is used to test the DisabledSupport client capability.
func (f *FourslashTest) getRefactorActionsIncludingDisabled(t *testing.T) []*lsproto.CodeAction {
t.Helper()

params := &lsproto.CodeActionParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
},
Range: lsproto.Range{
Start: f.currentCaretPosition,
End: f.currentCaretPosition,
},
Context: &lsproto.CodeActionContext{
Diagnostics: []*lsproto.Diagnostic{},
},
}

result := sendRequest(t, f, lsproto.TextDocumentCodeActionInfo, params)

var actions []*lsproto.CodeAction
if result.CommandOrCodeActionArray != nil {
for _, item := range *result.CommandOrCodeActionArray {
if item.CodeAction != nil && item.CodeAction.Kind != nil &&
isRefactoringKind(*item.CodeAction.Kind) {
actions = append(actions, item.CodeAction)
}
}
}

return actions
}

// VerifyRefactorDisabled verifies that a refactoring code action with the given title IS returned
// but with its Disabled field set (i.e. the action is not applicable).
func (f *FourslashTest) VerifyRefactorDisabled(t *testing.T, title string) {
t.Helper()
actions := f.getRefactorActionsIncludingDisabled(t)
for _, action := range actions {
if action.Title == title {
if action.Disabled == nil {
t.Fatalf("Expected refactoring %q to be disabled, but it was enabled", title)
}
return
}
}
t.Fatalf("Expected refactoring %q to be present (disabled), but it was not found. Got: %v", title, actionTitles(actions))
}

// VerifyRefactorWithOnlyAvailable verifies that a refactoring action with the given title IS available
// when the given Only filter is sent in the request context.
func (f *FourslashTest) VerifyRefactorWithOnlyAvailable(t *testing.T, title string, only []lsproto.CodeActionKind) {
t.Helper()
actions := f.getRefactorActionsWithOnly(t, &only)
for _, action := range actions {
if action.Title == title {
return
}
}
t.Fatalf("Expected refactoring %q to be available with Only=%v, but it was not (got: %v)", title, only, actionTitles(actions))
}

// VerifyRefactorWithOnlyNotAvailable verifies that a refactoring action with the given title is NOT available
// when the given Only filter is sent in the request context.
func (f *FourslashTest) VerifyRefactorWithOnlyNotAvailable(t *testing.T, title string, only []lsproto.CodeActionKind) {
t.Helper()
actions := f.getRefactorActionsWithOnly(t, &only)
for _, action := range actions {
if action.Title == title {
t.Fatalf("Expected refactoring %q to not be available with Only=%v, but it was", title, only)
}
}
}

func actionTitles(actions []*lsproto.CodeAction) []string {
titles := make([]string, len(actions))
for i, a := range actions {
titles[i] = a.Title
}
return titles
}

// isRefactoringKind checks if the given kind is a refactoring kind.
// Matches "refactor" and any subkind like "refactor.rewrite", "refactor.extract", etc.
func isRefactoringKind(kind lsproto.CodeActionKind) bool {
return kind == lsproto.CodeActionKindRefactor ||
string(kind) == "refactor" ||
strings.HasPrefix(string(kind), string(lsproto.CodeActionKindRefactor)+".")
}

func (f *FourslashTest) updateTextRangeForTextEdits(textRange core.TextRange, edits []*lsproto.TextEdit) core.TextRange {
script := f.getScriptInfo(f.activeFilename)
spans := make([]textEditSpan, 0, len(edits))
Expand Down Expand Up @@ -2014,7 +2199,7 @@ func (f *FourslashTest) updateTextRangeForTextEdits(textRange core.TextRange, ed
// applyEditsToContent applies text edits to a content string without mutating the file.
func (f *FourslashTest) applyEditsToContent(content string, edits []*lsproto.TextEdit) string {
script := f.getScriptInfo(f.activeFilename)
slices.SortFunc(edits, func(a, b *lsproto.TextEdit) int {
slices.SortStableFunc(edits, func(a, b *lsproto.TextEdit) int {
aStart := f.converters.LineAndCharacterToPosition(script, a.Range.Start)
bStart := f.converters.LineAndCharacterToPosition(script, b.Range.Start)
return int(aStart) - int(bStart)
Expand Down
Loading