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

Add UI/UX for setting the binary path #31

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 21 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"url": "https://github.com/plexsystems/vscode-protolint/issues"
},
"engines": {
"vscode": "^1.14.0"
"vscode": "^1.31.0"
},
"categories": [
"Linters"
Expand All @@ -38,16 +38,33 @@
"title": "Protolint: Lint protobuf file"
}
],
"keybindings":[
{
"command": "protolint.lint",
"key": "alt+shift+l",
"mac": "alt+shift+l",
"linux": "alt+shift+l",
"when": "editorTextFocus"
}
],
"configuration": {
"type": "object",
"title": "protolint",
"properties": {
"protolint.path": {
"type": "string",
"scope": "machine",
"default": "protolint",
"description": "Path of the protolint executable."
"markdownDescription": "Path of the protolint executable.\n\nDownload from: https://github.com/yoheimuta/protolint"
}
}
},
"menus": {
"commandPalette": [
{
"command": "protolint.lint",
"when": "editorLangId == proto3 || editorLangId == proto"
}
]
}
},
"scripts": {
Expand All @@ -62,7 +79,7 @@
"devDependencies": {
"@types/mocha": "^5.2.7",
"@types/node": "^12.0.8",
"@types/vscode": "^1.14.0",
"@types/vscode": "^1.31.0",
"mocha": "^9.2.2",
"rimraf": "^2.6.3",
"tslint": "^5.8.0",
Expand Down
23 changes: 23 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DocumentSelector } from "vscode";

export const PROTOLINT_REPO_URI = "https://github.com/yoheimuta/protolint";

export const PATH_CONFIGURATION_KEY = "path";

/**
* This works when the linter executable softlink is available
* in the shell.
*/
export const FAILOVER_PATH = "protolint";

/**
* Document filter to select protocol buffer files.
*
* @remarks
*
* At the moment allows only files on disk. Change `scheme` if need linting from memory.
*/
export const PROTOBUF_SELECTOR: DocumentSelector = [
{ language: "proto3", scheme: "file" },
{ language: "proto", scheme: "file" }
];
90 changes: 90 additions & 0 deletions src/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import * as vscode from 'vscode';
import Linter, { LinterError } from './linter';
import { PROTOBUF_SELECTOR } from './constants';
import { pickPathConfiguration } from './helpers';

export function subscribeToDocumentChanges(
context: vscode.ExtensionContext,
diagnosticCollection: vscode.DiagnosticCollection
): void {
if (vscode.window.activeTextEditor) {
refreshDiagnostics(vscode.window.activeTextEditor.document, diagnosticCollection);
}

context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(
editor => {
if (editor) {
refreshDiagnostics(editor.document, diagnosticCollection);
}
}
)
);

context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument(
doc => refreshDiagnostics(doc, diagnosticCollection)
)
);

context.subscriptions.push(
vscode.workspace.onDidCloseTextDocument(
doc => diagnosticCollection.delete(doc.uri)
)
);

// Refresh the diagnostics when the document language is changed
// to protocol buffers.
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument(
doc => refreshDiagnostics(doc, diagnosticCollection)
)
);

}

/**
* Analyzes the protocol buffer document for problems.
*
* @remarks
* If the document is not identified as protocol buffer, the diagnostics won't be added.
* It the document language is changed and it's not protocol buffer anymore, its
* diagnostics will be deleted.
*
* @param doc protocol buffer document to analyze
* @param diagnosticCollection diagnostic collection
*/
export async function refreshDiagnostics(
doc: vscode.TextDocument,
diagnosticCollection: vscode.DiagnosticCollection
): Promise<void> {
if (vscode.languages.match(PROTOBUF_SELECTOR, doc) === 0) {
diagnosticCollection.delete(doc.uri);
return;
}

if (Linter.isExecutableAvailable() === undefined) {
try {
const result = await pickPathConfiguration();
if (result === undefined) {
return;
}
} catch (error) {
return;
}
}

const linter = new Linter(doc);
const errors: LinterError[] = await linter.lint();
const diagnostics = errors.map(error => {
const diagnostic = new vscode.Diagnostic(error.range, error.proto.reason, vscode.DiagnosticSeverity.Warning);
diagnostic.source = 'protolint';
return diagnostic;
});

diagnosticCollection.set(doc.uri, diagnostics);
}
54 changes: 10 additions & 44 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,24 @@
import * as vscode from 'vscode';
import * as cp from 'child_process';
import Linter, { LinterError } from './linter';

import { refreshDiagnostics, subscribeToDocumentChanges } from './diagnostics';

const LINT_COMMAND = "protolint.lint";
const diagnosticCollection = vscode.languages.createDiagnosticCollection("protolint");

export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('protolint.lint', runLint);

vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => {
vscode.commands.executeCommand('protolint.lint');
});
context.subscriptions.push(diagnosticCollection);

context.subscriptions.push(
vscode.commands.registerCommand(LINT_COMMAND, runLint));

// Run the linter when the user changes the file that they are currently viewing
// so that the lint results show up immediately.
vscode.window.onDidChangeActiveTextEditor((e: vscode.TextEditor | undefined) => {
vscode.commands.executeCommand('protolint.lint');
});

// Verify that protolint can be successfully executed on the host machine by running the version command.
// In the event the binary cannot be executed, tell the user where to download protolint from.
let protoLintPath = vscode.workspace.getConfiguration('protolint').get<string>('path');
if (!protoLintPath) {
protoLintPath = "protolint"
}

const result = cp.spawnSync(protoLintPath, ['version']);
if (result.status !== 0) {
vscode.window.showErrorMessage("protolint was not detected using path `" + protoLintPath + "`. Download from: https://github.com/yoheimuta/protolint");
return;
}
subscribeToDocumentChanges(context, diagnosticCollection);
}

function runLint() {
let editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}

// We only want to run protolint on documents that are known to be
// protocol buffer files.
const doc = editor.document;
if (doc.languageId !== 'proto3' && doc.languageId !== 'proto') {
return;
}

doLint(doc, diagnosticCollection);
}

async function doLint(codeDocument: vscode.TextDocument, collection: vscode.DiagnosticCollection): Promise<void> {
const linter = new Linter(codeDocument);
const errors: LinterError[] = await linter.lint();
const diagnostics = errors.map(error => {
return new vscode.Diagnostic(error.range, error.proto.reason, vscode.DiagnosticSeverity.Warning);
});

collection.set(codeDocument.uri, diagnostics);
}
refreshDiagnostics(editor.document, diagnosticCollection);
}
91 changes: 91 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as vscode from 'vscode';

import { PROTOLINT_REPO_URI, PATH_CONFIGURATION_KEY } from './constants';
import Linter from './linter';

/**
* Attempts to pick the `protolint` executable path via the OS UI.
* Validates the path via {@link Linter.isExecutableValid}.
* If the path is correct, updates the extension configuration and returns
* this path.
*
* @returns the executable path, if the user locates the valid one, otherwise `undefined`.
*/
export async function locateExecutable(): Promise<string | undefined> {
try {
const userInput = await vscode.window.showOpenDialog({
canSelectFolders: false,
canSelectFiles: true,
canSelectMany: false,
openLabel: "Use for linting"
});

if (!userInput || userInput.length < 1) {
return undefined;
}

const path = userInput[0].fsPath;

if (Linter.isExecutableValid(path)) {
const config = vscode.workspace.getConfiguration('protolint');

// Always updates the global user settings, assuming the `path`
// scope is limited to 'machine' in package.json. It may become an
// issue if the scope changes (e.g. to 'window').
await config.update(PATH_CONFIGURATION_KEY, path, true);

return path;
} else {
return undefined;
}

} catch (error) {
return undefined;
}
}

/**
* If `protolint` executable isn't available with the current extension settings,
* shows a warning with buttons:
* - Download (opens {@link PROTOLINT_REPO_URI} in the browser).
* - Find the file (runs {@link locateExecutable}).
* @returns the executable path, if the user locates the valid one, otherwise `undefined`.
*/
export async function pickPathConfiguration(): Promise<string | undefined> {
const path = Linter.isExecutableAvailable();
if (path !== undefined) {
return path;
}

const DOWNLOAD_ACTION = "Download";
const FIND_ACTION = "Find the file";

const selection = await vscode.window.showWarningMessage(
"Protolint executable was not detected. Find the unpacked executable if already downloaded.",
DOWNLOAD_ACTION,
FIND_ACTION
);

if (selection !== undefined) {
switch (selection) {
case FIND_ACTION:
try {
const result = await locateExecutable();
if (result === undefined) {
vscode.window.showErrorMessage("A valid linter executable wasn't found. Try fixing it in the settings.");
}
return result;
} catch (error) {
vscode.window.showErrorMessage("A valid linter executable wasn't found. Try fixing it in the settings.");
return undefined;
}
break;

case DOWNLOAD_ACTION:
vscode.env.openExternal(vscode.Uri.parse(PROTOLINT_REPO_URI));
break;
}
}

return undefined;
}
Loading