diff --git a/TODO b/TODO index 1b26ffc..694dab4 100644 --- a/TODO +++ b/TODO @@ -1,27 +1,26 @@ MVP === - lint everything - delete lint on close file -- reasonable code layout TODO ==== - lint current file - [v] command clear all issues Long Term Desired Features ========================== - Fancy UIs for - arc patch - arc todo - arc unit - arc browse - arc paste (upload and download) - list open Differential in repo (for review) - get hovercard (content) for any object - push notifications from website ? - preview Remarkup - some magic to help the External Editor Link - arc lint: special-case some messages for better visibility (find more range/information.) - arc lint: support auto-fix (apply diff) - arc lint: `locations` field may be parsed into `relatedInformation`. diff --git a/src/arc_lint.ts b/src/arc_lint.ts new file mode 100644 index 0000000..d2ba33a --- /dev/null +++ b/src/arc_lint.ts @@ -0,0 +1,136 @@ +import * as vscode from 'vscode'; +import * as execa from 'execa'; +import * as path from 'path'; + +import { nonNeg } from './misc'; +import { ArcanistLintMessage } from './arcanist_types'; +import { setupCustomTranslators } from './arc_lint_translators'; + + +export function setup() { + setupCustomTranslators(customLintTranslator); + updateLintSeverityMap(); +} + +export function lintFile(document: vscode.TextDocument, errorCollection: vscode.DiagnosticCollection) { + if (document.uri.scheme != "file") return; + + function handleExecResult(value: execa.ExecaReturnValue) { + if (!value.stdout) return; + try { + const lintMessages = JSON.parse(value.stdout); + + for (const filename in lintMessages) { + // TODO: This only probably works because we call arc with a single file. + errorCollection.set(document.uri, lintJsonToDiagnostics(lintMessages[filename])); + } + } catch { + console.log("ppfff"); + } + } + + const filename = document.uri.path; + + execa( + 'arc', ['lint', '--output', 'json', '--', path.basename(filename)], + { cwd: path.dirname(filename) }, + ).then(handleExecResult, handleExecResult); +} + + /** + input: + ``` + { + "line": 248, + "char": 23, + "code": "SPELL1", + "severity": "warning", + "name": "Possible Spelling Mistake", + "description": "Possible spelling error. You wrote 'seperator', but did you mean 'separator'?", + "original": "Seperator", + "replacement": "Separator", + "granularity": 1, + "locations": [], + "bypassChangedLineFiltering": null, + "context": " magic = COLOR_RED;\n break;\n case 30:\n // printf(\"Record Seperator\");\n magic = COLOR_BLUE;\n break;\n case 31:" + } + ``` + output: + ``` + { + code: '', + message: 'cannot assign twice to immutable variable `x`', + range: new vscode.Range(new vscode.Position(3, 4), new vscode.Position(3, 10)), + severity: vscode.DiagnosticSeverity.Error, + source: '', + relatedInformation: [ + new vscode.DiagnosticRelatedInformation(new vscode.Location(document.uri, new vscode.Range(new vscode.Position(1, 8), new vscode.Position(1, 9))), 'first assignment to `x`') + ] + } + ``` + + Possible Extra features: + - quick-fix to apply patch + - try to get better message by parsing `description` field (per message code...) + - `locations` may be parsed into `relatedInformation`. + */ +export type LintTranslator = (lint: ArcanistLintMessage) => vscode.Diagnostic; +let customLintTranslator: Map = new Map(); + + +export function defaultLintTranslator(lint: ArcanistLintMessage): vscode.Diagnostic { + return { + code: lint.code, + message: message(lint), + severity: severity(lint), + source: 'arc lint', + range: new vscode.Range( + lint.line - 1, nonNeg(lint.char - 2), // it's an artificial 3-chars wide thing. + lint.line - 1, lint.char + 1), + }; +} + +function message(lint: ArcanistLintMessage) { + if (lint.description) + return lint.name + ": " + lint.description + return lint.name +} + +let lintSeverityMap: Map; +export function updateLintSeverityMap(): void { + let config = vscode.workspace.getConfiguration('arc-vscode.lint'); + let maxLevel: vscode.DiagnosticSeverity; + switch (config.maxDiagnosticsLevel as string) { + case 'hint': maxLevel = vscode.DiagnosticSeverity.Hint; break; + case 'info': maxLevel = vscode.DiagnosticSeverity.Information; break; + case 'warning': maxLevel = vscode.DiagnosticSeverity.Warning; break; + case 'error': + default: + maxLevel = vscode.DiagnosticSeverity.Error; break; + } + + function capped(level: vscode.DiagnosticSeverity): vscode.DiagnosticSeverity { + return level > maxLevel ? level : maxLevel; + } + + lintSeverityMap = new Map(); + lintSeverityMap.set('disabled', capped(vscode.DiagnosticSeverity.Hint)) + lintSeverityMap.set('autofix', capped(vscode.DiagnosticSeverity.Information)) + lintSeverityMap.set('advice', capped(vscode.DiagnosticSeverity.Information)) + lintSeverityMap.set('warning', capped(vscode.DiagnosticSeverity.Warning)) + lintSeverityMap.set('error', capped(vscode.DiagnosticSeverity.Error)) +} + +function severity(lint: ArcanistLintMessage): vscode.DiagnosticSeverity { + return lintSeverityMap.get(lint.severity as string) || vscode.DiagnosticSeverity.Error; +} + + +function lintJsonToDiagnostics(lintResults: Array): vscode.Diagnostic[] { + function translate(lint: ArcanistLintMessage): vscode.Diagnostic { + let t = customLintTranslator.get(lint.code) || defaultLintTranslator; + return t(lint); + } + + return lintResults.map(translate); +} diff --git a/src/arc_lint_translators.ts b/src/arc_lint_translators.ts new file mode 100644 index 0000000..4178952 --- /dev/null +++ b/src/arc_lint_translators.ts @@ -0,0 +1,48 @@ +import * as vscode from 'vscode'; + +import { LintTranslator, defaultLintTranslator } from './arc_lint' +import { nonNeg } from './misc'; + + +export function setupCustomTranslators(translators: Map) { + translators.set("SPELL1", lint => { + let d = defaultLintTranslator(lint) + + d.message = lint.description; + let len = (lint.original).length; + if (len > 0) { + d.range = new vscode.Range( + lint.line - 1, nonNeg(lint.char - 1), + lint.line - 1, lint.char + len - 1); + } + + return d + }); + + // "This line is 116 characters long, but the convention is 80 characters." + const re_TXT3_length = /\D(\d+) characters\.$/; + + translators.set('E501', lint => { + let d = defaultLintTranslator(lint) + + d.range = new vscode.Range( + lint.line - 1, lint.char - 1, + lint.line - 1, 1e9); + + return d; + }); + + translators.set('TXT3', lint => { + let d = defaultLintTranslator(lint) + + let match = (lint.description).match(re_TXT3_length); + if (match) { + let len = parseInt(match[1]); + d.range = new vscode.Range( + lint.line - 1, len, + lint.line - 1, 1e9); + } + + return d; + }); +} diff --git a/src/extension.ts b/src/extension.ts index 954ccf1..51646ab 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,218 +1,44 @@ import * as vscode from 'vscode'; -import * as execa from "execa"; -import * as path from "path"; -import { ArcanistLintMessage } from './arcanist_types'; - +import * as lint from './arc_lint'; export function activate(context: vscode.ExtensionContext) { const collection = vscode.languages.createDiagnosticCollection('arc lint'); - setupCustomTranslators(); - updateLintSeverityMap(); + lint.setup(); function d(disposable: vscode.Disposable) { context.subscriptions.push(disposable); } d(vscode.commands.registerCommand('arc-vscode.clearLint', () => collection.clear())); d(vscode.workspace.onDidSaveTextDocument(onTextDocumentEvent)); d(vscode.workspace.onDidOpenTextDocument(onTextDocumentEvent)); d(vscode.workspace.onDidChangeConfiguration(onChangeConfig)); if (vscode.window.activeTextEditor) { - lintFile(vscode.window.activeTextEditor.document); + lint.lintFile(vscode.window.activeTextEditor.document, collection); } d(vscode.window.onDidChangeActiveTextEditor(editor => { if (editor) { - lintFile(editor.document); + lint.lintFile(editor.document, collection); } })); function onTextDocumentEvent(document: vscode.TextDocument) { - lintFile(document); - } - - function lintFile(document: vscode.TextDocument) { - if (document.uri.scheme != "file") return; - - function handleExecResult(value: execa.ExecaReturnValue) { - if (!value.stdout) return; - try { - const lintMessages = JSON.parse(value.stdout); - - for (const filename in lintMessages) { - // TODO: This only probably works because we call arc with a single file. - collection.set(document.uri, lintJsonToDiagnostics(lintMessages[filename])); - } - } catch { - logError("ppfff"); - } - } - - const filename = document.uri.path; - - execa( - 'arc', ['lint', '--output', 'json', '--', path.basename(filename)], - { cwd: path.dirname(filename) }, - ).then(handleExecResult, handleExecResult); + lint.lintFile(document, collection); } } export function deactivate() { // TODO collection.clear(); } function onChangeConfig(e: vscode.ConfigurationChangeEvent) { if (!e.affectsConfiguration('arc-vscode.lint')) { return; } - updateLintSeverityMap(); -} - -function logError(x: any) { - console.log("this is error", x); -} - -type LintTranslator = (lint: ArcanistLintMessage) => vscode.Diagnostic; -let customLintTranslator: Map = new Map(); - -function lintJsonToDiagnostics(lintResults: Array): vscode.Diagnostic[] { - /* - input: Array of: - { - "line": 248, - "char": 23, - "code": "SPELL1", - "severity": "warning", - "name": "Possible Spelling Mistake", - "description": "Possible spelling error. You wrote 'seperator', but did you mean 'separator'?", - "original": "Seperator", - "replacement": "Separator", - "granularity": 1, - "locations": [], - "bypassChangedLineFiltering": null, - "context": " magic = COLOR_RED;\n break;\n case 30:\n // printf(\"Record Seperator\");\n magic = COLOR_BLUE;\n break;\n case 31:" - } - output: array of: - { - code: '', - message: 'cannot assign twice to immutable variable `x`', - range: new vscode.Range(new vscode.Position(3, 4), new vscode.Position(3, 10)), - severity: vscode.DiagnosticSeverity.Error, - source: '', - relatedInformation: [ - new vscode.DiagnosticRelatedInformation(new vscode.Location(document.uri, new vscode.Range(new vscode.Position(1, 8), new vscode.Position(1, 9))), 'first assignment to `x`') - ] - } - */ - - /* - Extra features: - - quick-fix to apply patch - - try to get better message by parsing `description` field (per message code...) - - `locations` may be parsed into `relatedInformation`. - */ - - function translate(lint: ArcanistLintMessage): vscode.Diagnostic { - let t = customLintTranslator.get(lint.code) || defaultTranslate; - return t(lint); - } - - return lintResults.map(translate); -} - -function nonNeg(n: number): number { - return n < 0 ? 0 : n -} - -let lintSeverityMap: Map; -function updateLintSeverityMap(): void { - let config = vscode.workspace.getConfiguration('arc-vscode.lint'); - let maxLevel: vscode.DiagnosticSeverity; - switch (config.maxDiagnosticsLevel as string) { - case 'hint': maxLevel = vscode.DiagnosticSeverity.Hint; break; - case 'info': maxLevel = vscode.DiagnosticSeverity.Information; break; - case 'warning': maxLevel = vscode.DiagnosticSeverity.Warning; break; - case 'error': - default: - maxLevel = vscode.DiagnosticSeverity.Error; break; - } - - function capped(level: vscode.DiagnosticSeverity): vscode.DiagnosticSeverity { - return level > maxLevel ? level : maxLevel; - } - - lintSeverityMap = new Map(); - lintSeverityMap.set('disabled', capped(vscode.DiagnosticSeverity.Hint)) - lintSeverityMap.set('autofix', capped(vscode.DiagnosticSeverity.Information)) - lintSeverityMap.set('advice', capped(vscode.DiagnosticSeverity.Information)) - lintSeverityMap.set('warning', capped(vscode.DiagnosticSeverity.Warning)) - lintSeverityMap.set('error', capped(vscode.DiagnosticSeverity.Error)) -} - -function severity(lint: ArcanistLintMessage): vscode.DiagnosticSeverity { - return lintSeverityMap.get(lint.severity as string) || vscode.DiagnosticSeverity.Error; -} - -function defaultTranslate(lint: ArcanistLintMessage): vscode.Diagnostic { - return { - code: lint.code, - message: message(lint), - severity: severity(lint), - source: 'arc lint', - range: new vscode.Range( - lint.line - 1, nonNeg(lint.char - 2), // it's an artificial 3-chars wide thing. - lint.line - 1, lint.char + 1), - }; -} -function message(lint: ArcanistLintMessage) { - if (lint.description) - return lint.name + ": " + lint.description - return lint.name -} - -function setupCustomTranslators() { - customLintTranslator.set("SPELL1", lint => { - let d = defaultTranslate(lint) - - d.message = lint.description; - let len = (lint.original).length; - if (len > 0) { - d.range = new vscode.Range( - lint.line - 1, nonNeg(lint.char - 1), - lint.line - 1, lint.char + len - 1); - } - - return d - }); - - // "This line is 116 characters long, but the convention is 80 characters." - const re_TXT3_length = /\D(\d+) characters\.$/; - - customLintTranslator.set('E501', lint => { - let d = defaultTranslate(lint) - - d.range = new vscode.Range( - lint.line - 1, lint.char - 1, - lint.line - 1, 1e9); - - return d; - }); - - customLintTranslator.set('TXT3', lint => { - let d = defaultTranslate(lint) - - let match = (lint.description).match(re_TXT3_length); - if (match) { - let len = parseInt(match[1]); - d.range = new vscode.Range( - lint.line - 1, len, - lint.line - 1, 1e9); - } - - return d; - }); + lint.updateLintSeverityMap(); } diff --git a/src/misc.ts b/src/misc.ts new file mode 100644 index 0000000..dece459 --- /dev/null +++ b/src/misc.ts @@ -0,0 +1,3 @@ +export function nonNeg(n: number): number { + return n < 0 ? 0 : n +}