diff --git a/CHANGELOG.md b/CHANGELOG.md index f6cf1b3..cb0b30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [0.0.16] + +- Added custom command `custom.listOpenedFiles` to get the list of all files currently opened +- Added custom command `custom.currentEditorContent` to get the content of the current (in-focus) editor as a string (if any) +- Added custom command `custom.registerEventHandler` so that an external HTTP server can handle events from VSCode API + ## [0.0.15] - Added custom error message when registering an external formatter via `custom.registerExternalFormatter`. diff --git a/README.md b/README.md index c64859b..5bcde4d 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,10 @@ As the extension progresses, I plan to add more _special_ commands (i.e. command - `custom.listInstalledExtensions`: get the list of installed extension IDs - `custom.getExtensionInfo`: get details of an installed extension by passing the extension ID - `custom.registerExternalFormatter`: registers an external formatter via a HTTP endpoint. The HTTP endpoint will receive a JSON body with the following properties`{"file": "", "snippet": "", "language": ""}` and it should return in the body the formatted code snippet (or the original if the code can't be formatted). - +- `custom.listOpenedFiles`: gets the list of all files currently opened (`string[]`) +- `custom.currentEditorContent`: to get the content of the current (in-focus) editor as a string (`string` or `null`) +- `custom.registerEventHandler`: so that an external HTTP server can handle events from VSCode API. Events supported right now are: + - `vscode.window.onDidChangeActiveTextEditor`: notifies the full path of the opened file whenever the active text editor changes ## To implement in the near future: - Add the ability to set a breakpoint at the specified file/line combination diff --git a/package.json b/package.json index 0f20717..e1a0212 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "This extension allows you to remotely control Visual Studio Code via a REST endpoint, taking automation to the next level.", "publisher": "dpar39", "license": "MIT", - "version": "0.0.15", + "version": "0.0.16", "engines": { "vscode": "^1.55.0" }, diff --git a/src/services/eventHandler.ts b/src/services/eventHandler.ts new file mode 100644 index 0000000..b609898 --- /dev/null +++ b/src/services/eventHandler.ts @@ -0,0 +1,80 @@ +import * as vscode from "vscode"; +import * as http from "http"; +import * as https from "https"; + +function sendEvent( + url: URL, + eventName: string, + eventData: any, + httpMethod: string = "POST", + onErrorMessage = "", +): Promise { + const payload = JSON.stringify({ name: eventName, data: eventData || null }); + const options = { + hostname: url.hostname, + port: url.port, + path: url.pathname, + method: httpMethod, + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention + "Content-Type": "application/json", + // eslint-disable-next-line @typescript-eslint/naming-convention + "Content-Length": Buffer.byteLength(payload), + }, + }; + + return new Promise((accept, reject) => { + const httpModule = url.protocol.startsWith("https") ? https : http; + const req = httpModule.request(options, (res) => { + let data = ""; + ``; + res.on("data", (chunk) => { + data += chunk; + }); + res.on("end", () => { + accept(); + }); + res.on("error", (err) => { + reject(err); + vscode.window.showErrorMessage(`Error notifying of event ${eventName}: ${err}`, "OK"); + }); + }); + req.on("error", (err) => { + req.destroy(); + vscode.window.showErrorMessage(`${onErrorMessage} - ${err}`, "OK"); + reject(err); + }); + req.write(payload); + req.end(); + }); +} + +let eventHandlerRegistrations: vscode.Disposable[] = []; +export async function registerEventHandler( + eventHandlerEndpoint: string, + eventTypes: string[], + httpMethod: string, + onErrorMessage: string, +) { + httpMethod = httpMethod || "POST"; + for (let ehr of eventHandlerRegistrations) { + ehr.dispose(); + } + eventHandlerRegistrations = []; + const url = new URL(eventHandlerEndpoint); + if (eventTypes.includes("vscode.window.onDidChangeActiveTextEditor")) { + eventHandlerRegistrations.push( + vscode.window.onDidChangeActiveTextEditor((editor) => { + sendEvent( + url, + "vscode.window.onDidChangeActiveTextEditor", + editor?.document.uri.fsPath, + httpMethod, + onErrorMessage, + ); + }), + ); + } + if (eventTypes.includes("foo")) { + } +} diff --git a/src/services/requestProcessor.ts b/src/services/requestProcessor.ts index d235615..7b4b84d 100644 --- a/src/services/requestProcessor.ts +++ b/src/services/requestProcessor.ts @@ -3,6 +3,7 @@ import * as path from "path"; import { quickPick } from "./quickPick"; import { registerExternalFormatter } from "./formatter"; +import { registerEventHandler } from "./eventHandler"; function createObject(arg: any): any { if (typeof arg === "object" && arg.hasOwnProperty("__type__")) { @@ -145,6 +146,18 @@ export async function processRemoteControlRequest(command: string, args: any[]): return await registerExternalFormatter(args[0], args[1], args[2], args[3]); } + if (command === "custom.registerEventHandler") { + return await registerEventHandler(args[0], args[1], args[2], args[3]); + } + + if (command === "custom.listOpenedFiles") { + return vscode.window.visibleTextEditors.map((editor) => editor.document.uri.fsPath); + } + + if (command === "custom.currentFileContent") { + return vscode.window.activeTextEditor?.document.getText() || null; + } + // try to run an arbitrary command with the arguments provided as is return await vscode.commands.executeCommand(command, ...args); } diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 9a929f1..9b287b2 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -1,10 +1,58 @@ import * as assert from "assert"; import { EXTENSION_ID } from "../../extension"; import * as fs from "fs"; +import { IncomingMessage, Server, ServerResponse, createServer } from "http"; // You can import and use all API from the 'vscode' module // as well as import your extension to test it import { makeRequest } from "./sendPostRequest"; +import { AddressInfo } from "net"; + +class MockHttpServer { + private _server: Server; + + constructor( + onRequestCallback: (data: any | undefined) => void, + onErrorCallback?: (err: Error) => void, + ) { + this._server = createServer((req: IncomingMessage, res: ServerResponse) => { + let body: Buffer[] = []; + req.on("data", (chunk) => { + body.push(chunk); + }); + req.on("end", () => { + if (body.length > 0) { + onRequestCallback(JSON.parse(Buffer.concat(body).toString())); + } else { + onErrorCallback?.(new Error("No data received")); + } + // eslint-disable-next-line @typescript-eslint/naming-convention + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({})); + }); + + req.on("error", (err) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + res.writeHead(500, { "Content-Type": "text/plain" }); + res.end("Server error."); + onErrorCallback?.(err); + }); + }); + } + + start(): Promise { + return new Promise((resolve, reject) => { + this._server.listen(0, () => { + const port = (this._server.address() as AddressInfo)!.port as number; + resolve(`http://localhost:${port}`); + }); + this._server.on("error", (err) => { + console.error(`Error starting mock HTTP server: ${err}`); + reject(err); + }); + }); + } +} suite("Extension Test Suite", () => { suiteSetup(async () => {}); @@ -43,9 +91,7 @@ suite("Extension Test Suite", () => { test("test can open document and get its content", async () => { const xx = await makeRequest("custom.goToFileLineCharacter", ["demo.py:17:28"]); - const content: string = (await makeRequest("custom.eval", [ - "vscode.window.activeTextEditor?.document.getText()", - ])) as string; + const content: string = (await makeRequest("custom.currentFileContent")) as string; const workspaceFolders = (await makeRequest("custom.workspaceFolders")) as any[]; const workspaceAbsPath = workspaceFolders[0].uri.slice("file://".length); const expectedContent = fs.readFileSync(workspaceAbsPath + "/demo.py", { @@ -53,4 +99,31 @@ suite("Extension Test Suite", () => { }); assert(content === expectedContent); }); + + test("get all opened files", async () => { + const xx = await makeRequest("custom.goToFileLineCharacter", ["demo.py:17:28"]); + const openedFiles: string[] = (await makeRequest("custom.listOpenedFiles")) as string[]; + assert(openedFiles.length > 0); + assert(openedFiles.some((file) => file.endsWith("workspace1/demo.py"))); + }); + + test("can get vscode.window.onDidChangeActiveTextEditor events", (done) => { + const server = new MockHttpServer((data) => { + console.log("Received event data:", data); + assert(data.name === "vscode.window.onDidChangeActiveTextEditor"); + if (data.data !== null) { + assert(data.data.endsWith("samples.http")); + done(); + } + }); + server.start().then(async (serverUrl) => { + await makeRequest("custom.registerEventHandler", [ + serverUrl, + ["vscode.window.onDidChangeActiveTextEditor"], + "POST", + "Failed to register event handler", + ]); + await makeRequest("custom.goToFileLineCharacter", ["samples.http:1:1"]); + }); + }); });