diff --git a/package.json b/package.json index 6acf73f2..3f1867cf 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,11 @@ "type": "boolean", "description": "Make larger cells collapsed by default", "default": false + }, + "vscode-db2i.resultsets.outputDecorations": { + "type": "boolean", + "description": "Show INOUT and OUT parameters inside the editor", + "default": true } } }, diff --git a/src/views/results/contributes.json b/src/views/results/contributes.json index de7ca328..47e66694 100644 --- a/src/views/results/contributes.json +++ b/src/views/results/contributes.json @@ -23,6 +23,11 @@ "type": "boolean", "description": "Make larger cells collapsed by default", "default": false + }, + "vscode-db2i.resultsets.outputDecorations": { + "type": "boolean", + "description": "Show INOUT and OUT parameters inside the editor", + "default": true } } }, diff --git a/src/views/results/editorUi.ts b/src/views/results/editorUi.ts new file mode 100644 index 00000000..ff60f9e8 --- /dev/null +++ b/src/views/results/editorUi.ts @@ -0,0 +1,75 @@ +import { ParsedStatementInfo } from "."; +import crypto from "crypto"; +import { ParameterResult } from "@ibm/mapepire-js"; +import { DecorationOptions, ThemeColor, window, Range, MarkdownString, DecorationRangeBehavior } from "vscode"; +import Configuration from "../../configuration"; + +let priorStatements: { [uniqueHash: string]: ParsedStatementInfo } = {}; + +const outputParameters = window.createTextEditorDecorationType({ + after: { + color: new ThemeColor(`editorGhostText.foreground`), + fontStyle: `italic`, + margin: `0 0 0 1em` + }, + rangeBehavior: DecorationRangeBehavior.ClosedClosed +}); + +export function registerRunStatement(stmt: ParsedStatementInfo) { + const uniqueUiId = crypto.randomBytes(16).toString("hex"); + priorStatements[uniqueUiId] = stmt; + return uniqueUiId; +} + +export function statementDone(uniqueId: string, options: { paramsOut?: ParameterResult[] } = {}) { + const existingStatement = priorStatements[uniqueId]; + const activeEditor = window.activeTextEditor; + + const shortValue = (v: any, short = true) => { + if (typeof v === "string") { + return short && v.length > 10 ? `${v.substring(0, 10)}...` : v; + } + return v || `-`; + }; + + if (existingStatement) { + // Huge assumption here the statement is in the active editor + + // TODO: feature flag + if (Configuration.get(`resultsets.outputDecorations`)) { + if (activeEditor) { + const document = activeEditor.document; + const startPosition = document.positionAt(existingStatement.group.range.start); + const endPosition = document.positionAt(existingStatement.group.range.end + 1); + + if (options.paramsOut && options.paramsOut.length > 0) { + const markdownString = new MarkdownString(); + options.paramsOut.forEach((p, i) => { + markdownString.appendMarkdown(`**Parameter ${i + 1}${p.name ? ` - ${p.name}` : ``}**:\n\n\`\`\`\n${p.value !== undefined ? p.value : `-`}\n\`\`\``); + + if (i !== options.paramsOut.length - 1) { + markdownString.appendMarkdown(`\n\n---\n\n`); + } + }); + + const shouldBeShort = options.paramsOut.length > 1; + const values = `=> ` + options.paramsOut.map((p) => shortValue(p.value, shouldBeShort)).join(", "); + + const decoration: DecorationOptions = { + range: new Range(startPosition, endPosition), + hoverMessage: markdownString, + renderOptions: { + after: { + contentText: values, + } + } + }; + + activeEditor.setDecorations(outputParameters, [decoration]); + } + } + } + + delete priorStatements[uniqueId]; + } +} \ No newline at end of file diff --git a/src/views/results/html.ts b/src/views/results/html.ts index cff91c53..f739be15 100644 --- a/src/views/results/html.ts +++ b/src/views/results/html.ts @@ -295,7 +295,7 @@ document.getElementById('resultset').onclick = function(e){ }; `; -export function generateScroller(basicSelect: string, parameters: SqlParameter[] = [], isCL: boolean = false, withCancel: boolean = false, updatable?: UpdatableInfo): string { +export function generateScroller(uiId: string, basicSelect: string, parameters: SqlParameter[] = [], isCL: boolean = false, withCancel: boolean = false, updatable?: UpdatableInfo): string { const withCollapsed = Configuration.get('collapsedResultSet'); return /*html*/` @@ -417,6 +417,7 @@ export function generateScroller(basicSelect: string, parameters: SqlParameter[] function fetchNextPage() { isFetching = true; vscode.postMessage({ + uiId: ${JSON.stringify(uiId)}, query: basicSelect, parameters: ${JSON.stringify(parameters)}, isCL: ${isCL}, diff --git a/src/views/results/index.ts b/src/views/results/index.ts index 053adeeb..a762845b 100644 --- a/src/views/results/index.ts +++ b/src/views/results/index.ts @@ -1,9 +1,9 @@ import * as vscode from "vscode"; +import crypto from "crypto"; import { SnippetString, ViewColumn, TreeView, window } from "vscode" import * as csv from "csv/sync"; - import { JobManager } from "../../config"; import Document from "../../language/sql/document"; import { ObjectRef, ParsedEmbeddedStatement, StatementGroup, StatementType } from "../../language/sql/types"; @@ -21,6 +21,7 @@ import { queryResultToRpgDs } from "./codegen"; import Configuration from "../../configuration"; import { getSqlDocument } from "../../language/providers/logic/parse"; import { getLiteralsFromStatement, getPriorBindableStatement } from "./binding"; +import { registerRunStatement } from "./editorUi"; export type StatementQualifier = "statement" | "bind" | "update" | "explain" | "onlyexplain" | "json" | "csv" | "cl" | "sql" | "rpg"; @@ -57,7 +58,7 @@ let doveResultsView = new DoveResultsView(); let doveResultsTreeView: TreeView = doveResultsView.getTreeView(); let doveNodeView = new DoveNodeView(); let doveNodeTreeView: TreeView = doveNodeView.getTreeView(); -let doveTreeDecorationProvider = new DoveTreeDecorationProvider(); // Self-registers as a tree decoration providor +let doveTreeDecorationProvider = new DoveTreeDecorationProvider(); // Self-registers as a tree decoration provider export function initialise(context: vscode.ExtensionContext) { setCancelButtonVisibility(false); @@ -378,6 +379,7 @@ async function runHandler(options?: StatementInfo) { updatableTable = refs[0]; } + const uiId = registerRunStatement(statementDetail); const basicSelect = statementDetail.content.split(eol).filter(line => !line.trimStart().startsWith(`--`)).join(eol); chosenView.setScrolling({ // Never errors @@ -385,6 +387,7 @@ async function runHandler(options?: StatementInfo) { withCancel: inWindow, ref: updatableTable, parameters, + uiId }) } diff --git a/src/views/results/resultSetPanelProvider.ts b/src/views/results/resultSetPanelProvider.ts index 85384abc..b8b548df 100644 --- a/src/views/results/resultSetPanelProvider.ts +++ b/src/views/results/resultSetPanelProvider.ts @@ -10,10 +10,13 @@ import { ObjectRef } from "../../language/sql/types"; import Table from "../../database/table"; import Statement from "../../database/statement"; import { TableColumn } from "../../types"; +import { statementDone } from "./editorUi"; +import { QueryResult } from "@ibm/mapepire-js"; export type SqlParameter = string|number; export interface ScrollerOptions { + uiId?: string; basicSelect: string; parameters?: SqlParameter[]; isCL?: boolean; @@ -106,7 +109,7 @@ export class ResultSetPanelProvider implements WebviewViewProvider { if (this.currentQuery.getState() !== "RUN_DONE") { setCancelButtonVisibility(true); - let queryResults = undefined; + let queryResults: QueryResult = undefined; let startTime = 0; let endTime = 0; let executionTime: number|undefined; @@ -118,7 +121,11 @@ export class ResultSetPanelProvider implements WebviewViewProvider { startTime = performance.now(); queryResults = await this.currentQuery.execute(); endTime = performance.now(); - executionTime = (endTime - startTime) + executionTime = (endTime - startTime); + + if (message.uiId) { + statementDone(message.uiId, {paramsOut: queryResults.output_parms}); + } } const jobId = this.currentQuery.getHostJob().id; @@ -285,7 +292,7 @@ export class ResultSetPanelProvider implements WebviewViewProvider { } } - this._view.webview.html = html.generateScroller(options.basicSelect, options.parameters, options.isCL, options.withCancel, updatable); + this._view.webview.html = html.generateScroller(options.uiId, options.basicSelect, options.parameters, options.isCL, options.withCancel, updatable); this._view.webview.postMessage({ command: `fetch`,