diff --git a/common.ts b/common.ts index 4bc3980..e4bfa03 100644 --- a/common.ts +++ b/common.ts @@ -39,6 +39,10 @@ export interface Formatter { * Gets the plugin info. */ getPluginInfo(): PluginInfo; + /** + * Gets what files the plugin matches based on the current configuration. + */ + getFileMatchingInfo(): FileMatchingInfo; /** * Gets the license text of the plugin. */ @@ -72,8 +76,13 @@ export interface PluginInfo { name: string; version: string; configKey: string; - fileExtensions: string[]; - fileNames: string[]; helpUrl: string; configSchemaUrl: string; + updateUrl?: string; +} + +/** Information about how the current config matches files. */ +export interface FileMatchingInfo { + fileExtensions: string[] | undefined; + fileNames: string[]; } diff --git a/deno.json b/deno.json index 3b2e95f..ac99dbd 100644 --- a/deno.json +++ b/deno.json @@ -1,4 +1,6 @@ { + "name": "@dprint/formatter", + "version": "0.0.0", "tasks": { "build:npm": "deno run -A ./scripts/build_npm.ts" }, @@ -8,5 +10,6 @@ "imports": { "@deno/dnt": "jsr:@deno/dnt@^0.41.2", "@std/assert": "jsr:@std/assert@^0.226.0" - } + }, + "exports": "./mod.ts" } diff --git a/deno.lock b/deno.lock index 451b69c..11ef59e 100644 --- a/deno.lock +++ b/deno.lock @@ -4,6 +4,7 @@ "specifiers": { "jsr:@deno/cache-dir@^0.8.0": "jsr:@deno/cache-dir@0.8.0", "jsr:@deno/dnt@^0.41.2": "jsr:@deno/dnt@0.41.2", + "jsr:@deno/graph@^0.69.7": "jsr:@deno/graph@0.69.10", "jsr:@std/assert@^0.218.2": "jsr:@std/assert@0.218.2", "jsr:@std/assert@^0.226.0": "jsr:@std/assert@0.226.0", "jsr:@std/bytes@^0.218.2": "jsr:@std/bytes@0.218.2", @@ -19,6 +20,7 @@ "@deno/cache-dir@0.8.0": { "integrity": "e87e80a404958f6350d903e6238b72afb92468378b0b32111f7a1e4916ac7fe7", "dependencies": [ + "jsr:@deno/graph@^0.69.7", "jsr:@std/fs@^0.218.2", "jsr:@std/io@^0.218.2" ] @@ -34,6 +36,9 @@ "npm:code-block-writer@^13.0.1" ] }, + "@deno/graph@0.69.10": { + "integrity": "38fe22ac5686f6ece5daeec5a4df65c6314d7d32adcc33f77917a13cfaffa26f" + }, "@std/assert@0.218.2": { "integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf" }, diff --git a/mod.ts b/mod.ts index 555c14f..de29504 100644 --- a/mod.ts +++ b/mod.ts @@ -4,6 +4,7 @@ import * as v4 from "./v4.ts"; export type { ConfigurationDiagnostic, + FileMatchingInfo, FormatRequest, Formatter, GlobalConfiguration, diff --git a/mod_test.ts b/mod_test.ts index fb59942..5377daa 100644 --- a/mod_test.ts +++ b/mod_test.ts @@ -1,5 +1,5 @@ -import { assertEquals } from "@std/assert"; -import { createFromBuffer, createStreaming, Formatter, GlobalConfiguration } from "./mod.ts"; +import { assert, assertEquals } from "@std/assert"; +import { createFromBuffer, createStreaming, type Formatter, type GlobalConfiguration } from "./mod.ts"; Deno.test("it should create streaming", async () => { const formatter = await createStreaming( @@ -68,11 +68,13 @@ function runGeneralJsonFormatterTests(formatter: Formatter) { name: "dprint-plugin-json", version: "0.13.0", configKey: "json", - fileExtensions: ["json", "jsonc"], - fileNames: [], helpUrl: "https://dprint.dev/plugins/json", configSchemaUrl: "https://plugins.dprint.dev/schemas/json-0.13.0.json", }); + assertEquals(formatter.getFileMatchingInfo(), { + fileExtensions: ["json", "jsonc"], + fileNames: [], + }); assertEquals(formatter.getResolvedConfig(), { "array.preferSingleLine": true, "commentLine.forceSpaceAfterSlashes": true, @@ -101,3 +103,98 @@ function runGeneralJsonFormatterTests(formatter: Formatter) { `{\n "test": [1, 2]\n}\n`, ); } + +Deno.test("should support v4", () => { + // this plugin file's code is here: https://github.com/dprint/dprint/blob/main/crates/test-plugin/src/lib.rs + const formatter = createFromBuffer( + Deno.readFileSync(new URL("./test/test_plugin_v4.wasm", import.meta.url)), + ); + + formatter.setConfig({}, { "ending": "formatted_wasm" }); + { + const result = formatter.formatText({ + filePath: "test.txt", + fileText: `test`, + }); + assertEquals(result, `test_formatted_wasm`); + } + formatter.setConfig({}, { "ending": "other" }); + { + const result = formatter.formatText({ + filePath: "test.txt", + fileText: `test`, + }); + assertEquals(result, `test_other`); + } + // these will trigger fd_write + { + const result = formatter.formatText({ + filePath: "test.txt", + fileText: `stderr: hi on stderr`, + }); + assertEquals(result, `stderr: hi on stderr_other`); + } + { + const result = formatter.formatText({ + filePath: "test.txt", + fileText: `stdout: hi on stdout`, + }); + assertEquals(result, `stdout: hi on stdout_other`); + } + + assertEquals(formatter.getPluginInfo(), { + name: "test-plugin", + version: "0.2.0", + configKey: "test-plugin", + helpUrl: "https://dprint.dev/plugins/test", + configSchemaUrl: "https://plugins.dprint.dev/test/schema.json", + updateUrl: "https://plugins.dprint.dev/dprint/test-plugin/latest.json", + }); + assertEquals(formatter.getFileMatchingInfo(), { + fileExtensions: ["txt"], + fileNames: [], + }); + + // some special config in this plugin + formatter.setConfig({}, { "file_extensions": ["asdf"], "file_names": ["some_name"] }); + assertEquals(formatter.getFileMatchingInfo(), { + fileExtensions: ["asdf"], + fileNames: ["some_name"], + }); + + assertEquals(formatter.getLicenseText().substring(0, 15), "The MIT License"); + + // test out host formatting + { + const result = formatter.formatText({ + filePath: "file.txt", + fileText: "plugin: text", + }, (request) => { + return request.fileText + "_host"; + }); + assertEquals(result, "plugin: text_host_formatted"); + } + // test host formatting with plugin config + { + const result = formatter.formatText({ + filePath: "file.txt", + fileText: "plugin-config: text", + }, (request) => { + assertEquals(request.overrideConfig, { "ending": "custom_config" }); + return request.fileText + "_host"; + }); + assertEquals(result, "plugin-config: text_host_formatted"); + } + // now try range formatting with host formatting + { + const result = formatter.formatText({ + filePath: "file.txt", + fileText: "plugin-range: text", + bytesRange: [0, 5], + }, (request) => { + assertEquals(request.bytesRange, [0, 5]); + return request.fileText + "_host"; + }); + assertEquals(result, "plugin-range: text_host_formatted"); + } +}); diff --git a/test/test_plugin_v4.wasm b/test/test_plugin_v4.wasm new file mode 100644 index 0000000..4b04d24 Binary files /dev/null and b/test/test_plugin_v4.wasm differ diff --git a/v3.ts b/v3.ts index 5e91ac0..6e959d6 100644 --- a/v3.ts +++ b/v3.ts @@ -134,12 +134,25 @@ export function createFromInstance( const length = get_resolved_config(); return JSON.parse(receiveString(wasmInstance, length)); }, + getFileMatchingInfo() { + const length = get_plugin_info(); + const pluginInfo = JSON.parse(receiveString(wasmInstance, length)) as PluginInfo; + return { + // deno-lint-ignore no-explicit-any + fileExtensions: (pluginInfo as any).fileExtensions ?? [], + // deno-lint-ignore no-explicit-any + fileNames: (pluginInfo as any).fileNames ?? [], + }; + }, getPluginInfo() { const length = get_plugin_info(); const pluginInfo = JSON.parse( receiveString(wasmInstance, length), ) as PluginInfo; - pluginInfo.fileNames = pluginInfo.fileNames ?? []; + // deno-lint-ignore no-explicit-any + delete (pluginInfo as any).fileNames; + // deno-lint-ignore no-explicit-any + delete (pluginInfo as any).fileExtensions; return pluginInfo; }, getLicenseText() { diff --git a/v4.ts b/v4.ts index 74d30ab..31e10d7 100644 --- a/v4.ts +++ b/v4.ts @@ -1,4 +1,5 @@ import type { FormatRequest, Formatter, GlobalConfiguration, Host, PluginInfo } from "./common.ts"; +import { FileMatchingInfo } from "./mod.ts"; const decoder = new TextDecoder(); const encoder = new TextEncoder(); @@ -22,16 +23,44 @@ export function createHost(): Host { createImportObject(): WebAssembly.Imports { let sharedBuffer = new Uint8Array(0); - const resetSharedBuffer = (length: number) => { - sharedBuffer = new Uint8Array(length); - }; - return { + env: { + "fd_write": ( + fd: number, + iovsPtr: number, + iovsLen: number, + nwrittenPtr: number, + ) => { + let totalWritten = 0; + // deno-lint-ignore no-explicit-any + const wasmMemoryBuffer = (instance.exports.memory as any).buffer; + const dataView = new DataView(wasmMemoryBuffer); + + for (let i = 0; i < iovsLen; i++) { + const iovsOffset = iovsPtr + i * 8; + const iovecBufPtr = dataView.getUint32(iovsOffset, true); + const iovecBufLen = dataView.getUint32(iovsOffset + 4, true); + + const buf = new Uint8Array(wasmMemoryBuffer, iovecBufPtr, iovecBufLen); + + if (fd === 1) { + Deno.stdout.writeSync(buf); + } else if (fd === 2) { + Deno.stderr.writeSync(buf); + } else { + return 1; // not supported fd + } + + totalWritten += iovecBufLen; + } + + dataView.setUint32(nwrittenPtr, totalWritten, true); + + return 0; // success + }, + }, dprint: { "host_has_cancelled": () => 0, - "host_clear_bytes": (length: number) => { - resetSharedBuffer(length); - }, "host_write_buffer": (pointer: number) => { getWasmBufferAtPointer(instance, pointer, sharedBuffer.length).set(sharedBuffer); }, @@ -104,6 +133,7 @@ export function createFromInstance( format_range, get_error_text, get_plugin_info, + get_config_file_matching, get_resolved_config, get_config_diagnostics, get_license_text, @@ -127,11 +157,13 @@ export function createFromInstance( const length = get_resolved_config(configId); return JSON.parse(receiveString(length)); }, + getFileMatchingInfo() { + const length = get_config_file_matching(configId); + return JSON.parse(receiveString(length)) as FileMatchingInfo; + }, getPluginInfo() { const length = get_plugin_info(); - const pluginInfo = JSON.parse(receiveString(length)) as PluginInfo; - pluginInfo.fileNames = pluginInfo.fileNames ?? []; - return pluginInfo; + return JSON.parse(receiveString(length)) as PluginInfo; }, getLicenseText() { const length = get_license_text(); @@ -155,8 +187,8 @@ export function createFromInstance( sendString(request.fileText); const responseCode = request.bytesRange != null - ? format_range(request.bytesRange[0], request.bytesRange[1]) - : format(); + ? format_range(configId, request.bytesRange[0], request.bytesRange[1]) + : format(configId); switch (responseCode) { case 0: // no change return request.fileText;