[forgive] Add basic codelens provider (#32476)
Adds a first codelens provider for successfully compiled functions. A later PR will add an actual command that will fire when the codelens is clicked 
lauren committed
Feb 25, 2025 at 18:55 UTC
92e65ca68f6bfc6be515ccacaa918e33b63911df
4 files changed
+129
-62
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts
+42
-35
@@ -176,41 +176,48 @@ export type CompilationMode = z.infer<typeof CompilationModeSchema>;
176
* babel or other unhandled exceptions).
177
*/
178
export type LoggerEvent =
179
- | {
180
- kind: 'CompileError';
181
- fnLoc: t.SourceLocation | null;
182
- detail: CompilerErrorDetailOptions;
183
- }
184
- | {
185
- kind: 'CompileDiagnostic';
186
- fnLoc: t.SourceLocation | null;
187
- detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
188
- }
189
- | {
190
- kind: 'CompileSkip';
191
- fnLoc: t.SourceLocation | null;
192
- reason: string;
193
- loc: t.SourceLocation | null;
194
- }
195
- | {
196
- kind: 'CompileSuccess';
197
- fnLoc: t.SourceLocation | null;
198
- fnName: string | null;
199
- memoSlots: number;
200
- memoBlocks: number;
201
- memoValues: number;
202
- prunedMemoBlocks: number;
203
- prunedMemoValues: number;
204
- }
205
- | {
206
- kind: 'PipelineError';
207
- fnLoc: t.SourceLocation | null;
208
- data: string;
209
- }
210
- | {
211
- kind: 'Timing';
212
- measurement: PerformanceMeasure;
213
- };
179
+ | CompileSuccessEvent
180
+ | CompileErrorEvent
181
+ | CompileDiagnosticEvent
182
+ | CompileSkipEvent
183
+ | PipelineErrorEvent
184
+ | TimingEvent;
185
+
186
+export type CompileErrorEvent = {
187
+ kind: 'CompileError';
188
+ fnLoc: t.SourceLocation | null;
189
+ detail: CompilerErrorDetailOptions;
190
+};
191
+export type CompileDiagnosticEvent = {
192
+ kind: 'CompileDiagnostic';
193
+ fnLoc: t.SourceLocation | null;
194
+ detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
195
+};
196
+export type CompileSuccessEvent = {
197
+ kind: 'CompileSuccess';
198
+ fnLoc: t.SourceLocation | null;
199
+ fnName: string | null;
200
+ memoSlots: number;
201
+ memoBlocks: number;
202
+ memoValues: number;
203
+ prunedMemoBlocks: number;
204
+ prunedMemoValues: number;
205
+};
206
+export type CompileSkipEvent = {
207
+ kind: 'CompileSkip';
208
+ fnLoc: t.SourceLocation | null;
209
+ reason: string;
210
+ loc: t.SourceLocation | null;
211
+};
212
+export type PipelineErrorEvent = {
213
+ kind: 'PipelineError';
214
+ fnLoc: t.SourceLocation | null;
215
+ data: string;
216
+};
217
+export type TimingEvent = {
218
+ kind: 'Timing';
219
+ measurement: PerformanceMeasure;
220
+};
221
222
export type Logger = {
223
logEvent: (filename: string | null, event: LoggerEvent) => void;
compiler/packages/react-forgive/server/src/compiler/compat.ts
new
+22
@@ -0,0 +1,22 @@
1
+import {SourceLocation} from 'babel-plugin-react-compiler/src';
2
+import {type Range} from 'vscode-languageserver';
3
+
4
+export function babelLocationToRange(loc: SourceLocation): Range | null {
5
+ if (typeof loc === 'symbol') {
6
+ return null;
7
+ }
8
+ return {
9
+ start: {line: loc.start.line - 1, character: loc.start.column},
10
+ end: {line: loc.end.line - 1, character: loc.end.column},
11
+ };
12
+}
13
+
14
+/**
15
+ * Refine range to only the first character.
16
+ */
17
+export function getRangeFirstCharacter(range: Range): Range {
18
+ return {
19
+ start: range.start,
20
+ end: range.start,
21
+ };
22
+}
compiler/packages/react-forgive/server/src/compiler/index.ts
+10
-2
@@ -11,10 +11,12 @@ import BabelPluginReactCompiler, {
11
type PluginOptions,
12
} from 'babel-plugin-react-compiler/src';
13
import * as babelParser from 'prettier/plugins/babel.js';
14
-import * as estreeParser from 'prettier/plugins/estree';
14
+import estreeParser from 'prettier/plugins/estree';
15
import * as typescriptParser from 'prettier/plugins/typescript';
16
import * as prettier from 'prettier/standalone';
17
18
+export let lastResult: BabelCore.BabelFileResult | null = null;
19
+
20
type CompileOptions = {
21
text: string;
22
file: string;
@@ -24,7 +26,7 @@ export async function compile({
26
text,
27
file,
28
options,
27
-}: CompileOptions): Promise<BabelCore.BabelFileResult> {
29
+}: CompileOptions): Promise<BabelCore.BabelFileResult | null> {
30
const ast = await parseAsync(text, {
31
sourceFileName: file,
32
parserOpts: {
@@ -32,6 +34,9 @@ export async function compile({
34
},
35
sourceType: 'module',
36
});
37
+ if (ast == null) {
38
+ return null;
39
+ }
40
const plugins =
41
options != null
42
? [[BabelPluginReactCompiler, options]]
@@ -54,5 +59,8 @@ export async function compile({
59
parser: 'babel-ts',
60
plugins: [babelParser, estreeParser, typescriptParser],
61
});
62
+ if (result.code != null) {
63
+ lastResult = result;
64
+ }
65
return result;
66
}
compiler/packages/react-forgive/server/src/index.ts
+55
-25
@@ -7,6 +7,7 @@
7
8
import {TextDocument} from 'vscode-languageserver-textdocument';
9
import {
10
+ CodeLens,
11
createConnection,
12
type InitializeParams,
13
type InitializeResult,
@@ -14,10 +15,15 @@ import {
15
TextDocuments,
16
TextDocumentSyncKind,
17
} from 'vscode-languageserver/node';
17
-import {compile} from './compiler';
18
+import {compile, lastResult} from './compiler';
19
import {type PluginOptions} from 'babel-plugin-react-compiler/src';
20
import {resolveReactConfig} from './compiler/options';
20
-import {type BabelFileResult} from '@babel/core';
21
+import {
22
+ CompileSuccessEvent,
23
+ defaultOptions,
24
+ LoggerEvent,
25
+} from 'babel-plugin-react-compiler/src/Entrypoint/Options';
26
+import {babelLocationToRange, getRangeFirstCharacter} from './compiler/compat';
27
28
const SUPPORTED_LANGUAGE_IDS = new Set([
29
'javascript',
@@ -30,11 +36,21 @@ const connection = createConnection(ProposedFeatures.all);
36
const documents = new TextDocuments(TextDocument);
37
38
let compilerOptions: PluginOptions | null = null;
33
-let lastResult: BabelFileResult | null = null;
39
+let compiledFns: Set<CompileSuccessEvent> = new Set();
40
41
connection.onInitialize((_params: InitializeParams) => {
42
// TODO(@poteto) get config fr
37
- compilerOptions = resolveReactConfig('.');
43
+ compilerOptions = resolveReactConfig('.') ?? defaultOptions;
44
+ compilerOptions = {
45
+ ...compilerOptions,
46
+ logger: {
47
+ logEvent(_filename: string | null, event: LoggerEvent) {
48
+ if (event.kind === 'CompileSuccess') {
49
+ compiledFns.add(event);
50
+ }
51
+ },
52
+ },
53
+ };
54
const result: InitializeResult = {
55
capabilities: {
56
textDocumentSync: TextDocumentSyncKind.Full,
@@ -48,44 +64,58 @@ connection.onInitialized(() => {
64
connection.console.log('initialized');
65
});
66
51
-documents.onDidOpen(async event => {
52
- if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) {
53
- const text = event.document.getText();
54
- const result = await compile({
55
- text,
56
- file: event.document.uri,
57
- options: compilerOptions,
58
- });
59
- if (result.code != null) {
60
- lastResult = result;
61
- }
62
- }
63
-});
64
-
67
documents.onDidChangeContent(async event => {
68
+ connection.console.info(`Changed: ${event.document.uri}`);
69
+ compiledFns.clear();
70
if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) {
71
const text = event.document.getText();
68
- const result = await compile({
72
+ await compile({
73
text,
74
file: event.document.uri,
75
options: compilerOptions,
76
});
73
- if (result.code != null) {
74
- lastResult = result;
75
- }
77
}
78
});
79
80
connection.onDidChangeWatchedFiles(change => {
81
+ compiledFns.clear();
82
connection.console.log(
83
change.changes.map(c => `File changed: ${c.uri}`).join('\n'),
84
);
85
});
86
87
connection.onCodeLens(params => {
86
- connection.console.log('lastResult: ' + JSON.stringify(lastResult, null, 2));
87
- connection.console.log('params: ' + JSON.stringify(params, null, 2));
88
- return [];
88
+ connection.console.info(`Handling codelens for: ${params.textDocument.uri}`);
89
+ if (compiledFns.size === 0) {
90
+ return;
91
+ }
92
+ const lenses: Array<CodeLens> = [];
93
+ for (const compiled of compiledFns) {
94
+ if (compiled.fnLoc != null) {
95
+ const fnLoc = babelLocationToRange(compiled.fnLoc);
96
+ if (fnLoc === null) continue;
97
+ const lens = CodeLens.create(
98
+ getRangeFirstCharacter(fnLoc),
99
+ compiled.fnLoc,
100
+ );
101
+ if (lastResult?.code != null) {
102
+ lens.command = {
103
+ title: 'Optimized by React Compiler',
104
+ command: 'todo',
105
+ };
106
+ }
107
+ lenses.push(lens);
108
+ }
109
+ }
110
+ return lenses;
111
+});
112
+
113
+connection.onCodeLensResolve(lens => {
114
+ connection.console.info(`Resolving codelens for: ${JSON.stringify(lens)}`);
115
+ if (lastResult?.code != null) {
116
+ connection.console.log(lastResult.code);
117
+ }
118
+ return lens;
119
});
120
121
documents.listen(connection);