@samitouri / QOS-React-2 / commits / 3a5335676f

[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 }