Skip to content

Commit 18bbf2f

Browse files
Implement support for Code Lenses (#2145)
1 parent e3beec5 commit 18bbf2f

File tree

72 files changed

+1840
-74
lines changed

Some content is hidden

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

72 files changed

+1840
-74
lines changed

_extension/package.json

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,23 @@
4040
],
4141
"default": "verbose",
4242
"description": "Trace TypeScript Go server communication.",
43-
"tags": ["experimental"]
43+
"tags": [
44+
"experimental"
45+
]
4446
},
4547
"typescript.native-preview.pprofDir": {
4648
"type": "string",
4749
"description": "Directory to write pprof profiles to.",
48-
"tags": ["experimental"]
50+
"tags": [
51+
"experimental"
52+
]
4953
},
5054
"typescript.native-preview.tsdk": {
5155
"type": "string",
5256
"description": "Path to the @typescript/native-preview package or tsgo binary directory. If not specified, the extension will look for it in the default location.",
53-
"tags": ["experimental"]
57+
"tags": [
58+
"experimental"
59+
]
5460
}
5561
}
5662
}
@@ -91,6 +97,11 @@
9197
"title": "Report Issue",
9298
"enablement": "typescript.native-preview.serverRunning",
9399
"category": "TypeScript Native Preview"
100+
},
101+
{
102+
"title": "Show References of CodeLens",
103+
"command": "typescript.native-preview.codeLens.showLocations",
104+
"enablement": "false"
94105
}
95106
]
96107
},

_extension/src/client.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import * as vscode from "vscode";
22
import {
3+
DocumentUri,
34
LanguageClient,
45
LanguageClientOptions,
6+
Location,
57
NotebookDocumentFilter,
8+
Position,
69
ServerOptions,
710
TextDocumentFilter,
811
TransportKind,
@@ -14,6 +17,8 @@ import {
1417
} from "./util";
1518
import { getLanguageForUri } from "./util";
1619

20+
const codeLensShowLocationsCommandName = "typescript.native-preview.codeLens.showLocations";
21+
1722
export class Client {
1823
private outputChannel: vscode.OutputChannel;
1924
private traceOutputChannel: vscode.OutputChannel;
@@ -32,6 +37,9 @@ export class Client {
3237
],
3338
outputChannel: this.outputChannel,
3439
traceOutputChannel: this.traceOutputChannel,
40+
initializationOptions: {
41+
codeLensShowLocationsCommandName,
42+
},
3543
diagnosticPullOptions: {
3644
onChange: true,
3745
onSave: true,
@@ -119,10 +127,34 @@ export class Client {
119127
await this.client.start();
120128
vscode.commands.executeCommand("setContext", "typescript.native-preview.serverRunning", true);
121129
this.onStartedCallbacks.forEach(callback => callback());
122-
return new vscode.Disposable(() => {
123-
if (this.client) {
124-
this.client.stop();
130+
131+
const codeLensLocationsCommand = vscode.commands.registerCommand(codeLensShowLocationsCommandName, (...args: unknown[]) => {
132+
if (args.length !== 3) {
133+
throw new Error("Unexpected number of arguments.");
125134
}
135+
136+
const lspUri = args[0] as DocumentUri;
137+
const lspPosition = args[1] as Position;
138+
const lspLocations = args[2] as Location[];
139+
140+
const editorUri = vscode.Uri.parse(lspUri);
141+
const editorPosition = new vscode.Position(lspPosition.line, lspPosition.character);
142+
const editorLocations = lspLocations.map(loc =>
143+
new vscode.Location(
144+
vscode.Uri.parse(loc.uri),
145+
new vscode.Range(
146+
new vscode.Position(loc.range.start.line, loc.range.start.character),
147+
new vscode.Position(loc.range.end.line, loc.range.end.character),
148+
),
149+
)
150+
);
151+
152+
vscode.commands.executeCommand("editor.action.showReferences", editorUri, editorPosition, editorLocations);
153+
});
154+
155+
return new vscode.Disposable(() => {
156+
this.client?.stop();
157+
codeLensLocationsCommand.dispose();
126158
vscode.commands.executeCommand("setContext", "typescript.native-preview.serverRunning", false);
127159
});
128160
}

internal/ast/ast.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2214,11 +2214,11 @@ func IsWriteAccess(node *Node) bool {
22142214
}
22152215

22162216
func IsWriteAccessForReference(node *Node) bool {
2217-
decl := getDeclarationFromName(node)
2217+
decl := GetDeclarationFromName(node)
22182218
return (decl != nil && declarationIsWriteAccess(decl)) || node.Kind == KindDefaultKeyword || IsWriteAccess(node)
22192219
}
22202220

2221-
func getDeclarationFromName(name *Node) *Declaration {
2221+
func GetDeclarationFromName(name *Node) *Declaration {
22222222
if name == nil || name.Parent == nil {
22232223
return nil
22242224
}

internal/diagnostics/diagnostics_generated.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/diagnostics/extraDiagnosticMessages.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@
1919
"category": "Message",
2020
"code": 100004
2121
},
22+
"{0} references": {
23+
"category": "Message",
24+
"code": 100005
25+
},
26+
"1 reference": {
27+
"category": "Message",
28+
"code": 100006
29+
},
30+
"{0} implementations": {
31+
"category": "Message",
32+
"code": 100007
33+
},
34+
"1 implementation": {
35+
"category": "Message",
36+
"code": 100008
37+
},
2238
"Non-relative paths are not allowed. Did you forget a leading './'?": {
2339
"category": "Error",
2440
"code": 5090

internal/fourslash/baselineutil.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const (
3434
renameCmd baselineCommand = "findRenameLocations"
3535
signatureHelpCmd baselineCommand = "SignatureHelp"
3636
smartSelectionCmd baselineCommand = "Smart Selection"
37+
codeLensesCmd baselineCommand = "Code Lenses"
3738
)
3839

3940
type baselineCommand string

internal/fourslash/fourslash.go

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ func (w *lspWriter) Write(msg *lsproto.Message) error {
115115
return nil
116116
}
117117

118-
func (r *lspWriter) Close() {
119-
close(r.c)
118+
func (w *lspWriter) Close() {
119+
close(w.c)
120120
}
121121

122122
var (
@@ -294,13 +294,16 @@ func (f *FourslashTest) nextID() int32 {
294294
return id
295295
}
296296

297+
const showCodeLensLocationsCommandName = "typescript.showCodeLensLocations"
298+
297299
func (f *FourslashTest) initialize(t *testing.T, capabilities *lsproto.ClientCapabilities) {
298300
params := &lsproto.InitializeParams{
299301
Locale: ptrTo("en-US"),
300302
InitializationOptions: &lsproto.InitializationOptions{
301303
// Hack: disable push diagnostics entirely, since the fourslash runner does not
302304
// yet gracefully handle non-request messages.
303-
DisablePushDiagnostics: ptrTo(true),
305+
DisablePushDiagnostics: ptrTo(true),
306+
CodeLensShowLocationsCommandName: ptrTo(showCodeLensLocationsCommandName),
304307
},
305308
}
306309
params.Capabilities = getCapabilitiesWithDefaults(capabilities)
@@ -1319,7 +1322,9 @@ func (f *FourslashTest) VerifyBaselineFindAllReferences(
13191322
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
13201323
},
13211324
Position: f.currentCaretPosition,
1322-
Context: &lsproto.ReferenceContext{},
1325+
Context: &lsproto.ReferenceContext{
1326+
IncludeDeclaration: true,
1327+
},
13231328
}
13241329
result := sendRequest(t, f, lsproto.TextDocumentReferencesInfo, params)
13251330
f.addResultToBaseline(t, findAllReferencesCmd, f.getBaselineForLocationsWithFileContents(*result.Locations, baselineFourslashLocationsOptions{
@@ -1330,6 +1335,61 @@ func (f *FourslashTest) VerifyBaselineFindAllReferences(
13301335
}
13311336
}
13321337

1338+
func (f *FourslashTest) VerifyBaselineCodeLens(t *testing.T, preferences *lsutil.UserPreferences) {
1339+
if preferences != nil {
1340+
reset := f.ConfigureWithReset(t, preferences)
1341+
defer reset()
1342+
}
1343+
1344+
foundAtLeastOneCodeLens := false
1345+
for _, openFile := range slices.Sorted(maps.Keys(f.openFiles)) {
1346+
params := &lsproto.CodeLensParams{
1347+
TextDocument: lsproto.TextDocumentIdentifier{
1348+
Uri: lsconv.FileNameToDocumentURI(openFile),
1349+
},
1350+
}
1351+
1352+
unresolvedCodeLensList := sendRequest(t, f, lsproto.TextDocumentCodeLensInfo, params)
1353+
if unresolvedCodeLensList.CodeLenses == nil || len(*unresolvedCodeLensList.CodeLenses) == 0 {
1354+
continue
1355+
}
1356+
foundAtLeastOneCodeLens = true
1357+
1358+
for _, unresolvedCodeLens := range *unresolvedCodeLensList.CodeLenses {
1359+
assert.Assert(t, unresolvedCodeLens != nil)
1360+
resolvedCodeLens := sendRequest(t, f, lsproto.CodeLensResolveInfo, unresolvedCodeLens)
1361+
assert.Assert(t, resolvedCodeLens != nil)
1362+
assert.Assert(t, resolvedCodeLens.Command != nil, "Expected resolved code lens to have a command.")
1363+
if len(resolvedCodeLens.Command.Command) > 0 {
1364+
assert.Equal(t, resolvedCodeLens.Command.Command, showCodeLensLocationsCommandName)
1365+
}
1366+
1367+
var locations []lsproto.Location
1368+
// commandArgs: (DocumentUri, Position, Location[])
1369+
if commandArgs := resolvedCodeLens.Command.Arguments; commandArgs != nil {
1370+
locs, err := roundtripThroughJson[[]lsproto.Location]((*commandArgs)[2])
1371+
if err != nil {
1372+
t.Fatalf("failed to re-encode code lens locations: %v", err)
1373+
}
1374+
locations = locs
1375+
}
1376+
1377+
f.addResultToBaseline(t, codeLensesCmd, f.getBaselineForLocationsWithFileContents(locations, baselineFourslashLocationsOptions{
1378+
marker: &RangeMarker{
1379+
fileName: openFile,
1380+
LSRange: resolvedCodeLens.Range,
1381+
Range: f.converters.FromLSPRange(f.getScriptInfo(openFile), resolvedCodeLens.Range),
1382+
},
1383+
markerName: "/*CODELENS: " + resolvedCodeLens.Command.Title + "*/",
1384+
}))
1385+
}
1386+
}
1387+
1388+
if !foundAtLeastOneCodeLens {
1389+
t.Fatalf("Expected at least one code lens in any open file, but got none.")
1390+
}
1391+
}
1392+
13331393
func (f *FourslashTest) MarkTestAsStradaServer() {
13341394
f.isStradaServer = true
13351395
}
@@ -2155,6 +2215,25 @@ func ptrTo[T any](v T) *T {
21552215
return &v
21562216
}
21572217

2218+
// This function is intended for spots where a complex
2219+
// value needs to be reinterpreted following some prior JSON deserialization.
2220+
// The default deserializer for `any` properties will give us a map at runtime,
2221+
// but we want to validate against, and use, the types as returned from the the language service.
2222+
//
2223+
// Use this function sparingly. You can treat it as a "map-to-struct" converter,
2224+
// but updating the original types is probably better in most cases.
2225+
func roundtripThroughJson[T any](value any) (T, error) {
2226+
var result T
2227+
bytes, err := json.Marshal(value)
2228+
if err != nil {
2229+
return result, fmt.Errorf("failed to marshal value to JSON: %w", err)
2230+
}
2231+
if err := json.Unmarshal(bytes, &result); err != nil {
2232+
return result, fmt.Errorf("failed to unmarshal value from JSON: %w", err)
2233+
}
2234+
return result, nil
2235+
}
2236+
21582237
// Insert text at the current caret position.
21592238
func (f *FourslashTest) Insert(t *testing.T, text string) {
21602239
f.typeText(t, text)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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/ls/lsutil"
8+
"github.com/microsoft/typescript-go/internal/testutil"
9+
)
10+
11+
func TestCodeLensFunctionExpressions01(t *testing.T) {
12+
t.Parallel()
13+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
14+
15+
const content = `
16+
// @filename: anonymousFunctionExpressions.ts
17+
export let anonFn1 = function () {};
18+
export const anonFn2 = function () {};
19+
20+
let anonFn3 = function () {};
21+
const anonFn4 = function () {};
22+
23+
// @filename: arrowFunctions.ts
24+
export let arrowFn1 = () => {};
25+
export const arrowFn2 = () => {};
26+
27+
let arrowFn3 = () => {};
28+
const arrowFn4 = () => {};
29+
30+
// @filename: namedFunctions.ts
31+
export let namedFn1 = function namedFn1() {
32+
namedFn1();
33+
}
34+
namedFn1();
35+
36+
export const namedFn2 = function namedFn2() {
37+
namedFn2();
38+
}
39+
namedFn2();
40+
41+
let namedFn3 = function namedFn3() {};
42+
const namedFn4 = function namedFn4() {};
43+
`
44+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
45+
f.VerifyBaselineCodeLens(t, &lsutil.UserPreferences{
46+
ReferencesCodeLensEnabled: true,
47+
ReferencesCodeLensShowOnAllFunctions: true,
48+
49+
ImplementationsCodeLensEnabled: true,
50+
ImplementationsCodeLensShowOnInterfaceMethods: true,
51+
ImplementationsCodeLensShowOnAllClassMethods: true,
52+
})
53+
}

0 commit comments

Comments
 (0)