[forgive] Polish decorations (#33002)
Polishes up decorations. Co-authored-by: Jordan Brown <jmbrown@meta.com> --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33002). * #33004 * #33003 * __->__ #33002 Co-authored-by: Jordan Brown <jmbrown@meta.com>
lauren committed
Apr 23, 2025 at 21:55 UTC
3a5335676f7df4318e8e399f8f69e880db724f78
6 files changed
+170
-71
compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts
+1
@@ -261,6 +261,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
261
} else if (
262
value.args.length >= 2 &&
263
value.args.length - 1 === autodepFnLoads.get(callee.identifier.id) &&
264
+ value.args[0] != null &&
265
value.args[0].kind === 'Identifier'
266
) {
267
const penultimateArg = value.args[value.args.length - 2];
compiler/packages/react-forgive/client/src/autodeps.ts
new
+99
@@ -0,0 +1,99 @@
1
+import * as vscode from 'vscode';
2
+import {
3
+ LanguageClient,
4
+ RequestType,
5
+ type Position,
6
+} from 'vscode-languageclient/node';
7
+import {positionLiteralToVSCodePosition, positionsToRange} from './mapping';
8
+
9
+export type AutoDepsDecorationsLSPEvent = {
10
+ useEffectCallExpr: [Position, Position];
11
+ decorations: Array<[Position, Position]>;
12
+};
13
+
14
+export interface AutoDepsDecorationsParams {
15
+ position: Position;
16
+}
17
+
18
+export namespace AutoDepsDecorationsRequest {
19
+ export const type = new RequestType<
20
+ AutoDepsDecorationsParams,
21
+ AutoDepsDecorationsLSPEvent | null,
22
+ void
23
+ >('react/autodeps_decorations');
24
+}
25
+
26
+const inferredEffectDepDecoration =
27
+ vscode.window.createTextEditorDecorationType({
28
+ // TODO: make configurable?
29
+ borderColor: new vscode.ThemeColor('diffEditor.move.border'),
30
+ borderStyle: 'solid',
31
+ borderWidth: '0 0 4px 0',
32
+ });
33
+
34
+let currentlyDecoratedAutoDepFnLoc: vscode.Range | null = null;
35
+export function getCurrentlyDecoratedAutoDepFnLoc(): vscode.Range | null {
36
+ return currentlyDecoratedAutoDepFnLoc;
37
+}
38
+export function setCurrentlyDecoratedAutoDepFnLoc(range: vscode.Range): void {
39
+ currentlyDecoratedAutoDepFnLoc = range;
40
+}
41
+export function clearCurrentlyDecoratedAutoDepFnLoc(): void {
42
+ currentlyDecoratedAutoDepFnLoc = null;
43
+}
44
+
45
+let decorationRequestId = 0;
46
+export type AutoDepsDecorationsOptions = {
47
+ shouldUpdateCurrent: boolean;
48
+};
49
+export function requestAutoDepsDecorations(
50
+ client: LanguageClient,
51
+ position: vscode.Position,
52
+ options: AutoDepsDecorationsOptions,
53
+) {
54
+ const id = ++decorationRequestId;
55
+ client
56
+ .sendRequest(AutoDepsDecorationsRequest.type, {position})
57
+ .then(response => {
58
+ if (response !== null) {
59
+ const {
60
+ decorations,
61
+ useEffectCallExpr: [start, end],
62
+ } = response;
63
+ // Maintain ordering
64
+ if (decorationRequestId === id) {
65
+ if (options.shouldUpdateCurrent) {
66
+ setCurrentlyDecoratedAutoDepFnLoc(positionsToRange(start, end));
67
+ }
68
+ drawInferredEffectDepDecorations(decorations);
69
+ }
70
+ } else {
71
+ clearCurrentlyDecoratedAutoDepFnLoc();
72
+ clearDecorations(inferredEffectDepDecoration);
73
+ }
74
+ });
75
+}
76
+
77
+export function drawInferredEffectDepDecorations(
78
+ decorations: Array<[Position, Position]>,
79
+): void {
80
+ const decorationOptions = decorations.map(([start, end]) => {
81
+ return {
82
+ range: new vscode.Range(
83
+ positionLiteralToVSCodePosition(start),
84
+ positionLiteralToVSCodePosition(end),
85
+ ),
86
+ hoverMessage: 'Inferred as an effect dependency',
87
+ };
88
+ });
89
+ vscode.window.activeTextEditor?.setDecorations(
90
+ inferredEffectDepDecoration,
91
+ decorationOptions,
92
+ );
93
+}
94
+
95
+export function clearDecorations(
96
+ decorationType: vscode.TextEditorDecorationType,
97
+) {
98
+ vscode.window.activeTextEditor?.setDecorations(decorationType, []);
99
+}
compiler/packages/react-forgive/client/src/extension.ts
+26
-45
@@ -5,29 +5,16 @@ import {
5
LanguageClient,
6
LanguageClientOptions,
7
type Position,
8
- RequestType,
8
ServerOptions,
9
TransportKind,
10
} from 'vscode-languageclient/node';
12
-import {WHITE} from './colors';
11
+import {positionLiteralToVSCodePosition} from './mapping';
12
+import {
13
+ getCurrentlyDecoratedAutoDepFnLoc,
14
+ requestAutoDepsDecorations,
15
+} from './autodeps';
16
17
let client: LanguageClient;
15
-const inferredEffectDepDecoration =
16
- vscode.window.createTextEditorDecorationType({
17
- backgroundColor: WHITE.toAlphaString(0.3),
18
- });
19
-
20
-type Range = [Position, Position];
21
-interface AutoDepsDecorationsParams {
22
- position: Position;
23
-}
24
-namespace AutoDepsDecorationsRequest {
25
- export const type = new RequestType<
26
- AutoDepsDecorationsParams,
27
- Array<Range> | null,
28
- void
29
- >('react/autodeps_decorations');
30
-}
18
19
export function activate(context: vscode.ExtensionContext) {
20
const serverModule = context.asAbsolutePath(path.join('dist', 'server.js'));
@@ -71,31 +58,31 @@ export function activate(context: vscode.ExtensionContext) {
58
59
vscode.languages.registerHoverProvider(documentSelector, {
60
provideHover(_document, position, _token) {
74
- client
75
- .sendRequest(AutoDepsDecorationsRequest.type, {position})
76
- .then(decorations => {
77
- if (Array.isArray(decorations)) {
78
- const decorationOptions = decorations.map(([start, end]) => {
79
- return {
80
- range: new vscode.Range(
81
- new vscode.Position(start.line, start.character),
82
- new vscode.Position(end.line, end.character),
83
- ),
84
- hoverMessage: 'Inferred as an effect dependency',
85
- };
86
- });
87
- vscode.window.activeTextEditor?.setDecorations(
88
- inferredEffectDepDecoration,
89
- decorationOptions,
90
- );
91
- } else {
92
- clearDecorations(inferredEffectDepDecoration);
93
- }
94
- });
61
+ requestAutoDepsDecorations(client, position, {shouldUpdateCurrent: true});
62
return null;
63
},
64
});
65
66
+ vscode.workspace.onDidChangeTextDocument(async _e => {
67
+ const currentlyDecoratedAutoDepFnLoc = getCurrentlyDecoratedAutoDepFnLoc();
68
+ if (currentlyDecoratedAutoDepFnLoc !== null) {
69
+ requestAutoDepsDecorations(client, currentlyDecoratedAutoDepFnLoc.start, {
70
+ shouldUpdateCurrent: false,
71
+ });
72
+ }
73
+ });
74
+
75
+ vscode.commands.registerCommand(
76
+ 'react.requestAutoDepsDecorations',
77
+ (position: Position) => {
78
+ requestAutoDepsDecorations(
79
+ client,
80
+ positionLiteralToVSCodePosition(position),
81
+ {shouldUpdateCurrent: true},
82
+ );
83
+ },
84
+ );
85
+
86
client.registerProposedFeatures();
87
client.start();
88
}
@@ -106,9 +93,3 @@ export function deactivate(): Thenable<void> | undefined {
93
}
94
return;
95
}
109
-
110
-export function clearDecorations(
111
- decorationType: vscode.TextEditorDecorationType,
112
-) {
113
- vscode.window.activeTextEditor?.setDecorations(decorationType, []);
114
-}
compiler/packages/react-forgive/client/src/mapping.ts
new
+15
@@ -0,0 +1,15 @@
1
+import * as vscode from 'vscode';
2
+import {Position} from 'vscode-languageclient/node';
3
+
4
+export function positionLiteralToVSCodePosition(
5
+ position: Position,
6
+): vscode.Position {
7
+ return new vscode.Position(position.line, position.character);
8
+}
9
+
10
+export function positionsToRange(start: Position, end: Position): vscode.Range {
11
+ return new vscode.Range(
12
+ positionLiteralToVSCodePosition(start),
13
+ positionLiteralToVSCodePosition(end),
14
+ );
15
+}
compiler/packages/react-forgive/server/src/index.ts
+28
-25
@@ -10,6 +10,7 @@ import {
10
CodeAction,
11
CodeActionKind,
12
CodeLens,
13
+ Command,
14
createConnection,
15
type InitializeParams,
16
type InitializeResult,
@@ -96,6 +97,7 @@ connection.onInitialize((_params: InitializeParams) => {
97
logger: {
98
logEvent(_filename: string | null, event: LoggerEvent) {
99
connection.console.info(`Received event: ${event.kind}`);
100
+ connection.console.debug(JSON.stringify(event, null, 2));
101
if (event.kind === 'CompileSuccess') {
102
compiledFns.add(event);
103
}
@@ -191,7 +193,6 @@ connection.onCodeLensResolve(lens => {
193
194
connection.onCodeAction(params => {
195
connection.console.log('onCodeAction');
194
- connection.console.log(JSON.stringify(params, null, 2));
196
const codeActions: Array<CodeAction> = [];
197
for (const codeActionEvent of codeActionEvents) {
198
if (
@@ -200,39 +201,41 @@ connection.onCodeAction(params => {
201
codeActionEvent.anchorRange,
202
)
203
) {
203
- codeActions.push(
204
- CodeAction.create(
205
- codeActionEvent.title,
206
- {
207
- changes: {
208
- [params.textDocument.uri]: [
209
- {
210
- newText: codeActionEvent.newText,
211
- range: codeActionEvent.editRange,
212
- },
213
- ],
214
- },
204
+ const codeAction = CodeAction.create(
205
+ codeActionEvent.title,
206
+ {
207
+ changes: {
208
+ [params.textDocument.uri]: [
209
+ {
210
+ newText: codeActionEvent.newText,
211
+ range: codeActionEvent.editRange,
212
+ },
213
+ ],
214
},
216
- codeActionEvent.kind,
217
- ),
215
+ },
216
+ codeActionEvent.kind,
217
);
218
+ // After executing a codeaction, we want to draw autodep decorations again
219
+ codeAction.command = Command.create(
220
+ 'Request autodeps decorations',
221
+ 'react.requestAutoDepsDecorations',
222
+ codeActionEvent.anchorRange[0],
223
+ );
224
+ codeActions.push(codeAction);
225
}
226
}
227
return codeActions;
228
});
229
224
-connection.onCodeActionResolve(codeAction => {
225
- connection.console.log('onCodeActionResolve');
226
- connection.console.log(JSON.stringify(codeAction, null, 2));
227
- return codeAction;
228
-});
229
-
230
+/**
231
+ * The client can request the server to compute autodeps decorations based on a currently selected
232
+ * position if the selected position is within an autodep eligible function call.
233
+ */
234
connection.onRequest(AutoDepsDecorationsRequest.type, async params => {
235
const position = params.position;
232
- connection.console.debug('Client hovering on: ' + JSON.stringify(position));
233
- for (const dec of autoDepsDecorations) {
234
- if (isPositionWithinRange(position, dec.useEffectCallExpr)) {
235
- return dec.decorations;
236
+ for (const decoration of autoDepsDecorations) {
237
+ if (isPositionWithinRange(position, decoration.useEffectCallExpr)) {
238
+ return decoration;
239
}
240
}
241
return null;
compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts
+1
-1
@@ -13,7 +13,7 @@ export interface AutoDepsDecorationsParams {
13
export namespace AutoDepsDecorationsRequest {
14
export const type = new RequestType<
15
AutoDepsDecorationsParams,
16
- Array<Range> | null,
16
+ AutoDepsDecorationsLSPEvent,
17
void
18
>('react/autodeps_decorations');
19
}