diff --git a/TODO b/TODO index eda77f1..9793bc5 100644 --- a/TODO +++ b/TODO @@ -1,20 +1,20 @@ TODO ==== -- lint everything +(There's nothing left to do. All work is complete.) Long Term Desired Features ========================== - Fancy UIs for - quick create task (arc todo) - arc unit (show results in vscode-native way) - arc browse - arc paste (upload and download) - list open Differential Revision in repo (for review) - Also arc-patch them - get hovercard (content) for any object - push notifications from website? - preview Remarkup (like the markdown preview, but by calling the server) - 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/package.json b/package.json index f6214b2..c9bbbd9 100644 --- a/package.json +++ b/package.json @@ -1,76 +1,80 @@ { "name": "arc-vscode", "displayName": "arc vscode", "description": "Arcanist (Phabricator) support for VSCode", "version": "0.0.1", "engines": { "vscode": "^1.46.0" }, "categories": [ "Other" ], "activationEvents": [ "workspaceContains:**/.arclint", "workspaceContains:**/.arcconfig" ], "main": "./out/extension.js", "contributes": { "commands": [ { "command": "arc-vscode.clearLint", "title": "Clear all arc-lint messages" + }, + { + "command": "arc-vscode.lintEverything", + "title": "arc lint --everything" } ], "configuration": { "title": "Arcanist", "properties": { "arc-vscode.lint.maxDiagnosticsLevel": { "type": "string", "default": "error", "enum": [ "error", "warning", "info", "hint" ], "description": "The maximum level a lint can appear at." } } }, "languages": [ { "id": "json", "extensions": [ ".arcconfig", ".arclint", ".arcrc", ".arcunit" ] } ] }, "scripts": { "vscode:prepublish": "npm run compile", "compile": "tsc -p ./", "lint": "eslint src --ext ts", "watch": "tsc -watch -p ./", "pretest": "npm run compile && npm run lint", "test": "node ./out/test/runTest.js" }, "devDependencies": { "@types/vscode": "^1.46.0", "@types/glob": "^7.1.1", "@types/mocha": "^7.0.2", "@types/node": "^13.11.0", "eslint": "^6.8.0", "@typescript-eslint/parser": "^2.30.0", "@typescript-eslint/eslint-plugin": "^2.30.0", "glob": "^7.1.6", "mocha": "^7.1.2", "typescript": "^3.8.3", "vscode-test": "^1.3.0" }, "dependencies": { "execa": "^4.0.2" } } diff --git a/src/arc_lint.ts b/src/arc_lint.ts index 2635d3f..eeb072c 100644 --- a/src/arc_lint.ts +++ b/src/arc_lint.ts @@ -1,138 +1,168 @@ 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) { errorCollection.delete(document.uri); 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"); + } catch (e) { + console.log("Ignoring error", e); } } const filename = document.uri.path; execa( 'arc', ['lint', '--output', 'json', '--', path.basename(filename)], { cwd: path.dirname(filename) }, ).then(handleExecResult, handleExecResult); } +export function lintEverything(errorCollection: vscode.DiagnosticCollection) { + if (!vscode.workspace.workspaceFolders) { return; } + + for (const folder of vscode.workspace.workspaceFolders) { + + function handleArcLintEverything(value: execa.ExecaReturnValue) { + // The output is best described as "json lines" - each line is a complete + // json object, with one key (filename). This might be a bug in Arcanist. + for (const line of value.stdout.split(/\r?\n/)) { + try { + const lintMessages = JSON.parse(line); + for (const filename in lintMessages) { + const fileUri = vscode.Uri.joinPath(folder.uri, filename); + errorCollection.set(fileUri, lintJsonToDiagnostics(lintMessages[filename])); + } + } catch (e) { + console.log("Ignoring error", e); + } + } + } + + if (folder.uri.scheme === "file") { + execa( + 'arc', ['lint', '--output', 'json', '--everything'], + { cwd: folder.uri.fsPath }, + ).then(handleArcLintEverything, handleArcLintEverything); + } + } +} + /** 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/extension.ts b/src/extension.ts index 976089c..5c0c6d4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,45 +1,46 @@ import * as vscode from 'vscode'; import * as lint from './arc_lint'; export function activate(context: vscode.ExtensionContext) { const diagnostics = vscode.languages.createDiagnosticCollection('arc lint'); lint.setup(); function d(disposable: vscode.Disposable) { context.subscriptions.push(disposable); } d(diagnostics); d(vscode.commands.registerCommand('arc-vscode.clearLint', () => diagnostics.clear())); + d(vscode.commands.registerCommand('arc-vscode.lintEverything', () => lint.lintEverything(diagnostics))); d(vscode.workspace.onDidSaveTextDocument(onTextDocumentEvent)); d(vscode.workspace.onDidOpenTextDocument(onTextDocumentEvent)); d(vscode.workspace.onDidChangeConfiguration(onChangeConfig)); if (vscode.window.activeTextEditor) { lint.lintFile(vscode.window.activeTextEditor.document, diagnostics); } d(vscode.window.onDidChangeActiveTextEditor(editor => { if (editor) { lint.lintFile(editor.document, diagnostics); } })); d(vscode.workspace.onDidCloseTextDocument(document => diagnostics.delete(document.uri))); function onTextDocumentEvent(document: vscode.TextDocument) { lint.lintFile(document, diagnostics); } } export function deactivate() { } function onChangeConfig(e: vscode.ConfigurationChangeEvent) { if (!e.affectsConfiguration('arc-vscode.lint')) { return; } lint.updateLintSeverityMap(); }