Skip to content

Commit 92e65ca

Browse files
authored
[forgive] Add basic codelens provider (facebook#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)
1 parent 403d4fb commit 92e65ca

File tree

4 files changed

+129
-62
lines changed

4 files changed

+129
-62
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -176,41 +176,48 @@ export type CompilationMode = z.infer<typeof CompilationModeSchema>;
176176
* babel or other unhandled exceptions).
177177
*/
178178
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+
};
214221

215222
export type Logger = {
216223
logEvent: (filename: string | null, event: LoggerEvent) => void;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import BabelPluginReactCompiler, {
1111
type PluginOptions,
1212
} from 'babel-plugin-react-compiler/src';
1313
import * as babelParser from 'prettier/plugins/babel.js';
14-
import * as estreeParser from 'prettier/plugins/estree';
14+
import estreeParser from 'prettier/plugins/estree';
1515
import * as typescriptParser from 'prettier/plugins/typescript';
1616
import * as prettier from 'prettier/standalone';
1717

18+
export let lastResult: BabelCore.BabelFileResult | null = null;
19+
1820
type CompileOptions = {
1921
text: string;
2022
file: string;
@@ -24,14 +26,17 @@ export async function compile({
2426
text,
2527
file,
2628
options,
27-
}: CompileOptions): Promise<BabelCore.BabelFileResult> {
29+
}: CompileOptions): Promise<BabelCore.BabelFileResult | null> {
2830
const ast = await parseAsync(text, {
2931
sourceFileName: file,
3032
parserOpts: {
3133
plugins: ['typescript', 'jsx'],
3234
},
3335
sourceType: 'module',
3436
});
37+
if (ast == null) {
38+
return null;
39+
}
3540
const plugins =
3641
options != null
3742
? [[BabelPluginReactCompiler, options]]
@@ -54,5 +59,8 @@ export async function compile({
5459
parser: 'babel-ts',
5560
plugins: [babelParser, estreeParser, typescriptParser],
5661
});
62+
if (result.code != null) {
63+
lastResult = result;
64+
}
5765
return result;
5866
}

compiler/packages/react-forgive/server/src/index.ts

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,23 @@
77

88
import {TextDocument} from 'vscode-languageserver-textdocument';
99
import {
10+
CodeLens,
1011
createConnection,
1112
type InitializeParams,
1213
type InitializeResult,
1314
ProposedFeatures,
1415
TextDocuments,
1516
TextDocumentSyncKind,
1617
} from 'vscode-languageserver/node';
17-
import {compile} from './compiler';
18+
import {compile, lastResult} from './compiler';
1819
import {type PluginOptions} from 'babel-plugin-react-compiler/src';
1920
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';
2127

2228
const SUPPORTED_LANGUAGE_IDS = new Set([
2329
'javascript',
@@ -30,11 +36,21 @@ const connection = createConnection(ProposedFeatures.all);
3036
const documents = new TextDocuments(TextDocument);
3137

3238
let compilerOptions: PluginOptions | null = null;
33-
let lastResult: BabelFileResult | null = null;
39+
let compiledFns: Set<CompileSuccessEvent> = new Set();
3440

3541
connection.onInitialize((_params: InitializeParams) => {
3642
// 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+
};
3854
const result: InitializeResult = {
3955
capabilities: {
4056
textDocumentSync: TextDocumentSyncKind.Full,
@@ -48,44 +64,58 @@ connection.onInitialized(() => {
4864
connection.console.log('initialized');
4965
});
5066

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-
6567
documents.onDidChangeContent(async event => {
68+
connection.console.info(`Changed: ${event.document.uri}`);
69+
compiledFns.clear();
6670
if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) {
6771
const text = event.document.getText();
68-
const result = await compile({
72+
await compile({
6973
text,
7074
file: event.document.uri,
7175
options: compilerOptions,
7276
});
73-
if (result.code != null) {
74-
lastResult = result;
75-
}
7677
}
7778
});
7879

7980
connection.onDidChangeWatchedFiles(change => {
81+
compiledFns.clear();
8082
connection.console.log(
8183
change.changes.map(c => `File changed: ${c.uri}`).join('\n'),
8284
);
8385
});
8486

8587
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;
89119
});
90120

91121
documents.listen(connection);

0 commit comments

Comments
 (0)