Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DUX-2091] handle absolute paths in annotations #66

Merged
merged 3 commits into from
Mar 28, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
Loading