-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13 from devnote-dev/commands
feat: extension commands
- Loading branch information
Showing
7 changed files
with
708 additions
and
1,003 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,5 @@ out | |
node_modules | ||
.vscode-test/ | ||
*.vsix | ||
package-lock.json | ||
yarn-error.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.