diff --git a/README.md b/README.md index 0a40e75..294e0c7 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,37 @@ # Arcanist: A VSCode Extension An extension for getting some of [Phabricator/Arcanist](https://phacility.com/phabricator/) features integrated with Visual Studio Code. ## Features * Show arc-lint notes in editor \ ![arc lint](images/lint.png) * Open file in Diffusion \ ![arc browse](images/browse.png) +* Show hovercards for Phabricator object mentions \ +![hovercard](images/hovercard.png) * Recognize Arcanist files as JSON ## Requirements This extension requires Arcanist to be installed and configured for the directory you work in; `arc` and all your linters should be in the PATH for vscode to be able to run them. \ Only reasonably recent versions of Arcanist are supported. ## Known Issues * Lints do not update as you type - you must save a file for changes to take effect. Lints also might drift when making large changes. * Most lints only show up as 3-characters-squiggle, which is hard to see.\ We suggest users install the `usernamehw.errorlens` extension. - +* When working with multiple Phabricator servers, the hovercard cache may get confused. ## Disclaimers This extension is not affiliated with, nor is it supported by, [Phacility](https://phacility.com/) or the Phabricator project. All trademarks are property of their respective owners. diff --git a/TODO b/TODO index 7beba86..b2b137e 100644 --- a/TODO +++ b/TODO @@ -1,25 +1,24 @@ TODO ==== - Some UI hint that arc-lint is running. Long Term Desired Features ========================== - Fancy UIs for - quick create task (arc todo) - arc unit (show results in vscode-native way, show code coverage) - arc paste (upload and download) - list open Differential Revision in repo (for review) - Also arc-patch them -- get hovercard (content) for any object - 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`. Features we Can have, but maybe we Shouldn't ============================================ - "recent activity" feed? - UI to show server-side blame (`diffusion.blame`), probably only useful for svn or linux.git. - push notifications from website? diff --git a/images/hovercard.png b/images/hovercard.png new file mode 100644 index 0000000..71a0723 Binary files /dev/null and b/images/hovercard.png differ diff --git a/package.json b/package.json index 59fe70b..b2b2fee 100644 --- a/package.json +++ b/package.json @@ -1,115 +1,115 @@ { "name": "arcanist", "displayName": "Arcanist", "description": "Phabricator/Arcanist support for VSCode", "publisher": "avive", - "version": "1.1.0", + "version": "1.2.0", "engines": { "vscode": "^1.46.0" }, "categories": [ "Other" ], "keywords": [ "Phabricator" ], "license": "MIT", "activationEvents": [ "workspaceContains:**/.arclint", "workspaceContains:**/.arcconfig" ], "main": "./out/extension.js", "contributes": { "menus": { "editor/title/context": [ { "command": "arc-vscode.browseFile" } ], "editor/title": [ { "command": "arc-vscode.browseFile" } ], "explorer/context": [ { "command": "arc-vscode.browseFile" } ] }, "commands": [ { "command": "arc-vscode.browseFile", "title": "Browse in Diffusion", "category": "arc", "icon": "$(account)" }, { "command": "arc-vscode.clearLint", "title": "Clear all arc-lint messages", "category": "arc" }, { "command": "arc-vscode.lintEverything", "title": "arc lint --everything", "category": "arc" } ], "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" ] } ] }, "repository": { "type": "git", "url": "https://github.com/avivey/arc-vscode.git" }, "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/glob": "^7.1.1", "@types/mocha": "^7.0.2", "@types/node": "^13.11.0", "@types/vscode": "^1.46.0", "@typescript-eslint/eslint-plugin": "^2.30.0", "@typescript-eslint/parser": "^2.30.0", "eslint": "^6.8.0", "glob": "^7.1.6", "mocha": "^7.1.2", "typescript": "^3.9.7", "vscode-test": "^1.3.0" }, "dependencies": { "execa": "^4.0.2", "vsce": "^1.81.1" } } diff --git a/src/arc_browse.ts b/src/arc_browse.ts index 1c8368a..886f825 100644 --- a/src/arc_browse.ts +++ b/src/arc_browse.ts @@ -1,36 +1,35 @@ import * as vscode from 'vscode'; -import * as execa from 'execa'; import * as path from 'path'; +import { arc, ReturnValue } from './exec_arc'; + var LOG: vscode.OutputChannel; export function setup(log: vscode.OutputChannel) { LOG = log; } export function browseFile(resource: vscode.Uri | undefined) { if (!resource) { if (!vscode.window.activeTextEditor) { return; } const document = vscode.window.activeTextEditor.document; resource = document.uri; } if (resource.scheme !== "file") { return; } const filename = resource.path; - function handleExecResult(value: execa.ExecaReturnValue) { + function handleExecResult(value: ReturnValue) { // In the happy case, arc-browse outputs nothing. if (!value.stdout) { return; } // If it does print something, it's an error message. vscode.window.showErrorMessage('arc-browse error:' + value.stdout); } - execa( - 'arc', ['browse', '--types', 'path', '--', path.basename(filename)], - { cwd: path.dirname(filename) }, - ).then(handleExecResult, handleExecResult); + arc(['browse', '--types', 'path', '--', path.basename(filename)], + handleExecResult, path.dirname(filename)); } diff --git a/src/arcanist_types.ts b/src/arcanist_types.ts index db59f01..965127f 100644 --- a/src/arcanist_types.ts +++ b/src/arcanist_types.ts @@ -1,26 +1,36 @@ /* { "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:" } */ export interface ArcanistLintMessage { line: number; char: number; code: string; severity: string; // could technically be an enum name: string; description?: string; original?: string; replacement?: string; // locations: string[]; // context: string; } + +export interface ArcanistHandle { + phid: string; + uri: string; + typeName: string; + type: string; + name: string; + fullName: string; + status: string; +} diff --git a/src/exec_arc.ts b/src/exec_arc.ts new file mode 100644 index 0000000..b9e0da8 --- /dev/null +++ b/src/exec_arc.ts @@ -0,0 +1,26 @@ +import * as execa from 'execa'; + +export type ReturnValue = execa.ExecaReturnValue; +type Handler = ((x: execa.ExecaReturnValue) => void); + +export type ConduitHandler = (x: ConduitResponse) => void; +export interface ConduitResponse { + error?: string; // TODO check + errorMessage?: string; // TODO check + response: any; +} + +export function arc(args: string[], handler: Handler, cwd?: string) { + execa('arc', args, { cwd: cwd }).then(handler, handler); +} + +export function callConduit(method: string, body: object, handler: ConduitHandler, cwd?: string) { + function hs(value: execa.ExecaReturnValue) { + const output = JSON.parse(value.stdout); + handler(output); + } + execa('arc', ['call-conduit', '--', method], { + input: JSON.stringify(body), + cwd: cwd, + }).then(hs, hs); +} diff --git a/src/extension.ts b/src/extension.ts index 0959cde..3b7529a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,53 +1,56 @@ import * as vscode from 'vscode'; import * as lint from './arc_lint'; import * as browse from './arc_browse'; +import * as hovercard from './hovercard'; export function activate(context: vscode.ExtensionContext) { const log = vscode.window.createOutputChannel("arcanist"); const diagnostics = vscode.languages.createDiagnosticCollection('arc lint'); lint.setup(log); browse.setup(log); function d(disposable: vscode.Disposable) { context.subscriptions.push(disposable); } d(diagnostics); d(log); + d(hovercard.register()); + d(vscode.commands.registerCommand("arc-vscode.browseFile", browse.browseFile)); 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(); } diff --git a/src/hovercard.ts b/src/hovercard.ts new file mode 100644 index 0000000..9bfc550 --- /dev/null +++ b/src/hovercard.ts @@ -0,0 +1,72 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; + +import { ArcanistHandle } from './arcanist_types'; +import { callConduit, ConduitResponse } from './exec_arc'; + +const MONOGRAM_REGEX = /\b[DFHLPTU]\d+/; +var object_cache: Map = new Map(); + +type Hovercard = vscode.MarkdownString | vscode.MarkdownString[]; + + +var selector: vscode.DocumentSelector = { scheme: 'file' }; +var provider: vscode.HoverProvider = { + provideHover: function (document, position, token): vscode.ProviderResult { + var wordRange = document.getWordRangeAtPosition(position, MONOGRAM_REGEX); + if (!wordRange) { + return null; + } + + var mono = document.getText(wordRange); + + if (object_cache.has(mono)) { + var value = object_cache.get(mono); + + return value ? new vscode.Hover(value, wordRange) : null; + } + + var cwd: string | undefined = undefined; + if (document.uri.scheme === 'file') { + cwd = path.dirname(document.uri.path); + } + + return new Promise((resolve, _) => { + function handler(v: ConduitResponse) { + if (v.error) { + object_cache.set(mono, null); + } + var hover = parsePhidLookup(v.response[mono]); + object_cache.set(mono, hover); + resolve(hover ? new vscode.Hover(hover, wordRange) : null); + } + + callConduit('phid.lookup', { names: [mono] }, handler, cwd); + }); + } +}; + + +function parsePhidLookup(input: ArcanistHandle): Hovercard | null { + if (!input) { + return null; + } + + var icon = 'whitespace'; + switch (input.status) { + case 'open': + icon = 'check'; + break; + case 'closed': + icon = 'tag'; + break; + } + + return new vscode.MarkdownString( + `$(${icon}) [${input.fullName}](${input.uri})`, + true); +} + +export function register(): vscode.Disposable { + return vscode.languages.registerHoverProvider(selector, provider); +}