} Clingo result or null if file not found
+ */
+async function runClingoWasmForFileWithProgress(vscode, progress, filePath, models = undefined, options = undefined) {
+ progress.report({
+ increment: 0,
+ message: "Starting Clingo...",
+ });
+
+ let fileContent;
+
+ // Check if the file exists
+ if (!fs.existsSync(filePath)) {
+ vscode.window.showErrorMessage(`File not found: ${filePath}`);
+ return null;
+ }
+
+ fileContent = await fs.promises.readFile(filePath, "utf8");
+
+ const additionalFiles = options?.filter((arg) => arg && !arg.startsWith("--")).map((filePath) => filePath.replace(/"/g, ""));
+
+ if (additionalFiles?.length) {
+ for (const additionalFilePath of additionalFiles) {
+ if (!fs.existsSync(additionalFilePath)) {
+ vscode.window.showErrorMessage(`File not found: ${filePath}`);
+ return null;
+ }
+ const additionalContent = await fs.promises.readFile(additionalFilePath, "utf8");
+ if (additionalContent.trim()) {
+ fileContent += `\n${additionalContent}`;
+ }
+ }
+ }
+
+ // Filter options for Clingo
+ const clingoOptions = options?.filter((arg) => arg.startsWith("--"));
+
+ progress.report({
+ increment: 50,
+ message: "Running Clingo WASM...",
+ });
+
+ // Run Clingo WASM with timeout
+ const wasmResult = await clingo.run(fileContent, models, clingoOptions);
+
+ progress.report({
+ increment: 100,
+ message: "Clingo finished successfully!",
+ });
+
+ // Validate the result
+ if (["ERROR", "UNSATISFIABLE", "UNKNOWN"].includes(wasmResult.Result)) {
+ if ("Error" in wasmResult) {
+ vscode.window.showErrorMessage(`Clingo WASM Error: ${wasmResult.Error}`);
+ } else {
+ vscode.window.showErrorMessage(`Clingo WASM Error: Unknown error`);
+ }
+ return null;
+ } else {
+ return wasmResult;
+ }
+}
+
+module.exports = { runClingoWasmForFileWithProgress };
diff --git a/src/testFiles/ex01.lp b/src/testFiles/ex01.lp
new file mode 100644
index 0000000..3c858b6
--- /dev/null
+++ b/src/testFiles/ex01.lp
@@ -0,0 +1,61 @@
+initial(2,3,3).
+initial(3,3,8).
+initial(4,3,1).
+initial(5,3,4).
+initial(6,3,6).
+initial(7,3,5).
+initial(8,3,9).
+initial(9,3,2).
+initial(1,4,5).
+initial(2,4,7).
+initial(3,4,2).
+initial(4,4,8).
+initial(5,4,1).
+initial(6,4,3).
+initial(7,4,9).
+initial(8,4,6).
+initial(9,4,4).
+initial(1,5,4).
+initial(2,5,1).
+initial(3,5,3).
+initial(4,5,9).
+initial(5,5,6).
+initial(6,5,2).
+initial(7,5,7).
+initial(8,5,5).
+initial(9,5,8).
+initial(1,6,9).
+initial(2,6,8).
+initial(3,6,6).
+initial(4,6,5).
+initial(5,6,7).
+initial(6,6,4).
+initial(7,6,2).
+initial(8,6,1).
+initial(9,6,3).
+initial(1,7,2).
+initial(2,7,5).
+initial(3,7,1).
+initial(4,7,6).
+initial(5,7,3).
+initial(6,7,8).
+initial(7,7,4).
+initial(8,7,7).
+initial(9,7,9).
+initial(1,8,8).
+initial(2,8,6).
+initial(3,8,4).
+initial(4,8,2).
+initial(5,8,9).
+initial(6,8,7).
+initial(7,8,1).
+initial(8,8,3).
+initial(9,8,5).
+initial(1,9,3).
+initial(2,9,9).
+initial(3,9,7).
+initial(4,9,4).
+initial(5,9,5).
+initial(6,9,1).
+initial(7,9,8).
+initial(8,9,2).
\ No newline at end of file
diff --git a/src/testFiles/exSyntaxError.lp b/src/testFiles/exSyntaxError.lp
new file mode 100644
index 0000000..a0e6cf4
--- /dev/null
+++ b/src/testFiles/exSyntaxError.lp
@@ -0,0 +1,14 @@
+subgrid_size(3).
+
+v(1..N*N) :- subgrid_size(N). % v: all possible values for x,y,z
+
+1{initial(X,Y,Z) : v(Z)}1 :- v(X), v(Y). % get all stable sudoku models with exactly 1 value z for each possible field x,y
+
+:- initial(A,B,C), initial(X,Y,Z), A=X, B!=Y, C=Z. % exclude all answers with duplicate z values per row X
+:- initial(A,B,C), initial(X,Y,Z), A!=X, B=Y, C=Z. % exclude all answers with duplicate z values per column Y
+:- initial(A,B,C), initial(X,Y,Z), B!=Y, A!=X, C=Z. % exclude all answers with duplicate z per subgrid N,
+((A-1)/N)*N + (B-1)/N == ((X-1)/N)*N+(Y-1)/N, subgrid_size(N). % (compare subgrids)
+
+sudoku(X,Y,Z) :- initial(X,Y,Z). % show output correctly as sudoku
+
+#show sudoku/3.
\ No newline at end of file
diff --git a/src/testFiles/sudoku.lp b/src/testFiles/sudoku.lp
new file mode 100644
index 0000000..4936f4c
--- /dev/null
+++ b/src/testFiles/sudoku.lp
@@ -0,0 +1,14 @@
+subgrid_size(3).
+
+v(1..N*N) :- subgrid_size(N). % v: all possible values for x,y,z
+
+1{initial(X,Y,Z) : v(Z)}1 :- v(X), v(Y). % get all stable sudoku models with exactly 1 value z for each possible field x,y
+
+:- initial(A,B,C), initial(X,Y,Z), A=X, B!=Y, C=Z. % exclude all answers with duplicate z values per row X
+:- initial(A,B,C), initial(X,Y,Z), A!=X, B=Y, C=Z. % exclude all answers with duplicate z values per column Y
+:- initial(A,B,C), initial(X,Y,Z), B!=Y, A!=X, C=Z, % exclude all answers with duplicate z per subgrid N,
+((A-1)/N)*N + (B-1)/N == ((X-1)/N)*N+(Y-1)/N, subgrid_size(N). % (compare subgrids)
+
+sudoku(X,Y,Z) :- initial(X,Y,Z). % show output correctly as sudoku
+
+#show sudoku/3.
\ No newline at end of file
diff --git a/src/testFiles/sudokuComplete.lp b/src/testFiles/sudokuComplete.lp
new file mode 100644
index 0000000..82134fa
--- /dev/null
+++ b/src/testFiles/sudokuComplete.lp
@@ -0,0 +1,76 @@
+subgrid_size(3).
+
+v(1..N*N) :- subgrid_size(N). % v: all possible values for x,y,z
+
+1{initial(X,Y,Z) : v(Z)}1 :- v(X), v(Y). % get all stable sudoku models with exactly 1 value z for each possible field x,y
+
+:- initial(A,B,C), initial(X,Y,Z), A=X, B!=Y, C=Z. % exclude all answers with duplicate z values per row X
+:- initial(A,B,C), initial(X,Y,Z), A!=X, B=Y, C=Z. % exclude all answers with duplicate z values per column Y
+:- initial(A,B,C), initial(X,Y,Z), B!=Y, A!=X, C=Z, % exclude all answers with duplicate z per subgrid N,
+((A-1)/N)*N + (B-1)/N == ((X-1)/N)*N+(Y-1)/N, subgrid_size(N). % (compare subgrids)
+
+sudoku(X,Y,Z) :- initial(X,Y,Z). % show output correctly as sudoku
+
+#show sudoku/3.
+
+initial(2,3,3).
+initial(3,3,8).
+initial(4,3,1).
+initial(5,3,4).
+initial(6,3,6).
+initial(7,3,5).
+initial(8,3,9).
+initial(9,3,2).
+initial(1,4,5).
+initial(2,4,7).
+initial(3,4,2).
+initial(4,4,8).
+initial(5,4,1).
+initial(6,4,3).
+initial(7,4,9).
+initial(8,4,6).
+initial(9,4,4).
+initial(1,5,4).
+initial(2,5,1).
+initial(3,5,3).
+initial(4,5,9).
+initial(5,5,6).
+initial(6,5,2).
+initial(7,5,7).
+initial(8,5,5).
+initial(9,5,8).
+initial(1,6,9).
+initial(2,6,8).
+initial(3,6,6).
+initial(4,6,5).
+initial(5,6,7).
+initial(6,6,4).
+initial(7,6,2).
+initial(8,6,1).
+initial(9,6,3).
+initial(1,7,2).
+initial(2,7,5).
+initial(3,7,1).
+initial(4,7,6).
+initial(5,7,3).
+initial(6,7,8).
+initial(7,7,4).
+initial(8,7,7).
+initial(9,7,9).
+initial(1,8,8).
+initial(2,8,6).
+initial(3,8,4).
+initial(4,8,2).
+initial(5,8,9).
+initial(6,8,7).
+initial(7,8,1).
+initial(8,8,3).
+initial(9,8,5).
+initial(1,9,3).
+initial(2,9,9).
+initial(3,9,7).
+initial(4,9,4).
+initial(5,9,5).
+initial(6,9,1).
+initial(7,9,8).
+initial(8,9,2).
\ No newline at end of file
diff --git a/src/unitTests/uni.test.js b/src/unitTests/uni.test.js
new file mode 100644
index 0000000..609bec8
--- /dev/null
+++ b/src/unitTests/uni.test.js
@@ -0,0 +1,67 @@
+// @ts-nocheck
+const fs = require("fs");
+const { runClingoWasmForFileWithProgress } = require("../runClingoWasmForFileWithProgress.js");
+
+describe("runClingoWasmForFile", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("should read a single ASP file and run Clingo WASM successfully", async () => {
+ const filePath = "src/testFiles/sudokuComplete.lp";
+
+ const result = await runClingoWasmForFileWithProgress({ window: jest.fn() }, { report: jest.fn() }, filePath, 0);
+
+ expect(result.Models.Number).toEqual(8);
+ });
+
+ it.each([
+ ["src/testFiles/sudoku.lp", "src/testFiles/ex01.lp"],
+ ["src/testFiles/ex01.lp", "src/testFiles/sudoku.lp"],
+ ])("should read a two ASP file and run Clingo WASM successfully", async (first, second) => {
+ const result = await runClingoWasmForFileWithProgress({ window: jest.fn() }, { report: jest.fn() }, first, 0, [second]);
+
+ expect(result.Models.Number).toEqual(8);
+ });
+
+ it("should handle errors when reading the file", async () => {
+ const filePath = "src/testFiles/nonExistentFile.lp";
+
+ const result = await runClingoWasmForFileWithProgress(
+ { window: { showErrorMessage: jest.fn() } },
+ { report: jest.fn() },
+ filePath,
+ 0
+ );
+
+ expect(result).toBe(null);
+ });
+
+ it("should handle errors when reading additional files", async () => {
+ const filePath = "src/testFiles/sudokuComplete.lp";
+ const additionalFilePath = "src/testFiles/nonExistentFile.lp";
+
+ const result = await runClingoWasmForFileWithProgress(
+ { window: { showErrorMessage: jest.fn() } },
+ { report: jest.fn() },
+ filePath,
+ 0,
+ [additionalFilePath]
+ );
+
+ expect(result).toBe(null);
+ });
+
+ it("should handle errors when running Clingo WASM", async () => {
+ const filePath = "src/testFiles/sudokuComplete.lp";
+
+ const result = await runClingoWasmForFileWithProgress(
+ { window: { showErrorMessage: jest.fn() } },
+ { report: jest.fn() },
+ "src/testFiles/exSyntaxError.lp",
+ 0
+ );
+
+ expect(result).toBe(null);
+ });
+});
diff --git a/src/webviewProvider.js b/src/webviewProvider.js
new file mode 100644
index 0000000..22dc2a0
--- /dev/null
+++ b/src/webviewProvider.js
@@ -0,0 +1,89 @@
+const vscode = require("vscode");
+const crypto = require("crypto");
+
+/**
+ * WebviewProvider class to manage the webview for the ASP extension.
+ */
+class WebviewProvider {
+ constructor(_extensionUri) {
+ this._extensionUri = _extensionUri;
+ }
+
+ get viewType() {
+ return "ASP.aspView";
+ }
+
+ resolveWebviewView(webviewView, _context, _token) {
+ this._view = webviewView;
+ webviewView.webview.options = {
+ // Allow scripts in the webview
+ enableScripts: true,
+ localResourceRoots: [this._extensionUri],
+ };
+ webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
+ webviewView.webview.onDidReceiveMessage((data) => {
+ switch (data.type) {
+ case "colorSelected": {
+ vscode.window.activeTextEditor?.insertSnippet(new vscode.SnippetString(`#${data.value}`));
+ break;
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns the HTML content for the webview.
+ * @param {*} webview Reference to the webview object
+ * @returns
+ */
+ _getHtmlForWebview(webview) {
+ // Get the local path to main script run in the webview, then convert it to a uri we can use in the webview.
+ const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "media", "main.js"));
+ // Do the same for the stylesheet.
+ const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "media", "vscode.css"));
+ const styleMainUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "media", "main.css"));
+ const clingoSolver = vscode.workspace.getConfiguration("aspLanguage").get("usePathClingo")
+ ? "your own version of Clingo from PATH"
+ : "the bundled WASM Clingo Solver";
+ // Use a nonce to only allow a specific script to be run.
+ const nonce = this._getNonce();
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+ ASP Output
+
+
+
+
+
+
+
+ `;
+ }
+
+ _getNonce() {
+ return crypto.randomBytes(32).toString("base64");
+ }
+}
+
+exports.WebviewProvider = WebviewProvider;