diff --git a/CHANGELOG.md b/CHANGELOG.md index 34ee71a8..3209b688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +# [Unreleased] + +## Fixed + +- Add 'Remove' option to notification if local parser installation fails to load. + # 0.7.0 Finally, switched to `node-tree-sitter`! 🎉 diff --git a/src/FileTree.ts b/src/FileTree.ts index 7bf24651..b65e0912 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 { @@ -72,11 +71,7 @@ export class FileTree implements vscode.Disposable { ); } - public static async new( - language: Language, - document: vscode.TextDocument - ): Promise> { - await parserFinishedInit; + public static new(language: Language, document: vscode.TextDocument): Result { const parser = new Parser(); const logger = getLogger(); try { diff --git a/src/Installer.ts b/src/Installer.ts index 51a4be82..13d14cb0 100644 --- a/src/Installer.ts +++ b/src/Installer.ts @@ -4,11 +4,10 @@ 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, rmSync } from "fs"; import Parser from "tree-sitter"; -import { existsSync } 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"; @@ -23,11 +22,11 @@ export function getAbsoluteBindingsDir(parsersDir: string, parserName: string): return path.resolve(path.join(parsersDir, parserName, "bindings", "node", "index.js")); } -export async function loadParser( +export function loadParser( parsersDir: string, parserName: string, subdirectory?: string -): Promise> { +): Result { const logger = getLogger(); const bindingsDir = getAbsoluteBindingsDir(parsersDir, parserName); @@ -35,34 +34,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)}`); } } @@ -120,7 +118,7 @@ export async function downloadAndBuildParser( } // try to load parser optimistically - const loadResult = await loadParser(parsersDir, parserName); + const loadResult = loadParser(parsersDir, parserName); if (loadResult.status === "ok") { return ok(undefined); } @@ -186,11 +184,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(); @@ -205,8 +208,6 @@ export async function getLanguage( const npm = "npm"; const treeSitterCli = configuration.getTreeSitterCliPath(); - await parserFinishedInit; - if (!existsSync(parserPackagePath)) { const doInstall = autoInstall ? "Yes" @@ -268,18 +269,42 @@ 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 }); } } - const loadResult = await loadParser(parsersDir, parserName, subdirectory); + const loadResult = loadParser(parsersDir, parserName, subdirectory); if (loadResult.status === "err") { 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..23a10a45 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) { @@ -52,11 +60,10 @@ export class CodeBlocksEditorProvider implements vscode.CustomTextEditorProvider for (const query of languageQueries) { queries.push(new Query(language.result, query)); } - const fileTreeResult = await FileTree.new(language.result, document); + const fileTreeResult = 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..13cf355d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,15 +1,14 @@ import * as BlockMode from "./BlockMode"; +import * as Installer from "./Installer"; +import * as configuration from "./configuration"; import * as vscode from "vscode"; 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,25 +44,47 @@ 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); + const tree = FileTree.new(language.result, activeDocument); if (tree.status === "ok") { 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 ac501c5d..0b688c22 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