Skip to content

Commit 42d297f

Browse files
potetojbrown215
andcommitted
[forgive] Hacky first pass at adding decorations for inferred deps
Draws basic decorations for inferred deps on hover. Co-authored-by: Jordan Brown <[email protected]>
1 parent 601e6c6 commit 42d297f

File tree

7 files changed

+151
-18
lines changed

7 files changed

+151
-18
lines changed

compiler/apps/playground/components/Editor/Input.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export default function Input({errors, language}: Props): JSX.Element {
3030
const store = useStore();
3131
const dispatchStore = useStoreDispatch();
3232

33+
useEffect(() => {
34+
console.log(monaco);
35+
console.log(errors);
36+
console.log(errors);
37+
});
38+
3339
// Set tab width to 2 spaces for the selected input file.
3440
useEffect(() => {
3541
if (!monaco) return;

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,8 @@ export type TimingEvent = {
222222
};
223223
export type AutoDepsDecorations = {
224224
kind: 'AutoDepsDecorations';
225-
useEffectCallExpr: t.SourceLocation | null;
226-
decorations: Array<t.SourceLocation | null>;
225+
useEffectCallExpr: t.SourceLocation;
226+
decorations: Array<t.SourceLocation>;
227227
};
228228

229229
export type Logger = {

compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts

+22-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import * as t from '@babel/types';
19
import {CompilerError, SourceLocation} from '..';
210
import {
311
ArrayExpression,
@@ -212,14 +220,20 @@ export function inferEffectDependencies(fn: HIRFunction): void {
212220
}
213221

214222
// For LSP autodeps feature.
215-
fn.env.logger?.logEvent(fn.env.filename, {
216-
kind: 'AutoDepsDecorations',
217-
useEffectCallExpr:
218-
typeof value.loc !== 'symbol' ? value.loc : null,
219-
decorations: collectDepUsages(usedDeps, fnExpr.value).map(loc =>
220-
typeof loc !== 'symbol' ? loc : null,
221-
),
222-
});
223+
const decorations: Array<t.SourceLocation> = [];
224+
for (const loc of collectDepUsages(usedDeps, fnExpr.value)) {
225+
if (typeof loc === 'symbol') {
226+
continue;
227+
}
228+
decorations.push(loc);
229+
}
230+
if (typeof value.loc !== 'symbol') {
231+
fn.env.logger?.logEvent(fn.env.filename, {
232+
kind: 'AutoDepsDecorations',
233+
useEffectCallExpr: value.loc,
234+
decorations,
235+
});
236+
}
223237

224238
newInstructions.push({
225239
id: makeInstructionId(0),

compiler/packages/react-forgive/client/src/extension.ts

+35-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import * as path from 'path';
2-
import {ExtensionContext, window as Window} from 'vscode';
2+
import * as vscode from 'vscode';
33

44
import {
55
LanguageClient,
66
LanguageClientOptions,
7+
Position,
78
ServerOptions,
89
TransportKind,
910
} from 'vscode-languageclient/node';
1011

1112
let client: LanguageClient;
1213

13-
export function activate(context: ExtensionContext) {
14+
export function activate(context: vscode.ExtensionContext) {
1415
const serverModule = context.asAbsolutePath(path.join('dist', 'server.js'));
16+
const documentSelector = [
17+
{scheme: 'file', language: 'javascriptreact'},
18+
{scheme: 'file', language: 'typescriptreact'},
19+
];
1520

1621
// If the extension is launched in debug mode then the debug server options are used
1722
// Otherwise the run options are used
@@ -27,10 +32,7 @@ export function activate(context: ExtensionContext) {
2732
};
2833

2934
const clientOptions: LanguageClientOptions = {
30-
documentSelector: [
31-
{scheme: 'file', language: 'javascriptreact'},
32-
{scheme: 'file', language: 'typescriptreact'},
33-
],
35+
documentSelector,
3436
progressOnInitialization: true,
3537
};
3638

@@ -43,12 +45,38 @@ export function activate(context: ExtensionContext) {
4345
clientOptions,
4446
);
4547
} catch {
46-
Window.showErrorMessage(
48+
vscode.window.showErrorMessage(
4749
`React Analyzer couldn't be started. See the output channel for details.`,
4850
);
4951
return;
5052
}
5153

54+
vscode.languages.registerHoverProvider(documentSelector, {
55+
provideHover(_document, position, _token) {
56+
client
57+
.sendRequest('react/autodepsdecorations', position)
58+
.then((decorations: Array<[Position, Position]>) => {
59+
for (const [start, end] of decorations) {
60+
const range = new vscode.Range(
61+
new vscode.Position(start.line, start.character),
62+
new vscode.Position(end.line, end.character),
63+
);
64+
const vscodeDecoration =
65+
vscode.window.createTextEditorDecorationType({
66+
backgroundColor: 'red',
67+
});
68+
vscode.window.activeTextEditor?.setDecorations(vscodeDecoration, [
69+
{
70+
range,
71+
hoverMessage: 'hehe',
72+
},
73+
]);
74+
}
75+
});
76+
return null;
77+
},
78+
});
79+
5280
client.registerProposedFeatures();
5381
client.start();
5482
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {AutoDepsDecorations} from 'babel-plugin-react-compiler/src/Entrypoint';
2+
import {Position} from 'vscode-languageserver-textdocument';
3+
import {sourceLocationToRange} from '../utils/lsp-adapter';
4+
5+
export type Range = [Position, Position];
6+
export type AutoDepsDecorationsLSPEvent = {
7+
useEffectCallExpr: Range;
8+
decorations: Array<Range>;
9+
};
10+
11+
export function mapCompilerEventToLSPEvent(
12+
event: AutoDepsDecorations,
13+
): AutoDepsDecorationsLSPEvent {
14+
return {
15+
useEffectCallExpr: sourceLocationToRange(event.useEffectCallExpr),
16+
decorations: event.decorations.map(sourceLocationToRange),
17+
};
18+
}

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

+57-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {TextDocument} from 'vscode-languageserver-textdocument';
8+
import {Position, TextDocument} from 'vscode-languageserver-textdocument';
99
import {
1010
CodeLens,
1111
createConnection,
@@ -24,6 +24,10 @@ import {
2424
LoggerEvent,
2525
} from 'babel-plugin-react-compiler/src/Entrypoint/Options';
2626
import {babelLocationToRange, getRangeFirstCharacter} from './compiler/compat';
27+
import {
28+
AutoDepsDecorationsLSPEvent,
29+
mapCompilerEventToLSPEvent,
30+
} from './custom-requests/autodepsdecorations';
2731

2832
const SUPPORTED_LANGUAGE_IDS = new Set([
2933
'javascript',
@@ -37,17 +41,48 @@ const documents = new TextDocuments(TextDocument);
3741

3842
let compilerOptions: PluginOptions | null = null;
3943
let compiledFns: Set<CompileSuccessEvent> = new Set();
44+
let autoDepsDecorations: Array<AutoDepsDecorationsLSPEvent> = [];
4045

4146
connection.onInitialize((_params: InitializeParams) => {
4247
// TODO(@poteto) get config fr
4348
compilerOptions = resolveReactConfig('.') ?? defaultOptions;
4449
compilerOptions = {
4550
...compilerOptions,
51+
environment: {
52+
...compilerOptions.environment,
53+
inferEffectDependencies: [
54+
{
55+
function: {
56+
importSpecifierName: 'useEffect',
57+
source: 'react',
58+
},
59+
numRequiredArgs: 1,
60+
},
61+
{
62+
function: {
63+
importSpecifierName: 'useSpecialEffect',
64+
source: 'shared-runtime',
65+
},
66+
numRequiredArgs: 2,
67+
},
68+
{
69+
function: {
70+
importSpecifierName: 'default',
71+
source: 'useEffectWrapper',
72+
},
73+
numRequiredArgs: 1,
74+
},
75+
],
76+
},
4677
logger: {
4778
logEvent(_filename: string | null, event: LoggerEvent) {
79+
connection.console.info(`Received event: ${event.kind}`);
4880
if (event.kind === 'CompileSuccess') {
4981
compiledFns.add(event);
5082
}
83+
if (event.kind === 'AutoDepsDecorations') {
84+
autoDepsDecorations.push(mapCompilerEventToLSPEvent(event));
85+
}
5186
},
5287
},
5388
};
@@ -67,6 +102,7 @@ connection.onInitialized(() => {
67102
documents.onDidChangeContent(async event => {
68103
connection.console.info(`Changed: ${event.document.uri}`);
69104
compiledFns.clear();
105+
autoDepsDecorations = [];
70106
if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) {
71107
const text = event.document.getText();
72108
await compile({
@@ -79,6 +115,7 @@ documents.onDidChangeContent(async event => {
79115

80116
connection.onDidChangeWatchedFiles(change => {
81117
compiledFns.clear();
118+
autoDepsDecorations = [];
82119
connection.console.log(
83120
change.changes.map(c => `File changed: ${c.uri}`).join('\n'),
84121
);
@@ -118,6 +155,25 @@ connection.onCodeLensResolve(lens => {
118155
return lens;
119156
});
120157

158+
connection.onRequest('react/autodepsdecorations', (position: Position) => {
159+
connection.console.log('Client hovering on: ' + JSON.stringify(position));
160+
connection.console.log(JSON.stringify(autoDepsDecorations, null, 2));
161+
162+
for (const dec of autoDepsDecorations) {
163+
// TODO: extract to helper
164+
if (
165+
position.line >= dec.useEffectCallExpr[0].line &&
166+
position.line <= dec.useEffectCallExpr[1].line
167+
) {
168+
connection.console.log(
169+
'found decoration: ' + JSON.stringify(dec.decorations),
170+
);
171+
return dec.decorations;
172+
}
173+
}
174+
return null;
175+
});
176+
121177
documents.listen(connection);
122178
connection.listen();
123179
connection.console.info(`React Analyzer running in node ${process.version}`);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as t from '@babel/types';
2+
import {Position} from 'vscode-languageserver/node';
3+
4+
export function sourceLocationToRange(
5+
loc: t.SourceLocation,
6+
): [Position, Position] {
7+
return [
8+
{line: loc.start.line - 1, character: loc.start.column},
9+
{line: loc.end.line - 1, character: loc.end.column},
10+
];
11+
}

0 commit comments

Comments
 (0)