@samitouri / QOS-React / commits / 92e65ca68f

[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 ![Screenshot 2025-02-25 at 6 40 20 PM](https://github.com/user-attachments/assets/924586e0-f70a-45d1-b0e6-a89af9371c8d)

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);