Skip to content

feat: support resource operations #140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 121 additions & 26 deletions lib/adapters/apply-edit-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import type * as atomIde from "atom-ide-base"
import Convert from "../convert"
import { LanguageClientConnection, ApplyWorkspaceEditParams, ApplyWorkspaceEditResponse } from "../languageclient"
import {
LanguageClientConnection,
ApplyWorkspaceEditParams,
ApplyWorkspaceEditResponse,
WorkspaceEdit,
TextDocumentEdit,
CreateFile,
RenameFile,
DeleteFile,
DocumentUri,
} from "../languageclient"
import { TextBuffer, TextEditor } from "atom"
import * as fs from "fs"
import * as rimraf from "rimraf"

/** Public: Adapts workspace/applyEdit commands to editors. */
export default class ApplyEditAdapter {
Expand Down Expand Up @@ -30,36 +42,35 @@ export default class ApplyEditAdapter {
}

public static async onApplyEdit(params: ApplyWorkspaceEditParams): Promise<ApplyWorkspaceEditResponse> {
let changes = params.edit.changes || {}

if (params.edit.documentChanges) {
changes = {}
params.edit.documentChanges.forEach((change) => {
if (change && "textDocument" in change && change.textDocument) {
changes[change.textDocument.uri] = change.edits
}
})
}
return ApplyEditAdapter.apply(params.edit)
}

const uris = Object.keys(changes)
public static async apply(workspaceEdit: WorkspaceEdit): Promise<ApplyWorkspaceEditResponse> {
ApplyEditAdapter.normalize(workspaceEdit)

// Keep checkpoints from all successful buffer edits
const checkpoints: Array<{ buffer: TextBuffer; checkpoint: number }> = []

const promises = uris.map(async (uri) => {
const path = Convert.uriToPath(uri)
const editor = (await atom.workspace.open(path, {
searchAllPanes: true,
// Open new editors in the background.
activatePane: false,
activateItem: false,
})) as TextEditor
const buffer = editor.getBuffer()
// Get an existing editor for the file, or open a new one if it doesn't exist.
const edits = Convert.convertLsTextEdits(changes[uri])
const checkpoint = ApplyEditAdapter.applyEdits(buffer, edits)
checkpoints.push({ buffer, checkpoint })
})
const promises = (workspaceEdit.documentChanges || []).map(
async (edit): Promise<void> => {
if (!TextDocumentEdit.is(edit)) {
return ApplyEditAdapter.handleResourceOperation(edit).catch((err) => {
throw Error(`Error during ${edit.kind} resource operation: ${err.message}`)
})
}
const path = Convert.uriToPath(edit.textDocument.uri)
const editor = (await atom.workspace.open(path, {
searchAllPanes: true,
// Open new editors in the background.
activatePane: false,
activateItem: false,
})) as TextEditor
const buffer = editor.getBuffer()
const edits = Convert.convertLsTextEdits(edit.edits)
const checkpoint = ApplyEditAdapter.applyEdits(buffer, edits)
checkpoints.push({ buffer, checkpoint })
}
)

// Apply all edits or fail and revert everything
const applied = await Promise.all(promises)
Expand All @@ -78,6 +89,90 @@ export default class ApplyEditAdapter {
return { applied }
}

private static async handleResourceOperation(edit: CreateFile | RenameFile | DeleteFile): Promise<void> {
if (DeleteFile.is(edit)) {
const path = Convert.uriToPath(edit.uri)
const stats: boolean | fs.Stats = await fs.promises.lstat(path).catch(() => false)
const ignoreIfNotExists = edit.options?.ignoreIfNotExists

if (!stats) {
if (ignoreIfNotExists) {
return
}
throw Error(`Target doesn't exist.`)
}

if (stats.isDirectory()) {
if (edit.options?.recursive) {
return new Promise((resolve, reject) => {
rimraf(path, { glob: false }, (err) => {
if (err) {
reject(err)
}
resolve()
})
})
}
return fs.promises.rmdir(path, { recursive: edit.options?.recursive })
}

return fs.promises.unlink(path)
}
if (RenameFile.is(edit)) {
const oldPath = Convert.uriToPath(edit.oldUri)
const newPath = Convert.uriToPath(edit.newUri)
const exists = await fs.promises
.access(newPath)
.then(() => true)
.catch(() => false)
const ignoreIfExists = edit.options?.ignoreIfExists
const overwrite = edit.options?.overwrite

if (exists && ignoreIfExists && !overwrite) {
return
}

if (exists && !ignoreIfExists && !overwrite) {
throw Error(`Target exists.`)
}

return fs.promises.rename(oldPath, newPath)
}
if (CreateFile.is(edit)) {
const path = Convert.uriToPath(edit.uri)
const exists = await fs.promises
.access(path)
.then(() => true)
.catch(() => false)
const ignoreIfExists = edit.options?.ignoreIfExists
const overwrite = edit.options?.overwrite

if (exists && ignoreIfExists && !overwrite) {
return
}

return fs.promises.writeFile(path, "")
}
}

private static normalize(workspaceEdit: WorkspaceEdit): void {
const documentChanges = workspaceEdit.documentChanges || []

if (!("documentChanges" in workspaceEdit) && "changes" in workspaceEdit) {
Object.keys(workspaceEdit.changes || []).forEach((uri: DocumentUri) => {
documentChanges.push({
textDocument: {
version: null,
uri,
},
edits: workspaceEdit.changes![uri],
})
})
}

workspaceEdit.documentChanges = documentChanges
}

/** Private: Do some basic sanity checking on the edit ranges. */
private static validateEdit(buffer: TextBuffer, edit: atomIde.TextEdit, prevEdit: atomIde.TextEdit | null): void {
const path = buffer.getPath() || ""
Expand Down
1 change: 1 addition & 0 deletions lib/auto-languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export default class AutoLanguageClient {
documentChanges: true,
normalizesLineEndings: false,
changeAnnotationSupport: undefined,
resourceOperations: ["create", "rename", "delete"],
},
workspaceFolders: false,
didChangeConfiguration: {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"prettier": "prettier-config-atomic",
"atomTestRunner": "./test/runner",
"dependencies": {
"@types/rimraf": "^3.0.0",
"atom-ide-base": "^2.4.0",
"rimraf": "^3.0.2",
"vscode-jsonrpc": "6.0.0",
"vscode-languageserver-protocol": "3.16.0",
"vscode-languageserver-types": "3.16.0",
Expand Down
Loading