Skip to content

Commit 3c7a723

Browse files
authored
Merge pull request #473 from PowerShell/kapilmb/formatter-fixes
Fixes some issues with code formatter
2 parents e5c62a8 + 7a0a142 commit 3c7a723

File tree

1 file changed

+134
-22
lines changed

1 file changed

+134
-22
lines changed

src/features/DocumentFormatter.ts

Lines changed: 134 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@
22
* Copyright (C) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------*/
44

5+
import * as path from "path";
56
import vscode = require('vscode');
67
import {
7-
languages,
88
TextDocument,
99
TextEdit,
1010
FormattingOptions,
1111
CancellationToken,
1212
DocumentFormattingEditProvider,
1313
DocumentRangeFormattingEditProvider,
1414
Range,
15+
TextEditor
1516
} from 'vscode';
16-
import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient';
17+
import { LanguageClient, RequestType } from 'vscode-languageclient';
1718
import Window = vscode.window;
1819
import { IFeature } from '../feature';
1920
import * as Settings from '../settings';
@@ -60,7 +61,7 @@ interface ScriptRegion {
6061

6162
interface MarkerCorrection {
6263
name: string;
63-
edits: ScriptRegion[]
64+
edits: ScriptRegion[];
6465
}
6566

6667
function editComparer(leftOperand: ScriptRegion, rightOperand: ScriptRegion): number {
@@ -81,7 +82,57 @@ function editComparer(leftOperand: ScriptRegion, rightOperand: ScriptRegion): nu
8182
}
8283
}
8384

85+
class DocumentLocker {
86+
private lockedDocuments: Object;
87+
88+
constructor() {
89+
this.lockedDocuments = new Object();
90+
}
91+
92+
isLocked(document: TextDocument): boolean {
93+
return this.isLockedInternal(this.getKey(document));
94+
}
95+
96+
lock(document: TextDocument, unlockWhenDone?: Thenable<any>): void {
97+
this.lockInternal(this.getKey(document), unlockWhenDone);
98+
}
99+
100+
unlock(document: TextDocument): void {
101+
this.unlockInternal(this.getKey(document));
102+
}
103+
104+
unlockAll(): void {
105+
Object.keys(this.lockedDocuments).slice().forEach(documentKey => this.unlockInternal(documentKey));
106+
}
107+
108+
private getKey(document: TextDocument): string {
109+
return document.uri.toString();
110+
}
111+
112+
private lockInternal(documentKey: string, unlockWhenDone?: Thenable<any>): void {
113+
if (!this.isLockedInternal(documentKey)) {
114+
this.lockedDocuments[documentKey] = true;
115+
}
116+
117+
if (unlockWhenDone !== undefined) {
118+
unlockWhenDone.then(() => this.unlockInternal(documentKey));
119+
}
120+
}
121+
122+
private unlockInternal(documentKey: string): void {
123+
if (this.isLockedInternal(documentKey)) {
124+
delete this.lockedDocuments[documentKey];
125+
}
126+
}
127+
128+
private isLockedInternal(documentKey: string): boolean {
129+
return this.lockedDocuments.hasOwnProperty(documentKey);
130+
}
131+
}
132+
84133
class PSDocumentFormattingEditProvider implements DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider {
134+
private static documentLocker = new DocumentLocker();
135+
private static statusBarTracker = new Object();
85136
private languageClient: LanguageClient;
86137

87138
// The order in which the rules will be executed starting from the first element.
@@ -95,6 +146,10 @@ class PSDocumentFormattingEditProvider implements DocumentFormattingEditProvider
95146
// hence we keep this as an option but set it true by default.
96147
private aggregateUndoStop: boolean;
97148

149+
private get emptyPromise(): Promise<TextEdit[]> {
150+
return Promise.resolve(TextEdit[0]);
151+
}
152+
98153
constructor(aggregateUndoStop = true) {
99154
this.aggregateUndoStop = aggregateUndoStop;
100155
}
@@ -112,19 +167,54 @@ class PSDocumentFormattingEditProvider implements DocumentFormattingEditProvider
112167
options: FormattingOptions,
113168
token: CancellationToken): TextEdit[] | Thenable<TextEdit[]> {
114169

115-
let textEdits: Thenable<TextEdit[]> = this.executeRulesInOrder(document, range, options, 0);
116-
AnimatedStatusBar.showAnimatedStatusBarMessage("Formatting PowerShell document", textEdits);
170+
let editor: TextEditor = this.getEditor(document);
171+
if (editor === undefined) {
172+
return this.emptyPromise;
173+
}
174+
175+
// Check if the document is already being formatted.
176+
// If so, then ignore the formatting request.
177+
if (this.isDocumentLocked(document)) {
178+
return this.emptyPromise;
179+
}
180+
181+
let textEdits: Thenable<TextEdit[]> = this.executeRulesInOrder(editor, range, options, 0);
182+
this.lockDocument(document, textEdits);
183+
PSDocumentFormattingEditProvider.showStatusBar(document, textEdits);
117184
return textEdits;
118185
}
119186

120-
executeRulesInOrder(
121-
document: TextDocument,
187+
setLanguageClient(languageClient: LanguageClient): void {
188+
this.languageClient = languageClient;
189+
190+
// setLanguageClient is called while restarting a session,
191+
// so this makes sure we clean up the document locker and
192+
// any residual status bars
193+
PSDocumentFormattingEditProvider.documentLocker.unlockAll();
194+
PSDocumentFormattingEditProvider.disposeAllStatusBars();
195+
}
196+
197+
private getEditor(document: TextDocument): TextEditor {
198+
return Window.visibleTextEditors.find((e, n, obj) => { return e.document === document; });
199+
}
200+
201+
private isDocumentLocked(document: TextDocument): boolean {
202+
return PSDocumentFormattingEditProvider.documentLocker.isLocked(document);
203+
}
204+
205+
private lockDocument(document: TextDocument, unlockWhenDone: Thenable<any>): void {
206+
PSDocumentFormattingEditProvider.documentLocker.lock(document, unlockWhenDone);
207+
}
208+
209+
private executeRulesInOrder(
210+
editor: TextEditor,
122211
range: Range,
123212
options: FormattingOptions,
124213
index: number): Thenable<TextEdit[]> {
125214
if (this.languageClient !== null && index < this.ruleOrder.length) {
126-
let rule = this.ruleOrder[index];
215+
let rule: string = this.ruleOrder[index];
127216
let uniqueEdits: ScriptRegion[] = [];
217+
let document: TextDocument = editor.document;
128218
let edits: ScriptRegion[];
129219

130220
return this.languageClient.sendRequest(
@@ -165,22 +255,29 @@ class PSDocumentFormattingEditProvider implements DocumentFormattingEditProvider
165255
// we do not return a valid array because our text edits
166256
// need to be executed in a particular order and it is
167257
// easier if we perform the edits ourselves
168-
return this.applyEdit(uniqueEdits, range, 0, index);
258+
return this.applyEdit(editor, uniqueEdits, range, 0, index);
169259
})
170260
.then(() => {
171261
// execute the same rule again if we left out violations
172262
// on the same line
263+
let newIndex: number = index + 1;
173264
if (uniqueEdits.length !== edits.length) {
174-
return this.executeRulesInOrder(document, range, options, index);
265+
newIndex = index;
175266
}
176-
return this.executeRulesInOrder(document, range, options, index + 1);
267+
268+
return this.executeRulesInOrder(editor, range, options, newIndex);
177269
});
178270
} else {
179-
return Promise.resolve(TextEdit[0]);
271+
return this.emptyPromise;
180272
}
181273
}
182274

183-
applyEdit(edits: ScriptRegion[], range: Range, markerIndex: number, ruleIndex: number): Thenable<void> {
275+
private applyEdit(
276+
editor: TextEditor,
277+
edits: ScriptRegion[],
278+
range: Range,
279+
markerIndex: number,
280+
ruleIndex: number): Thenable<void> {
184281
if (markerIndex >= edits.length) {
185282
return;
186283
}
@@ -194,7 +291,7 @@ class PSDocumentFormattingEditProvider implements DocumentFormattingEditProvider
194291
edit.endLineNumber - 1,
195292
edit.endColumnNumber - 1);
196293
if (range === null || range.contains(editRange)) {
197-
return Window.activeTextEditor.edit((editBuilder) => {
294+
return editor.edit((editBuilder) => {
198295
editBuilder.replace(
199296
editRange,
200297
edit.text);
@@ -203,15 +300,15 @@ class PSDocumentFormattingEditProvider implements DocumentFormattingEditProvider
203300
undoStopAfter: undoStopAfter,
204301
undoStopBefore: undoStopBefore
205302
}).then((isEditApplied) => {
206-
return this.applyEdit(edits, range, markerIndex + 1, ruleIndex);
303+
return this.applyEdit(editor, edits, range, markerIndex + 1, ruleIndex);
207304
}); // TODO handle rejection
208305
}
209306
else {
210-
return this.applyEdit(edits, range, markerIndex + 1, ruleIndex);
307+
return this.applyEdit(editor, edits, range, markerIndex + 1, ruleIndex);
211308
}
212309
}
213310

214-
getSelectionRange(document: TextDocument): Range {
311+
private getSelectionRange(document: TextDocument): Range {
215312
let editor = vscode.window.visibleTextEditors.find(editor => editor.document === document);
216313
if (editor !== undefined) {
217314
return editor.selection as Range;
@@ -220,11 +317,7 @@ class PSDocumentFormattingEditProvider implements DocumentFormattingEditProvider
220317
return null;
221318
}
222319

223-
setLanguageClient(languageClient: LanguageClient): void {
224-
this.languageClient = languageClient;
225-
}
226-
227-
getSettings(rule: string): any {
320+
private getSettings(rule: string): any {
228321
let psSettings: Settings.ISettings = Settings.load(Utils.PowerShellLanguageId);
229322
let ruleSettings = new Object();
230323
ruleSettings["Enable"] = true;
@@ -247,6 +340,25 @@ class PSDocumentFormattingEditProvider implements DocumentFormattingEditProvider
247340
settings[rule] = ruleSettings;
248341
return settings;
249342
}
343+
344+
private static showStatusBar(document: TextDocument, hideWhenDone: Thenable<any>): void {
345+
let statusBar = AnimatedStatusBar.showAnimatedStatusBarMessage("Formatting PowerShell document", hideWhenDone);
346+
this.statusBarTracker[document.uri.toString()] = statusBar;
347+
hideWhenDone.then(() => {
348+
this.disposeStatusBar(document.uri.toString());
349+
});
350+
}
351+
352+
private static disposeStatusBar(documentUri: string) {
353+
if (this.statusBarTracker.hasOwnProperty(documentUri)) {
354+
this.statusBarTracker[documentUri].dispose();
355+
delete this.statusBarTracker[documentUri];
356+
}
357+
}
358+
359+
private static disposeAllStatusBars() {
360+
Object.keys(this.statusBarTracker).slice().forEach((key) => this.disposeStatusBar(key));
361+
}
250362
}
251363

252364
export class DocumentFormatterFeature implements IFeature {

0 commit comments

Comments
 (0)