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
+ }
0 commit comments