Skip to content

Commit 052e34e

Browse files
authored
feat: add depth decorator (#1683)
1 parent e584246 commit 052e34e

File tree

9 files changed

+422
-27
lines changed

9 files changed

+422
-27
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,11 @@
275275
"default": [],
276276
"description": "Add additional params to `sqlfmt`"
277277
},
278+
"dbt.disableDepthsCalculation": {
279+
"type": "boolean",
280+
"description": "Disable DAG depth calculation and decoration",
281+
"default": false
282+
},
278283
"dbt.deferConfigPerProject": {
279284
"type": "object",
280285
"description": "Run subset of models without building their parent models",
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import {
2+
DecorationOptions,
3+
Disposable,
4+
Hover,
5+
HoverProvider,
6+
MarkdownString,
7+
Position,
8+
Range,
9+
TextDocument,
10+
TextEditor,
11+
TextEditorDecorationType,
12+
window,
13+
workspace,
14+
} from "vscode";
15+
import { DBTProjectContainer } from "../manifest/dbtProjectContainer";
16+
import { provideSingleton } from "../utils";
17+
import { getDepthColor } from "../utils";
18+
19+
@provideSingleton(DepthDecorationProvider)
20+
export class DepthDecorationProvider implements HoverProvider, Disposable {
21+
private disposables: Disposable[] = [];
22+
private readonly REF_PATTERN =
23+
/\{\{\s*ref\s*\(\s*['"]([^'"]+)['"]\s*\)\s*\}\}/;
24+
private readonly decorationType: TextEditorDecorationType;
25+
private projectToDepthMap: Map<string, Map<string, number>> = new Map();
26+
27+
constructor(private dbtProjectContainer: DBTProjectContainer) {
28+
this.decorationType = window.createTextEditorDecorationType({
29+
after: {
30+
margin: "0 0 0 5px",
31+
textDecoration: "none",
32+
},
33+
});
34+
35+
this.dbtProjectContainer.onManifestChanged((event) => {
36+
event.added?.forEach((added) => {
37+
this.projectToDepthMap.set(
38+
added.project.projectRoot.fsPath,
39+
added.modelDepthMap,
40+
);
41+
});
42+
event.removed?.forEach((removed) => {
43+
this.projectToDepthMap.delete(removed.projectRoot.fsPath);
44+
});
45+
46+
window.visibleTextEditors.forEach((editor) => {
47+
this.updateDecorations(editor);
48+
});
49+
});
50+
51+
this.disposables.push(
52+
window.onDidChangeActiveTextEditor((editor) => {
53+
if (editor) {
54+
this.updateDecorations(editor);
55+
}
56+
}),
57+
workspace.onDidChangeTextDocument((event) => {
58+
if (
59+
window.activeTextEditor &&
60+
event.document === window.activeTextEditor.document
61+
) {
62+
this.updateDecorations(window.activeTextEditor);
63+
}
64+
}),
65+
workspace.onDidChangeConfiguration((event) => {
66+
if (event.affectsConfiguration("dbt")) {
67+
window.visibleTextEditors.forEach((editor) => {
68+
this.updateDecorations(editor);
69+
});
70+
}
71+
}),
72+
);
73+
}
74+
75+
dispose() {
76+
this.decorationType.dispose();
77+
while (this.disposables.length) {
78+
const x = this.disposables.pop();
79+
if (x) {
80+
x.dispose();
81+
}
82+
}
83+
}
84+
85+
private updateDecorations(editor: TextEditor): void {
86+
const project = this.dbtProjectContainer.findDBTProject(
87+
editor.document.uri,
88+
);
89+
if (!project) {
90+
return;
91+
}
92+
93+
const projectRoot = project.projectRoot.fsPath;
94+
const depthMapForProject = this.projectToDepthMap.get(projectRoot);
95+
if (!depthMapForProject) {
96+
return;
97+
}
98+
99+
const text = editor.document.getText();
100+
const decorations: DecorationOptions[] = [];
101+
const pattern = new RegExp(this.REF_PATTERN, "g");
102+
let match;
103+
104+
while ((match = pattern.exec(text)) !== null) {
105+
const startPos = editor.document.positionAt(match.index);
106+
const endPos = editor.document.positionAt(match.index + match[0].length);
107+
const refRange = new Range(startPos, endPos);
108+
109+
const modelName = match[1];
110+
const depth = depthMapForProject.get(modelName);
111+
112+
if (depth !== undefined) {
113+
decorations.push(this.createDepthDecoration(refRange, depth));
114+
}
115+
}
116+
117+
editor.setDecorations(this.decorationType, decorations);
118+
}
119+
120+
private createDepthDecoration(
121+
refRange: Range,
122+
depth: number,
123+
): DecorationOptions {
124+
const color = getDepthColor(depth);
125+
const depthText = `(${depth})`;
126+
127+
return {
128+
range: refRange,
129+
renderOptions: {
130+
after: {
131+
contentText: depthText,
132+
backgroundColor: color,
133+
color: "white",
134+
fontWeight: "bold",
135+
margin: "0 0 0 5px",
136+
},
137+
},
138+
};
139+
}
140+
141+
public provideHover(
142+
document: TextDocument,
143+
position: Position,
144+
): Hover | undefined {
145+
const project = this.dbtProjectContainer.findDBTProject(document.uri);
146+
if (!project) {
147+
return;
148+
}
149+
150+
const projectRoot = project.projectRoot.fsPath;
151+
const depthMapForProject = this.projectToDepthMap.get(projectRoot);
152+
if (!depthMapForProject) {
153+
return;
154+
}
155+
156+
const text = document.getText();
157+
let matches: RegExpMatchArray[] = [];
158+
try {
159+
const pattern = new RegExp(this.REF_PATTERN, "g");
160+
matches = Array.from(text.matchAll(pattern));
161+
} catch (error) {
162+
console.error("Error matching ref pattern in provideHover:", error);
163+
return undefined;
164+
}
165+
166+
for (const match of matches) {
167+
if (match.index === undefined) {
168+
continue;
169+
}
170+
const startPos = document.positionAt(match.index);
171+
const endPos = document.positionAt(match.index + match[0].length);
172+
const refRange = new Range(startPos, endPos);
173+
174+
if (refRange.contains(position)) {
175+
const modelName = match[1];
176+
const depth = depthMapForProject.get(modelName);
177+
178+
if (depth !== undefined) {
179+
const color = getDepthColor(depth);
180+
const markdown = new MarkdownString(
181+
`**DAG Depth:** <span style="color:${color}">${depth}</span>\n\n` +
182+
`The longest path of models between a source and this model is ${depth} nodes long.`,
183+
);
184+
markdown.isTrusted = true;
185+
markdown.supportHtml = true;
186+
return new Hover(markdown, refRange);
187+
}
188+
}
189+
}
190+
191+
return undefined;
192+
}
193+
}

src/hover_provider/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { provideSingleton } from "../utils";
44
import { ModelHoverProvider } from "./modelHoverProvider";
55
import { SourceHoverProvider } from "./sourceHoverProvider";
66
import { MacroHoverProvider } from "./macroHoverProvider";
7+
import { DepthDecorationProvider } from "./depthDecorationProvider";
78

89
@provideSingleton(HoverProviders)
910
export class HoverProviders implements Disposable {
@@ -13,6 +14,7 @@ export class HoverProviders implements Disposable {
1314
private modelHoverProvider: ModelHoverProvider,
1415
private sourceHoverProvider: SourceHoverProvider,
1516
private macroHoverProvider: MacroHoverProvider,
17+
private depthDecorationProvider: DepthDecorationProvider,
1618
) {
1719
this.disposables.push(
1820
languages.registerHoverProvider(
@@ -32,6 +34,12 @@ export class HoverProviders implements Disposable {
3234
this.macroHoverProvider,
3335
),
3436
);
37+
this.disposables.push(
38+
languages.registerHoverProvider(
39+
DBTPowerUserExtension.DBT_SQL_SELECTOR,
40+
this.depthDecorationProvider,
41+
),
42+
);
3543
}
3644

3745
dispose() {

src/manifest/event/manifestCacheChangedEvent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface ManifestCacheProjectAddedEvent {
2121
testMetaMap: TestMetaMap;
2222
docMetaMap: DocMetaMap;
2323
exposureMetaMap: ExposureMetaMap;
24+
modelDepthMap: Map<string, number>;
2425
}
2526

2627
export interface ManifestCacheProjectRemovedEvent {

src/manifest/parsers/index.ts

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, readFileSync } from "fs";
1+
import { readFileSync } from "fs";
22
import { provide } from "inversify-binding-decorators";
33
import * as path from "path";
44
import { Uri } from "vscode";
@@ -15,6 +15,8 @@ import { TelemetryService } from "../../telemetry";
1515
import { ExposureParser } from "./exposureParser";
1616
import { MetricParser } from "./metricParser";
1717
import { ChildrenParentParser } from "./childrenParentParser";
18+
import { ModelDepthParser } from "./modelDepthParser";
19+
import { createFullPathForNode } from "./utils";
1820

1921
@provide(ManifestParser)
2022
export class ManifestParser {
@@ -33,6 +35,7 @@ export class ManifestParser {
3335
private docParser: DocParser,
3436
private terminal: DBTTerminal,
3537
private telemetry: TelemetryService,
38+
private modelDepthParser: ModelDepthParser,
3639
) {}
3740

3841
public async parseManifest(project: DBTProject) {
@@ -71,6 +74,7 @@ export class ManifestParser {
7174
},
7275
docMetaMap: new Map(),
7376
exposureMetaMap: new Map(),
77+
modelDepthMap: new Map(),
7478
},
7579
],
7680
};
@@ -130,6 +134,13 @@ export class ManifestParser {
130134
exposuresMetaMapPromise,
131135
]);
132136

137+
// Calculate model depths
138+
const modelDepthMap = this.modelDepthParser.createModelDepthsMap(
139+
nodes,
140+
parentMetaMap,
141+
childMetaMap,
142+
);
143+
133144
const graphMetaMap = this.graphParser.createGraphMetaMap(
134145
project,
135146
parentMetaMap,
@@ -181,6 +192,7 @@ export class ManifestParser {
181192
testMetaMap: testMetaMap,
182193
docMetaMap: docMetaMap,
183194
exposureMetaMap: exposureMetaMap,
195+
modelDepthMap,
184196
},
185197
],
186198
};
@@ -216,29 +228,4 @@ export class ManifestParser {
216228
}
217229
}
218230

219-
export const createFullPathForNode: (
220-
projectName: string,
221-
rootPath: string,
222-
packageName: string,
223-
packagePath: string,
224-
relativeFilePath: string,
225-
) => string | undefined = (
226-
projectName,
227-
rootPath,
228-
packageName,
229-
packagePath,
230-
relativeFilePath,
231-
) => {
232-
if (packageName !== projectName) {
233-
const rootPathWithPackage = path.join(
234-
packagePath,
235-
packageName,
236-
relativeFilePath,
237-
);
238-
if (existsSync(rootPathWithPackage)) {
239-
return rootPathWithPackage;
240-
}
241-
return undefined;
242-
}
243-
return path.join(rootPath, relativeFilePath);
244-
};
231+
export { createFullPathForNode } from "./utils";

0 commit comments

Comments
 (0)