diff --git a/src/context/asyncContext.ts b/src/context/asyncContext.ts index 05e70df..2b0cbe0 100644 --- a/src/context/asyncContext.ts +++ b/src/context/asyncContext.ts @@ -321,6 +321,21 @@ export default class AsyncContext extends Context { } } + /** + * Cancels a query by reconnecting the SQL client. + * + * This action will release the private client + * and create a new pool of clients. + * + */ + async cancelQuery() { + try { + await this.clients.sql?.reconnect(); + } catch (err) { + throw new ExtensionError(Errors.cancelQueryError, err); + } + } + /** * Adds a new profile. * diff --git a/src/providers/query.ts b/src/providers/query.ts index bf42a76..d1bc88b 100644 --- a/src/providers/query.ts +++ b/src/providers/query.ts @@ -33,8 +33,17 @@ export const buildRunSQLCommand = (context: AsyncContext) => { // the results from one query can overlap the results // from another. We only want to display the last results. const id = randomUUID(); + let queryCanceled = false; try { - resultsProvider.setQueryId(id); + const cancelHandler = () => { + console.log("[RunSQLCommand]", "Canceling query."); + queryCanceled = true; + context.cancelQuery(); + } + resultsProvider.setQuery( + id, + cancelHandler + ); try { const statements = await context.parseSql(query); console.log("[RunSQLCommand]", "Running statements: ", statements); @@ -78,13 +87,20 @@ export const buildRunSQLCommand = (context: AsyncContext) => { sql: statement.sql }); - resultsProvider.setResults(id, - undefined, - { - message: error.toString(), - position: error.position, + if (!queryCanceled) { + resultsProvider.setResults(id, + undefined, + { + message: error.toString(), + position: error.position, + query, + }); + } else { + resultsProvider.setResults(id, undefined, { + message: "Query canceled.", query, - }); + }); + } // Break for-loop. break; diff --git a/src/providers/results.ts b/src/providers/results.ts index 88187c7..cb12ad8 100644 --- a/src/providers/results.ts +++ b/src/providers/results.ts @@ -21,6 +21,10 @@ export default class ResultsProvider implements vscode.WebviewViewProvider { // It is used to display the results and not overlap them from the results of a laggy query. private lastQueryId: string | undefined; + // Handles query cancelation. After running this function + // the context will reconnect the SQL client. + private cancelHandler: undefined | (() => void); + // The provider can be invoked from `materialize.run`. // When this happens, the inner rendering script will not be ready // to listen changes. This variable holds the pending data to render @@ -34,14 +38,19 @@ export default class ResultsProvider implements vscode.WebviewViewProvider { constructor(private readonly _extensionUri: vscode.Uri) { this._extensionUri = _extensionUri; this.isScriptReady = false; + this.cancelHandler = undefined; } /** * Cleans the results and sets a latest query id. * @param id */ - public setQueryId(id: string) { + public setQuery( + id: string, + cancelHandler: () => void, + ) { this.lastQueryId = id; + this.cancelHandler = cancelHandler; if (this._view) { console.log("[ResultsProvider]", "New query."); @@ -113,6 +122,12 @@ export default class ResultsProvider implements vscode.WebviewViewProvider { console.error("[ResultsProvider]", error); break; } + case "cancelQuery": { + if (this.cancelHandler) { + this.cancelHandler(); + } + break; + } case "ready": { console.log("[ResultsProvider]", "The script is now ready."); this.isScriptReady = true; diff --git a/src/providers/scripts/results.ts b/src/providers/scripts/results.ts index 3d011f8..5b1ffed 100644 --- a/src/providers/scripts/results.ts +++ b/src/providers/scripts/results.ts @@ -78,6 +78,7 @@ import { timerElement.style.paddingTop = "0.5rem"; document.body.appendChild(timerElement); } + // Reset and show the timer content timerElement.textContent = 'Time elapsed:'; timerElement.style.display = 'block'; @@ -131,6 +132,18 @@ import { }, 500); } }, 103); + + const cancelQuery = () => { + vscode.postMessage({ type: "cancelQuery" }); + }; + const cancelElement = document.createElement('vscode-button'); + cancelElement.innerHTML = "Cancel"; + cancelElement.style.float = "right"; + cancelElement.id = "cancelButton"; + cancelElement.setAttribute("appearance", "secondary"); + cancelElement.onclick = cancelQuery; + container.appendChild(cancelElement); + break; } @@ -139,6 +152,10 @@ import { if (progressRing) { progressRing.style.display = "none"; } + const cancelButton = document.getElementById("cancelButton"); + if (cancelButton) { + cancelButton.style.display = "none"; + } const { data: results } = message; console.log("[Results.js]", "New message - Results: ", results); diff --git a/src/utilities/error.ts b/src/utilities/error.ts index d875513..9a3a58d 100644 --- a/src/utilities/error.ts +++ b/src/utilities/error.ts @@ -157,5 +157,9 @@ export enum Errors { /** * Raises when a fetch failes after a minute. */ - fetchTimeoutError = "Failed to fetch after a minute." + fetchTimeoutError = "Failed to fetch after a minute.", + /** + * Raises when trying to cancel a query. + */ + cancelQueryError = "Failed to cancel the query." }