diff --git a/src/annotations.ts b/src/annotations.ts index e9ebf76..d0bd176 100644 --- a/src/annotations.ts +++ b/src/annotations.ts @@ -33,7 +33,7 @@ Portions of this software are derived from [ghcid](https://github.com/ndmitchell > OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import { dirname } from 'path' +import { dirname, isAbsolute } from 'path' import * as vscode from 'vscode' import { Annotation, AnnotationsConfig, LanguageConfig, alloglot } from './config' @@ -47,7 +47,7 @@ export function makeAnnotations(output: vscode.OutputChannel, config: LanguageCo const quickFixes = vscode.languages.registerCodeActionsProvider( languageId, - { provideCodeActions: (document, range, context) => context.diagnostics.map(utils.asQuickFixes).flat() }, + { provideCodeActions: (document, range, context) => context.diagnostics.map(asQuickFixes).flat() }, { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] } ) @@ -61,16 +61,16 @@ export function makeAnnotations(output: vscode.OutputChannel, config: LanguageCo function watchAnnotationsFile(languageId: string, cfg: AnnotationsConfig): vscode.Disposable { const diagnostics = vscode.languages.createDiagnosticCollection(`${alloglot.collections.annotations}-${languageId}-${cfg.file}`) - const messagePath = utils.path(cfg.mapping.message) - const filePath = utils.path(cfg.mapping.file) - const startLinePath = utils.path(cfg.mapping.startLine) - const startColumnPath = utils.path(cfg.mapping.startColumn) - const endLinePath = utils.path(cfg.mapping.endLine) - const endColumnPath = utils.path(cfg.mapping.endColumn) - const sourcePath = utils.path(cfg.mapping.source) - const severityPath = utils.path(cfg.mapping.severity) - const replacementsPath = utils.path>(cfg.mapping.replacements) - const referenceCodePath = utils.path(cfg.mapping.referenceCode) + const messagePath = path(cfg.mapping.message) + const filePath = path(cfg.mapping.file) + const startLinePath = path(cfg.mapping.startLine) + const startColumnPath = path(cfg.mapping.startColumn) + const endLinePath = path(cfg.mapping.endLine) + const endColumnPath = path(cfg.mapping.endColumn) + const sourcePath = path(cfg.mapping.source) + const severityPath = path(cfg.mapping.severity) + const replacementsPath = path>(cfg.mapping.replacements) + const referenceCodePath = path(cfg.mapping.referenceCode) function marshalAnnotation(json: any): Annotation | undefined { const message = messagePath(json) @@ -90,7 +90,7 @@ function watchAnnotationsFile(languageId: string, cfg: AnnotationsConfig): vscod return { message, file, startLine, startColumn, endLine, endColumn, replacements, source: sourcePath(json) || `${cfg.file}`, - severity: utils.parseSeverity(severityPath(json)), + severity: parseSeverity(severityPath(json)), referenceCode: referenceCodePath(json)?.toString(), } } @@ -115,16 +115,14 @@ function watchAnnotationsFile(languageId: string, cfg: AnnotationsConfig): vscod return sorted } - function clearAnnotations(uri: vscode.Uri): void { + function addAnnotations(annFile: vscode.Uri): void { diagnostics.clear() - } - - function addAnnotations(uri: vscode.Uri): void { - clearAnnotations(uri) - const basedir = vscode.Uri.file(dirname(uri.fsPath)) - vscode.workspace.fs.readFile(uri).then(bytes => { - annotationsBySourceFile(readAnnotations(bytes)).forEach((anns, file) => { - diagnostics.set(vscode.Uri.joinPath(basedir, file), anns.map(ann => utils.annotationAsDiagnostic(basedir, ann))) + const basedir = vscode.Uri.file(dirname(annFile.fsPath)) + vscode.workspace.fs.readFile(annFile).then(bytes => { + annotationsBySourceFile(readAnnotations(bytes)).forEach((anns, srcFile) => { + const srcUri = fileUri(basedir, srcFile) + const diags = anns.map(ann => annotationAsDiagnostic(basedir, ann)) + diagnostics.set(srcUri, diags) }) }) } @@ -135,75 +133,85 @@ function watchAnnotationsFile(languageId: string, cfg: AnnotationsConfig): vscod watcher.onDidChange(addAnnotations) watcher.onDidCreate(addAnnotations) - watcher.onDidDelete(clearAnnotations) + watcher.onDidDelete(() => diagnostics.clear()) return watcher }) || [] - return vscode.Disposable.from(...watchers) + const cleanup = vscode.workspace.onDidSaveTextDocument(doc => { + if (doc.languageId === languageId) { + diagnostics.delete(doc.uri) + } + }) + + return vscode.Disposable.from(cleanup, ...watchers) } -namespace utils { - export function annotationAsDiagnostic(basedir: vscode.Uri, ann: Annotation): vscode.Diagnostic { - const range = new vscode.Range( - new vscode.Position(ann.startLine - 1, ann.startColumn - 1), - new vscode.Position(ann.endLine - 1, ann.endColumn - 1) - ) - - // we are abusing the relatedInformation field to store replacements - // we look them up later when we need to create quick fixes - const relatedInformation = ann.replacements.map(replacement => { - const uri = vscode.Uri.joinPath(basedir, ann.file) - const location = new vscode.Location(uri, range) - return new vscode.DiagnosticRelatedInformation(location, replacement) - }) +function annotationAsDiagnostic(basedir: vscode.Uri, ann: Annotation): vscode.Diagnostic { + const range = new vscode.Range( + new vscode.Position(ann.startLine - 1, ann.startColumn - 1), + new vscode.Position(ann.endLine - 1, ann.endColumn - 1) + ) - // i wish they gave an all-args constructor - const diagnostic = new vscode.Diagnostic(range, ann.message, asDiagnosticSeverity(ann.severity)) - diagnostic.source = ann.source - diagnostic.relatedInformation = relatedInformation - diagnostic.code = ann.referenceCode - return diagnostic - } + // we are abusing the relatedInformation field to store replacements + // we look them up later when we need to create quick fixes + const relatedInformation = ann.replacements.map(replacement => { + const srcUri = fileUri(basedir, ann.file) + const srcLocation = new vscode.Location(srcUri, range) + return new vscode.DiagnosticRelatedInformation(srcLocation, replacement) + }) + + // i wish they gave an all-args constructor + const diagnostic = new vscode.Diagnostic(range, ann.message, asDiagnosticSeverity(ann.severity)) + diagnostic.source = ann.source + diagnostic.relatedInformation = relatedInformation + diagnostic.code = ann.referenceCode + return diagnostic +} - function asDiagnosticSeverity(sev: Annotation['severity']): vscode.DiagnosticSeverity { - switch (sev) { - case 'error': return vscode.DiagnosticSeverity.Error - case 'warning': return vscode.DiagnosticSeverity.Warning - case 'info': return vscode.DiagnosticSeverity.Information - case 'hint': return vscode.DiagnosticSeverity.Hint - } +function asDiagnosticSeverity(sev: Annotation['severity']): vscode.DiagnosticSeverity { + switch (sev) { + case 'error': return vscode.DiagnosticSeverity.Error + case 'warning': return vscode.DiagnosticSeverity.Warning + case 'info': return vscode.DiagnosticSeverity.Information + case 'hint': return vscode.DiagnosticSeverity.Hint } +} - // this depends on the fact that we're abusing the `relatedInformation` field - // see `annotationAsDiagnostic` above - export function asQuickFixes(diag: vscode.Diagnostic): Array { - const actions = diag.relatedInformation?.map(info => { - const action = new vscode.CodeAction(diag.message, vscode.CodeActionKind.QuickFix) - action.diagnostics = [diag] - action.edit = new vscode.WorkspaceEdit - action.edit.replace(info.location.uri, info.location.range, info.message) - return action - }) - return actions || [] - } +// this depends on the fact that we're abusing the `relatedInformation` field +// see `annotationAsDiagnostic` above +function asQuickFixes(diag: vscode.Diagnostic): Array { + const actions = diag.relatedInformation?.map(info => { + const action = new vscode.CodeAction(diag.message, vscode.CodeActionKind.QuickFix) + action.diagnostics = [diag] + action.edit = new vscode.WorkspaceEdit + action.edit.replace(info.location.uri, info.location.range, info.message) + return action + }) + return actions || [] +} - export function path(keys: Array | undefined): (json: any) => T | undefined { - if (!keys) return () => undefined - else return json => { - const result = keys.reduce((acc, key) => acc?.[key], json) - if (result) return result as T - else return - } +function path(keys: Array | undefined): (json: any) => T | undefined { + if (!keys) return () => undefined + else return json => { + const result = keys.reduce((acc, key) => acc?.[key], json) + if (result) return result as T + else return undefined } +} - export function parseSeverity(raw: string | undefined): Annotation['severity'] { - if (!raw) return 'error' - const lower = raw.toLowerCase() - if (lower.includes('error')) return 'error' - if (lower.includes('warning')) return 'warning' - if (lower.includes('info')) return 'info' - if (lower.includes('hint')) return 'hint' - return 'error' - } +function parseSeverity(raw: string | undefined): Annotation['severity'] { + if (!raw) return 'error' + const lower = raw.toLowerCase() + if (lower.includes('error')) return 'error' + if (lower.includes('warning')) return 'warning' + if (lower.includes('info')) return 'info' + if (lower.includes('hint')) return 'hint' + return 'error' +} + +function fileUri(basedir: vscode.Uri, file: string): vscode.Uri { + return isAbsolute(file) + ? vscode.Uri.file(file) + : vscode.Uri.joinPath(basedir, file) }