Skip to content

Commit b1b92db

Browse files
committed
Adds formatting support
Adds support for `findent` and `fprettify`. Fixes #29.
1 parent 389a6e2 commit b1b92db

File tree

5 files changed

+224
-1
lines changed

5 files changed

+224
-1
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Adds support for formatting with `findent` and `fprettify`
13+
([#29](https://github.com/krvajal/vscode-fortran-support/issues/29))
14+
- Adds prompts for installing Fortran IntelliSense and fortran-language-server
15+
16+
### Removed
17+
18+
- Removes `paths.js` for detecting binaries in favour of `which`
19+
1020
## [2.3.1]
1121

1222
### Changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- Code autocompletion (beta)
1818
- Symbols provider
1919
- Debugger, uses Microsoft's [C/C++ extension](https://github.com/Microsoft/vscode-cpptools)
20+
- Formatting with [findent](https://sourceforge.net/projects/findent/) or [fprettify](https://github.com/pseewald/fprettify)
2021

2122
![symbol_nav](./doc/symbol_nav.png)
2223

@@ -140,6 +141,41 @@ More details about how to setup the debugger can be found in Microsoft's website
140141
}
141142
```
142143

144+
## Formatting
145+
146+
Two formatters are supported [`findent`](https://sourceforge.net/projects/findent/)
147+
and [`fprettify`](https://github.com/pseewald/fprettify). `fprettify` can be installed
148+
automatically through the extension using `pip`, while `findent` must be installed
149+
by the user manually.
150+
151+
The formatter is controlled by the user option
152+
153+
```json
154+
{
155+
"fortran.formatting.formatter": "Disabled" | "findent" | "fprettify",
156+
}
157+
```
158+
159+
Additional arguments to the formatter can be input using
160+
161+
```json
162+
{
163+
"fortran.formatting.args":, ["-Cn", "-Rr"]
164+
}
165+
```
166+
167+
If the formatter is not present in the `PATH` its location can be input with
168+
169+
```json
170+
{
171+
"fortran.formattting.path": "/custom-path-to-formatter-binary",
172+
}
173+
```
174+
175+
### NOTE: About `findent`
176+
177+
`findent` can also be used to generate dependency files for a project.
178+
143179
## Requirements
144180

145181
For the linter to work you need to have `gfortran` on your path, or wherever you configure it to be.

package.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"Programming Languages",
3737
"Snippets",
3838
"Linters",
39-
"Debuggers"
39+
"Debuggers",
40+
"Formatters"
4041
],
4142
"activationEvents": [
4243
"onLanguage:FortranFreeForm"
@@ -157,6 +158,26 @@
157158
],
158159
"description": "Specify additional options to use when calling the gfortran compiler"
159160
},
161+
"fortran.formatting.formatter": {
162+
"type": "string",
163+
"default": "Disabled",
164+
"enum": [
165+
"findent",
166+
"fprettify",
167+
"Disabled"
168+
],
169+
"description": "Fortran formatter, currently supports findent and fprettify"
170+
},
171+
"fortran.formatting.args": {
172+
"type": "array",
173+
"default": [],
174+
"description": "Additional arguments for the formatter"
175+
},
176+
"fortran.formatting.path": {
177+
"type": "string",
178+
"default": "",
179+
"description": "Specify the full path of where the formatter is installed"
180+
},
160181
"fortran.provideSymbols": {
161182
"type": "boolean",
162183
"default": true,

src/extension.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { FORTRAN_FREE_FORM_ID, EXTENSION_ID, promptForMissingTool } from './lib/
1212
import { LoggingService } from './services/logging-service'
1313
import * as pkg from '../package.json'
1414
import { LANG_SERVER_TOOL_ID } from './lib/tools'
15+
import { FortranFormattingProvider } from './features/formatting-provider'
1516

1617
export function activate(context: vscode.ExtensionContext) {
1718
const loggingService = new LoggingService()
@@ -28,6 +29,18 @@ export function activate(context: vscode.ExtensionContext) {
2829
loggingService.logInfo('Linter is not enabled')
2930
}
3031

32+
if (extensionConfig.get('formatter') !== 'Disabled') {
33+
let disposable: vscode.Disposable =
34+
vscode.languages.registerDocumentFormattingEditProvider(
35+
FORTRAN_FREE_FORM_ID,
36+
new FortranFormattingProvider(loggingService)
37+
);
38+
context.subscriptions.push(disposable);
39+
}
40+
else {
41+
loggingService.logInfo('Formatting is disabled')
42+
}
43+
3144
if (extensionConfig.get('provideCompletion', true)) {
3245
let completionProvider = new FortranCompletionProvider(loggingService)
3346
vscode.languages.registerCompletionItemProvider(

src/features/formatting-provider.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
'use strict';
2+
3+
import * as fs from 'fs';
4+
import * as path from 'path';
5+
import * as which from 'which';
6+
import * as vscode from 'vscode';
7+
import * as cp from 'child_process';
8+
9+
import { FORMATTERS } from '../lib/tools';
10+
import { LoggingService } from '../services/logging-service';
11+
import { EXTENSION_ID, promptForMissingTool } from '../lib/helper';
12+
13+
14+
export class FortranFormattingProvider
15+
implements vscode.DocumentFormattingEditProvider {
16+
17+
constructor(private logger: LoggingService) { }
18+
19+
public provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.ProviderResult<vscode.TextEdit[]> {
20+
21+
let formatterName: string = this.getFormatter();
22+
23+
if (formatterName === 'fprettify') {
24+
this.doFormatFprettify(document);
25+
}
26+
else if (formatterName === 'findent') {
27+
this.doFormatFindent(document);
28+
}
29+
else {
30+
this.logger.logError('Cannot format document with formatter set to Disabled')
31+
}
32+
33+
return
34+
}
35+
36+
/**
37+
* Use `fprettyfy` to format a Fortran file.
38+
*
39+
* @param document vscode.TextDocument document to operate on
40+
*/
41+
private doFormatFprettify(document: vscode.TextDocument) {
42+
43+
const formatterName: string = 'fprettify';
44+
let formatterPath: string = this.getFormatterPath();
45+
// If no formatter path is present check that formatter is present in $PATH
46+
if (!formatterPath) {
47+
if (!which.sync(formatterName, { nothrow: true })) {
48+
this.logger.logWarning(`Formatter: ${formatterName} not detected in your system.
49+
Attempting to install now.`);
50+
let msg = `Installing ${formatterName} through pip with --user option`;
51+
promptForMissingTool(formatterName, msg, 'Python');
52+
}
53+
}
54+
let formatter: string = path.join(formatterPath, formatterName);
55+
56+
let args: string[] = [document.fileName, ...this.getFormatterArgs()];
57+
// args.push('--silent'); // TODO: pass?
58+
59+
// Get current file (name rel to path), run extension can be in a shell??
60+
let process = cp.spawn(formatter, args);
61+
62+
// if the findent then capture the output from that and parse it back to the file
63+
process.stdout.on('data', (data) => { this.logger.logInfo(`formatter stdout: ${data}`) });
64+
process.stderr.on('data', (data) => { this.logger.logError(`formatter stderr: ${data}`) });
65+
process.on('close', (code: number) => { if (code !== 0) this.logger.logInfo(`formatter exited with code: ${code}`) });
66+
process.on('error', (code) => { this.logger.logInfo(`formatter exited with code: ${code}`) });
67+
68+
}
69+
70+
/**
71+
* Use `findent` to format a Fortran file.
72+
* Creates a temporary file where the output is placed and then deleted
73+
*
74+
* @param document vscode.TextDocument document to operate on
75+
*/
76+
private doFormatFindent(document: vscode.TextDocument) {
77+
78+
const formatterName: string = 'findent';
79+
let formatterPath: string = this.getFormatterPath();
80+
let formatter: string = path.join(formatterPath, formatterName);
81+
82+
// Annoyingly findent only outputs to a file and not to a stream so
83+
// let us go and create a temporary file
84+
let out = document.uri.path + '.findent.tmp';
85+
let args: string = ['< ' + document.fileName + ' >', out, ...this.getFormatterArgs()].join(' ');
86+
formatter = formatter + ' ' + args;
87+
88+
// @note It is wise to have all IO operations being synchronous we don't
89+
// want to copy or delete the temp file before findent has completed.
90+
// I cannot forsee a situation where a performance bottleneck is created
91+
// since findent performs something like ~100k lines per second
92+
cp.execSync(formatter, { stdio: 'inherit' });
93+
fs.copyFileSync(out, document.fileName);
94+
fs.unlinkSync(out);
95+
96+
}
97+
98+
/**
99+
* Get the formatter type
100+
* Currently supporting: `findent` and `fprettify`
101+
*
102+
* Formatters are defined in FORMATTERS (./lib/tools.ts)
103+
*
104+
* @returns {string} formatter name or `Disabled`
105+
*/
106+
private getFormatter(): string {
107+
108+
let config = vscode.workspace.getConfiguration(EXTENSION_ID)
109+
const formatter: string = config.get('formatting.formatter', 'Disabled')
110+
111+
if (!FORMATTERS.includes(formatter)) {
112+
this.logger.logError(`Unsupported formatter: ${formatter}`)
113+
}
114+
return formatter
115+
}
116+
117+
/**
118+
* Read in any custom arguments for the formatter
119+
*
120+
* @returns {string[]} list of additional arguments
121+
*/
122+
private getFormatterArgs(): string[] {
123+
let config = vscode.workspace.getConfiguration(EXTENSION_ID)
124+
const args: string[] = config.get('formatting.args', [])
125+
126+
return args
127+
}
128+
129+
/**
130+
* Installation directory for formatter (if not in PATH)
131+
*
132+
* @returns {string} path of formatter
133+
*/
134+
private getFormatterPath(): string {
135+
let config = vscode.workspace.getConfiguration(EXTENSION_ID)
136+
const formatterPath: string = config.get('formatting.path', '')
137+
if (formatterPath !== '') {
138+
this.logger.logInfo(`Formatter located in: ${formatterPath}`)
139+
}
140+
141+
return formatterPath
142+
}
143+
}

0 commit comments

Comments
 (0)