diff --git a/CHANGELOG.md b/CHANGELOG.md index 610db139..47c6b010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [2.5.0] + +### Added + +- Adds support for formatting with `findent` and `fprettify` + ([#29](https://github.com/krvajal/vscode-fortran-support/issues/29)) + ## [2.4.3] ### Changed @@ -22,6 +29,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ([#257](https://github.com/krvajal/vscode-fortran-support/issues/257)) - Linting is now operational for `FortranFixedForm` ([#258](https://github.com/krvajal/vscode-fortran-support/issues/258)) +- Fixes dummy variable list erroneous syntax highlighting + ([#264](https://github.com/krvajal/vscode-fortran-support/issues/264)) ### Changed @@ -29,6 +38,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). `FortranFixedForm`, an alias has been added for backwards compatibility ([#259](https://github.com/krvajal/vscode-fortran-support/issues/259)) +### Added + +- Adds prompts for installing Fortran IntelliSense and fortran-language-server + ### Removed - Removes `paths.js` for detecting binaries in favour of `which` @@ -306,7 +319,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Initial release -[unreleased]: https://github.com/krvajal/vscode-fortran-support/compare/v2.4.3...HEAD +[unreleased]: https://github.com/krvajal/vscode-fortran-support/compare/v2.5.0...HEAD +[2.5.0]: https://github.com/krvajal/vscode-fortran-support/compare/v2.4.3...v2.5.0 [2.4.3]: https://github.com/krvajal/vscode-fortran-support/compare/v2.4.2...v2.4.3 [2.4.2]: https://github.com/krvajal/vscode-fortran-support/compare/v2.4.1...v2.4.2 [2.4.1]: https://github.com/krvajal/vscode-fortran-support/compare/v2.4.0...v2.4.1 diff --git a/README.md b/README.md index 97a030b9..dcdc51c8 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ - Code autocompletion (beta) - Symbols provider - Debugger, uses Microsoft's [C/C++ extension](https://github.com/Microsoft/vscode-cpptools) +- Formatting with [findent](https://github.com/gnikit/findent-pypi) or [fprettify](https://github.com/pseewald/fprettify) ![symbol_nav](./doc/symbol_nav.png) @@ -137,6 +138,44 @@ More details about how to setup the debugger can be found in Microsoft's website } ``` +## Formatting + +Two formatters are supported [`findent`](https://github.com/gnikit/findent-pypi) +and [`fprettify`](https://github.com/pseewald/fprettify). Both of them can be +installed with `pip` automatically through the extension. + +findent | fprettify +:-------------------------:|:-------------------------: +![](./images/findent-demo.gif) | ![](./images/fprettify-demo.gif) + +The formatter is controlled by the user option + +```json +{ + "fortran.formatting.formatter": "Disabled" | "findent" | "fprettify", +} +``` + +Additional arguments to the formatter can be input using + +```json +{ + "fortran.formatting.args":, ["-Cn", "--align-paren=1"] +} +``` + +If the formatter is not present in the `PATH` its location can be input with + +```json +{ + "fortran.formattting.path": "/custom-path-to-formatter-binary" +} +``` + +### NOTE: About `findent` + +`findent` can also be used to generate dependency files for a project. + ## Requirements For the linter to work you need to have `gfortran` on your path, or wherever you configure it to be. diff --git a/images/findent-demo.gif b/images/findent-demo.gif new file mode 100644 index 00000000..b66005a4 Binary files /dev/null and b/images/findent-demo.gif differ diff --git a/images/fprettify-demo.gif b/images/fprettify-demo.gif new file mode 100644 index 00000000..030fbd2c Binary files /dev/null and b/images/fprettify-demo.gif differ diff --git a/package-lock.json b/package-lock.json index 0c1d3796..6c2ec454 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "linter-gfortran", - "version": "2.4.0", + "version": "2.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "linter-gfortran", - "version": "2.4.0", + "version": "2.5.0", "license": "MIT", "dependencies": { "vscode-languageclient": "^7.0.0", diff --git a/package.json b/package.json index 72b744b9..0f1c3200 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "linter-gfortran", "displayName": "Modern Fortran", "description": "Modern Fortran language support, including syntax highlighting and error detection.", - "version": "2.4.0", + "version": "2.5.0", "publisher": "krvajalm", "license": "MIT", "author": { @@ -36,7 +36,8 @@ "Programming Languages", "Snippets", "Linters", - "Debuggers" + "Debuggers", + "Formatters" ], "activationEvents": [ "onLanguage:FortranFreeForm", @@ -159,6 +160,26 @@ ], "description": "Specify additional options to use when calling the gfortran compiler" }, + "fortran.formatting.formatter": { + "type": "string", + "default": "findent", + "enum": [ + "findent", + "fprettify", + "Disabled" + ], + "description": "Fortran formatter, currently supports findent and fprettify" + }, + "fortran.formatting.args": { + "type": "array", + "default": [], + "description": "Additional arguments for the formatter" + }, + "fortran.formatting.path": { + "type": "string", + "default": "", + "description": "Specify the full path of where the formatter is installed" + }, "fortran.provideSymbols": { "type": "boolean", "default": true, diff --git a/src/extension.ts b/src/extension.ts index 6abe3434..df0b13c3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,6 +12,7 @@ import { FORTRAN_DOCUMENT_SELECTOR, EXTENSION_ID, promptForMissingTool } from '. import { LoggingService } from './services/logging-service'; import * as pkg from '../package.json'; import { LANG_SERVER_TOOL_ID } from './lib/tools'; +import { FortranFormattingProvider } from './features/formatting-provider'; export function activate(context: vscode.ExtensionContext) { const loggingService = new LoggingService(); @@ -29,6 +30,17 @@ export function activate(context: vscode.ExtensionContext) { loggingService.logInfo('Linter is not enabled'); } + if (extensionConfig.get('formatter') !== 'Disabled') { + const disposable: vscode.Disposable = vscode.languages.registerDocumentFormattingEditProvider( + FORTRAN_DOCUMENT_SELECTOR, + new FortranFormattingProvider(loggingService) + ); + context.subscriptions.push(disposable); + loggingService.logInfo('Formatting is enabled'); + } else { + loggingService.logInfo('Formatting is disabled'); + } + if (extensionConfig.get('provideCompletion', true)) { const completionProvider = new FortranCompletionProvider(loggingService); vscode.languages.registerCompletionItemProvider(FORTRAN_DOCUMENT_SELECTOR, completionProvider); diff --git a/src/features/formatting-provider.ts b/src/features/formatting-provider.ts new file mode 100644 index 00000000..d419d0e6 --- /dev/null +++ b/src/features/formatting-provider.ts @@ -0,0 +1,162 @@ +'use strict'; + +import * as fs from 'fs'; +import * as path from 'path'; +import * as which from 'which'; +import * as vscode from 'vscode'; +import * as cp from 'child_process'; + +import { FORMATTERS } from '../lib/tools'; +import { LoggingService } from '../services/logging-service'; +import { EXTENSION_ID, promptForMissingTool } from '../lib/helper'; + +export class FortranFormattingProvider implements vscode.DocumentFormattingEditProvider { + constructor(private logger: LoggingService) {} + + public provideDocumentFormattingEdits( + document: vscode.TextDocument, + options: vscode.FormattingOptions, + token: vscode.CancellationToken + ): vscode.ProviderResult { + const formatterName: string = this.getFormatter(); + + if (formatterName === 'fprettify') { + this.doFormatFprettify(document); + } else if (formatterName === 'findent') { + this.doFormatFindent(document); + } else { + this.logger.logError('Cannot format document with formatter set to Disabled'); + } + + return; + } + + /** + * Use `fprettify` to format a Fortran file. + * + * @param document vscode.TextDocument document to operate on + */ + private doFormatFprettify(document: vscode.TextDocument) { + // fprettify can only do FortranFreeFrom + if (document.languageId !== 'FortranFreeForm') { + this.logger.logError(`fprettify can only format FortranFreeForm, change + to findent for FortranFixedForm formatting`); + return; + } + + const formatterName = 'fprettify'; + const formatterPath: string = this.getFormatterPath(); + // If no formatter path is present check that formatter is present in $PATH + if (!formatterPath) { + if (!which.sync(formatterName, { nothrow: true })) { + this.logger.logWarning(`Formatter: ${formatterName} not detected in your system. + Attempting to install now.`); + const msg = `Installing ${formatterName} through pip with --user option`; + promptForMissingTool(formatterName, msg, 'Python'); + } + } + const formatter: string = path.join(formatterPath, formatterName); + + const args: string[] = [document.fileName, ...this.getFormatterArgs()]; + // args.push('--silent'); // TODO: pass? + + // Get current file (name rel to path), run extension can be in a shell?? + const process = cp.spawn(formatter, args); + + // if the findent then capture the output from that and parse it back to the file + process.stdout.on('data', data => { + this.logger.logInfo(`formatter stdout: ${data}`); + }); + process.stderr.on('data', data => { + this.logger.logError(`formatter stderr: ${data}`); + }); + process.on('close', (code: number) => { + if (code !== 0) this.logger.logInfo(`formatter exited with code: ${code}`); + }); + process.on('error', code => { + this.logger.logInfo(`formatter exited with code: ${code}`); + }); + } + + /** + * Use `findent` to format a Fortran file. + * Creates a temporary file where the output is placed and then deleted + * + * @param document vscode.TextDocument document to operate on + */ + private doFormatFindent(document: vscode.TextDocument) { + const formatterName = 'findent'; + const formatterPath: string = this.getFormatterPath(); + // If no formatter path is present check that formatter is present in $PATH + if (!formatterPath) { + if (!which.sync(formatterName, { nothrow: true })) { + this.logger.logWarning(`Formatter: ${formatterName} not detected in your system. + Attempting to install now.`); + const msg = `Installing ${formatterName} through pip with --user option`; + promptForMissingTool(formatterName, msg, 'Python'); + } + } + let formatter: string = path.join(formatterPath, formatterName); + + // Annoyingly findent only outputs to a file and not to a stream so + // let us go and create a temporary file + const out = document.uri.path + '.findent.tmp'; + const args: string = ['< ' + document.fileName + ' >', out, ...this.getFormatterArgs()].join( + ' ' + ); + formatter = formatter + ' ' + args; + + // @note It is wise to have all IO operations being synchronous we don't + // want to copy or delete the temp file before findent has completed. + // I cannot forsee a situation where a performance bottleneck is created + // since findent performs something like ~100k lines per second + cp.execSync(formatter, { stdio: 'inherit' }); + fs.copyFileSync(out, document.fileName); + fs.unlinkSync(out); + } + + /** + * Get the formatter type + * Currently supporting: `findent` and `fprettify` + * + * Formatters are defined in FORMATTERS (./lib/tools.ts) + * + * @returns {string} formatter name or `Disabled` + */ + private getFormatter(): string { + const config = vscode.workspace.getConfiguration(EXTENSION_ID); + const formatter: string = config.get('formatting.formatter', 'Disabled'); + + if (!FORMATTERS.includes(formatter)) { + this.logger.logError(`Unsupported formatter: ${formatter}`); + } + return formatter; + } + + /** + * Read in any custom arguments for the formatter + * + * @returns {string[]} list of additional arguments + */ + private getFormatterArgs(): string[] { + const config = vscode.workspace.getConfiguration(EXTENSION_ID); + const args: string[] = config.get('formatting.args', []); + + return args; + } + + /** + * Installation directory for formatter (if not in PATH) + * + * @returns {string} path of formatter + */ + private getFormatterPath(): string { + const config = vscode.workspace.getConfiguration(EXTENSION_ID); + const formatterPath: string = config.get('formatting.path', ''); + if (formatterPath !== '') { + this.logger.logInfo(`Formatter located in: ${formatterPath}`); + } + + return formatterPath; + } +}