Skip to content

Commit

Permalink
Merge pull request #13 from devnote-dev/commands
Browse files Browse the repository at this point in the history
feat: extension commands
  • Loading branch information
veelenga authored Mar 28, 2023
2 parents eb066cf + 58aeccf commit 115bb1c
Show file tree
Hide file tree
Showing 7 changed files with 708 additions and 1,003 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ out
node_modules
.vscode-test/
*.vsix
package-lock.json
yarn-error.log
48 changes: 28 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,57 +1,65 @@
{
"name": "crystal-ameba",
"name": "crystal-ameba",
"displayName": "crystal-ameba",
"publisher": "veelenga",
"publisher": "veelenga",
"description": "Ameba extension for Visual Studio Code",
"version": "0.0.5",
"license": "MIT",
"engines": {
"version": "0.0.5",
"license": "MIT",
"engines": {
"vscode": "^1.22.0"
},
"repository": {
"type": "git",
"url": "https://github.com/crystal-ameba/vscode-crystal-ameba.git"
"url": "https://github.com/crystal-ameba/vscode-crystal-ameba.git"
},
"categories": [
"Programming Languages",
"Linters"
],
"activationEvents": [
"onLanguage:crystal",
"onCommand:crystal.ameba"
"onCommand:crystal.ameba.lint"
],
"main": "./out/extension.js",
"main": "./out/extension.js",
"contributes": {
"languages": [
{
"id": "crystal",
"id": "crystal",
"extensions": [
".cr"
]
}
],
"commands": [
{
"command": "crystal.ameba",
"title": "Crystal: lint by Ameba"
"command": "crystal.ameba.lint",
"title": "Crystal Ameba: run lints"
},
{
"command": "crystal.ameba.restart",
"title": "Crystal Ameba: restart extension"
},
{
"command": "crystal.ameba.disable",
"title": "Crystal Ameba: disable lints (workspace)"
}
]
},
"scripts": {
"vscode:prepublish": "yarn run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "yarn run compile && node ./node_modules/vscode/bin/test"
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "yarn run compile && node ./node_modules/vscode/bin/test"
},
"devDependencies": {
"@types/mocha": "^2.2.42",
"@types/node": "^10.12.21",
"tslint": "^5.12.1",
"typescript": "^3.3.1",
"vscode": "^1.1.28"
"@types/node": "^10.12.21",
"tslint": "^5.12.1",
"typescript": "^3.3.1",
"vscode": "^1.1.28"
},
"resolutions": {
"minimist": "0.2.1"
}
}
}
242 changes: 92 additions & 150 deletions src/ameba.ts
Original file line number Diff line number Diff line change
@@ -1,184 +1,126 @@
import * as vscode from 'vscode';
import * as cp from 'child_process';
import { exec } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { AmebaIssue, AmebaFile } from './amebaOutput';
import { TaskQueue, Task } from './taskQueue';
import { getConfig, AmebaConfig } from './configuration';
import {
commands,
Diagnostic,
DiagnosticCollection,
DiagnosticSeverity,
Range,
TextDocument,
Uri,
window,
workspace
} from 'vscode';
import { AmebaOutput } from './amebaOutput';
import { CpuInfo } from 'os';

function isFileUri(uri: vscode.Uri): boolean {
return uri.scheme === 'file';
}

function getCurrentPath(fileName: string): string {
return vscode.workspace.rootPath || path.dirname(fileName);
}

function getCommandArguments(fileName: string, rootPath: string): string[] {
let commandArguments = [fileName, '--format', 'json'];
const extensionConfig = getConfig();
if (extensionConfig.configFileName !== '') {
let configPath = `${rootPath}/${extensionConfig.configFileName}`;
if (fs.existsSync(configPath)) {
const config = ['--config', configPath];
commandArguments = commandArguments.concat(config);
}
}

return commandArguments;
}
import { AmebaConfig, getConfig } from './configuration';
import { Task, TaskQueue } from './taskQueue';

export class Ameba {
private diag: vscode.DiagnosticCollection;
private taskQueue: TaskQueue = new TaskQueue();
private diag: DiagnosticCollection;
private taskQueue: TaskQueue;
public config: AmebaConfig;

constructor(
diagnostics: vscode.DiagnosticCollection,
additionalArguments: string[] = [],
) {
constructor(diagnostics: DiagnosticCollection) {
this.diag = diagnostics;
// this.additionalArguments = additionalArguments;
this.taskQueue = new TaskQueue();
this.config = getConfig();
}

public execute(document: vscode.TextDocument, onComplete?: () => void): void {
if (document.languageId !== 'crystal' || document.isUntitled || !isFileUri(document.uri)) {
public execute(document: TextDocument): void {
if (document.languageId !== 'crystal' || document.isUntitled || document.uri.scheme !== 'file') {
return;
}

const fileName = document.fileName;
const uri = document.uri;
let currentPath = getCurrentPath(fileName);

let onDidExec = (error: Error | null, stdout: string, stderr: string) => {
if (this.hasError(error, stderr)) {
return;
}
const args = [this.config.command, document.fileName, '--format', 'json'];
if (this.config.configFileName.length) {
const dir = workspace.getWorkspaceFolder(document.uri)!.uri.fsPath;
args.push('--config', path.join(dir, this.config.configFileName));
}

this.diag.delete(uri);
let ameba = this.parse(stdout);

if (ameba === undefined || ameba === null) {
return;
}

let entries: [vscode.Uri, vscode.Diagnostic[]][] = [];
ameba.sources.forEach((source: AmebaFile) => {
let diagnostics: Array<vscode.Diagnostic> = [];
source.issues.forEach((issue: AmebaIssue) => {
const loc = issue.location;
let end_loc = issue.end_location;
if (end_loc === null || end_loc.line === null) {
end_loc = {
line: loc.line,
column: loc.column
};
const task = new Task(document.uri, token => {
const proc = exec(args.join(' '), (err, stdout, stderr) => {
if (token.isCanceled) return;
this.diag.delete(document.uri);

if (err && stderr.length) {
if ((process.platform == 'win32' && err.code === 1) || err.code === 127) {
window.showErrorMessage(
`Could not execute Ameba file${args[0] === 'ameba' ? '.' : ` at ${args[0]}`}`,
'Disable (workspace)'
).then(
disable => disable && commands.executeCommand('crystal.ameba.disable'),
_ => {}
);
} else {
window.showErrorMessage(stderr);
}
const range = new vscode.Range(
loc.line - 1, loc.column - 1,
end_loc.line - 1, end_loc.column - 1
);
const sev = this.convertSeverity(issue.severity);
const message = `[${issue.rule_name}] ${issue.message}`;
const diagnostic = new vscode.Diagnostic(range, message, sev);
diagnostics.push(diagnostic);
});
entries.push([uri, diagnostics]);
});
this.diag.set(entries);
};

const args = getCommandArguments(fileName, currentPath);
return;
}

let task = new Task(uri, token => {
let process = this.executeAmeba(args, document.getText(), {}, (error, stdout, stderr) => {
if (token.isCanceled) {
let results: AmebaOutput;
try {
results = JSON.parse(stdout);
} catch (err) {
console.error(`Ameba: failed parsing JSON: ${err}`);
window.showErrorMessage('Ameba: failed to parse JSON response.');
return;
}
onDidExec(error, stdout, stderr);
token.finished();
if (onComplete) {
onComplete();

if (!results.summary.issues_count) return;
const diagnostics: [Uri, Diagnostic[]][] = [];

for (let source of results.sources) {
if (!source.issues.length) continue;
let parsed: Diagnostic[] = [];

source.issues.forEach(issue => {
let start = issue.location;
let end = issue.end_location;
const range = new Range(
start.line - 1,
start.column - 1,
end.line - 1,
end.column
);

const diag = new Diagnostic(
range,
`[${issue.rule_name}] ${issue.message}`,
this.parseSeverity(issue.severity)
);
parsed.push(diag);
});

diagnostics.push([document.uri, parsed]);
}

this.diag.set(diagnostics);
});
return () => process.kill();

return () => proc.kill();
});

this.taskQueue.enqueue(task);
}

private convertSeverity(severity: String) {
private parseSeverity(severity: string): DiagnosticSeverity {
switch(severity) {
case "Error":
return vscode.DiagnosticSeverity.Error;
case "Warning":
return vscode.DiagnosticSeverity.Warning;
case "Refactoring":
return vscode.DiagnosticSeverity.Hint;
case 'Error':
return DiagnosticSeverity.Error;
case 'Warning':
return DiagnosticSeverity.Warning;
case 'Refactoring':
return DiagnosticSeverity.Hint;
default:
return vscode.DiagnosticSeverity.Information;
return DiagnosticSeverity.Information;
}
}

public clear(document: vscode.TextDocument): void {
public clear(document: TextDocument): void {
let uri = document.uri;
if (isFileUri(uri)) {
if (uri.scheme === 'file') {
this.taskQueue.cancel(uri);
this.diag.delete(uri);
}
}

private executeAmeba(
args: string[],
fileContents: string,
options: cp.ExecFileOptions,
cb: (err: Error | null, stdout: string, stderr: string) => void): cp.ChildProcess {
let cmd = `${this.config.command} ${args.join(' ')}`;
let child = cp.exec(cmd, options, cb);
child.stdin.write(fileContents);
child.stdin.end();
return child;
}

// checking ameba output has error
private hasError(error: Error | null, stderr: string): boolean {
let errorOutput = stderr.toString();
if (error && (<any>error).code === 'ENOENT') {
console.error(error);
vscode.window.showWarningMessage(`${this.config.command} is not executable`);
return true;
} else if (error && (<any>error).code === 127) {
vscode.window.showWarningMessage(stderr);
return true;
} else if (errorOutput.length > 0) {
vscode.window.showErrorMessage(stderr);
return true;
}

return false;
}

private parse(output: string): AmebaOutput | null {
if (output.length < 1) {
let message = `command ${this.config.command} returns empty output!`;
vscode.window.showWarningMessage(message);

return null;
}

try {
return JSON.parse(output);
} catch(e) {
if (e instanceof SyntaxError) {
let errorMessage = `Error on parsing output (It might non-JSON output) : "${output}"`;
vscode.window.showWarningMessage(errorMessage);

return null;
}
}

return null;
}
}
}
Loading

0 comments on commit 115bb1c

Please sign in to comment.