Skip to content

Commit 556315e

Browse files
authored
Merge pull request #14 from fe-dudu/fe-dudu/analysis-oxc-migration
feat: rebuild the webview graph stack for the new analysis model
2 parents 0f1aae9 + ecf6e60 commit 556315e

40 files changed

Lines changed: 292 additions & 984 deletions

src/extension/commands/scan.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { buildGraph } from '../../core/graph/buildGraph';
66
import type { GraphRoot } from '../../core/graph/graphBuilder';
77
import { getLayoutConfig, persistScanScope } from '../../core/workspace/config';
88
import { scopeToLabel } from '../../core/workspace/scope';
9-
import type { AnalysisResult, GraphData, ScanScope, ScannedFile, WebviewPayload } from '../../shared/types';
9+
import type { AnalysisResult, GraphData, ScanScope, ScannedFile, WebviewPayload } from '../../shared/contracts';
1010
import type { RqvActivityViewProvider } from '../views/activityView';
1111
import { GraphPanel } from '../views/graphPanel';
1212
import { getWorkspaceFolders, scopeForWorkspace } from '../workspace/folders';
@@ -25,9 +25,36 @@ interface ScanAndPublishOptions {
2525
}
2626

2727
function mergeAnalysis(results: AnalysisResult[]): AnalysisResult {
28+
const seenRecords = new Set<string>();
2829
return results.reduce<AnalysisResult>(
2930
(acc, current) => {
30-
acc.records.push(...current.records);
31+
for (const record of current.records) {
32+
const dedupeKey = [
33+
record.file,
34+
record.loc.line,
35+
record.loc.column,
36+
record.relation,
37+
record.operation,
38+
record.queryKey.id,
39+
record.queryKey.display,
40+
record.queryKey.matchMode,
41+
record.queryKey.resolution,
42+
record.queryKey.source,
43+
record.resolution,
44+
record.clientScopeId ?? '',
45+
record.executionScopeId ?? '',
46+
record.suiteScopeId ?? '',
47+
record.declaresDirectly === true ? '1' : '0',
48+
].join('|');
49+
50+
if (seenRecords.has(dedupeKey)) {
51+
continue;
52+
}
53+
54+
seenRecords.add(dedupeKey);
55+
acc.records.push(record);
56+
}
57+
3158
acc.scannedFiles.push(...current.scannedFiles);
3259
acc.parseErrors.push(...current.parseErrors);
3360
acc.filesScanned += current.filesScanned;
@@ -74,6 +101,7 @@ function buildScannedFiles(
74101
): ScannedFile[] {
75102
const multiRoot = targets.length > 1;
76103
const scannedFiles: ScannedFile[] = [];
104+
const seenPaths = new Set<string>();
77105

78106
analyses.forEach((analysis, index) => {
79107
const target = targets[index];
@@ -83,6 +111,11 @@ function buildScannedFiles(
83111

84112
const workspaceName = target.workspace.name;
85113
analysis.scannedFiles.forEach((absolutePath) => {
114+
if (seenPaths.has(absolutePath)) {
115+
return;
116+
}
117+
118+
seenPaths.add(absolutePath);
86119
const relativePath = normalizeRelativePath(target.workspace.uri.fsPath, absolutePath);
87120
const depth = computeDepth(relativePath);
88121
scannedFiles.push({
@@ -151,7 +184,7 @@ export async function scanAndPublish({
151184
await persistScanScope(scope);
152185
}
153186

154-
const panel = GraphPanel.createOrShow(context.extensionUri);
187+
const panel = GraphPanel.createOrShow(context.extensionUri, getLayoutConfig());
155188

156189
const result = await vscode.window.withProgress(
157190
{

src/extension/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import * as vscode from 'vscode';
22

33
import { revealInCode } from './commands/reveal';
44
import { RqvActivityViewProvider } from './views/activityView';
5-
import { GraphPanel, getDefaultPayload } from './views/graphPanel';
5+
import { GraphPanel } from './views/graphPanel';
66
import { getDefaultScopeWorkspace, getWorkspaceFolders } from './workspace/folders';
77
import { getLayoutConfig, getScanScopeConfig } from '../core/workspace/config';
88
import { promptScope } from '../core/workspace/scope';
9-
import type { WebviewPayload } from '../shared/types';
9+
import type { WebviewPayload } from '../shared/contracts';
1010

1111
let latestPayload: WebviewPayload | undefined;
1212
let activityViewProvider: RqvActivityViewProvider | undefined;
@@ -49,12 +49,13 @@ export function activate(context: vscode.ExtensionContext): void {
4949
});
5050

5151
const openPanel = vscode.commands.registerCommand('rqv.openGraphPanel', () => {
52-
const panel = GraphPanel.createOrShow(context.extensionUri);
53-
const payload = latestPayload ?? getDefaultPayload(getLayoutConfig());
52+
const panel = GraphPanel.createOrShow(context.extensionUri, getLayoutConfig());
5453
if (latestPayload) {
5554
activityViewProvider?.updateFromPayload(latestPayload);
5655
}
57-
panel.update(payload);
56+
if (latestPayload) {
57+
panel.update(latestPayload);
58+
}
5859
});
5960

6061
const scanNow = vscode.commands.registerCommand('rqv.scanNow', async () => {

src/extension/views/activityView.ts

Lines changed: 8 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import * as vscode from 'vscode';
22

3-
import type { WebviewPayload } from '../../shared/types';
3+
import type { WebviewPayload } from '../../shared/contracts';
4+
import {
5+
inferProjectFromPath,
6+
makeProjectRelativePath,
7+
normalizePathSegments,
8+
parseProjectScope,
9+
} from '../../shared/path';
410

511
interface LastScanSummary {
612
parseErrors: number;
@@ -62,81 +68,6 @@ interface RelatedFileNode {
6268
}>;
6369
}
6470

65-
function normalizePathSegments(input: string): string[] {
66-
return input.split('/').filter(Boolean);
67-
}
68-
69-
function stripWorkspacePrefix(filePath: string, workspace: string): string {
70-
if (!workspace) {
71-
return filePath;
72-
}
73-
74-
const prefix = `${workspace}/`;
75-
if (!filePath.startsWith(prefix)) {
76-
return filePath;
77-
}
78-
79-
return filePath.slice(prefix.length);
80-
}
81-
82-
function parseProjectScope(metricScope: unknown): { root: string; project: string } | null {
83-
if (typeof metricScope !== 'string') {
84-
return null;
85-
}
86-
87-
const colonIndex = metricScope.indexOf(':');
88-
if (colonIndex < 0) {
89-
const normalized = metricScope.trim();
90-
if (!normalized) {
91-
return null;
92-
}
93-
94-
return { root: '', project: normalized };
95-
}
96-
97-
const root = metricScope.slice(0, colonIndex).trim();
98-
const suffix = metricScope.slice(colonIndex + 1).trim();
99-
if (!suffix || suffix === '.' || suffix === '*') {
100-
return { root, project: root || 'workspace' };
101-
}
102-
103-
return { root, project: suffix };
104-
}
105-
106-
function inferProjectFromPath(filePath: string, workspace: string): string {
107-
const scopedPath = stripWorkspacePrefix(filePath, workspace);
108-
const segments = normalizePathSegments(scopedPath);
109-
if (segments.length >= 2) {
110-
return `${segments[0]}/${segments[1]}`;
111-
}
112-
if (segments.length === 1) {
113-
return segments[0];
114-
}
115-
116-
return workspace || 'workspace';
117-
}
118-
119-
function projectRelativePath(filePath: string, workspace: string, project: string): string {
120-
const scopedPath = stripWorkspacePrefix(filePath, workspace);
121-
const pathSegments = normalizePathSegments(scopedPath);
122-
const projectSegments = normalizePathSegments(project);
123-
124-
if (pathSegments.length === 0) {
125-
return filePath;
126-
}
127-
128-
const matchesPrefix =
129-
projectSegments.length > 0 &&
130-
projectSegments.every((segment, index) => pathSegments[index] && pathSegments[index] === segment);
131-
132-
if (!matchesPrefix) {
133-
return scopedPath;
134-
}
135-
136-
const remainder = pathSegments.slice(projectSegments.length).join('/');
137-
return remainder || pathSegments[pathSegments.length - 1] || scopedPath;
138-
}
139-
14071
function buildRelatedFilesTree(
14172
files: Array<{
14273
labelPath: string;
@@ -277,7 +208,7 @@ function createRelatedFilesRoot(payload: WebviewPayload): ActivityNode {
277208
const workspace = workspaceByPath.get(node.label) ?? '';
278209
const parsedScope = parseProjectScope(node.metrics?.projectScope);
279210
const baseProject = parsedScope?.project ?? inferProjectFromPath(node.label, workspace);
280-
return projectRelativePath(node.label, workspace, baseProject);
211+
return makeProjectRelativePath(node.label, workspace, baseProject);
281212
})(),
282213
absolutePath: node.file,
283214
}))

src/extension/views/graphPanel.ts

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
import * as vscode from 'vscode';
22

3-
import type { LayoutConfig, WebviewPayload } from '../../shared/types';
3+
import type { LayoutConfig, WebviewPayload } from '../../shared/contracts';
4+
5+
function createDefaultPayload(layout: LayoutConfig): WebviewPayload {
6+
return {
7+
graph: {
8+
nodes: [],
9+
edges: [],
10+
summary: {
11+
files: 0,
12+
actions: 0,
13+
queryKeys: 0,
14+
parseErrors: 0,
15+
},
16+
parseErrors: [],
17+
},
18+
scannedFiles: [],
19+
scopeLabel: 'No scan has run yet',
20+
layout,
21+
};
22+
}
423

524
function nonce(): string {
625
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
@@ -14,7 +33,7 @@ function nonce(): string {
1433
export class GraphPanel {
1534
private static current: GraphPanel | undefined;
1635

17-
static createOrShow(extensionUri: vscode.Uri): GraphPanel {
36+
static createOrShow(extensionUri: vscode.Uri, layout: LayoutConfig): GraphPanel {
1837
if (GraphPanel.current) {
1938
GraphPanel.current.panel.reveal(vscode.ViewColumn.One);
2039
return GraphPanel.current;
@@ -26,7 +45,7 @@ export class GraphPanel {
2645
localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'dist')],
2746
});
2847

29-
GraphPanel.current = new GraphPanel(panel, extensionUri);
48+
GraphPanel.current = new GraphPanel(panel, extensionUri, layout);
3049
return GraphPanel.current;
3150
}
3251

@@ -40,9 +59,10 @@ export class GraphPanel {
4059
private latestPayload?: WebviewPayload;
4160
private isReady = false;
4261

43-
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
62+
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, layout: LayoutConfig) {
4463
this.panel = panel;
4564
this.extensionUri = extensionUri;
65+
this.latestPayload = createDefaultPayload(layout);
4666
this.panel.webview.html = this.renderHtml();
4767
this.themeChangeListener = vscode.window.onDidChangeActiveColorTheme(() => {
4868
this.postTheme();
@@ -137,22 +157,3 @@ export class GraphPanel {
137157
</html>`;
138158
}
139159
}
140-
141-
export function getDefaultPayload(layout: LayoutConfig): WebviewPayload {
142-
return {
143-
graph: {
144-
nodes: [],
145-
edges: [],
146-
summary: {
147-
files: 0,
148-
actions: 0,
149-
queryKeys: 0,
150-
parseErrors: 0,
151-
},
152-
parseErrors: [],
153-
},
154-
scannedFiles: [],
155-
scopeLabel: 'No scan has run yet',
156-
layout,
157-
};
158-
}

src/extension/workspace/folders.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as path from 'node:path';
22
import * as vscode from 'vscode';
33

4-
import type { ScanScope } from '../../shared/types';
4+
import type { ScanScope } from '../../shared/contracts';
55

66
export function getWorkspaceFolders(): vscode.WorkspaceFolder[] {
77
return [...(vscode.workspace.workspaceFolders ?? [])];

src/webview/App.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
11
import { ReactFlowProvider } from '@xyflow/react';
22
import { useCallback, useState } from 'react';
33

4-
import type { WebviewPayload } from './types/model';
54
import { GraphCanvas } from './components/GraphCanvas';
6-
import { defaultPayload } from './utils/constants';
75
import { useHostThemeSync } from './utils/hostTheme';
6+
import type { WebviewPayload } from '../shared/contracts';
7+
8+
function createDefaultPayload(): WebviewPayload {
9+
return {
10+
graph: {
11+
nodes: [],
12+
edges: [],
13+
summary: {
14+
files: 0,
15+
actions: 0,
16+
queryKeys: 0,
17+
parseErrors: 0,
18+
},
19+
parseErrors: [],
20+
},
21+
scannedFiles: [],
22+
scopeLabel: 'No scan has run yet',
23+
layout: {
24+
direction: 'LR',
25+
engine: 'dagre',
26+
verticalSpacing: 30,
27+
horizontalSpacing: 500,
28+
},
29+
};
30+
}
831

932
export default function App() {
10-
const [payload, setPayload] = useState<WebviewPayload>(defaultPayload);
33+
const [payload, setPayload] = useState<WebviewPayload>(createDefaultPayload());
1134
const updatePayload = useCallback((nextPayload: WebviewPayload) => {
1235
setPayload(nextPayload);
1336
}, []);

0 commit comments

Comments
 (0)