11import type * as atomIde from "atom-ide-base"
22import Convert from "../convert"
3- import { LanguageClientConnection , ApplyWorkspaceEditParams , ApplyWorkspaceEditResponse } from "../languageclient"
3+ import {
4+ LanguageClientConnection ,
5+ ApplyWorkspaceEditParams ,
6+ ApplyWorkspaceEditResponse ,
7+ WorkspaceEdit ,
8+ TextDocumentEdit ,
9+ CreateFile ,
10+ RenameFile ,
11+ DeleteFile ,
12+ DocumentUri ,
13+ } from "../languageclient"
414import { TextBuffer , TextEditor } from "atom"
15+ import { promises as fsp , Stats } from "fs"
16+ import * as rimraf from "rimraf"
517
618/** Public: Adapts workspace/applyEdit commands to editors. */
719export default class ApplyEditAdapter {
@@ -17,7 +29,7 @@ export default class ApplyEditAdapter {
1729 // Sort edits in reverse order to prevent edit conflicts.
1830 edits . sort ( ( edit1 , edit2 ) => - edit1 . oldRange . compare ( edit2 . oldRange ) )
1931 edits . reduce ( ( previous : atomIde . TextEdit | null , current ) => {
20- ApplyEditAdapter . validateEdit ( buffer , current , previous )
32+ validateEdit ( buffer , current , previous )
2133 buffer . setTextInRange ( current . oldRange , current . newText )
2234 return current
2335 } , null )
@@ -30,36 +42,35 @@ export default class ApplyEditAdapter {
3042 }
3143
3244 public static async onApplyEdit ( params : ApplyWorkspaceEditParams ) : Promise < ApplyWorkspaceEditResponse > {
33- let changes = params . edit . changes || { }
34-
35- if ( params . edit . documentChanges ) {
36- changes = { }
37- params . edit . documentChanges . forEach ( ( change ) => {
38- if ( change && "textDocument" in change && change . textDocument ) {
39- changes [ change . textDocument . uri ] = change . edits
40- }
41- } )
42- }
45+ return ApplyEditAdapter . apply ( params . edit )
46+ }
4347
44- const uris = Object . keys ( changes )
48+ public static async apply ( workspaceEdit : WorkspaceEdit ) : Promise < ApplyWorkspaceEditResponse > {
49+ normalize ( workspaceEdit )
4550
4651 // Keep checkpoints from all successful buffer edits
4752 const checkpoints : Array < { buffer : TextBuffer ; checkpoint : number } > = [ ]
4853
49- const promises = uris . map ( async ( uri ) => {
50- const path = Convert . uriToPath ( uri )
51- const editor = ( await atom . workspace . open ( path , {
52- searchAllPanes : true ,
53- // Open new editors in the background.
54- activatePane : false ,
55- activateItem : false ,
56- } ) ) as TextEditor
57- const buffer = editor . getBuffer ( )
58- // Get an existing editor for the file, or open a new one if it doesn't exist.
59- const edits = Convert . convertLsTextEdits ( changes [ uri ] )
60- const checkpoint = ApplyEditAdapter . applyEdits ( buffer , edits )
61- checkpoints . push ( { buffer, checkpoint } )
62- } )
54+ const promises = ( workspaceEdit . documentChanges || [ ] ) . map (
55+ async ( edit ) : Promise < void > => {
56+ if ( ! TextDocumentEdit . is ( edit ) ) {
57+ return ApplyEditAdapter . handleResourceOperation ( edit ) . catch ( ( err ) => {
58+ throw Error ( `Error during ${ edit . kind } resource operation: ${ err . message } ` )
59+ } )
60+ }
61+ const path = Convert . uriToPath ( edit . textDocument . uri )
62+ const editor = ( await atom . workspace . open ( path , {
63+ searchAllPanes : true ,
64+ // Open new editors in the background.
65+ activatePane : false ,
66+ activateItem : false ,
67+ } ) ) as TextEditor
68+ const buffer = editor . getBuffer ( )
69+ const edits = Convert . convertLsTextEdits ( edit . edits )
70+ const checkpoint = ApplyEditAdapter . applyEdits ( buffer , edits )
71+ checkpoints . push ( { buffer, checkpoint } )
72+ }
73+ )
6374
6475 // Apply all edits or fail and revert everything
6576 const applied = await Promise . all ( promises )
@@ -78,17 +89,100 @@ export default class ApplyEditAdapter {
7889 return { applied }
7990 }
8091
81- /** Private: Do some basic sanity checking on the edit ranges. */
82- private static validateEdit ( buffer : TextBuffer , edit : atomIde . TextEdit , prevEdit : atomIde . TextEdit | null ) : void {
83- const path = buffer . getPath ( ) || ""
84- if ( prevEdit && edit . oldRange . end . compare ( prevEdit . oldRange . start ) > 0 ) {
85- throw Error ( `Found overlapping edit ranges in ${ path } ` )
92+ private static async handleResourceOperation ( edit : CreateFile | RenameFile | DeleteFile ) : Promise < void > {
93+ if ( DeleteFile . is ( edit ) ) {
94+ const path = Convert . uriToPath ( edit . uri )
95+ const stats : boolean | Stats = await fsp . lstat ( path ) . catch ( ( ) => false )
96+ const ignoreIfNotExists = edit . options ?. ignoreIfNotExists
97+
98+ if ( ! stats ) {
99+ if ( ignoreIfNotExists !== false ) {
100+ return
101+ }
102+ throw Error ( `Target doesn't exist.` )
103+ }
104+
105+ if ( stats . isDirectory ( ) ) {
106+ if ( edit . options ?. recursive ) {
107+ return new Promise ( ( resolve , reject ) => {
108+ rimraf ( path , { glob : false } , ( err ) => {
109+ if ( err ) {
110+ reject ( err )
111+ }
112+ resolve ( )
113+ } )
114+ } )
115+ }
116+ return fsp . rmdir ( path , { recursive : edit . options ?. recursive } )
117+ }
118+
119+ return fsp . unlink ( path )
86120 }
87- const startRow = edit . oldRange . start . row
88- const startCol = edit . oldRange . start . column
89- const lineLength = buffer . lineLengthForRow ( startRow )
90- if ( lineLength == null || startCol > lineLength ) {
91- throw Error ( `Out of range edit on ${ path } :${ startRow + 1 } :${ startCol + 1 } ` )
121+ if ( RenameFile . is ( edit ) ) {
122+ const oldPath = Convert . uriToPath ( edit . oldUri )
123+ const newPath = Convert . uriToPath ( edit . newUri )
124+ const exists = await fsp
125+ . access ( newPath )
126+ . then ( ( ) => true )
127+ . catch ( ( ) => false )
128+ const ignoreIfExists = edit . options ?. ignoreIfExists
129+ const overwrite = edit . options ?. overwrite
130+
131+ if ( exists && ignoreIfExists && ! overwrite ) {
132+ return
133+ }
134+
135+ if ( exists && ! ignoreIfExists && ! overwrite ) {
136+ throw Error ( `Target exists.` )
137+ }
138+
139+ return fsp . rename ( oldPath , newPath )
92140 }
141+ if ( CreateFile . is ( edit ) ) {
142+ const path = Convert . uriToPath ( edit . uri )
143+ const exists = await fsp
144+ . access ( path )
145+ . then ( ( ) => true )
146+ . catch ( ( ) => false )
147+ const ignoreIfExists = edit . options ?. ignoreIfExists
148+ const overwrite = edit . options ?. overwrite
149+
150+ if ( exists && ignoreIfExists && ! overwrite ) {
151+ return
152+ }
153+
154+ return fsp . writeFile ( path , "" )
155+ }
156+ }
157+ }
158+
159+ function normalize ( workspaceEdit : WorkspaceEdit ) : void {
160+ const documentChanges = workspaceEdit . documentChanges || [ ]
161+
162+ if ( ! ( "documentChanges" in workspaceEdit ) && "changes" in workspaceEdit ) {
163+ Object . keys ( workspaceEdit . changes || [ ] ) . forEach ( ( uri : DocumentUri ) => {
164+ documentChanges . push ( {
165+ textDocument : {
166+ version : null ,
167+ uri,
168+ } ,
169+ edits : workspaceEdit . changes ! [ uri ] ,
170+ } )
171+ } )
172+ }
173+
174+ workspaceEdit . documentChanges = documentChanges
175+ }
176+
177+ function validateEdit ( buffer : TextBuffer , edit : atomIde . TextEdit , prevEdit : atomIde . TextEdit | null ) : void {
178+ const path = buffer . getPath ( ) || ""
179+ if ( prevEdit && edit . oldRange . end . compare ( prevEdit . oldRange . start ) > 0 ) {
180+ throw Error ( `Found overlapping edit ranges in ${ path } ` )
181+ }
182+ const startRow = edit . oldRange . start . row
183+ const startCol = edit . oldRange . start . column
184+ const lineLength = buffer . lineLengthForRow ( startRow )
185+ if ( lineLength == null || startCol > lineLength ) {
186+ throw Error ( `Out of range edit on ${ path } :${ startRow + 1 } :${ startCol + 1 } ` )
93187 }
94188}
0 commit comments