diff --git a/src/domain.ts b/src/domain.ts index 3be04dc9f..d77dbb0ac 100755 --- a/src/domain.ts +++ b/src/domain.ts @@ -10,11 +10,15 @@ export type DocMetaMap = Map; export type NodeMetaType = NodeMetaData; export type SourceMetaType = SourceTable; -interface MacroMetaData { +export interface MacroMetaData { path: string | undefined; // in dbt cloud, packages are not downloaded locally line: number; character: number; uniqueId: string; + description?: string; + arguments?: { name: string; type: string; description: string }[]; + name: string; + depends_on: DependsOn; } interface MetricMetaData { @@ -34,6 +38,7 @@ export interface NodeMetaData { columns: { [columnName: string]: ColumnMetaData }; config: Config; resource_type: string; + depends_on: DependsOn; } export interface ColumnMetaData { diff --git a/src/hover_provider/index.ts b/src/hover_provider/index.ts old mode 100755 new mode 100644 index a55555597..af19fdab4 --- a/src/hover_provider/index.ts +++ b/src/hover_provider/index.ts @@ -3,6 +3,7 @@ import { DBTPowerUserExtension } from "../dbtPowerUserExtension"; import { provideSingleton } from "../utils"; import { ModelHoverProvider } from "./modelHoverProvider"; import { SourceHoverProvider } from "./sourceHoverProvider"; +import { MacroHoverProvider } from "./macroHoverProvider"; @provideSingleton(HoverProviders) export class HoverProviders implements Disposable { @@ -11,6 +12,7 @@ export class HoverProviders implements Disposable { constructor( private modelHoverProvider: ModelHoverProvider, private sourceHoverProvider: SourceHoverProvider, + private macroHoverProvider: MacroHoverProvider, ) { this.disposables.push( languages.registerHoverProvider( @@ -24,6 +26,12 @@ export class HoverProviders implements Disposable { this.sourceHoverProvider, ), ); + this.disposables.push( + languages.registerHoverProvider( + DBTPowerUserExtension.DBT_SQL_SELECTOR, + this.macroHoverProvider, + ), + ); } dispose() { diff --git a/src/hover_provider/macroHoverProvider.ts b/src/hover_provider/macroHoverProvider.ts new file mode 100644 index 000000000..bca33821b --- /dev/null +++ b/src/hover_provider/macroHoverProvider.ts @@ -0,0 +1,94 @@ +import { + CancellationToken, + HoverProvider, + Hover, + Position, + ProviderResult, + TextDocument, + Disposable, +} from "vscode"; +import { TelemetryService } from "../telemetry"; +import { generateMacroHoverMarkdown } from "./utils"; +import { DBTTerminal } from "../dbt_client/dbtTerminal"; +import { QueryManifestService } from "../services/queryManifestService"; +import { provideSingleton } from "../utils"; +import { + MacroMetaData, + MacroMetaMap, + NodeMetaData, + NodeMetaMap, + SourceMetaMap, +} from "../domain"; + +@provideSingleton(MacroHoverProvider) +export class MacroHoverProvider implements HoverProvider, Disposable { + private disposables: Disposable[] = []; + + constructor( + private telemetry: TelemetryService, + private dbtTerminal: DBTTerminal, + private queryManifestService: QueryManifestService, + ) {} + + dispose() { + while (this.disposables.length) { + const x = this.disposables.pop(); + if (x) { + x.dispose(); + } + } + } + + provideHover( + document: TextDocument, + position: Position, + token: CancellationToken, + ): ProviderResult { + const hoverText = document.getText( + document.getWordRangeAtPosition(position), + ); + + this.dbtTerminal.debug("MacroHoverProvider", `checking: ${hoverText}`); + const eventResult = this.queryManifestService.getEventByDocument( + document.uri, + ); + if (!eventResult) { + return; + } + const { macroMetaMap, nodeMetaMap } = eventResult; + const macroMeta = macroMetaMap.get(hoverText); + if (!macroMeta) { + return null; + } + + const referencedBy = this.getNodesReferencingMacro( + macroMeta.uniqueId, + macroMetaMap, + nodeMetaMap, + ); + const hoverContent = generateMacroHoverMarkdown( + macroMeta, + referencedBy, + eventResult, + ); + this.telemetry.sendTelemetryEvent("provideMacroHover"); + return new Hover(hoverContent); + } + + private getNodesReferencingMacro( + macroMetaName: string, + macroMetaMap: MacroMetaMap, + nodeMetaMap: NodeMetaMap, + ) { + const referencedBy: (MacroMetaData | NodeMetaData)[] = []; + const allNodes = [...macroMetaMap.values(), ...nodeMetaMap.values()]; + + allNodes.forEach((node) => { + if (node.depends_on.macros.includes(macroMetaName)) { + referencedBy.push(node); + } + }); + + return referencedBy; + } +} diff --git a/src/hover_provider/utils.ts b/src/hover_provider/utils.ts index 5a489533a..c48ca86bf 100644 --- a/src/hover_provider/utils.ts +++ b/src/hover_provider/utils.ts @@ -1,5 +1,11 @@ -import { MarkdownString } from "vscode"; -import { NodeMetaType, SourceMetaType } from "../domain"; +import { MarkdownString, Uri } from "vscode"; +import { + MacroMetaData, + NodeMetaData, + NodeMetaType, + SourceMetaType, +} from "../domain"; +import { ManifestCacheProjectAddedEvent } from "../manifest/event/manifestCacheChangedEvent"; export function generateHoverMarkdownString( node: NodeMetaType | SourceMetaType, @@ -14,11 +20,7 @@ export function generateHoverMarkdownString( if (node.description !== "") { content.appendMarkdown(`
${node.description}`); } - content.appendText("\n"); - content.appendText("\n"); - content.appendMarkdown("---"); - content.appendText("\n"); - content.appendText("\n"); + addSeparator(content); for (const colKey in node.columns) { const column = node.columns[colKey]; content.appendMarkdown( @@ -38,3 +40,84 @@ export function generateHoverMarkdownString( } return content; } + +export const generateMacroHoverMarkdown = ( + node: MacroMetaData, + referencedBy: (MacroMetaData | NodeMetaData)[], + event: ManifestCacheProjectAddedEvent, +) => { + const content = new MarkdownString(); + content.supportHtml = true; + content.isTrusted = true; + content.appendMarkdown( + `(Macro) ${node.name}`, + ); + if (node.description !== "") { + content.appendMarkdown(`
${node.description}`); + } + addSeparator(content); + node.arguments?.forEach((macroArg) => { + content.appendMarkdown( + `(argument) ${macroArg.name}  `, + ); + if (macroArg.type !== null) { + content.appendMarkdown( + `- ${macroArg.type.toLowerCase()}`, + ); + } + if (macroArg.description !== "") { + content.appendMarkdown( + `
${macroArg.description}`, + ); + } + content.appendMarkdown("
"); + }); + + if (referencedBy.length) { + addSeparator(content); + content.appendMarkdown( + `(Referenced by) ${referencedBy + .map((node) => buildLink(node)) + .join(", ")}`, + ); + content.appendMarkdown("
"); + } + + if (node.depends_on.macros?.length || node.depends_on.nodes?.length) { + const dependsOn = [ + ...(node.depends_on.macros?.map((m) => + [...event.macroMetaMap.values()].find((macro) => macro.uniqueId === m), + ) || []), + ...(node.depends_on.nodes?.map((m) => + [...event.nodeMetaMap.values()].find((macro) => macro.uniqueId === m), + ) || []), + ]; + addSeparator(content); + content.appendMarkdown( + `(Depends on) ${dependsOn + .map((node) => buildLink(node)) + .join(", ")}`, + ); + } + + return content; +}; + +const addSeparator = (content: MarkdownString) => { + content.appendText("\n"); + content.appendText("\n"); + content.appendMarkdown("---"); + content.appendText("\n"); + content.appendText("\n"); +}; + +const buildLink = (node: MacroMetaData | NodeMetaData | undefined) => { + if (!node) { + return; + } + if (!node.path) { + return node.name; + } + + return `[${node.name}](${Uri.file(node.path)} "${node.uniqueId}")`; +}; diff --git a/src/manifest/parsers/macroParser.ts b/src/manifest/parsers/macroParser.ts old mode 100755 new mode 100644 index c6b163454..1e3689f2e --- a/src/manifest/parsers/macroParser.ts +++ b/src/manifest/parsers/macroParser.ts @@ -35,7 +35,7 @@ export class MacroParser { } for (const key in macros) { const macro = macros[key]; - const { package_name, name, original_file_path } = macro; + const { package_name, name, original_file_path, depends_on } = macro; const packageName = package_name; const macroName = packageName === projectName ? name : `${packageName}.${name}`; @@ -66,6 +66,10 @@ export class MacroParser { line: index, character: currentLine.indexOf(name), uniqueId: key, + description: macro.description, + arguments: macro.arguments, + name, + depends_on, }); break; } diff --git a/src/manifest/parsers/nodeParser.ts b/src/manifest/parsers/nodeParser.ts index 9690df86a..47d34ad4d 100755 --- a/src/manifest/parsers/nodeParser.ts +++ b/src/manifest/parsers/nodeParser.ts @@ -49,6 +49,7 @@ export class NodeParser { patch_path, config, resource_type, + depends_on, } = nodesMap; const fullPath = createFullPathForNode( projectName, @@ -70,6 +71,7 @@ export class NodeParser { patch_path, config, resource_type, + depends_on, }); } this.terminal.debug(