From 458b45e9face1c36b0766b7ab221bdd9a7191106 Mon Sep 17 00:00:00 2001 From: "Daniel P. Brice" Date: Mon, 11 Mar 2024 16:18:59 -0700 Subject: [PATCH] Deactivate command (#42) --- CHANGELOG.md | 7 +++++ README.md | 25 ++++++++++++----- package.json | 16 +++++++++-- src/apisearch.ts | 9 +++--- src/config.ts | 67 ++++++++++++++++++++++++++++---------------- src/extension.ts | 72 +++++++++++++++++++++++++++++++----------------- src/formatter.ts | 8 ++++-- src/tags.ts | 61 ++++++++++++++++++++-------------------- src/utils.ts | 20 ++++++++------ 9 files changed, 181 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c21a594..3049418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to the "alloglot" extension will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). This project adhere's to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] + +- Add `deactivateCommand` to `TConfig` to run on extension `deactivate()` +- Add `verboseOutput` to `TConfig`. Hide some existing output behind said config. +- Minor bugfixes in async processes. +- Minor wording changes in some UI output messages. + ## [2.5.1] - Stream activate command stdout to output channel. diff --git a/README.md b/README.md index 5bf7063..bdfbed7 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ Most of the properties are optional, so you can make use of only the features th ```json { - "alloglot.activationCommand": "ghcid", + "alloglot.activateCommand": "ghcid", + "alloglot.deactivateCommand": "pgrep ghc | xargs kill", "alloglot.languages": [ { "languageId": "cabal", @@ -102,18 +103,28 @@ This allows use of the features you want without unwanted features getting in yo /** * Extension configuration. */ -export type Config = { - /** - * An array of per-language configurations. - */ - languages: Array - +export type TConfig = { /** * A shell command to run on activation. * The command will run asynchronously. * It will be killed (if it's still running) on deactivation. */ activateCommand?: string + + /** + * A shell command to run on deactivation. + */ + deactivateCommand?: string + + /** + * An array of per-language configurations. + */ + languages?: Array + + /** + * If `true`, Alloglot will log more output. + */ + verboseOutput?: boolean } /** diff --git a/package.json b/package.json index 51fd31a..1c8892b 100644 --- a/package.json +++ b/package.json @@ -23,15 +23,15 @@ "commands": [ { "command": "alloglot.command.restart", - "title": "Restart Alloglot" + "title": "Alloglot: Restart Alloglot" }, { "command": "alloglot.command.apisearch", - "title": "Go to API Search" + "title": "Alloglot: Go to API Search" }, { "command": "alloglot.command.suggestimports", - "title": "Suggest Imports..." + "title": "Alloglot: Suggest Imports..." } ], "menus": { @@ -54,6 +54,16 @@ "description": "A shell command to run on activation. The command will run asynchronously. It will be killed (if it's still running) on deactivation.", "default": null }, + "alloglot.deactivateCommand": { + "type":"string", + "description": "A shell command to run on deactivation.", + "default":null + }, + "alloglot.verboseOutput": { + "type":"boolean", + "description": "If `true`, Alloglot will log more output.", + "default":null + }, "alloglot.languages": { "type": "array", "description": "An array of language configurations. See README.md for schema.", diff --git a/src/apisearch.ts b/src/apisearch.ts index da85ece..7c8b37a 100644 --- a/src/apisearch.ts +++ b/src/apisearch.ts @@ -206,20 +206,19 @@ Portions of this software are derived from [vscode-goto-documentation](https://g import * as vscode from 'vscode' -import { Config, alloglot } from './config' +import { TConfig, alloglot } from './config' -export function makeApiSearch(output: vscode.OutputChannel, config: Config): vscode.Disposable { +export function makeApiSearch(output: vscode.OutputChannel, config: TConfig): vscode.Disposable { const { languages } = config if (!languages || languages.length === 0) return vscode.Disposable.from() + output.appendLine(alloglot.ui.creatingApiSearch(languages.map(lang => lang.languageId))) + const langs: Map = new Map() languages.forEach(lang => { lang.languageId && lang.apiSearchUrl && langs.set(lang.languageId, lang.apiSearchUrl) }) - output.appendLine(alloglot.ui.creatingApiSearch) - config.languages?.forEach(lang => output.appendLine(`\t${lang.languageId}`)) - return vscode.commands.registerTextEditorCommand( alloglot.commands.apiSearch, editor => { diff --git a/src/config.ts b/src/config.ts index 72fb590..7c9ef84 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,7 +4,7 @@ import * as vscode from 'vscode' /** * Extension configuration. */ -export type Config = { +export type TConfig = { /** * A shell command to run on activation. * The command will run asynchronously. @@ -12,10 +12,20 @@ export type Config = { */ activateCommand?: string + /** + * A shell command to run on deactivation. + */ + deactivateCommand?: string + /** * An array of per-language configurations. */ languages?: Array + + /** + * If `true`, Alloglot will log more output. + */ + verboseOutput?: boolean } /** @@ -177,10 +187,10 @@ export type AnnotationsMapping = { } export namespace Config { - export function make(output: vscode.OutputChannel): Config { - const empty: Config = {} + export function make(output: vscode.OutputChannel): TConfig { + const empty: TConfig = {} - function readFallback(): Config | undefined { + function readFallback(): TConfig | undefined { const workspaceFolders = vscode.workspace.workspaceFolders?.map(folder => folder.uri) try { if (workspaceFolders && workspaceFolders.length > 0) { @@ -197,23 +207,27 @@ export namespace Config { } } - function readSettings(): Config | undefined { + function readSettings(): TConfig | undefined { output.appendLine(alloglot.ui.readingWorkspaceSettings) const workspaceSettings = vscode.workspace.getConfiguration(alloglot.config.root) const activateCommand = workspaceSettings.get(alloglot.config.activateCommand) + const deactivateCommand = workspaceSettings.get(alloglot.config.deactivateCommand) const languages = workspaceSettings.get>(alloglot.config.languages) - const settingsExist = !!(activateCommand || languages) + const verboseOutput = workspaceSettings.get(alloglot.config.verboseOutput) + const settingsExist = !!(activateCommand || languages || deactivateCommand || verboseOutput) output.appendLine(alloglot.ui.workspaceConfigExists(settingsExist)) - if (settingsExist) return { activateCommand, languages } + if (settingsExist) return { activateCommand, deactivateCommand, languages, verboseOutput } return undefined } return sanitizeConfig(readSettings() || readFallback() || empty) } - function sanitizeConfig(config: Config): Config { + function sanitizeConfig(config: TConfig): TConfig { return { activateCommand: config.activateCommand?.trim(), + deactivateCommand: config.deactivateCommand?.trim(), + verboseOutput: !!config.verboseOutput, languages: config.languages?.filter(lang => { // make sure no fields are whitespace-only // we mutate the original object because typescript doesn't have a `filterMap` function @@ -247,36 +261,39 @@ export namespace alloglot { export const root = 'alloglot' as const export namespace ui { - export const activateCommandDone = (cmd: string) => `Activation command ${cmd} has completed.` + export const activateCommandDone = (cmd: string) => `Activation command “${cmd}” has completed.` export const addImport = (moduleName: string) => `Add import: ${moduleName}` export const annotationsStarted = 'Annotations started.' export const appliedEdit = (success: boolean) => `Applied edit: ${success}` export const applyingTransformations = (t: any, x: string) => `Applying ${JSON.stringify(t)} to ${x}` - export const commandKilled = (cmd: string) => `Killed \`\`${cmd}''.` - export const commandLogs = (cmd: string, logs: string) => `Logs from \`\`${cmd}'':\n\t${logs}` - export const commandNoOutput = (cmd: string) => `Received no output from \`\`${cmd}''.` + export const commandKilled = (cmd: string) => `Killed “${cmd}”.` + export const commandLogs = (cmd: string, logs: string) => `Logs from “${cmd}”:\n\t${logs}` + export const commandNoOutput = (cmd: string) => `Received no output from “${cmd}”.` export const couldNotReadFallback = (err: any) => `Could not read fallback configuration: ${err}` - export const creatingApiSearch = 'Creating API search command for languages...' - export const creatingTagsSource = (path: string) => `Creating tags source for ${path}` + export const creatingApiSearch = (langIds: Array) => `Creating API search command for languages: ${langIds}` + export const creatingTagsSource = (path: string) => `Creating tags source for path: ${path}` + export const deactivatingAlloglot = `Deactivating Alloglot...` + export const deactivateCommandDone = (cmd: string) => `Deactivation command has completed: ${cmd}` + export const deactivateCommandFailed = (err: any) => `Deactivation command has completed: ${err}` export const disposingAlloglot = 'Disposing Alloglot...' - export const errorRunningCommand = (cmd: string, err: any) => `Error running \`\`${cmd}'':\n\t${err}` + export const errorRunningCommand = (cmd: string, err: any) => `Error running “${cmd}”:\n\t${err}` export const fileMatcherResult = (result: any) => `Match: ${result}` export const findingImportPosition = 'Finding import position...' export const formatterStarted = 'Formatter started.' export const foundBlankLine = (line: number) => `Found blank line at line ${line}` export const foundImportPosition = (line: number) => `Found import at line ${line}` - export const killingCommand = (cmd: string) => `Killing \`\`${cmd}''...` + export const killingCommand = (cmd: string) => `Killing “${cmd}”...` export const languageClientStarted = 'Language client started.' export const languageClientStopped = 'Language client stopped.' - export const makingImportSuggestion = (tag: any) => `Making import suggestion for ${JSON.stringify(tag)}` + export const makingImportSuggestion = (tag: any) => `Making import suggestion for tag: ${JSON.stringify(tag)}` export const noBlankLineFound = 'No blank line found. Inserting import at start of file.' export const noWorkspaceFolders = 'No workspace folders found. Cannot read fallback configuration.' export const parsedTagLine = (tag: any) => `Parsed tag: ${JSON.stringify(tag)}` export const parsingTagLine = (line: string) => `Parsing tag line: ${line}` export const pickedSuggestion = (suggestion: any) => `Picked: ${JSON.stringify(suggestion)}` export const providingCodeActions = 'Providing code actions...' - export const ranCommand = (cmd: string) => `Ran \`\`${cmd}''.` - export const readingFallbackConfig = (path: string) => `Reading fallback configuration from ${path}` + export const ranCommand = (cmd: string) => `Ran “${cmd}”.` + export const readingFallbackConfig = (path: string) => `Reading fallback configuration from path: ${path}` export const readingWorkspaceSettings = 'Reading configuration from workspace settings' export const registeredCompletionsProvider = 'Registered completions provider.' export const registeredDefinitionsProvider = 'Registered definitions provider.' @@ -284,11 +301,11 @@ export namespace alloglot { export const registeringCompletionsProvider = 'Registering completions provider...' export const registeringDefinitionsProvider = 'Registering definitions provider...' export const registeringImportsProvider = 'Registering imports provider...' - export const renderedImportLine = (line?: string) => `Rendered import: ${line}` + export const renderedImportLine = (line?: string) => `Rendered import line: ${line}` export const renderedModuleName = (name?: string) => `Rendered module name: ${name}` - export const renderingImportLine = (tag: any) => `Rendering import line for ${JSON.stringify(tag)}` + export const renderingImportLine = (tag: any) => `Rendering import line for tag: ${JSON.stringify(tag)}` export const restartingAlloglot = 'Restarting Alloglot...' - export const runningCommand = (cmd: string, cwd?: string) => `Running \`\`${cmd}'' in \`\`${cwd}''...` + export const runningCommand = (cmd: string, cwd?: string) => `Running “${cmd}” in “${cwd}”...` export const runningSuggestImports = 'Running suggest imports...' export const splittingOutputChannel = (name: string) => `Creating new output channel: ${name}` export const startingAlloglot = 'Starting Alloglot...' @@ -299,13 +316,14 @@ export namespace alloglot { export const stoppingLanguageClient = 'Stopping language client...' export const tagsStarted = 'Tags started.' export const transformationResult = (x: string) => `Result: ${x}` - export const usingActivateCommandOutput = (channelId: string) => `Activation command stdout broadcasting to channel ${channelId}` + export const usingActivateCommandOutput = (channelId: string) => `Activation command stdout broadcasting to channel: ${channelId}` export const usingConfig = (config: any) => `Using configuration:\n${JSON.stringify(config, null, 2)}` export const usingFileMatcher = (matcher: any) => `File matcher: ${matcher}` export const workspaceConfigExists = (exists: boolean) => `Configuration exists in settings: ${exists}` } export namespace collections { + const root = `${alloglot.root}.collections` as const export const annotations = `${root}.annotations` as const } @@ -317,6 +335,7 @@ export namespace alloglot { export const client = 'client' as const export const tags = 'tags' as const export const tagsSource = 'tagssource' as const + export const importsProvider = 'importsprovider' as const } export namespace commands { @@ -331,5 +350,7 @@ export namespace alloglot { export const fallbackPath = `.vscode/${root}.json` as const export const languages = 'languages' as const export const activateCommand = 'activateCommand' as const + export const deactivateCommand = 'deactivateCommand' as const + export const verboseOutput = 'verboseOutput' as const } } diff --git a/src/extension.ts b/src/extension.ts index 26a238f..06bb277 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,60 +3,81 @@ import * as vscode from 'vscode' import { makeAnnotations } from './annotations' import { makeApiSearch } from './apisearch' import { makeClient } from './client' -import { Config, alloglot } from './config' +import { Config, TConfig, alloglot } from './config' import { makeFormatter } from './formatter' import { makeTags } from './tags' import { AsyncProcess, HierarchicalOutputChannel, IHierarchicalOutputChannel } from './utils' let globalOutput: vscode.OutputChannel | undefined let globalContext: vscode.ExtensionContext | undefined +let globalConfig: TConfig | undefined export function activate(context: vscode.ExtensionContext): void { - if (globalOutput) { - globalOutput.dispose() - globalOutput = undefined - } - globalContext = context const output = HierarchicalOutputChannel.make(alloglot.root) globalOutput = output - output.show(true) output.appendLine(alloglot.ui.startingAlloglot) const config = Config.make(output) + globalConfig = config output.appendLine(alloglot.ui.usingConfig(config)) const langs = config.languages || [] + const verboseOutput = !!config.verboseOutput + context.subscriptions.push( // Start the activation command if it's configured. makeActivationCommand(output.local(alloglot.components.activateCommand), config.activateCommand), // Make a single API search command because VSCode can't dynamically create commands. makeApiSearch(output.local(alloglot.components.apiSearch), config), ...langs.map(lang => makeAnnotations(output.local(`${alloglot.components.annotations}-${lang.languageId}`), lang)), - ...langs.map(lang => makeFormatter(output.local(`${alloglot.components.formatter}-${lang.languageId}`), lang)), + ...langs.map(lang => makeFormatter(output.local(`${alloglot.components.formatter}-${lang.languageId}`), lang, verboseOutput)), ...langs.map(lang => makeClient(output.local(`${alloglot.components.client}-${lang.languageId}`), lang)), - ...langs.map(lang => makeTags(output.local(`${alloglot.components.tags}-${lang.languageId}`), lang)), + ...langs.map(lang => makeTags(output.local(`${alloglot.components.tags}-${lang.languageId}`), lang, verboseOutput)), // Restart the extension when the configuration changes. - vscode.workspace.onDidChangeConfiguration(ev => ev.affectsConfiguration(alloglot.config.root) && restart(output, context)), + vscode.workspace.onDidChangeConfiguration(ev => ev.affectsConfiguration(alloglot.config.root) && restart()), // Restart the extension when the user runs the restart command. - vscode.commands.registerCommand(alloglot.commands.restart, () => restart(output, context)), + vscode.commands.registerCommand(alloglot.commands.restart, () => restart()) ) } -export function deactivate() { - disposeAll(globalOutput, globalContext) -} +export function deactivate(): void { + const command = globalConfig?.deactivateCommand + const basedir = vscode.workspace.workspaceFolders?.[0].uri -function disposeAll(output?: vscode.OutputChannel, context?: vscode.ExtensionContext) { - output && output.appendLine(alloglot.ui.disposingAlloglot) - context?.subscriptions.forEach(sub => sub.dispose()) + function cleanup(): void { + globalOutput?.appendLine(alloglot.ui.deactivatingAlloglot) + globalContext && globalContext.subscriptions.forEach(sub => sub.dispose()) + globalOutput?.dispose() + globalOutput = undefined + globalConfig = undefined + } + + if (command) { + const proc = AsyncProcess.make({ output: globalOutput, command, basedir }, () => { }) + + proc.then(() => { + globalOutput?.appendLine(alloglot.ui.deactivateCommandDone(command)) + cleanup() + proc.dispose() + }) + + proc.catch(err => { + globalOutput?.appendLine(alloglot.ui.deactivateCommandFailed(err)) + cleanup() + proc.dispose() + }) + + } else { + cleanup() + } } -function restart(output?: vscode.OutputChannel, context?: vscode.ExtensionContext) { - output && output.appendLine(alloglot.ui.restartingAlloglot) - disposeAll(output, context) - context && activate(context) +function restart(): void { + globalOutput && globalOutput.appendLine(alloglot.ui.restartingAlloglot) + deactivate() + globalContext && activate(globalContext) } function makeActivationCommand(parentOutput: IHierarchicalOutputChannel, command: string | undefined): vscode.Disposable { @@ -64,8 +85,9 @@ function makeActivationCommand(parentOutput: IHierarchicalOutputChannel, command const basedir = vscode.workspace.workspaceFolders?.[0].uri const output = parentOutput.split() - return vscode.Disposable.from( - AsyncProcess.make({ output, command, basedir }, () => parentOutput.appendLine(alloglot.ui.activateCommandDone(command))), - output - ) + const proc = AsyncProcess.make({ output, command, basedir }, () => { + parentOutput.appendLine(alloglot.ui.activateCommandDone(command)) + }) + + return vscode.Disposable.from(proc, output) } diff --git a/src/formatter.ts b/src/formatter.ts index 59c8f0c..a942a1f 100644 --- a/src/formatter.ts +++ b/src/formatter.ts @@ -31,7 +31,7 @@ import { AsyncProcess, Disposal } from './utils' /** * Register a custom document formatter for a language. */ -export function makeFormatter(output: vscode.OutputChannel, config: LanguageConfig): vscode.Disposable { +export function makeFormatter(output: vscode.OutputChannel, config: LanguageConfig, verboseOutput: boolean): vscode.Disposable { const { languageId, formatCommand } = config if (!languageId || !formatCommand) return vscode.Disposable.from() @@ -51,7 +51,11 @@ export function makeFormatter(output: vscode.OutputChannel, config: LanguageConf document.lineAt(document.lineCount - 1).rangeIncludingLineBreak.end, ); - const proc = AsyncProcess.make({ output, command, basedir, stdin }, stdout => [new vscode.TextEdit(entireDocument, stdout)]) + const proc = AsyncProcess.make( + { output: verboseOutput ? output : undefined, command, basedir, stdin }, + stdout => [new vscode.TextEdit(entireDocument, stdout)] + ) + disposal.insert(proc) return proc } diff --git a/src/tags.ts b/src/tags.ts index 6206e30..7e4c38b 100644 --- a/src/tags.ts +++ b/src/tags.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode' import { LanguageConfig, StringTransformation, alloglot } from './config' import { AsyncProcess, Disposal, IAsyncProcess, IHierarchicalOutputChannel } from './utils' -export function makeTags(output: IHierarchicalOutputChannel, config: LanguageConfig): vscode.Disposable { +export function makeTags(output: IHierarchicalOutputChannel, config: LanguageConfig, verboseOutput: boolean): vscode.Disposable { const { languageId, tags } = config if (!languageId || !tags) return vscode.Disposable.from() @@ -17,12 +17,12 @@ export function makeTags(output: IHierarchicalOutputChannel, config: LanguageCon if (!completionsProvider && !definitionsProvider && !importsProvider) return vscode.Disposable.from() output.appendLine(alloglot.ui.startingTags) - const tagsSourceOutput = output.local(alloglot.components.tagsSource).split() + const tagsSourceOutput = verboseOutput ? output.local(alloglot.components.tagsSource).split() : undefined const tagsSource = TagsSource.make({ languageId, basedir, tagsUri, output: tagsSourceOutput, initTagsCommand, refreshTagsCommand }) const disposal = Disposal.make() disposal.insert(tagsSource) - disposal.insert(tagsSourceOutput) + tagsSourceOutput && disposal.insert(tagsSourceOutput) if (completionsProvider) { output.appendLine(alloglot.ui.registeringCompletionsProvider) @@ -68,10 +68,11 @@ export function makeTags(output: IHierarchicalOutputChannel, config: LanguageCon if (importsProvider) { output.appendLine(alloglot.ui.registeringImportsProvider) + const importsProviderOutput = verboseOutput ? output.local(alloglot.components.importsProvider).split() : undefined const { matchFromFilepath, importLinePattern, renderModuleName } = importsProvider function applyStringTransformation(cmd: StringTransformation, xs: Array): Array { - output.appendLine(`Applying ${JSON.stringify(cmd)} to ${JSON.stringify(xs)}`) + importsProviderOutput?.appendLine(`Applying ${JSON.stringify(cmd)} to ${JSON.stringify(xs)}`) switch (cmd.tag) { case "replace": return xs.map(x => x.replace(new RegExp(cmd.from, 'g'), cmd.to)) @@ -89,58 +90,58 @@ export function makeTags(output: IHierarchicalOutputChannel, config: LanguageCon } function applyStringTransformations(cmds: Array, x: string): string { - output.appendLine(alloglot.ui.applyingTransformations(cmds, x)) + importsProviderOutput?.appendLine(alloglot.ui.applyingTransformations(cmds, x)) let buffer = [x] cmds.forEach(cmd => buffer = applyStringTransformation(cmd, buffer)) const result = buffer.join() - output.appendLine(alloglot.ui.transformationResult(result)) + importsProviderOutput?.appendLine(alloglot.ui.transformationResult(result)) return result } function renderImportLine(tag: TagsSource.Tag): { renderedImport?: string, renderedModuleName?: string } { - output.appendLine(alloglot.ui.renderingImportLine(tag)) + importsProviderOutput?.appendLine(alloglot.ui.renderingImportLine(tag)) const fileMatcher = new RegExp(matchFromFilepath) - output.appendLine(alloglot.ui.usingFileMatcher(fileMatcher)) + importsProviderOutput?.appendLine(alloglot.ui.usingFileMatcher(fileMatcher)) const match = tag.file.match(fileMatcher) - output.appendLine(alloglot.ui.fileMatcherResult(match)) + importsProviderOutput?.appendLine(alloglot.ui.fileMatcherResult(match)) const symbol = tag.symbol const renderedModuleName = match && match.length > 0 ? applyStringTransformations(renderModuleName, match[0]) : undefined - output.appendLine(alloglot.ui.renderedModuleName(renderedModuleName)) + importsProviderOutput?.appendLine(alloglot.ui.renderedModuleName(renderedModuleName)) const renderedImport = renderedModuleName ? importLinePattern.replace('${module}', renderedModuleName).replace('${symbol}', symbol) + '\n' : undefined - output.appendLine(alloglot.ui.renderedImportLine(renderedImport)) + importsProviderOutput?.appendLine(alloglot.ui.renderedImportLine(renderedImport)) return { renderedImport, renderedModuleName } } function findImportPosition(document: vscode.TextDocument): vscode.Position { - output.appendLine(alloglot.ui.findingImportPosition) + importsProviderOutput?.appendLine(alloglot.ui.findingImportPosition) const importMatcher = new RegExp(importLinePattern.replace('${module}', '(.*)').replace('${symbol}', '(.*)')) const fullText = document.getText().split('\n') const firstImportLine = fullText.findIndex(line => line.match(importMatcher)) if (firstImportLine >= 0) { - output.appendLine(alloglot.ui.foundImportPosition(firstImportLine)) + importsProviderOutput?.appendLine(alloglot.ui.foundImportPosition(firstImportLine)) return new vscode.Position(firstImportLine, 0) } const firstBlankLine = fullText.findIndex(line => line.match(/^\s*$/)) if (firstBlankLine >= 0) { - output.appendLine(alloglot.ui.foundBlankLine(firstBlankLine)) + importsProviderOutput?.appendLine(alloglot.ui.foundBlankLine(firstBlankLine)) return new vscode.Position(firstBlankLine, 0) } - output.appendLine(alloglot.ui.noBlankLineFound) + importsProviderOutput?.appendLine(alloglot.ui.noBlankLineFound) return new vscode.Position(0, 0) } function makeImportSuggestion(document: vscode.TextDocument, tag: TagsSource.Tag): ImportSuggestion | undefined { - output.appendLine(alloglot.ui.makingImportSuggestion(tag)) + importsProviderOutput?.appendLine(alloglot.ui.makingImportSuggestion(tag)) const { renderedImport, renderedModuleName } = renderImportLine(tag) if (!renderedImport || !renderedModuleName) return undefined const position = findImportPosition(document) @@ -156,7 +157,7 @@ export function makeTags(output: IHierarchicalOutputChannel, config: LanguageCon } function runSuggestImports(editor: vscode.TextEditor): void { - output.appendLine(alloglot.ui.runningSuggestImports) + importsProviderOutput?.appendLine(alloglot.ui.runningSuggestImports) const { document, selection } = editor const wordRange = document.getWordRangeAtPosition(selection.start) if (!wordRange) return undefined @@ -165,9 +166,9 @@ export function makeTags(output: IHierarchicalOutputChannel, config: LanguageCon return Array.from(uniqueModules.values()) }) vscode.window.showQuickPick(suggestions).then(pick => { - output.appendLine(`Picked: ${JSON.stringify(pick)}`) + importsProviderOutput?.appendLine(alloglot.ui.pickedSuggestion(pick)) pick?.edit && vscode.workspace.applyEdit(pick.edit).then(success => { - output.appendLine(alloglot.ui.appliedEdit(success)) + importsProviderOutput?.appendLine(alloglot.ui.appliedEdit(success)) }) }) } @@ -175,7 +176,7 @@ export function makeTags(output: IHierarchicalOutputChannel, config: LanguageCon disposal.insert(vscode.commands.registerTextEditorCommand(alloglot.commands.suggestImports, runSuggestImports)) disposal.insert(vscode.languages.registerCodeActionsProvider(languageId, { provideCodeActions(document, range) { - output.appendLine(alloglot.ui.providingCodeActions) + importsProviderOutput?.appendLine(alloglot.ui.providingCodeActions) return getImportSuggestions(document, range).then(xs => xs.map(x => { const action = new vscode.CodeAction(x.label, vscode.CodeActionKind.QuickFix) action.edit = x.edit @@ -211,14 +212,14 @@ namespace TagsSource { languageId: string, basedir: vscode.Uri, tagsUri: vscode.Uri, - output: vscode.OutputChannel, + output?: vscode.OutputChannel, initTagsCommand?: string, refreshTagsCommand?: string } export function make(config: Config): ITagsSource { const { languageId, basedir, tagsUri, output, initTagsCommand, refreshTagsCommand } = config - output.appendLine(alloglot.ui.creatingTagsSource(tagsUri.fsPath)) + output?.appendLine(alloglot.ui.creatingTagsSource(tagsUri.fsPath)) const disposal = Disposal.make() @@ -244,7 +245,7 @@ namespace TagsSource { findPrefix(prefix, limit = 100) { if (!prefix) return Promise.resolve([]) const escaped = prefix.replace(/(["\s'$`\\])/g, '\\$1') - const proc = grep(config, output, new RegExp(`^${escaped}`), limit) + const proc = grep(config, new RegExp(`^${escaped}`), limit, output) disposal.insert(proc) return proc }, @@ -252,7 +253,7 @@ namespace TagsSource { findExact(exact, limit = 100) { if (!exact) return Promise.resolve([]) const escaped = exact.replace(/(["\s'$`\\])/g, '\\$1') - const proc = grep(config, output, new RegExp(`^${escaped}\\t`), limit) + const proc = grep(config, new RegExp(`^${escaped}\\t`), limit, output) disposal.insert(proc) return proc }, @@ -264,21 +265,21 @@ namespace TagsSource { } } - function grep(config: Config, output: vscode.OutputChannel, regexp: RegExp, limit: number): IAsyncProcess> { + function grep(config: Config, regexp: RegExp, limit: number, output?: vscode.OutputChannel): IAsyncProcess> { const { tagsUri, basedir } = config const command = `grep -P '${regexp.source}' ${tagsUri.fsPath} | head -n ${limit}` - output.appendLine(`Searching for ${regexp} in ${tagsUri.fsPath}...`) - return AsyncProcess.make({ output, command, basedir }, stdout => filterMap(stdout.split('\n'), line => parseTag(output, line))) + output?.appendLine(`Searching for ${regexp} in ${tagsUri.fsPath}...`) + return AsyncProcess.make({ output, command, basedir }, stdout => filterMap(stdout.split('\n'), line => parseTag(line, output))) } - function parseTag(output: vscode.OutputChannel, line: string): Tag | undefined { - output.appendLine(alloglot.ui.parsingTagLine(line)) + function parseTag(line: string, output?: vscode.OutputChannel): Tag | undefined { + output?.appendLine(alloglot.ui.parsingTagLine(line)) const [symbol, file, rawLineNumber] = line.split('\t') let lineNumber = parseInt(rawLineNumber) if (!symbol || !file || !lineNumber) return undefined const tag = { symbol, file, lineNumber } - output.appendLine(alloglot.ui.parsedTagLine(tag)) + output?.appendLine(alloglot.ui.parsedTagLine(tag)) return tag } diff --git a/src/utils.ts b/src/utils.ts index e96a376..12ab5f9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -13,6 +13,7 @@ export namespace Disposal { */ export function make(): IDisposal { const disposables: Array = [] + return { insert(disposable) { disposables.push(disposable) @@ -29,10 +30,10 @@ export interface IAsyncProcess extends vscode.Disposable, Promise { } export namespace AsyncProcess { type Spec = { - output: vscode.OutputChannel command: string basedir?: vscode.Uri stdin?: string + output?: vscode.OutputChannel } /** @@ -50,35 +51,36 @@ export namespace AsyncProcess { // giving this an `any` signature allows us to add a `dispose` method. // it's a little bit jank, but i don't know how else to do it. const asyncProc: any = new Promise((resolve, reject) => { - output.appendLine(`Running '${command}' in '${cwd}'...`) + output?.appendLine(alloglot.ui.runningCommand(command, cwd)) const proc = exec(command, { cwd, signal }, (error, stdout, stderr) => { if (error) { - output.appendLine(alloglot.ui.errorRunningCommand(command, error)) + output?.appendLine(alloglot.ui.errorRunningCommand(command, error)) reject(error) } - stderr && output.appendLine(alloglot.ui.commandLogs(command, stderr)) - !stdout && output.appendLine(alloglot.ui.commandNoOutput(command)) + stderr && output?.appendLine(alloglot.ui.commandLogs(command, stderr)) + !stdout && output?.appendLine(alloglot.ui.commandNoOutput(command)) resolve(f(stdout)) }) - proc.stdout?.on('data', chunk => output.append(stripAnsi(chunk))) + proc.stdout?.on('data', chunk => output?.append(stripAnsi(chunk))) stdin && proc.stdin?.write(stdin) proc.stdin?.end() - output.appendLine(alloglot.ui.ranCommand(command)) }) asyncProc.dispose = () => { if (controller) { - output.appendLine(alloglot.ui.killingCommand(command)) + output?.appendLine(alloglot.ui.killingCommand(command)) controller.abort() controller = undefined // ensure `dispose()` is idempotent - output.appendLine(alloglot.ui.commandKilled(command)) + output?.appendLine(alloglot.ui.commandKilled(command)) } } + asyncProc.then(() => output?.appendLine(alloglot.ui.ranCommand(command))) + return asyncProc as IAsyncProcess }