Skip to content

Commit

Permalink
[DUX-2091] handle absolute paths in annotations (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
friedbrice authored Mar 28, 2024
1 parent 03f367c commit 0f4e5f1
Showing 1 changed file with 88 additions and 80 deletions.
168 changes: 88 additions & 80 deletions src/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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] }
)

Expand All @@ -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<string>(cfg.mapping.message)
const filePath = utils.path<string>(cfg.mapping.file)
const startLinePath = utils.path<number>(cfg.mapping.startLine)
const startColumnPath = utils.path<number>(cfg.mapping.startColumn)
const endLinePath = utils.path<number>(cfg.mapping.endLine)
const endColumnPath = utils.path<number>(cfg.mapping.endColumn)
const sourcePath = utils.path<string>(cfg.mapping.source)
const severityPath = utils.path<string>(cfg.mapping.severity)
const replacementsPath = utils.path<string | Array<string>>(cfg.mapping.replacements)
const referenceCodePath = utils.path<string | number>(cfg.mapping.referenceCode)
const messagePath = path<string>(cfg.mapping.message)
const filePath = path<string>(cfg.mapping.file)
const startLinePath = path<number>(cfg.mapping.startLine)
const startColumnPath = path<number>(cfg.mapping.startColumn)
const endLinePath = path<number>(cfg.mapping.endLine)
const endColumnPath = path<number>(cfg.mapping.endColumn)
const sourcePath = path<string>(cfg.mapping.source)
const severityPath = path<string>(cfg.mapping.severity)
const replacementsPath = path<string | Array<string>>(cfg.mapping.replacements)
const referenceCodePath = path<string | number>(cfg.mapping.referenceCode)

function marshalAnnotation(json: any): Annotation | undefined {
const message = messagePath(json)
Expand All @@ -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(),
}
}
Expand All @@ -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)
})
})
}
Expand All @@ -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<vscode.CodeAction> {
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<vscode.CodeAction> {
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<T>(keys: Array<string> | 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<T>(keys: Array<string> | 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)
}

0 comments on commit 0f4e5f1

Please sign in to comment.