Skip to content

Commit 0c1c7e9

Browse files
cdaguerreaminya
andauthored
feat: support resource operations (#140)
* feat: support resource operations * chore: add tests on resource operations * chore: use async fs calls * chore: ran prettier * chore: account for @aminya s review * chore: move private functions outside of class Co-authored-by: Amin Yahyaabadi <[email protected]>
1 parent 68386e3 commit 0c1c7e9

File tree

4 files changed

+369
-37
lines changed

4 files changed

+369
-37
lines changed

lib/adapters/apply-edit-adapter.ts

+131-37
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
import type * as atomIde from "atom-ide-base"
22
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"
414
import { 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. */
719
export 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
}

lib/auto-languageclient.ts

+1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export default class AutoLanguageClient {
122122
documentChanges: true,
123123
normalizesLineEndings: false,
124124
changeAnnotationSupport: undefined,
125+
resourceOperations: ["create", "rename", "delete"],
125126
},
126127
workspaceFolders: false,
127128
didChangeConfiguration: {

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
"prettier": "prettier-config-atomic",
2727
"atomTestRunner": "./test/runner",
2828
"dependencies": {
29+
"@types/rimraf": "^3.0.0",
2930
"atom-ide-base": "^2.4.0",
31+
"rimraf": "^3.0.2",
3032
"vscode-jsonrpc": "6.0.0",
3133
"vscode-languageserver-protocol": "3.16.0",
3234
"vscode-languageserver-types": "3.16.0",

0 commit comments

Comments
 (0)