diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9ae27d2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "bracketSpacing": true, +} diff --git a/RuboCop.novaextension/extension.json b/RuboCop.novaextension/extension.json index cc3c191..bb191cb 100644 --- a/RuboCop.novaextension/extension.json +++ b/RuboCop.novaextension/extension.json @@ -14,5 +14,14 @@ ], "entitlements": { "process": true - } + }, + "config-workspace": [ + { + "key": "rubocop.format-on-save", + "title": "Format on save", + "description": "Whether to format documents when saved.", + "type": "boolean", + "default": true + } + ] } diff --git a/Source/Scripts/Formatter.js b/Source/Scripts/Formatter.js new file mode 100644 index 0000000..24af94a --- /dev/null +++ b/Source/Scripts/Formatter.js @@ -0,0 +1,38 @@ +// +// RuboCop Extension for Nova +// Linter.js +// +// Copyright © 2019-2020 Justin Mecham. All rights reserved. +// + +const RuboCopProcess = require("./RuboCopProcess"); + +class Formatter { + constructor() { + this.issues = new IssueCollection(); + this.process = new RuboCopProcess(); + } + + async formatDocument(document) { + if (document.syntax !== "ruby") return; + + const contentRange = new Range(0, document.length); + const content = document.getTextInRange(contentRange); + + return this.formatString(content, document.path); + } + + async formatString(string, uri) { + // this.process.onComplete(offenses => { + // this.issues.set(uri, offenses.map(offense => offense.issue)); + // }); + // + this.process.execute(string, uri, true); + } + + removeIssues(uri) { + this.issues.remove(uri); + } +} + +module.exports = Formatter; diff --git a/Source/Scripts/Linter.js b/Source/Scripts/Linter.js index 4cb8469..26c7f48 100644 --- a/Source/Scripts/Linter.js +++ b/Source/Scripts/Linter.js @@ -8,7 +8,6 @@ const RuboCopProcess = require("./RuboCopProcess"); class Linter { - constructor() { this.issues = new IssueCollection(); this.process = new RuboCopProcess(); @@ -24,8 +23,11 @@ class Linter { } async lintString(string, uri) { - this.process.onComplete(offenses => { - this.issues.set(uri, offenses.map(offense => offense.issue)); + this.process.onComplete((offenses) => { + this.issues.set( + uri, + offenses.map((offense) => offense.issue) + ); }); this.process.execute(string, uri); @@ -34,7 +36,6 @@ class Linter { removeIssues(uri) { this.issues.remove(uri); } - } module.exports = Linter; diff --git a/Source/Scripts/RuboCopProcess.js b/Source/Scripts/RuboCopProcess.js index 8d20d04..97e2c82 100644 --- a/Source/Scripts/RuboCopProcess.js +++ b/Source/Scripts/RuboCopProcess.js @@ -8,7 +8,6 @@ const Offense = require("./Offense"); class RuboCopProcess { - constructor() { this.isNotified = false; } @@ -18,26 +17,31 @@ class RuboCopProcess { return Promise.resolve(this._isBundled); } - if (!(nova.workspace.contains("Gemfile") || nova.workspace.contains("gems.rb"))) { + if ( + !( + nova.workspace.contains("Gemfile") || + nova.workspace.contains("gems.rb") + ) + ) { this._isBundled = false; return Promise.resolve(false); } - return new Promise(resolve => { + return new Promise((resolve) => { const process = new Process("/usr/bin/env", { args: ["bundle", "exec", "rubocop", "--version"], cwd: nova.workspace.path, - shell: true + shell: true, }); let output = ""; - process.onStdout(line => output += line.trim()); - process.onDidExit(status => { + process.onStdout((line) => (output += line.trim())); + process.onDidExit((status) => { if (status === 0) { console.log(`Found RuboCop ${output} (Bundled)`); - resolve(this._isBundled = true); + resolve((this._isBundled = true)); } else { - resolve(this._isBundled = false); + resolve((this._isBundled = false)); } }); @@ -50,21 +54,21 @@ class RuboCopProcess { return Promise.resolve(this._isGlobal); } - return new Promise(resolve => { + return new Promise((resolve) => { const process = new Process("/usr/bin/env", { args: ["rubocop", "--version"], cwd: nova.workspace.path, - shell: true + shell: true, }); let output = ""; - process.onStdout(line => output += line.trim()); - process.onDidExit(status => { + process.onStdout((line) => (output += line.trim())); + process.onDidExit((status) => { if (status === 0) { console.log(`Found RuboCop ${output} (Global)`); - resolve(this._isGlobal = true); + resolve((this._isGlobal = true)); } else { - resolve(this._isGlobal = false); + resolve((this._isGlobal = false)); } }); @@ -84,38 +88,41 @@ class RuboCopProcess { args: commandArguments, cwd: nova.workspace.path, shell: true, - stdio: "pipe" + stdio: "pipe", }); return process; } - async execute(content, uri) { + async execute(content, uri, format = false) { let workspaceOverlap = uri.indexOf(nova.workspace.path); let relativePath = ''; - if (workspaceOverlap != -1) { relativePath = uri.substring(workspaceOverlap + nova.workspace.path.length + 1); } else { relativePath = uri; } - - const defaultArguments = [ - "rubocop", - "--format=json", - "--stdin", - relativePath - ]; + + const defaultArguments = ["rubocop", "--format=json"]; + if (format) { + defaultArguments.push("--auto-correct"); + } else { + defaultArguments.push("--stdin"); + } + defaultArguments.push(relativePath); + const process = await this.process(defaultArguments); if (!process) return; let output = ""; let errorOutput = ""; - process.onStdout(line => output += line); - process.onStderr(line => errorOutput += line); - process.onDidExit(status => { + process.onStdout((line) => (output += line)); + process.onStderr((line) => (errorOutput += line)); + process.onDidExit((status) => { // See: https://github.com/rubocop-hq/rubocop/blob/master/manual/basic_usage.md#exit-codes - status >= 2 ? this.handleError(errorOutput) : this.handleOutput(output, errorOutput); + status >= 2 + ? this.handleError(errorOutput) + : this.handleOutput(output, errorOutput); }); process.start(); @@ -146,12 +153,14 @@ class RuboCopProcess { try { const parsedOutput = JSON.parse(output); const offenses = parsedOutput["files"][0]["offenses"]; - + + if (offenses) { + this.offenses = offenses.map((offense) => new Offense(offense)); + } + // TODO: Enable a "Debug" Preference // console.info(JSON.stringify(offenses, null, " ")); - - this.offenses = offenses.map(offense => new Offense(offense)); - } catch(error) { + } catch (error) { console.error(error); } @@ -165,19 +174,25 @@ class RuboCopProcess { const request = new NotificationRequest("rubocop-not-found"); request.title = nova.localize("RuboCop Not Found"); - request.body = nova.localize("The \"rubocop\" command could not be found in your environment."); + request.body = nova.localize( + 'The "rubocop" command could not be found in your environment.' + ); request.actions = [nova.localize("OK"), nova.localize("Help")]; const notificationPromise = nova.notifications.add(request); - notificationPromise.then((response) => { - if (response.actionIdx === 1) { // Help - nova.openConfig(); - } - }).catch((error) => { - console.error(error); - }).finally(() => { - this.isNotified = true; - }); + notificationPromise + .then((response) => { + if (response.actionIdx === 1) { + // Help + nova.openConfig(); + } + }) + .catch((error) => { + console.error(error); + }) + .finally(() => { + this.isNotified = true; + }); } notifyUserOfError(errorMessage) { @@ -195,7 +210,6 @@ class RuboCopProcess { onComplete(callback) { this._onCompleteCallback = callback; } - } module.exports = RuboCopProcess; diff --git a/Source/Scripts/main.js b/Source/Scripts/main.js index 7d2f88f..20548e9 100644 --- a/Source/Scripts/main.js +++ b/Source/Scripts/main.js @@ -6,19 +6,40 @@ // const Linter = require("./Linter"); +const Formatter = require("./Formatter"); -exports.activate = function() { +exports.activate = function () { const linter = new Linter(); - + const formatter = new Formatter(); + let format = false; + + nova.workspace.config.observe( + 'rubocop.format-on-save', + () => format = nova.workspace.config.get('rubocop.format-on-save') + + ) + + nova.workspace.onDidAddTextEditor((editor) => { linter.lintDocument(editor.document); - editor.onWillSave(editor => linter.lintDocument(editor.document)); - editor.onDidStopChanging(editor => linter.lintDocument(editor.document)); - editor.document.onDidChangeSyntax(document => linter.lintDocument(document)); + editor.onWillSave((editor) => { + linter.lintDocument(editor.document) + if (format) { + formatter.formatDocument(editor.document) + } + }); + + editor.onDidStopChanging((editor) => + linter.lintDocument(editor.document) + ); + + editor.document.onDidChangeSyntax((document) => + linter.lintDocument(document) + ); - editor.onDidDestroy(destroyedEditor => { - let anotherEditor = nova.workspace.textEditors.find(editor => { + editor.onDidDestroy((destroyedEditor) => { + let anotherEditor = nova.workspace.textEditors.find((editor) => { return editor.document.uri === destroyedEditor.document.uri; });