Skip to content

Commit 37ad0e3

Browse files
authored
Merge pull request #426 from PowerShell/kapilmb/code-formatting
Add feature to format PowerShell source files
2 parents 2399e15 + 03224b9 commit 37ad0e3

File tree

4 files changed

+308
-12
lines changed

4 files changed

+308
-12
lines changed

package.json

+10
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,16 @@
329329
"type": "boolean",
330330
"default": false,
331331
"description": "Launches the language service with the /waitForDebugger flag to force it to wait for a .NET debugger to attach before proceeding."
332+
},
333+
"powershell.codeFormatting.openBraceOnSameLine":{
334+
"type":"boolean",
335+
"default": true,
336+
"description": "Places open brace on the same line as its associated statement."
337+
},
338+
"powershell.codeFormatting.newLineAfterOpenBrace":{
339+
"type":"boolean",
340+
"default": true,
341+
"description": "A new line must follow an open brace."
332342
}
333343
}
334344
}

src/features/DocumentFormatter.ts

+272
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import vscode = require('vscode');
6+
import {
7+
languages,
8+
TextDocument,
9+
TextEdit,
10+
FormattingOptions,
11+
CancellationToken,
12+
DocumentFormattingEditProvider,
13+
DocumentRangeFormattingEditProvider,
14+
Range,
15+
} from 'vscode';
16+
import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient';
17+
import Window = vscode.window;
18+
import { IFeature } from '../feature';
19+
import * as Settings from '../settings';
20+
import * as Utils from '../utils';
21+
22+
export namespace ScriptFileMarkersRequest {
23+
export const type: RequestType<any, any, void> = { get method(): string { return "powerShell/getScriptFileMarkers"; } };
24+
}
25+
26+
// TODO move some of the common interface to a separate file?
27+
interface ScriptFileMarkersRequestParams {
28+
filePath: string;
29+
settings: any;
30+
}
31+
32+
interface ScriptFileMarkersRequestResultParams {
33+
markers: ScriptFileMarker[];
34+
}
35+
36+
interface ScriptFileMarker {
37+
message: string;
38+
level: ScriptFileMarkerLevel;
39+
scriptRegion: ScriptRegion;
40+
correction: MarkerCorrection;
41+
}
42+
43+
enum ScriptFileMarkerLevel {
44+
Information = 0,
45+
Warning,
46+
Error
47+
}
48+
49+
interface ScriptRegion {
50+
file: string;
51+
text: string;
52+
startLineNumber: number;
53+
startColumnNumber: number;
54+
startOffset: number;
55+
endLineNumber: number;
56+
endColumnNumber: number;
57+
endOffset: number;
58+
}
59+
60+
interface MarkerCorrection {
61+
name: string;
62+
edits: ScriptRegion[]
63+
}
64+
65+
function editComparer(leftOperand: ScriptRegion, rightOperand: ScriptRegion): number {
66+
if (leftOperand.startLineNumber < rightOperand.startLineNumber) {
67+
return -1;
68+
} else if (leftOperand.startLineNumber > rightOperand.startLineNumber) {
69+
return 1;
70+
} else {
71+
if (leftOperand.startColumnNumber < rightOperand.startColumnNumber) {
72+
return -1;
73+
}
74+
else if (leftOperand.startColumnNumber > rightOperand.startColumnNumber) {
75+
return 1;
76+
}
77+
else {
78+
return 0;
79+
}
80+
}
81+
}
82+
83+
class PSDocumentFormattingEditProvider implements DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider {
84+
private languageClient: LanguageClient;
85+
86+
// The order in which the rules will be executed starting from the first element.
87+
private readonly ruleOrder: string[] = [
88+
"PSPlaceCloseBrace",
89+
"PSPlaceOpenBrace",
90+
"PSUseConsistentIndentation"];
91+
92+
// Allows edits to be undone and redone is a single step.
93+
// It is usefuld to have undo stops after every edit while debugging
94+
// hence we keep this as an option but set it true by default.
95+
private aggregateUndoStop: boolean;
96+
97+
constructor(aggregateUndoStop = true) {
98+
this.aggregateUndoStop = aggregateUndoStop;
99+
}
100+
101+
provideDocumentFormattingEdits(
102+
document: TextDocument,
103+
options: FormattingOptions,
104+
token: CancellationToken): TextEdit[] | Thenable<TextEdit[]> {
105+
return this.provideDocumentRangeFormattingEdits(document, null, options, token);
106+
}
107+
108+
provideDocumentRangeFormattingEdits(
109+
document: TextDocument,
110+
range: Range,
111+
options: FormattingOptions,
112+
token: CancellationToken): TextEdit[] | Thenable<TextEdit[]> {
113+
return this.executeRulesInOrder(document, range, options, 0);
114+
}
115+
116+
executeRulesInOrder(
117+
document: TextDocument,
118+
range: Range,
119+
options: FormattingOptions,
120+
index: number): Thenable<TextEdit[]> | TextEdit[] {
121+
if (this.languageClient !== null && index < this.ruleOrder.length) {
122+
let rule = this.ruleOrder[index];
123+
let uniqueEdits: ScriptRegion[] = [];
124+
let edits: ScriptRegion[];
125+
return this.languageClient.sendRequest(
126+
ScriptFileMarkersRequest.type,
127+
{
128+
filePath: document.fileName,
129+
settings: this.getSettings(rule)
130+
})
131+
.then((result: ScriptFileMarkersRequestResultParams) => {
132+
edits = result.markers.map(marker => { return marker.correction.edits[0]; });
133+
134+
// sort in decending order of the edits
135+
edits.sort((left: ScriptRegion, right: ScriptRegion) => {
136+
return -1 * editComparer(left, right);
137+
});
138+
139+
// We cannot handle multiple edits at the same point hence we
140+
// filter the markers so that there is only one edit per line
141+
// This ideally should not happen but it is good to have some additional safeguard
142+
if (edits.length > 0) {
143+
uniqueEdits.push(edits[0]);
144+
for (let edit of edits.slice(1)) {
145+
if (editComparer(uniqueEdits[uniqueEdits.length - 1], edit) !== 0) {
146+
uniqueEdits.push(edit);
147+
}
148+
}
149+
}
150+
151+
// we need to update the range as the edits might
152+
// have changed the original layout
153+
if (range !== null) {
154+
let tempRange: Range = this.getSelectionRange(document);
155+
if (tempRange !== null) {
156+
range = tempRange;
157+
}
158+
}
159+
160+
// we do not return a valid array because our text edits
161+
// need to be executed in a particular order and it is
162+
// easier if we perform the edits ourselves
163+
return this.applyEdit(uniqueEdits, range, 0, index);
164+
})
165+
.then(() => {
166+
// execute the same rule again if we left out violations
167+
// on the same line
168+
if (uniqueEdits.length !== edits.length) {
169+
return this.executeRulesInOrder(document, range, options, index);
170+
}
171+
return this.executeRulesInOrder(document, range, options, index + 1);
172+
});
173+
} else {
174+
return TextEdit[0];
175+
}
176+
}
177+
178+
applyEdit(edits: ScriptRegion[], range: Range, markerIndex: number, ruleIndex: number): Thenable<void> {
179+
if (markerIndex >= edits.length) {
180+
return;
181+
}
182+
183+
let undoStopAfter = !this.aggregateUndoStop || (ruleIndex === this.ruleOrder.length - 1 && markerIndex === edits.length - 1);
184+
let undoStopBefore = !this.aggregateUndoStop || (ruleIndex === 0 && markerIndex === 0);
185+
let edit: ScriptRegion = edits[markerIndex];
186+
let editRange: Range = new vscode.Range(
187+
edit.startLineNumber - 1,
188+
edit.startColumnNumber - 1,
189+
edit.endLineNumber - 1,
190+
edit.endColumnNumber - 1);
191+
if (range === null || range.contains(editRange)) {
192+
return Window.activeTextEditor.edit((editBuilder) => {
193+
editBuilder.replace(
194+
editRange,
195+
edit.text);
196+
},
197+
{
198+
undoStopAfter: undoStopAfter,
199+
undoStopBefore: undoStopBefore
200+
}).then((isEditApplied) => {
201+
return this.applyEdit(edits, range, markerIndex + 1, ruleIndex);
202+
}); // TODO handle rejection
203+
}
204+
else {
205+
return this.applyEdit(edits, range, markerIndex + 1, ruleIndex);
206+
}
207+
}
208+
209+
getSelectionRange(document: TextDocument): Range {
210+
let editor = vscode.window.visibleTextEditors.find(editor => editor.document === document);
211+
if (editor !== undefined) {
212+
return editor.selection as Range;
213+
}
214+
215+
return null;
216+
}
217+
218+
setLanguageClient(languageClient: LanguageClient): void {
219+
this.languageClient = languageClient;
220+
}
221+
222+
getSettings(rule: string): any {
223+
let psSettings: Settings.ISettings = Settings.load(Utils.PowerShellLanguageId);
224+
let ruleSettings = new Object();
225+
ruleSettings["Enable"] = true;
226+
227+
switch (rule) {
228+
case "PSPlaceOpenBrace":
229+
ruleSettings["OnSameLine"] = psSettings.codeFormatting.openBraceOnSameLine;
230+
ruleSettings["NewLineAfter"] = psSettings.codeFormatting.newLineAfterOpenBrace;
231+
break;
232+
233+
case "PSUseConsistentIndentation":
234+
ruleSettings["IndentationSize"] = vscode.workspace.getConfiguration("editor").get<number>("tabSize");
235+
break;
236+
237+
default:
238+
break;
239+
}
240+
241+
let settings: Object = new Object();
242+
settings[rule] = ruleSettings;
243+
return settings;
244+
}
245+
}
246+
247+
export class DocumentFormatterFeature implements IFeature {
248+
private formattingEditProvider: vscode.Disposable;
249+
private rangeFormattingEditProvider: vscode.Disposable;
250+
private languageClient: LanguageClient;
251+
private documentFormattingEditProvider: PSDocumentFormattingEditProvider;
252+
253+
constructor() {
254+
this.documentFormattingEditProvider = new PSDocumentFormattingEditProvider();
255+
this.formattingEditProvider = vscode.languages.registerDocumentFormattingEditProvider(
256+
"powershell",
257+
this.documentFormattingEditProvider);
258+
this.rangeFormattingEditProvider = vscode.languages.registerDocumentRangeFormattingEditProvider(
259+
"powershell",
260+
this.documentFormattingEditProvider);
261+
}
262+
263+
public setLanguageClient(languageclient: LanguageClient): void {
264+
this.languageClient = languageclient;
265+
this.documentFormattingEditProvider.setLanguageClient(languageclient);
266+
}
267+
268+
public dispose(): any {
269+
this.formattingEditProvider.dispose();
270+
this.rangeFormattingEditProvider.dispose();
271+
}
272+
}

src/main.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { SelectPSSARulesFeature } from './features/SelectPSSARules';
2222
import { FindModuleFeature } from './features/PowerShellFindModule';
2323
import { NewFileOrProjectFeature } from './features/NewFileOrProject';
2424
import { ExtensionCommandsFeature } from './features/ExtensionCommands';
25+
import { DocumentFormatterFeature } from './features/DocumentFormatter';
2526

2627
// NOTE: We will need to find a better way to deal with the required
2728
// PS Editor Services version...
@@ -104,6 +105,7 @@ export function activate(context: vscode.ExtensionContext): void {
104105
new SelectPSSARulesFeature(),
105106
new CodeActionsFeature(),
106107
new NewFileOrProjectFeature(),
108+
new DocumentFormatterFeature(),
107109
new DebugSessionFeature(),
108110
new PickPSHostProcessFeature()
109111
];

src/settings.ts

+24-12
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66

77
import vscode = require('vscode');
88

9+
export interface ICodeFormattingSettings {
10+
openBraceOnSameLine: boolean;
11+
newLineAfterOpenBrace: boolean;
12+
}
13+
914
export interface IScriptAnalysisSettings {
10-
enable?: boolean
11-
settingsPath: string
15+
enable?: boolean;
16+
settingsPath: string;
1217
}
1318

1419
export interface IDeveloperSettings {
@@ -19,31 +24,38 @@ export interface IDeveloperSettings {
1924
}
2025

2126
export interface ISettings {
22-
useX86Host?: boolean,
23-
enableProfileLoading?: boolean,
24-
scriptAnalysis?: IScriptAnalysisSettings,
25-
developer?: IDeveloperSettings,
27+
useX86Host?: boolean;
28+
enableProfileLoading?: boolean;
29+
scriptAnalysis?: IScriptAnalysisSettings;
30+
developer?: IDeveloperSettings;
31+
codeFormatting?: ICodeFormattingSettings;
2632
}
2733

2834
export function load(myPluginId: string): ISettings {
29-
let configuration = vscode.workspace.getConfiguration(myPluginId);
35+
let configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(myPluginId);
3036

31-
let defaultScriptAnalysisSettings = {
37+
let defaultScriptAnalysisSettings: IScriptAnalysisSettings = {
3238
enable: true,
3339
settingsPath: ""
3440
};
3541

36-
let defaultDeveloperSettings = {
42+
let defaultDeveloperSettings: IDeveloperSettings = {
3743
powerShellExePath: undefined,
3844
bundledModulesPath: "../modules/",
3945
editorServicesLogLevel: "Normal",
4046
editorServicesWaitForDebugger: false
41-
}
47+
};
48+
49+
let defaultCodeFormattingSettings: ICodeFormattingSettings = {
50+
openBraceOnSameLine: true,
51+
newLineAfterOpenBrace: true
52+
};
4253

4354
return {
4455
useX86Host: configuration.get<boolean>("useX86Host", false),
4556
enableProfileLoading: configuration.get<boolean>("enableProfileLoading", false),
4657
scriptAnalysis: configuration.get<IScriptAnalysisSettings>("scriptAnalysis", defaultScriptAnalysisSettings),
47-
developer: configuration.get<IDeveloperSettings>("developer", defaultDeveloperSettings)
48-
}
58+
developer: configuration.get<IDeveloperSettings>("developer", defaultDeveloperSettings),
59+
codeFormatting: configuration.get<ICodeFormattingSettings>("codeFormatting", defaultCodeFormattingSettings)
60+
};
4961
}

0 commit comments

Comments
 (0)