1
1
import type * as atomIde from "atom-ide-base"
2
2
import 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"
4
14
import { TextBuffer , TextEditor } from "atom"
15
+ import { promises as fsp , Stats } from "fs"
16
+ import * as rimraf from "rimraf"
5
17
6
18
/** Public: Adapts workspace/applyEdit commands to editors. */
7
19
export default class ApplyEditAdapter {
@@ -17,7 +29,7 @@ export default class ApplyEditAdapter {
17
29
// Sort edits in reverse order to prevent edit conflicts.
18
30
edits . sort ( ( edit1 , edit2 ) => - edit1 . oldRange . compare ( edit2 . oldRange ) )
19
31
edits . reduce ( ( previous : atomIde . TextEdit | null , current ) => {
20
- ApplyEditAdapter . validateEdit ( buffer , current , previous )
32
+ validateEdit ( buffer , current , previous )
21
33
buffer . setTextInRange ( current . oldRange , current . newText )
22
34
return current
23
35
} , null )
@@ -30,36 +42,35 @@ export default class ApplyEditAdapter {
30
42
}
31
43
32
44
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
+ }
43
47
44
- const uris = Object . keys ( changes )
48
+ public static async apply ( workspaceEdit : WorkspaceEdit ) : Promise < ApplyWorkspaceEditResponse > {
49
+ normalize ( workspaceEdit )
45
50
46
51
// Keep checkpoints from all successful buffer edits
47
52
const checkpoints : Array < { buffer : TextBuffer ; checkpoint : number } > = [ ]
48
53
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
+ )
63
74
64
75
// Apply all edits or fail and revert everything
65
76
const applied = await Promise . all ( promises )
@@ -78,17 +89,100 @@ export default class ApplyEditAdapter {
78
89
return { applied }
79
90
}
80
91
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 )
86
120
}
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 )
92
140
}
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 } ` )
93
187
}
94
188
}
0 commit comments