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