From 227af7814a1e42737383ed4ce361242c36504dad Mon Sep 17 00:00:00 2001 From: Isabel Zimmerman <54685329+isabelizimm@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:48:40 -0400 Subject: [PATCH] Automatically open QMD files in visual/source editor if specified (#577) * auto open in default or specified viewer * clean up for clarity * use onDidChangeActiveTextEditor * Apply suggestions from code review Co-authored-by: Davis Vaughan * changes from review * move listener to VisualEditorProvider * only have one editor listener * check if this is a diff * track editor associations * fix typing error * update from review * move everything to Tab API * clean up unused code * linting * only watch opened tabs * preserve focus on tab swaps * Apply suggestions from code review Co-authored-by: Davis Vaughan * add comments --------- Co-authored-by: Davis Vaughan --- apps/vscode/package.json | 2 +- apps/vscode/src/main.ts | 6 +- apps/vscode/src/providers/editor/editor.ts | 233 +++++++++++++-------- apps/vscode/src/providers/editor/toggle.ts | 71 ++++++- packages/quarto-core/src/document.ts | 2 +- yarn.lock | 8 +- 6 files changed, 214 insertions(+), 108 deletions(-) diff --git a/apps/vscode/package.json b/apps/vscode/package.json index 8ef61882..4feea2ed 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -1383,7 +1383,7 @@ "@types/semver": "^7.3.13", "@types/tmp": "^0.2.3", "@types/uuid": "^9.0.0", - "@types/vscode": "1.66.0", + "@types/vscode": "1.75.0", "@types/which": "^2.0.2", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index c9195e58..ba795b8b 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -24,7 +24,6 @@ import { activateQuartoAssistPanel } from "./providers/assist/panel"; import { activateCommon } from "./extension"; import { activatePreview } from "./providers/preview/preview"; import { activateRender } from "./providers/render"; -import { initQuartoContext, quartoContextUnavailable } from "quarto-core"; import { activateStatusBar } from "./providers/statusbar"; import { walkthroughCommands } from "./providers/walkthrough"; import { activateLuaTypes } from "./providers/lua-types"; @@ -33,11 +32,12 @@ import { activateEditor } from "./providers/editor/editor"; import { activateCopyFiles } from "./providers/copyfiles"; import { activateZotero } from "./providers/zotero/zotero";; import { extensionHost } from "./host"; +import { initQuartoContext } from "quarto-core"; import { configuredQuartoPath } from "./core/quarto"; import { activateDenoConfig } from "./providers/deno-config"; export async function activate(context: vscode.ExtensionContext) { - + // create extension host const host = extensionHost(); @@ -129,5 +129,5 @@ export async function activate(context: vscode.ExtensionContext) { export async function deactivate() { return deactivateLsp(); -} +} diff --git a/apps/vscode/src/providers/editor/editor.ts b/apps/vscode/src/providers/editor/editor.ts index f80cf909..8b01043c 100644 --- a/apps/vscode/src/providers/editor/editor.ts +++ b/apps/vscode/src/providers/editor/editor.ts @@ -13,20 +13,20 @@ * */ -import path, { extname } from "path"; - +import path, { extname, win32 } from "path"; +import { determineMode } from "./toggle" import debounce from "lodash.debounce"; -import { +import { window, - workspace, - ExtensionContext, - Disposable, - CustomTextEditorProvider, - TextDocument, - WebviewPanel, - CancellationToken, - Uri, + workspace, + ExtensionContext, + Disposable, + CustomTextEditorProvider, + TextDocument, + WebviewPanel, + CancellationToken, + Uri, Webview, Range, env, @@ -34,7 +34,8 @@ import { ViewColumn, Selection, TextEditorRevealType, - GlobPattern + GlobPattern, + TabInputText } from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; @@ -58,18 +59,18 @@ import { MarkdownEngine } from "../../markdown/engine"; import { lspClientTransport } from "core-node"; import { editorSourceJsonRpcServer } from "editor-core"; import { JsonRpcRequestTransport } from "core"; -import { - editInSourceModeCommand, - editInVisualModeCommand, - reopenEditorInSourceMode +import { + editInSourceModeCommand, + editInVisualModeCommand, + reopenEditorInSourceMode } from "./toggle"; import { ExtensionHost } from "../../host"; - +import { TabInputCustom } from "vscode"; export interface QuartoVisualEditor extends QuartoEditor { - hasFocus() : Promise; - getActiveBlockContext() : Promise; - setBlockSelection(context: CodeViewActiveBlockContext, action: CodeViewSelectionAction) : Promise; + hasFocus(): Promise; + getActiveBlockContext(): Promise; + setBlockSelection(context: CodeViewActiveBlockContext, action: CodeViewSelectionAction): Promise; } export function activateEditor( @@ -78,7 +79,7 @@ export function activateEditor( quartoContext: QuartoContext, lspClient: LanguageClient, engine: MarkdownEngine -) : Command[] { +): Command[] { // register the provider context.subscriptions.push(VisualEditorProvider.register(context, host, quartoContext, lspClient, engine)); @@ -88,33 +89,36 @@ export function activateEditor( export class VisualEditorProvider implements CustomTextEditorProvider { - + // track the last contents of any active untitled docs (used // for recovering from attempt to edit ) private static activeUntitled?: { uri: Uri, content: string }; // track the last edited line of code in text editors (used for syncing position) - private static editorLastSourcePos = new Map(); + private static editorLastSourcePos = new Map(); // track the list source location in visual editors (used for syncing position) - private static visualEditorLastSourcePos = new Map(); + private static visualEditorLastSourcePos = new Map(); // track pending switch to source private static visualEditorPendingSwitchToSource = new Set(); + // track pending switch to visual + private static editorPendingSwitchToVisual = new Set(); + // track pending xref navigations - private static visualEditorPendingXRefNavigations = new Map(); + private static visualEditorPendingXRefNavigations = new Map(); // track visual editors private static visualEditors = visualEditorTracker(); public static register( - context: ExtensionContext, + context: ExtensionContext, host: ExtensionHost, quartoContext: QuartoContext, lspClient: LanguageClient, engine: MarkdownEngine - ) : Disposable { + ): Disposable { // setup request transport const lspRequest = lspClientTransport(lspClient); @@ -139,8 +143,55 @@ export class VisualEditorProvider implements CustomTextEditorProvider { } })); + context.subscriptions.push(window.tabGroups.onDidChangeTabs(async (t) => { + const tabs = t.opened; + + if (tabs.length > 0) { + for (const tab of tabs) { + if (tab.label.endsWith(".qmd") && (tab.input instanceof TabInputText || tab.input instanceof TabInputCustom)) { + // determine what mode editor should be in + const uri = tab.input.uri; + + const isTextEditor = tab.input instanceof TabInputText; + const viewType = isTextEditor ? "textEditor" : tab.input.viewType; + + // get file contents + const fileData = await workspace.fs.readFile(uri); + const fileContent = Buffer.from(fileData).toString('utf8'); + const editorMode = determineMode(fileContent, uri); + let isSwitch = this.visualEditorPendingSwitchToSource.has(uri.toString()) || this.editorPendingSwitchToVisual.has(uri.toString()); + if (this.editorPendingSwitchToVisual.has(uri.toString())) { + this.editorPendingSwitchToVisual.delete(uri.toString()); + } + + // The `tab` we get from the change event is not precisely the same + // as the tab in `window.tabGroups`, so if we try and close `tab` we + // get a "tab not found" error. The one we care about does exist, but we have + // manually find it via URI, which is a stable field to match on. + if (editorMode && editorMode != viewType && !isSwitch) { + const allTabs = window.tabGroups.all.flatMap(group => group.tabs); + + // find tab to close if swapping editor type + const tabToClose = allTabs.find(tab => + ((tab.input instanceof TabInputText) || (tab.input instanceof TabInputCustom)) && + (tab.input?.uri?.toString() === uri?.toString()) + ); + if (!tabToClose) { + return; + } + await window.tabGroups.close(tabToClose, true); + await commands.executeCommand("vscode.openWith", uri, editorMode); + return; + } + } + } + } + })); + + // when the active editor changes see if we have a visual editor position for it context.subscriptions.push(window.onDidChangeActiveTextEditor(debounce(() => { + // resolve active editor const editor = window.activeTextEditor; if (!editor) { @@ -149,9 +200,10 @@ export class VisualEditorProvider implements CustomTextEditorProvider { const document = editor.document; if (document && isQuartoDoc(document)) { const uri = document.uri.toString(); - + // check for switch (one shot) const isSwitch = this.visualEditorPendingSwitchToSource.has(uri); + this.visualEditorPendingSwitchToSource.delete(uri); // check for pos (one shot) @@ -161,12 +213,11 @@ export class VisualEditorProvider implements CustomTextEditorProvider { if (!isSwitch) { return; } - if (pos) { // find the index let cursorIndex = -1; - for (let i=(pos.locations.length-1); i>=0; i--) { + for (let i = (pos.locations.length - 1); i >= 0; i--) { if (pos.pos >= pos.locations[i].pos) { cursorIndex = i; break; @@ -178,11 +229,11 @@ export class VisualEditorProvider implements CustomTextEditorProvider { source.getSourcePosLocations(document.getText()).then(locations => { // map to source line const selLine = (cursorIndex !== -1 && (locations.length > cursorIndex)) - ? (locations[cursorIndex] || locations[locations.length-1]).pos - 1 + ? (locations[cursorIndex] || locations[locations.length - 1]).pos - 1 : 0; // navigate - const selRange = new Range(selLine, 0, selLine, 0); + const selRange = new Range(selLine, 0, selLine, 0); editor.selection = new Selection(selRange.start, selRange.end); editor.revealRange(selRange, TextEditorRevealType.InCenter); }); @@ -209,11 +260,15 @@ export class VisualEditorProvider implements CustomTextEditorProvider { this.visualEditorPendingSwitchToSource.add(document.uri.toString()); } - public static activeEditor(includeVisible?: boolean) : QuartoVisualEditor | undefined { + public static recordPendingSwitchToVisual(document: TextDocument) { + this.editorPendingSwitchToVisual.add(document.uri.toString()); + } + + public static activeEditor(includeVisible?: boolean): QuartoVisualEditor | undefined { const editor = this.visualEditors.activeEditor(includeVisible); if (editor) { - return { - document: editor.document, + return { + document: editor.document, hasFocus: async () => { return await editor.editor.isFocused(); }, @@ -229,14 +284,14 @@ export class VisualEditorProvider implements CustomTextEditorProvider { setBlockSelection: async (context, action) => { await editor.editor.setBlockSelection(context, action); }, - viewColumn: editor.webviewPanel.viewColumn + viewColumn: editor.webviewPanel.viewColumn }; } else { return undefined; } } - public static editorForUri(uri: Uri) : TrackedEditor | undefined { + public static editorForUri(uri: Uri): TrackedEditor | undefined { return this.visualEditors.editorForUri(uri); } @@ -245,23 +300,23 @@ export class VisualEditorProvider implements CustomTextEditorProvider { } constructor(private readonly context: ExtensionContext, - private readonly extensionHost: ExtensionHost, - private readonly quartoContext: QuartoContext, - private readonly lspRequest: JsonRpcRequestTransport, - private readonly engine: MarkdownEngine) {} + private readonly extensionHost: ExtensionHost, + private readonly quartoContext: QuartoContext, + private readonly lspRequest: JsonRpcRequestTransport, + private readonly engine: MarkdownEngine) { } + - public async resolveCustomTextEditor( document: TextDocument, webviewPanel: WebviewPanel, _token: CancellationToken ) { - // if the document is untitled then capture its contents (as vscode throws it on the floor + // if the document is untitled then capture its contents (as vscode throws it on the floor // and we may need it to do a re-open) - const untitledContent = - (document.isUntitled && - VisualEditorProvider.activeUntitled?.uri.toString() === document.uri.toString()) + const untitledContent = + (document.isUntitled && + VisualEditorProvider.activeUntitled?.uri.toString() === document.uri.toString()) ? VisualEditorProvider.activeUntitled.content : undefined; @@ -277,11 +332,11 @@ export class VisualEditorProvider implements CustomTextEditorProvider { const kLearnMore = "Learn More..."; const result = await window.showInformationMessage( "You are activating Quarto visual markdown editing mode.", - { - modal: true, - detail: - "Visual mode enables you to author using a familiar word processor style interface.\n\n" + - "Markdown code will be re-formatted using the Pandoc markdown writer." + { + modal: true, + detail: + "Visual mode enables you to author using a familiar word processor style interface.\n\n" + + "Markdown code will be re-formatted using the Pandoc markdown writer." }, kUseVisualMode, kLearnMore @@ -297,7 +352,7 @@ export class VisualEditorProvider implements CustomTextEditorProvider { this.context.globalState.update(kVisualModeConfirmed, true); } } - + // some storage locations const projectDir = document.isUntitled ? undefined : projectDirForDocument(document.fileName); const workspaceDir = this.quartoContext.workspaceDir; @@ -318,11 +373,11 @@ export class VisualEditorProvider implements CustomTextEditorProvider { const xref = VisualEditorProvider.visualEditorPendingXRefNavigations.get(sourceUri); VisualEditorProvider.visualEditorPendingXRefNavigations.delete(sourceUri); - // sync manager + // sync manager const syncManager = editorSyncManager( - document, - client.editor, - this.lspRequest, + document, + client.editor, + this.lspRequest, xref || sourcePos ); @@ -330,12 +385,12 @@ export class VisualEditorProvider implements CustomTextEditorProvider { const host: VSCodeVisualEditorHost = { // editor is querying for context - getHostContext: async () : Promise => { + getHostContext: async (): Promise => { return { documentPath: document.isUntitled ? null : document.fileName, projectDir, - resourceDir: document.isUntitled - ? (workspaceDir || process.cwd()) + resourceDir: document.isUntitled + ? (workspaceDir || process.cwd()) : path.dirname(document.fileName), isWindowsDesktop: isWindows(), executableLanguages: this.extensionHost.executableLanguages(true, document, this.engine) @@ -442,9 +497,9 @@ export class VisualEditorProvider implements CustomTextEditorProvider { // setup server on webview iframe disposables.push(visualEditorServer( - webviewPanel, - this.lspRequest, - host, + webviewPanel, + this.lspRequest, + host, prefsServer, vscodeCodeViewServer(this.engine, document, this.lspRequest) )); @@ -454,7 +509,7 @@ export class VisualEditorProvider implements CustomTextEditorProvider { // monitor image file changes const kImagePattern = '**/*.{png,svg,jpg,jpeg}'; - const globPattern : GlobPattern = docDir + const globPattern: GlobPattern = docDir ? { baseUri: Uri.file(docDir), base: docDir, pattern: kImagePattern } : kImagePattern; const watcher = workspace.createFileSystemWatcher(globPattern); @@ -467,13 +522,13 @@ export class VisualEditorProvider implements CustomTextEditorProvider { watcher.onDidDelete(onChange); // load editor webview (include current doc path in localResourceRoots) - webviewPanel.webview.options = { + webviewPanel.webview.options = { localResourceRoots: [ - this.context.extensionUri, + this.context.extensionUri, ...(workspace.workspaceFolders ? workspace.workspaceFolders.map(folder => folder.uri) : []), ...(docDir ? [Uri.file(docDir)] : []) ], - enableScripts: true + enableScripts: true }; webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview); @@ -486,7 +541,7 @@ export class VisualEditorProvider implements CustomTextEditorProvider { disposable.dispose(); } }); - + } private editorAssetUri(webview: Webview, file: string) { @@ -503,7 +558,7 @@ export class VisualEditorProvider implements CustomTextEditorProvider { * Get the static html used for the editor webviews. */ private getHtmlForWebview(webview: Webview): string { - + const scriptUri = this.editorAssetUri(webview, "index.js"); const stylesUri = this.editorAssetUri(webview, "style.css"); const codiconsUri = this.extensionResourceUrl(webview, [ @@ -553,7 +608,7 @@ export class VisualEditorProvider implements CustomTextEditorProvider { } async function navigateToFile(baseDoc: TextDocument, file: string, xref?: XRef) { - + const docDir = path.dirname(baseDoc.uri.fsPath); const filePath = path.normalize(path.isAbsolute(file) ? file : path.join(docDir, file)); const uri = Uri.file(filePath); @@ -575,11 +630,11 @@ async function navigateToFile(baseDoc: TextDocument, file: string, xref?: XRef) } else { await openWith(VisualEditorProvider.viewType); } - + } else if (ext === ".ipynb") { - + await openWith("jupyter-notebook"); - + } else { const doc = await workspace.openTextDocument(uri); @@ -608,13 +663,13 @@ interface VisualEditorTracker { activeEditor: (includeVisible?: boolean) => TrackedEditor | undefined; } -function visualEditorTracker() : VisualEditorTracker { +function visualEditorTracker(): VisualEditorTracker { const activeEditors = new Array(); return { - track: (document: TextDocument, webviewPanel: WebviewPanel, editor: VSCodeVisualEditor) : Disposable => { - activeEditors.push({document, webviewPanel, editor}); + track: (document: TextDocument, webviewPanel: WebviewPanel, editor: VSCodeVisualEditor): Disposable => { + activeEditors.push({ document, webviewPanel, editor }); return { dispose: () => { const idx = activeEditors.findIndex(editor => editor.webviewPanel === webviewPanel); @@ -631,20 +686,20 @@ function visualEditorTracker() : VisualEditorTracker { return activeEditors.find(editor => { try { return editor.webviewPanel.active || (includeVisible && editor.webviewPanel.visible); - } catch(err) { + } catch (err) { // we've seen activeEditors hold on to references to disposed editors (can't on the // surface see how this would occur as we subscribe to dispose, but as an insurance // policy let's eat any exception that occurs, since a single zombie webviewPanel // would prevent rendering of other panels return false; - } - + } + }); } }; } -function focusTracker(webviewPanel: WebviewPanel, editor: VSCodeVisualEditor) : Disposable { +function focusTracker(webviewPanel: WebviewPanel, editor: VSCodeVisualEditor): Disposable { let hasFocus = false; let cancelled = false; @@ -655,18 +710,18 @@ function focusTracker(webviewPanel: WebviewPanel, editor: VSCodeVisualEditor) : editor.focus(); }; - // if we are focused when the window loses focus then restore on re-focus - let reFocus = false; - const evWindow = window.onDidChangeWindowState(async (event) => { - if (!event.focused && hasFocus) { - reFocus = true; - } else if (event.focused && reFocus) { - setTimeout(async () => { + // if we are focused when the window loses focus then restore on re-focus + let reFocus = false; + const evWindow = window.onDidChangeWindowState(async (event) => { + if (!event.focused && hasFocus) { + reFocus = true; + } else if (event.focused && reFocus) { + setTimeout(async () => { await focusEditor(); reFocus = false; - }, 200); - } - }); + }, 200); + } + }); // periodically check for focus const timer = setInterval(async () => { diff --git a/apps/vscode/src/providers/editor/toggle.ts b/apps/vscode/src/providers/editor/toggle.ts index e0344be9..2f73bd7e 100644 --- a/apps/vscode/src/providers/editor/toggle.ts +++ b/apps/vscode/src/providers/editor/toggle.ts @@ -14,14 +14,65 @@ */ import { commands, window, workspace, TextDocument, ViewColumn } from "vscode"; +import * as quarto from "quarto-core"; import { Command } from "../../core/command"; import { isQuartoDoc, kQuartoLanguageId } from "../../core/doc"; import { VisualEditorProvider } from "./editor"; +import { Uri } from "vscode"; +export function determineMode(text: string, uri: Uri): string | undefined { + let editorOpener = undefined; + // check if file itself has a mode + if (hasEditorMode(text, "source")) { + editorOpener = "textEditor"; + } + else if (hasEditorMode(text, "visual")) { + editorOpener = VisualEditorProvider.viewType; + } + // check if has a _quarto.yml or _quarto.yaml file with editor specified + else { + editorOpener = modeFromQuartoYaml(uri); + } + return editorOpener; +} + +export function modeFromQuartoYaml(uri: Uri): string | undefined { + const metadataFiles = quarto.metadataFilesForDocument(uri.fsPath); + if (!metadataFiles) { + return undefined; + } + if (metadataFiles) { + for (const metadataFile of metadataFiles) { + const yamlText = quarto.yamlFromMetadataFile(metadataFile); + if (yamlText?.editor === "source") { + return "textEditor"; + } + if (yamlText?.editor === "visual") { + return VisualEditorProvider.viewType; + } + } + } + return undefined; +} -export function editInVisualModeCommand() : Command { +export function hasEditorMode(doc: string, mode: string): boolean { + + if (doc) { + const match = doc.match(quarto.kRegExYAML); + if (match) { + const yaml = match[0]; + return ( + !!yaml.match(new RegExp("editor:\\s+" + mode + "\\s*$", "gm")) || + !!yaml.match(new RegExp("^[ \\t]*" + "mode:\\s*" + mode + "\\s*$", "gm")) + ); + } + } + return false; +} + +export function editInVisualModeCommand(): Command { return { id: "quarto.editInVisualMode", execute() { @@ -33,14 +84,14 @@ export function editInVisualModeCommand() : Command { }; } -export function editInSourceModeCommand() : Command { +export function editInSourceModeCommand(): Command { return { id: "quarto.editInSourceMode", execute() { const activeVisual = VisualEditorProvider.activeEditor(); if (activeVisual) { reopenEditorInSourceMode(activeVisual.document, '', activeVisual.viewColumn); - } + } } }; } @@ -49,14 +100,14 @@ export async function reopenEditorInVisualMode( document: TextDocument, viewColumn?: ViewColumn ) { - + // save then close await commands.executeCommand("workbench.action.files.save"); await commands.executeCommand('workbench.action.closeActiveEditor'); - + VisualEditorProvider.recordPendingSwitchToVisual(document); // open in visual mode - await commands.executeCommand("vscode.openWith", - document.uri, + await commands.executeCommand("vscode.openWith", + document.uri, VisualEditorProvider.viewType, { viewColumn @@ -65,8 +116,8 @@ export async function reopenEditorInVisualMode( } export async function reopenEditorInSourceMode( - document: TextDocument, - untitledContent?: string, + document: TextDocument, + untitledContent?: string, viewColumn?: ViewColumn ) { if (!document.isUntitled) { @@ -91,5 +142,5 @@ export async function reopenEditorInSourceMode( await window.showTextDocument(doc, viewColumn, false); } }); - + } diff --git a/packages/quarto-core/src/document.ts b/packages/quarto-core/src/document.ts index a0df6ed5..765a3597 100644 --- a/packages/quarto-core/src/document.ts +++ b/packages/quarto-core/src/document.ts @@ -110,7 +110,7 @@ export function filePathForDoc(doc: Document) { return URI.parse(doc.uri).fsPath; } -const kRegExYAML = +export const kRegExYAML = /(^)(---[ \t]*[\r\n]+(?![ \t]*[\r\n]+)[\W\w]*?[\r\n]+(?:---|\.\.\.))([ \t]*)$/gm; export function isQuartoDocWithFormat(doc: Document | string, format: string) { diff --git a/yarn.lock b/yarn.lock index e84b8253..84e85629 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2221,10 +2221,10 @@ resolved "https://registry.yarnpkg.com/@types/vscode-webview/-/vscode-webview-1.57.1.tgz#0bf2c9d57698b99be2bb2813272169f7f62eb714" integrity sha512-ghW5SfuDmsGDS2A4xkvGsLwDRNc3Vj5rS6rPOyPm/IryZuf3wceZKxgYaUoW+k9f0f/CB7y2c1rRsdOWZWn0PQ== -"@types/vscode@1.66.0": - version "1.66.0" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.66.0.tgz#e90e1308ad103f2bd31b72c17b8031b4cec0713d" - integrity sha512-ZfJck4M7nrGasfs4A4YbUoxis3Vu24cETw3DERsNYtDZmYSYtk6ljKexKFKhImO/ZmY6ZMsmegu2FPkXoUFImA== +"@types/vscode@1.75.0": + version "1.75.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.75.0.tgz#f1892a727db9a0eb4997058a804170b8c0ba218e" + integrity sha512-SAr0PoOhJS6FUq5LjNr8C/StBKALZwDVm3+U4pjF/3iYkt3GioJOPV/oB1Sf1l7lROe4TgrMyL5N1yaEgTWycw== "@types/wcwidth@^1.0.0": version "1.0.0"