From 0c5502d01a8f5784336d86505d1efbbab0de6328 Mon Sep 17 00:00:00 2001 From: halohalospecial Date: Wed, 29 Jun 2016 23:29:52 +0800 Subject: [PATCH] Add `Work Directory` option --- README.md | 51 +++++++-- lib/linter-elm-make.js | 241 ++++++++++++++++++++++++++--------------- package.json | 7 +- 3 files changed, 197 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 764ed66..83a6a40 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,40 @@ Lint your Elm files in Atom with [linter](https://github.com/atom-community/lint ## Installation 1. [Install `elm`](http://elm-lang.org/install). -1. `$ apm install linter` -1. `$ apm install language-elm` -1. `$ apm install linter-elm-make` -1. `$ which elm-make` and set that as your executable path in this installed package's configuration. +1. Install [`linter`](https://github.com/atom-community/linter), [`language-elm`](https://github.com/atom-community/language-elm), and [`linter-elm-make`](https://github.com/atom-community/linter-elm-make) from the Settings view (`Edit` > `Preferences` > `Install`) or by running these from the command line: +``` +apm install linter +apm install language-elm +apm install linter-elm-make +``` +1. Run `which elm-make` (Unix/Linux) or `where.exe elm-make` (Windows) from the command line and set the result as your executable path in this installed package's configuration. ## Configuration -#### Lint On The Fly +#### `Lint On The Fly` By default, linting is only done after saving the file. If you want to lint while typing, turn on the `Lint On The Fly` option in the package settings. Also make sure that the `Lint As You Type` option is enabled in the [linter](https://github.com/atom-community/linter) package settings. -#### Always Compile Main +#### `Always Compile Main` If enabled, the main file(s) will always be compiled instead of the active file. The main file can be set using `Linter Elm Make: Set Main Path`. If not set, the linter will look for `Main.elm` files in the source directories. Take note that if this is enabled, modules unreachable from the main modules will not be linted. Disabled by default. -#### Report Warnings +#### `Report Warnings` Show `elm-make` warnings. Enabled by default. +#### `Work Directory` +- If this is not blank, the linter will copy the source files from the project directory into this directory and use this as the working directory for `elm-make`. This can be an absolute path or relative to the path of `elm-package.json`. + + If `Lint On The Fly` is disabled, this option will prevent the linter from using your project directory's `elm-stuff`. This can be useful if you're using other tools to build your output files. + +- If this is blank and `Lint On The Fly` is enabled, the linter will create a temporary directory before running the first linting process for the project. It will then copy the source files from the project directory into the temporary directory. The linter will do all of this again when Atom gets restarted. Setting the value of `Work Directory` will shorten the duration of the first lint after a restart since the linter does not need to create a temporary directory and copy files anymore. + +- If this is blank and `Lint On The Fly` is disabled, the linter will use the project directory as the working directory for `elm-make`. + +If this option is not blank, a file watcher will watch the project directory for source file changes and synchronize those with the work directory. + +IMPORTANT WARNING: If the work directory is inside the project directory and you want to change the value of `Work Directory`, delete the work directory first! Else, the linter will consider the work directory as part of your project. + +If this option makes no sense and/or is confusing, just leave it blank. :) + ## Quick Fixes #### `Linter Elm Make: Quick Fix` @@ -44,14 +62,27 @@ You may also add something like this in your `keymap.cson`: 'f6': 'core:confirm' ``` -## Other Useful Commands +## Useful Commands + +#### `Linter Elm Make: Set Main Path` +Sets the main path of the project and saves it to `elm-package.json`. + +Example: +``` +"linter-elm-make": { + "mainPath": "Todo.elm" +} +``` +The main path is only relevant if `Always Compile Main` is enabled. See [above](https://github.com/mybuddymichael/linter-elm-make#always-compile-main). + +#### `Linter Elm Make: Clear Project Build Artifacts` +Deletes the `.elmi` and `.elmo` files in your project's build artifacts directory (e.g. elm-stuff/build-artifacts/0.17.0/user/project/1.0.0). This is useful after toggling `Lint On The Fly` and/or `Always Compile Main` to prevent confusing lint results. If using a work directory or temporary directory, the artifact files of that directory will also be deleted. #### `Linter Elm Make: Toggle Lint On The Fly` #### `Linter Elm Make: Toggle Always Compile Main` -#### `Linter Elm Make: Clear Project Build Artifacts` -Deletes the `*.elmi` and `*.elmo` files in your project's build artifacts directory (e.g. elm-stuff/build-artifacts/0.17.0/user/project/1.0.0). This is useful after toggling `Lint On The Fly` and/or `Always Compile Main` to prevent confusing lint results. +#### `Linter Elm Make: Toggle Report Warnings` ## Prior Art diff --git a/lib/linter-elm-make.js b/lib/linter-elm-make.js index d28f44e..9174853 100644 --- a/lib/linter-elm-make.js +++ b/lib/linter-elm-make.js @@ -9,6 +9,7 @@ const helpers = require('atom-linter'); const fs = require('fs-extra'); const tmp = require('tmp'); const chokidar = require('chokidar'); +const readDir = require('readdir'); module.exports = { config: { @@ -38,6 +39,13 @@ module.exports = { type: 'boolean', default: true, order: 4 + }, + workDirectory: { + title: 'Work Directory', + description: 'If this is not blank, the linter will copy the source files from the project directory into this directory and use this as the working directory for `elm-make`. This can be an absolute path or relative to the path of `elm-package.json`. If this is blank and `Lint On The Fly` is enabled, the linter will create a temporary work directory for the project. If this is blank and `Lint On The Fly` is disabled, the linter will use the project directory as the working directory for `elm-make`. IMPORTANT WARNING: If the work directory is inside the project directory and you want to change the value of `Work Directory`, delete the work directory first! Else, the linter will consider the work directory as part of your project.', + type: 'string', + default: '', + order: 5 } }, activate() { @@ -54,7 +62,8 @@ module.exports = { }); } this.subscriptions = new CompositeDisposable(); - this.compileDirectories = {}; + this.workDirectories = {}; + this.watchers = {}; this.problems = {}; this.quickFixes = {}; this.quickFixView = new QuickFixView(); @@ -98,15 +107,15 @@ module.exports = { }); editor.onDidDestroy(() => { if (atom.config.get('linter-elm-make.lintOnTheFly')) { - // If editor was modified before it was destroyed, revert the contents of the associated compile file to the actual file's contents. + // If editor was modified before it was destroyed, revert the contents of the associated work file to the actual file's contents. if (editor.isModified()) { const editorFilePath = editor.getPath(); if (editorFilePath) { const projectDirectory = lookupElmPackage(path.dirname(editorFilePath)); - const compileDirectory = self.compileDirectories[projectDirectory]; - if (compileDirectory) { - const compileFilePath = path.join(compileDirectory, editorFilePath.replace(projectDirectory, '')); - fs.writeFileSync(compileFilePath, fs.readFileSync(editorFilePath).toString()); + const workDirectory = self.workDirectories[projectDirectory]; + if (workDirectory) { + const workFilePath = path.join(workDirectory, editorFilePath.replace(projectDirectory, '')); + fs.writeFileSync(workFilePath, fs.readFileSync(editorFilePath).toString()); } } } @@ -131,15 +140,15 @@ module.exports = { if (atom.config.get('linter-elm-make.lintOnTheFly') && item && isElmEditor(item)) { if (self.prevElmEditor) { - // When an editor loses focus, update the associated compile file. + // When an editor loses focus, update the associated work file. const prevTextEditorPath = self.prevElmEditor.getPath(); if (prevTextEditorPath) { const projectDirectory = lookupElmPackage(path.dirname(prevTextEditorPath)); if (projectDirectory) { - const compileDirectory = self.compileDirectories[projectDirectory]; - if (compileDirectory) { - const compileFilePath = path.join(compileDirectory, prevTextEditorPath.replace(projectDirectory, '')); - fs.writeFileSync(compileFilePath, self.prevElmEditor.getText()); + const workDirectory = self.workDirectories[projectDirectory]; + if (workDirectory) { + const workFilePath = path.join(workDirectory, prevTextEditorPath.replace(projectDirectory, '')); + fs.writeFileSync(workFilePath, self.prevElmEditor.getText()); } } } @@ -158,7 +167,8 @@ module.exports = { }, deactivate() { this.subscriptions.dispose(); - this.compileDirectories = null; + this.workDirectories = null; + this.watchers = null; this.problems = null; this.quickFixes = null; this.quickFixView.destroy(); @@ -280,14 +290,14 @@ module.exports = { console.log('linter-elm-make: cleared project directory build artifacts - ' + buildArtifactsDirectory); } if (atom.config.get('linter-elm-make.lintOnTheFly')) { - // If linting on the fly, also delete the build artifacts in the compile directory. + // If linting on the fly, also delete the build artifacts in the work directory. if (projectDirectory) { - const compileDirectory = self.compileDirectories[projectDirectory]; - if (compileDirectory) { - const compileBuildArtifactsDirectory = buildArtifactsDirectory.replace(projectDirectory, compileDirectory); - deleteBuildArtifacts(compileBuildArtifactsDirectory); + const workDirectory = self.workDirectories[projectDirectory]; + if (workDirectory) { + const workDirectoryBuildArtifactsDirectory = buildArtifactsDirectory.replace(projectDirectory, workDirectory); + deleteBuildArtifacts(workDirectoryBuildArtifactsDirectory); if (atom.inDevMode()) { - console.log('linter-elm-make: cleared compile directory build artifacts - ' + compileBuildArtifactsDirectory); + console.log('linter-elm-make: cleared work directory build artifacts - ' + workDirectoryBuildArtifactsDirectory); } } } @@ -310,75 +320,42 @@ module.exports = { if (projectDirectory === null) { return []; } + const configWorkDirectory = atom.config.get('linter-elm-make.workDirectory'); + if (configWorkDirectory && configWorkDirectory.trim() !== '') { + if (!self.workDirectories[projectDirectory]) { + const workDirectory = path.resolve(projectDirectory, configWorkDirectory); + self.workDirectories[projectDirectory] = workDirectory; + // If work directory does not exist, create it and copy source files from project directory. + if (!fs.existsSync(workDirectory)) { + if (atom.inDevMode()) { + console.log('linter-elm-make: created work directory - ' + workDirectory + ' -> ' + projectDirectory); + } + self.copyProjectToWorkDirectory(projectDirectory, workDirectory); + } + self.watchProjectDirectory(projectDirectory, workDirectory); + } + } if (atom.config.get('linter-elm-make.lintOnTheFly')) { // Lint on the fly. - // If `compileDirectories` does not have an entry for `projectDirectory` yet... - if (!self.compileDirectories[projectDirectory]) { + if (!self.workDirectories[projectDirectory]) { // Create a temporary directory for the project. - const compileDirectory = tmp.dirSync({prefix: 'linter-elm-make'}).name; + const workDirectory = tmp.dirSync({prefix: 'linter-elm-make'}).name; if (atom.inDevMode()) { - console.log('linter-elm-make: created temporary directory - ' + projectDirectory + ' -> ' + compileDirectory); + console.log('linter-elm-make: created temporary work directory - ' + workDirectory + ' -> ' + projectDirectory); } - self.compileDirectories[projectDirectory] = compileDirectory; - // Recursively copy `.elm` files, `elm-package.json`, and `elm-stuff` from project directory to temporary directory. - // Initial linting might take time depending on the project directory size. - fs.copySync(projectDirectory, compileDirectory, { - preserveTimestamps: true, - filter: (filePath) => { - return path.dirname(filePath).replace(projectDirectory, '').startsWith(path.sep + 'elm-stuff' + path.sep) || - path.basename(filePath) === 'elm-package.json' || - path.extname(filePath) === '.elm'; - } - }); - // TODO Do not copy folders without `.elm` files (`fs-extra` issue). - // Watch project directory for file changes. - let watcher = chokidar.watch(['elm-package.json', '**/*.elm'], { - cwd: projectDirectory, - usePolling: true, useFsEvents: true, persistent: true, - ignored: ['**/elm-stuff/**/*.elm', '**\elm-stuff\**\*.elm'], ignoreInitial: true, - followSymlinks: false, interval: 100, alwaysStat: false, depth: undefined, - awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }, - ignorePermissionErrors: false, atomic: false - }); - watcher.on('add', (filename) => { - if (path.basename === 'elm-package.json' || path.extname(filename) === '.elm') { - const filePath = path.join(projectDirectory, filename); - if (atom.inDevMode()) { - console.log('linter-elm-make: add detected - ', filePath); - } - fs.copySync(filePath, path.join(compileDirectory, filename)); - } - }); - watcher.on('unlink', (filename) => { - if (atom.inDevMode()) { - console.log('linter-elm-make: unlink detected - ', path.join(projectDirectory, filename)); - } - fs.removeSync(path.join(compileDirectory, filename)); - }); - watcher.on('unlinkDir', (dirname) => { - if (atom.inDevMode()) { - console.log('linter-elm-make: unlinkDir detected - ', path.join(projectDirectory, dirname)); - } - fs.removeSync(path.join(compileDirectory, dirname)); - }); - watcher.on('change', (filename) => { - if (path.basename === 'elm-package.json') { - forceLintActiveElmEditor(); - } - }); - // TODO Handle when projectDirectory gets renamed or deleted. - // TODO When to close watcher, delete temporary directory, and delete self.compileDirectories[projectDirectory]? - return self.compileInCompileDirectory(editorFilePath, compileDirectory, editor, projectDirectory); + self.workDirectories[projectDirectory] = workDirectory; + self.copyProjectToWorkDirectory(projectDirectory, workDirectory); + self.watchProjectDirectory(projectDirectory, workDirectory); } - // If `projectDirectory` already has an entry in `compileDirectories`... - return self.compileInCompileDirectory(editorFilePath, self.compileDirectories[projectDirectory], editor, projectDirectory); + return self.compileInWorkDirectory(editorFilePath, self.workDirectories[projectDirectory], editor, projectDirectory); } else { // Lint on save. + const workDirectory = self.workDirectories[projectDirectory] || projectDirectory; if (atom.config.get('linter-elm-make.alwaysCompileMain')) { - return self.compileMainFiles(editorFilePath, projectDirectory, projectDirectory, editor, (mainFilePath) => mainFilePath); + return self.compileMainFiles(editorFilePath, projectDirectory, workDirectory, editor, (mainFilePath) => mainFilePath); } else { // Compile active file. - return self.executeElmMake(editorFilePath, editorFilePath, projectDirectory, editor, projectDirectory); + return self.executeElmMake(editorFilePath, editorFilePath, workDirectory, editor, projectDirectory); } } } @@ -388,27 +365,113 @@ module.exports = { this.subscriptions.add(atom.config.observe('linter-elm-make.lintOnTheFly', lintOnTheFly => { linter.lintOnFly = lintOnTheFly; })); + this.subscriptions.add(atom.config.observe('linter-elm-make.workDirectory', workDirectory => { + for (let projectDirectory in self.watchers) { + if (self.watchers.hasOwnProperty(projectDirectory)) { + self.watchers[projectDirectory].close(); + } + } + self.watchers = {}; + self.workDirectories = {}; + // TODO Also delete temporary directories. + })); return linter; }, - provideGetCompileDirectory() { + copyProjectToWorkDirectory(projectDirectory, workDirectory) { + // Recursively copy `.elm` files, `elm-package.json`, and `elm-stuff` from project directory to work directory. + // Initial linting might take time depending on the project directory size. + const filePathFilter = (filePath) => { + return fs.lstatSync(filePath).isFile() && + (path.dirname(filePath).replace(projectDirectory, '').startsWith(path.sep + 'elm-stuff' + path.sep) || + path.basename(filePath) === 'elm-package.json' || + path.extname(filePath) === '.elm'); + }; + // If work directory is inside project directory... + if ((workDirectory + path.sep).startsWith(projectDirectory)) { + const files = readDir.readSync(projectDirectory, null, readDir.ABSOLUTE_PATHS); + files.forEach((file) => { + fs.copySync(file, file.replace(projectDirectory, workDirectory), { + preserveTimestamps: true, + filter: filePathFilter + }); + }); + } else { + fs.copySync(projectDirectory, workDirectory, { + preserveTimestamps: true, + filter: filePathFilter + }); + } + }, + watchProjectDirectory(projectDirectory, workDirectory) { + // Watch project directory for file changes. + let ignored = ['**' + path.sep + 'elm-stuff' + path.sep + '**' + path.sep + '*.elm']; + if ((workDirectory + path.sep).startsWith(projectDirectory)) { + ignored.push(workDirectory.replace(projectDirectory + path.sep, '') + path.sep + '**'); + } + let watcher = chokidar.watch(['elm-package.json', '**/*.elm'], { + cwd: projectDirectory, + usePolling: true, useFsEvents: true, persistent: true, + ignored: ignored, ignoreInitial: true, + followSymlinks: false, interval: 100, alwaysStat: false, depth: undefined, + awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }, + ignorePermissionErrors: false, atomic: false + }); + this.watchers[projectDirectory] = watcher; + watcher.on('add', (filename) => { + const filePath = path.join(projectDirectory, filename); + if (!filePath.startsWith(workDirectory + path.sep) && (path.basename(filePath) === 'elm-package.json' || path.extname(filePath) === '.elm')) { + if (atom.inDevMode()) { + console.log('linter-elm-make: add detected - ', filePath); + } + fs.copySync(filePath, path.join(workDirectory, filename)); + } + }); + watcher.on('unlink', (filename) => { + const filePath = path.join(projectDirectory, filename); + if (atom.inDevMode()) { + console.log('linter-elm-make: unlink detected - ', filePath); + } + if (!filePath.startsWith(workDirectory + path.sep)) { + fs.removeSync(path.join(workDirectory, filename)); + } + }); + watcher.on('unlinkDir', (dirname) => { + const dirPath = path.join(projectDirectory, dirname); + if (atom.inDevMode()) { + console.log('linter-elm-make: unlinkDir detected - ', dirPath); + } + if (!dirPath.startsWith(workDirectory + path.sep)) { + fs.removeSync(path.join(workDirectory, dirname)); + } + }); + watcher.on('change', (filename) => { + const filePath = path.join(projectDirectory, filename); + if (!filePath.startsWith(workDirectory + path.sep) && path.basename === 'elm-package.json') { + forceLintActiveElmEditor(); + } + }); + // TODO Handle when projectDirectory gets renamed or deleted. + // TODO When to close watcher, delete temporary directory, and delete self.workDirectories[projectDirectory]? + }, + provideGetWorkDirectory() { const self = this; return (filePath) => { const projectDirectory = lookupElmPackage(path.dirname(filePath)); if (projectDirectory === null) { return null; } - return self.compileDirectories[projectDirectory]; + return self.workDirectories[projectDirectory] || projectDirectory; }; }, - compileInCompileDirectory(editorFilePath, compileDirectory, editor, projectDirectory) { - const compileFilePath = path.join(compileDirectory, editorFilePath.replace(projectDirectory, '')); + compileInWorkDirectory(editorFilePath, workDirectory, editor, projectDirectory) { + const workFilePath = path.join(workDirectory, editorFilePath.replace(projectDirectory, '')); // Write contents of active editor to associated compile file. - fs.writeFileSync(compileFilePath, editor.getText()); + fs.writeFileSync(workFilePath, editor.getText()); if (atom.config.get('linter-elm-make.alwaysCompileMain')) { - return this.compileMainFiles(editorFilePath, projectDirectory, compileDirectory, editor, (mainFilePath) => mainFilePath.replace(projectDirectory, compileDirectory)); + return this.compileMainFiles(editorFilePath, projectDirectory, workDirectory, editor, (mainFilePath) => mainFilePath.replace(projectDirectory, workDirectory)); } else { // Compile active file. - return this.executeElmMake(editorFilePath, compileFilePath, compileDirectory, editor, projectDirectory); + return this.executeElmMake(editorFilePath, workFilePath, workDirectory, editor, projectDirectory); } }, compileMainFiles(editorFilePath, projectDirectory, cwd, editor, mainFilePathTransformFunction) { @@ -539,12 +602,12 @@ module.exports = { [problem.region.end.line - 1, problem.region.end.column - 1] ); let filePath = problem.file; - if (problem.file.startsWith('.')) { + if (problem.file.startsWith('.' + path.sep)) { // `problem.file` has a relative path (e.g. `././A.elm`) . Convert to absolute. filePath = path.join(cwd, path.normalize(problem.file)); } if (cwd !== projectDirectory) { - // problem.file is a compile file + // problem.file is a work file filePath = filePath.replace(cwd, projectDirectory); } return { @@ -557,13 +620,13 @@ module.exports = { }); } }); - const problemsWithoutCompileFiles = problemsByLine.map((problems) => { + const problemsWithoutWorkFiles = problemsByLine.map((problems) => { return problems.filter((problem) => { - // Filter out compile files. + // Filter out work files. return problem.filePath.startsWith(projectDirectory); }); }); - const allProblems = flattenArray(problemsWithoutCompileFiles); + const allProblems = flattenArray(problemsWithoutWorkFiles); // Store problems for each file path. const uniqueFilePaths = new Set(allProblems.map(({filePath}) => filePath)); let getProblemsOfFilePath = (fpath) => { @@ -641,7 +704,7 @@ function lookupElmPackage(directory) { const parentDirectory = path.join(directory, ".."); if (parentDirectory === directory) { atom.notifications.addError('No `elm-package.json` beneath or above the edited file', { - detail: 'You can generate an `elm-package.json` file by executing `elm package install` in the command line.', + detail: 'You can generate an `elm-package.json` file by running `elm package install` from the command line.', dismissable: true }); return null; diff --git a/package.json b/package.json index d858829..821a042 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "atom-linter": "^5.0.1", "fs-extra": "0.30.0", "tmp": "0.0.28", - "chokidar": "1.5.2" + "chokidar": "1.5.2", + "readdir": "0.0.13" }, "providedServices": { "linter": { @@ -23,9 +24,9 @@ "1.0.0": "provideLinter" } }, - "linter-elm-make-get-compile-directory": { + "linter-elm-make-get-work-directory": { "versions": { - "1.0.0": "provideGetCompileDirectory" + "1.0.0": "provideGetWorkDirectory" } } },