diff --git a/CHANGELOG.md b/CHANGELOG.md index 34ee71a8..25bcbdbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ This means faster parsing, faster language installation, and no need for - Support for `node` v16, it might still work, but the extension now requires `node` v18+. +## Fixed + +- If local parser installation is corrupt, add 'Remove' option to notification. + # 0.6.1 ## Added diff --git a/src/FileTree.ts b/src/FileTree.ts index 079a854a..8a2d4a3f 100644 --- a/src/FileTree.ts +++ b/src/FileTree.ts @@ -8,7 +8,6 @@ import { Language } from "./Installer"; import { Selection } from "./Selection"; import { getLanguageConfig } from "./configuration"; import { getLogger } from "./outputChannel"; -import { parserFinishedInit } from "./extension"; function positionToPoint(pos: vscode.Position): Parser.Point { return { @@ -76,7 +75,6 @@ export class FileTree implements vscode.Disposable { language: Language, document: vscode.TextDocument ): Promise> { - await parserFinishedInit; const parser = new Parser(); const logger = getLogger(); try { diff --git a/src/Installer.ts b/src/Installer.ts index f8ab2516..97542ac5 100644 --- a/src/Installer.ts +++ b/src/Installer.ts @@ -4,10 +4,9 @@ import * as tar from "tar"; import * as vscode from "vscode"; import { ExecException, ExecOptions, exec } from "child_process"; import { Result, err, ok } from "./result"; -import { existsSync } from "fs"; +import { existsSync, rmSync } from "fs"; import { getLogger } from "./outputChannel"; import { mkdir } from "fs/promises"; -import { parserFinishedInit } from "./extension"; import which from "which"; const NPM_INSTALL_URL = "https://nodejs.org/en/download"; @@ -34,34 +33,33 @@ export async function loadParser( const msg = `Expected parser directory doesn't exist: ${bindingsDir}`; logger.log(msg); return err(msg); - } else { - await parserFinishedInit; - try { - logger.log(`Loading parser from ${bindingsDir}`); + } - // using dynamic import causes issues on windows - // make sure to test well on windows before changing this - // TODO(02/11/24): change to dynamic import - // let { default: language } = (await import(bindingsDir)) as { default: Language }; + try { + logger.log(`Loading parser from ${bindingsDir}`); - // eslint-disable-next-line @typescript-eslint/no-var-requires - let language = require(bindingsDir) as Language; + // using dynamic import causes issues on windows + // make sure to test well on windows before changing this + // TODO(02/11/24): change to dynamic import + // let { default: language } = (await import(bindingsDir)) as { default: Language }; - logger.log(`Got language: ${JSON.stringify(Object.keys(language))}`); + // eslint-disable-next-line @typescript-eslint/no-var-requires + let language = require(bindingsDir) as Language; - if (subdirectory !== undefined) { - logger.log(`Loading subdirectory: ${subdirectory}`); - // @ts-expect-error we know this is a language - language = language[subdirectory] as Language; + logger.log(`Got language: ${JSON.stringify(Object.keys(language))}`); - logger.log(`Got subdirectory language: ${JSON.stringify(Object.keys(language))}`); - } + if (subdirectory !== undefined) { + logger.log(`Loading subdirectory: ${subdirectory}`); + // @ts-expect-error we know this is a language + language = language[subdirectory] as Language; - return ok(language); - } catch (error) { - logger.log(`Failed to load ${bindingsDir} > ${JSON.stringify(error)}`); - return err(`Failed to load ${bindingsDir} > ${JSON.stringify(error)}`); + logger.log(`Got subdirectory language: ${JSON.stringify(Object.keys(language))}`); } + + return ok(language); + } catch (error) { + logger.log(`Failed to load ${bindingsDir} > ${JSON.stringify(error)}`); + return err(`Failed to load ${bindingsDir} > ${JSON.stringify(error)}`); } } @@ -183,11 +181,16 @@ async function runCmd( }); } +export type GetLanguageError = { + cause: "downloadFailed" | "loadFailed"; + msg: string; +}; + export async function getLanguage( parsersDir: string, languageId: string, autoInstall = false -): Promise> { +): Promise> { const logger = getLogger(); const ignoredLanguageIds = configuration.getIgnoredLanguageIds(); @@ -202,8 +205,6 @@ export async function getLanguage( const npm = "npm"; const treeSitterCli = configuration.getTreeSitterCliPath(); - await parserFinishedInit; - if (!existsSync(parserPackagePath)) { const doInstall = autoInstall ? "Yes" @@ -265,7 +266,7 @@ export async function getLanguage( const msg = `Failed to download/build parser for language ${languageId} > ${downloadResult.result}`; logger.log(msg); - return err(msg); + return err({ cause: "downloadFailed", msg }); } } @@ -274,9 +275,33 @@ export async function getLanguage( const msg = `Failed to load parser for language ${languageId} > ${loadResult.result}`; logger.log(msg); - return err(msg); + return err({ cause: "loadFailed", msg }); } logger.log(`Successfully loaded parser for language ${languageId}`); return ok(loadResult.result); } + +export async function askRemoveLanguage(parsersDir: string, languageId: string, msg: string): Promise { + const doRemove = await vscode.window.showErrorMessage( + `Failed to load parser for ${languageId}: ${msg}`, + "Remove", + "Ok" + ); + + if (doRemove === "Remove") { + removeLanguage(parsersDir, languageId); + } +} + +export function removeLanguage(parsersDir: string, languageId: string): void { + const logger = getLogger(); + + const { parserName } = configuration.getLanguageConfig(languageId); + const parserPackagePath = getAbsoluteParserDir(parsersDir, parserName); + + if (existsSync(parserPackagePath)) { + rmSync(parserPackagePath, { recursive: true, force: true }); + } + logger.log(`Removed parser '${parserPackagePath}'`); +} diff --git a/src/editor/CodeBlocksEditorProvider.ts b/src/editor/CodeBlocksEditorProvider.ts index 8c83ba68..faf6e645 100644 --- a/src/editor/CodeBlocksEditorProvider.ts +++ b/src/editor/CodeBlocksEditorProvider.ts @@ -32,16 +32,24 @@ export class CodeBlocksEditorProvider implements vscode.CustomTextEditorProvider let language = await Installer.getLanguage(this.extensionParsersDirPath, languageId); while (language.status !== "ok") { + const items = + language.result.cause === "loadFailed" + ? (["Remove", "Ok"] as const) + : (["Retry", "Ok"] as const); + const choice = await vscode.window.showErrorMessage( - `Parser installation failed: ${language.result}`, - "Retry", - "Ok" + `Parser installation failed: ${JSON.stringify(language.result)}`, + ...items ); - if (choice !== "Retry") { + + if (choice === "Remove") { + Installer.removeLanguage(this.extensionParsersDirPath, languageId); + return; + } else if (choice === "Retry") { + language = await Installer.getLanguage(this.extensionParsersDirPath, languageId); + } else { return; } - - language = await Installer.getLanguage(this.extensionParsersDirPath, languageId); } if (language.result === undefined) { @@ -54,9 +62,8 @@ export class CodeBlocksEditorProvider implements vscode.CustomTextEditorProvider } const fileTreeResult = await FileTree.new(language.result, document); if (fileTreeResult.status === "err") { - await vscode.window.showErrorMessage( - `Failed to load parser for ${languageId}: ${JSON.stringify(fileTreeResult.result)}` - ); + const msg = JSON.stringify(fileTreeResult.result); + await Installer.askRemoveLanguage(this.extensionParsersDirPath, languageId, msg); return; } diff --git a/src/extension.ts b/src/extension.ts index e72b6b11..9f5e00d5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,15 +1,14 @@ import * as BlockMode from "./BlockMode"; +import * as configuration from "./configuration"; import * as vscode from "vscode"; +import * as Installer from "./Installer"; import { CodeBlocksEditorProvider } from "./editor/CodeBlocksEditorProvider"; import { FileTree } from "./FileTree"; import { TreeViewer } from "./TreeViewer"; -import { getLanguage } from "./Installer"; import { getLogger } from "./outputChannel"; import { join } from "path"; import { state } from "./state"; -export const parserFinishedInit = Promise.resolve(); - async function reopenWithCodeBocksEditor(): Promise { const activeTabInput = vscode.window.tabGroups.activeTabGroup.activeTab?.input as { [key: string]: unknown; @@ -45,15 +44,39 @@ async function getEditorFileTree( } const activeDocument = editor.document; - const language = await getLanguage(parsersDir, activeDocument.languageId); - if (language.status === "err" || language.result === undefined) { - if (language.status === "err") { - void vscode.window.showErrorMessage(`Failed to get language: ${language.result}`); - } else { - logger.log(`No language found for ${activeDocument.languageId}`); + const languageId = activeDocument.languageId; + const language = await Installer.getLanguage(parsersDir, languageId); + + // sup-optimal conditional to make tsc happy + // tl;dr this is handling logic for 'language not received' scenarios + if (language.result === undefined || language.status === "err") { + if (language.status === "ok") { + logger.log(`No language found for ${languageId}`); + return undefined; } - return undefined; + switch (language.result.cause) { + case "downloadFailed": { + const doIgnore = await vscode.window.showErrorMessage( + `Failed to download language: ${language.result.msg}`, + "Add to ignore", + "Ok" + ); + + if (doIgnore === "Add to ignore") { + // fail silently if we can't add to ignore list + // we don't want to have two consecutive error messages + await configuration.addIgnoredLanguageId(languageId); + } + + return undefined; + } + + case "loadFailed": { + await Installer.askRemoveLanguage(parsersDir, languageId, language.result.msg); + return undefined; + } + } } const tree = await FileTree.new(language.result, activeDocument); @@ -61,9 +84,7 @@ async function getEditorFileTree( return tree.result; } - void vscode.window.showErrorMessage( - `Failed to load parser for ${activeDocument.languageId}: ${JSON.stringify(tree.result)}` - ); + await Installer.askRemoveLanguage(parsersDir, languageId, JSON.stringify(tree.result)); return undefined; } diff --git a/src/test/suite/Installer.test.ts b/src/test/suite/Installer.test.ts index c91dae6c..d495e731 100644 --- a/src/test/suite/Installer.test.ts +++ b/src/test/suite/Installer.test.ts @@ -9,7 +9,7 @@ export async function testParser(language: string, content?: string): Promise