Skip to content

Commit 2a2e04d

Browse files
authored
Implement selection ranges (#1939)
1 parent 5c2cafa commit 2a2e04d

File tree

109 files changed

+3620
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+3620
-1
lines changed

internal/ast/ast.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8706,6 +8706,10 @@ func (node *TemplateHead) Clone(f NodeFactoryCoercible) *Node {
87068706
return cloneNode(f.AsNodeFactory().NewTemplateHead(node.Text, node.RawText, node.TemplateFlags), node.AsNode(), f.AsNodeFactory().hooks)
87078707
}
87088708

8709+
func IsTemplateHead(node *Node) bool {
8710+
return node.Kind == KindTemplateHead
8711+
}
8712+
87098713
// TemplateMiddle
87108714

87118715
type TemplateMiddle struct {
@@ -8726,6 +8730,10 @@ func (node *TemplateMiddle) Clone(f NodeFactoryCoercible) *Node {
87268730
return cloneNode(f.AsNodeFactory().NewTemplateMiddle(node.Text, node.RawText, node.TemplateFlags), node.AsNode(), f.AsNodeFactory().hooks)
87278731
}
87288732

8733+
func IsTemplateMiddle(node *Node) bool {
8734+
return node.Kind == KindTemplateMiddle
8735+
}
8736+
87298737
// TemplateTail
87308738

87318739
type TemplateTail struct {
@@ -8746,6 +8754,10 @@ func (node *TemplateTail) Clone(f NodeFactoryCoercible) *Node {
87468754
return cloneNode(f.AsNodeFactory().NewTemplateTail(node.Text, node.RawText, node.TemplateFlags), node.AsNode(), f.AsNodeFactory().hooks)
87478755
}
87488756

8757+
func IsTemplateTail(node *Node) bool {
8758+
return node.Kind == KindTemplateTail
8759+
}
8760+
87498761
// TemplateLiteralTypeNode
87508762

87518763
type TemplateLiteralTypeNode struct {
@@ -9635,6 +9647,10 @@ func (node *JSDocTypeExpression) Clone(f NodeFactoryCoercible) *Node {
96359647
return cloneNode(f.AsNodeFactory().NewJSDocTypeExpression(node.Type), node.AsNode(), f.AsNodeFactory().hooks)
96369648
}
96379649

9650+
func IsJSDocTypeExpression(node *Node) bool {
9651+
return node.Kind == KindJSDocTypeExpression
9652+
}
9653+
96389654
// JSDocNonNullableType
96399655

96409656
type JSDocNonNullableType struct {
@@ -10565,6 +10581,10 @@ func (node *JSDocTypeLiteral) Clone(f NodeFactoryCoercible) *Node {
1056510581
return cloneNode(f.AsNodeFactory().NewJSDocTypeLiteral(node.JSDocPropertyTags, node.IsArrayType), node.AsNode(), f.AsNodeFactory().hooks)
1056610582
}
1056710583

10584+
func IsJSDocTypeLiteral(node *Node) bool {
10585+
return node.Kind == KindJSDocTypeLiteral
10586+
}
10587+
1056810588
// JSDocSignature
1056910589
type JSDocSignature struct {
1057010590
TypeNodeBase

internal/fourslash/_scripts/convertFourslash.mts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
190190
return parseBaselineQuickInfo(callExpression.arguments);
191191
case "baselineSignatureHelp":
192192
return [parseBaselineSignatureHelp(callExpression.arguments)];
193+
case "baselineSmartSelection":
194+
return [parseBaselineSmartSelection(callExpression.arguments)];
193195
case "baselineGoToDefinition":
194196
case "baselineGetDefinitionAtPosition":
195197
case "baselineGoToType":
@@ -1422,6 +1424,16 @@ function parseBaselineSignatureHelp(args: ts.NodeArray<ts.Expression>): Cmd {
14221424
};
14231425
}
14241426

1427+
function parseBaselineSmartSelection(args: ts.NodeArray<ts.Expression>): Cmd {
1428+
if (args.length !== 0) {
1429+
// All calls are currently empty!
1430+
throw new Error("Expected no arguments in verify.baselineSmartSelection");
1431+
}
1432+
return {
1433+
kind: "verifyBaselineSmartSelection",
1434+
};
1435+
}
1436+
14251437
function parseKind(expr: ts.Expression): string | undefined {
14261438
if (!ts.isStringLiteral(expr)) {
14271439
console.error(`Expected string literal for kind, got ${expr.getText()}`);
@@ -1591,6 +1603,10 @@ interface VerifyBaselineSignatureHelpCmd {
15911603
kind: "verifyBaselineSignatureHelp";
15921604
}
15931605

1606+
interface VerifyBaselineSmartSelection {
1607+
kind: "verifyBaselineSmartSelection";
1608+
}
1609+
15941610
interface VerifyBaselineRenameCmd {
15951611
kind: "verifyBaselineRename" | "verifyBaselineRenameAtRangesWithText";
15961612
args: string[];
@@ -1635,6 +1651,7 @@ type Cmd =
16351651
| VerifyBaselineGoToDefinitionCmd
16361652
| VerifyBaselineQuickInfoCmd
16371653
| VerifyBaselineSignatureHelpCmd
1654+
| VerifyBaselineSmartSelection
16381655
| GoToCmd
16391656
| EditCmd
16401657
| VerifyQuickInfoCmd
@@ -1754,6 +1771,8 @@ function generateCmd(cmd: Cmd): string {
17541771
return `f.VerifyBaselineHover(t)`;
17551772
case "verifyBaselineSignatureHelp":
17561773
return `f.VerifyBaselineSignatureHelp(t)`;
1774+
case "verifyBaselineSmartSelection":
1775+
return `f.VerifyBaselineSelectionRanges(t)`;
17571776
case "goTo":
17581777
return generateGoToCommand(cmd);
17591778
case "edit":

internal/fourslash/baselineutil.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func getBaselineFileName(t *testing.T, command string) string {
4747

4848
func getBaselineExtension(command string) string {
4949
switch command {
50-
case "QuickInfo", "SignatureHelp":
50+
case "QuickInfo", "SignatureHelp", "Smart Selection":
5151
return "baseline"
5252
case "Auto Imports":
5353
return "baseline.md"
@@ -61,6 +61,11 @@ func getBaselineExtension(command string) string {
6161
func getBaselineOptions(command string) baseline.Options {
6262
subfolder := "fourslash/" + normalizeCommandName(command)
6363
switch command {
64+
case "Smart Selection":
65+
return baseline.Options{
66+
Subfolder: subfolder,
67+
IsSubmodule: true,
68+
}
6469
case "findRenameLocations":
6570
return baseline.Options{
6671
Subfolder: subfolder,

internal/fourslash/fourslash.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,149 @@ func (f *FourslashTest) VerifyBaselineSignatureHelp(t *testing.T) {
12941294
}
12951295
}
12961296

1297+
func (f *FourslashTest) VerifyBaselineSelectionRanges(t *testing.T) {
1298+
markers := f.Markers()
1299+
var result strings.Builder
1300+
newLine := "\n"
1301+
1302+
for i, marker := range markers {
1303+
if i > 0 {
1304+
result.WriteString(newLine + strings.Repeat("=", 80) + newLine + newLine)
1305+
}
1306+
1307+
script := f.getScriptInfo(marker.FileName())
1308+
fileContent := script.content
1309+
1310+
// Add the marker position indicator
1311+
markerPos := marker.Position
1312+
baselineContent := fileContent[:markerPos] + "/**/" + fileContent[markerPos:] + newLine
1313+
result.WriteString(baselineContent)
1314+
1315+
// Get selection ranges at this marker
1316+
params := &lsproto.SelectionRangeParams{
1317+
TextDocument: lsproto.TextDocumentIdentifier{
1318+
Uri: ls.FileNameToDocumentURI(marker.FileName()),
1319+
},
1320+
Positions: []lsproto.Position{marker.LSPosition},
1321+
}
1322+
1323+
resMsg, selectionRangeResult, resultOk := sendRequest(t, f, lsproto.TextDocumentSelectionRangeInfo, params)
1324+
markerNameStr := *core.OrElse(marker.Name, ptrTo("(unnamed)"))
1325+
if resMsg == nil {
1326+
t.Fatalf("Nil response received for selection range request at marker '%s'", markerNameStr)
1327+
}
1328+
if !resultOk {
1329+
if resMsg.AsResponse().Error != nil {
1330+
t.Fatalf("Error response for selection range request at marker '%s': %v", markerNameStr, resMsg.AsResponse().Error)
1331+
}
1332+
t.Fatalf("Unexpected selection range response type at marker '%s': %T", markerNameStr, resMsg.AsResponse().Result)
1333+
}
1334+
1335+
if selectionRangeResult.SelectionRanges == nil || len(*selectionRangeResult.SelectionRanges) == 0 {
1336+
result.WriteString("No selection ranges available\n")
1337+
continue
1338+
}
1339+
1340+
selectionRange := (*selectionRangeResult.SelectionRanges)[0]
1341+
1342+
// Add blank line after source code section
1343+
result.WriteString(newLine)
1344+
1345+
// Walk through the selection range chain
1346+
for selectionRange != nil {
1347+
start := int(f.converters.LineAndCharacterToPosition(script, selectionRange.Range.Start))
1348+
end := int(f.converters.LineAndCharacterToPosition(script, selectionRange.Range.End))
1349+
1350+
// Create a masked version of the file showing only this range
1351+
runes := []rune(fileContent)
1352+
masked := make([]rune, len(runes))
1353+
for i, ch := range runes {
1354+
if i >= start && i < end {
1355+
// Keep characters in the selection range
1356+
if ch == ' ' {
1357+
masked[i] = '•'
1358+
} else if ch == '\n' || ch == '\r' {
1359+
masked[i] = ch // Keep line breaks as-is, will add arrow later
1360+
} else {
1361+
masked[i] = ch
1362+
}
1363+
} else {
1364+
// Replace characters outside the range
1365+
if ch == '\n' || ch == '\r' {
1366+
masked[i] = ch
1367+
} else {
1368+
masked[i] = ' '
1369+
}
1370+
}
1371+
}
1372+
1373+
maskedStr := string(masked)
1374+
1375+
// Add line break arrows
1376+
maskedStr = strings.ReplaceAll(maskedStr, "\n", "↲\n")
1377+
maskedStr = strings.ReplaceAll(maskedStr, "\r", "↲\r")
1378+
1379+
// Remove blank lines
1380+
lines := strings.Split(maskedStr, "\n")
1381+
var nonBlankLines []string
1382+
for _, line := range lines {
1383+
trimmed := strings.TrimSpace(line)
1384+
if trimmed != "" && trimmed != "↲" {
1385+
nonBlankLines = append(nonBlankLines, line)
1386+
}
1387+
}
1388+
maskedStr = strings.Join(nonBlankLines, "\n")
1389+
1390+
// Find leading and trailing width of non-whitespace characters
1391+
maskedRunes := []rune(maskedStr)
1392+
isRealCharacter := func(ch rune) bool {
1393+
return ch != '•' && ch != '↲' && !stringutil.IsWhiteSpaceLike(ch)
1394+
}
1395+
1396+
leadingWidth := -1
1397+
for i, ch := range maskedRunes {
1398+
if isRealCharacter(ch) {
1399+
leadingWidth = i
1400+
break
1401+
}
1402+
}
1403+
1404+
trailingWidth := -1
1405+
for j := len(maskedRunes) - 1; j >= 0; j-- {
1406+
if isRealCharacter(maskedRunes[j]) {
1407+
trailingWidth = j
1408+
break
1409+
}
1410+
}
1411+
1412+
if leadingWidth != -1 && trailingWidth != -1 && leadingWidth <= trailingWidth {
1413+
// Clean up middle section
1414+
prefix := string(maskedRunes[:leadingWidth])
1415+
middle := string(maskedRunes[leadingWidth : trailingWidth+1])
1416+
suffix := string(maskedRunes[trailingWidth+1:])
1417+
1418+
middle = strings.ReplaceAll(middle, "•", " ")
1419+
middle = strings.ReplaceAll(middle, "↲", "")
1420+
1421+
maskedStr = prefix + middle + suffix
1422+
}
1423+
1424+
// Add blank line before multi-line ranges
1425+
if strings.Contains(maskedStr, "\n") {
1426+
result.WriteString(newLine)
1427+
}
1428+
1429+
result.WriteString(maskedStr)
1430+
if !strings.HasSuffix(maskedStr, "\n") {
1431+
result.WriteString(newLine)
1432+
}
1433+
1434+
selectionRange = selectionRange.Parent
1435+
}
1436+
}
1437+
f.addResultToBaseline(t, "Smart Selection", strings.TrimSuffix(result.String(), "\n"))
1438+
}
1439+
12971440
func (f *FourslashTest) VerifyBaselineDocumentHighlights(
12981441
t *testing.T,
12991442
preferences *ls.UserPreferences,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/fourslash"
7+
"github.com/microsoft/typescript-go/internal/testutil"
8+
)
9+
10+
func TestSmartSelection_JSDocTags10(t *testing.T) {
11+
t.Parallel()
12+
13+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
14+
const content = `/**
15+
* @template T
16+
* @extends {/**/Set<T>}
17+
*/
18+
class A extends B {
19+
}`
20+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
21+
f.VerifyBaselineSelectionRanges(t)
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/fourslash"
7+
"github.com/microsoft/typescript-go/internal/testutil"
8+
)
9+
10+
func TestSmartSelection_JSDocTags11(t *testing.T) {
11+
t.Parallel()
12+
13+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
14+
const content = `const x = 1;
15+
type Foo = {
16+
/** comment */
17+
/*2*/readonly /*1*/status: number;
18+
};`
19+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
20+
f.VerifyBaselineSelectionRanges(t)
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/fourslash"
7+
"github.com/microsoft/typescript-go/internal/testutil"
8+
)
9+
10+
func TestSmartSelection_JSDocTags12(t *testing.T) {
11+
t.Parallel()
12+
13+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
14+
const content = `type B = {};
15+
type A = {
16+
a(/** Comment */ /*1*/p0: number, /** Comment */ /*2*/p1: number, /** Comment */ /*3*/p2: number): string;
17+
};`
18+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
19+
f.VerifyBaselineSelectionRanges(t)
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/fourslash"
7+
"github.com/microsoft/typescript-go/internal/testutil"
8+
)
9+
10+
func TestSmartSelection_JSDocTags13(t *testing.T) {
11+
t.Parallel()
12+
13+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
14+
const content = `let a;
15+
let b: {
16+
/** Comment */ /*1*/p0: number
17+
/** Comment */ /*2*/p1: number
18+
/** Comment */ /*3*/p2: number
19+
};
20+
let c;`
21+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
22+
f.VerifyBaselineSelectionRanges(t)
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/fourslash"
7+
"github.com/microsoft/typescript-go/internal/testutil"
8+
)
9+
10+
func TestSmartSelection_JSDocTags1(t *testing.T) {
11+
t.Parallel()
12+
13+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
14+
const content = `/**
15+
* @returns {Array<{ value: /**/string }>}
16+
*/
17+
function foo() { return [] }`
18+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
19+
f.VerifyBaselineSelectionRanges(t)
20+
}

0 commit comments

Comments
 (0)