From 643736cc21ae2c9af79e8b2d328466bf149f5284 Mon Sep 17 00:00:00 2001 From: Christian Daguerre Date: Sat, 10 Apr 2021 16:23:19 +0200 Subject: [PATCH 1/6] feat: support resource operations --- lib/adapters/apply-edit-adapter.ts | 71 +++++++++++++++++++++++------- lib/auto-languageclient.ts | 1 + 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/lib/adapters/apply-edit-adapter.ts b/lib/adapters/apply-edit-adapter.ts index e49b7be1..2795b26e 100644 --- a/lib/adapters/apply-edit-adapter.ts +++ b/lib/adapters/apply-edit-adapter.ts @@ -1,7 +1,18 @@ 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'; /** Public: Adapts workspace/applyEdit commands to editors. */ export default class ApplyEditAdapter { @@ -33,24 +44,20 @@ export default class ApplyEditAdapter { } public static async onApplyEdit(params: ApplyWorkspaceEditParams): Promise { - 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 - } - }) - } - - const uris = Object.keys(changes) + return ApplyEditAdapter.apply(params.edit) + } + public static async apply(workspaceEdit: WorkspaceEdit): Promise { + 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 promises = (workspaceEdit.documentChanges || []).map(async (edit): Promise => { + if (!TextDocumentEdit.is(edit)) { + return ApplyEditAdapter.handleResourceOperation(edit) + } + const path = Convert.uriToPath(edit.textDocument.uri) const editor = (await atom.workspace.open(path, { searchAllPanes: true, // Open new editors in the background. @@ -58,8 +65,7 @@ export default class ApplyEditAdapter { 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 edits = Convert.convertLsTextEdits(edit.edits) const checkpoint = ApplyEditAdapter.applyEdits(buffer, edits) checkpoints.push({ buffer, checkpoint }) }) @@ -81,6 +87,37 @@ export default class ApplyEditAdapter { return { applied } } + private static async handleResourceOperation(edit: (CreateFile | RenameFile | DeleteFile)): Promise + { + if (DeleteFile.is(edit)) { + return fs.promises.unlink(Convert.uriToPath(edit.uri)) + } + if (RenameFile.is(edit)) { + return fs.promises.rename(Convert.uriToPath(edit.oldUri), Convert.uriToPath(edit.newUri)) + } + if (CreateFile.is(edit)) { + return fs.promises.writeFile(edit.uri, '') + } + } + + private static normalize(workspaceEdit: WorkspaceEdit): void { + const documentChanges = workspaceEdit.documentChanges || [] + + if (!workspaceEdit.hasOwnProperty('documentChanges') && workspaceEdit.hasOwnProperty('changes')) { + Object.keys(workspaceEdit.changes || []).forEach((uri: DocumentUri) => { + documentChanges.push({ + textDocument: { + version: null, + uri: 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() || "" diff --git a/lib/auto-languageclient.ts b/lib/auto-languageclient.ts index 62eed42b..bdac9488 100644 --- a/lib/auto-languageclient.ts +++ b/lib/auto-languageclient.ts @@ -122,6 +122,7 @@ export default class AutoLanguageClient { documentChanges: true, normalizesLineEndings: false, changeAnnotationSupport: undefined, + resourceOperations: ["create", "rename", "delete"] }, workspaceFolders: false, didChangeConfiguration: { From b75cc535ff7f173041882f4112975b4f34052323 Mon Sep 17 00:00:00 2001 From: Christian Daguerre Date: Tue, 13 Apr 2021 14:35:27 +0200 Subject: [PATCH 2/6] chore: add tests on resource operations --- lib/adapters/apply-edit-adapter.ts | 76 ++++++-- package.json | 2 + test/adapters/apply-edit-adapter.test.ts | 233 +++++++++++++++++++++++ 3 files changed, 299 insertions(+), 12 deletions(-) diff --git a/lib/adapters/apply-edit-adapter.ts b/lib/adapters/apply-edit-adapter.ts index 2795b26e..03c0527b 100644 --- a/lib/adapters/apply-edit-adapter.ts +++ b/lib/adapters/apply-edit-adapter.ts @@ -12,7 +12,8 @@ import { DocumentUri } from "../languageclient" import { TextBuffer, TextEditor } from "atom" -import * as fs from 'fs'; +import * as fs from "fs" +import * as rimraf from "rimraf" /** Public: Adapts workspace/applyEdit commands to editors. */ export default class ApplyEditAdapter { @@ -49,14 +50,16 @@ export default class ApplyEditAdapter { public static async apply(workspaceEdit: WorkspaceEdit): Promise { ApplyEditAdapter.normalize(workspaceEdit) - + // Keep checkpoints from all successful buffer edits const checkpoints: Array<{ buffer: TextBuffer; checkpoint: number }> = [] const promises = (workspaceEdit.documentChanges || []).map(async (edit): Promise => { if (!TextDocumentEdit.is(edit)) { - return ApplyEditAdapter.handleResourceOperation(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, @@ -87,23 +90,72 @@ export default class ApplyEditAdapter { return { applied } } - private static async handleResourceOperation(edit: (CreateFile | RenameFile | DeleteFile)): Promise - { + private static async handleResourceOperation(edit: (CreateFile | RenameFile | DeleteFile)): Promise { if (DeleteFile.is(edit)) { - return fs.promises.unlink(Convert.uriToPath(edit.uri)) + const path = Convert.uriToPath(edit.uri) + const exists = fs.existsSync(path) + const ignoreIfNotExists = edit.options?.ignoreIfNotExists + + if (!exists) { + if (ignoreIfNotExists) { + return + } + throw Error(`Target doesn't exist.`) + } + + const isDirectory = fs.lstatSync(path).isDirectory() + + if (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)) { - return fs.promises.rename(Convert.uriToPath(edit.oldUri), Convert.uriToPath(edit.newUri)) + const oldPath = Convert.uriToPath(edit.oldUri) + const newPath = Convert.uriToPath(edit.newUri) + const exists = fs.existsSync(newPath) + 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)) { - return fs.promises.writeFile(edit.uri, '') + const path = Convert.uriToPath(edit.uri) + const exists = fs.existsSync(path) + 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 (!workspaceEdit.hasOwnProperty('documentChanges') && workspaceEdit.hasOwnProperty('changes')) { + if (!('documentChanges' in workspaceEdit) && ('changes' in workspaceEdit)) { Object.keys(workspaceEdit.changes || []).forEach((uri: DocumentUri) => { documentChanges.push({ textDocument: { @@ -114,7 +166,7 @@ export default class ApplyEditAdapter { }) }) } - + workspaceEdit.documentChanges = documentChanges } diff --git a/package.json b/package.json index 86083d68..54f9f865 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ }, "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", diff --git a/test/adapters/apply-edit-adapter.test.ts b/test/adapters/apply-edit-adapter.test.ts index 406e4818..a1f83c82 100644 --- a/test/adapters/apply-edit-adapter.test.ts +++ b/test/adapters/apply-edit-adapter.test.ts @@ -1,5 +1,7 @@ import { expect } from "chai" import * as path from "path" +import * as os from "os" +import * as fs from "fs" import * as sinon from "sinon" import ApplyEditAdapter from "../../lib/adapters/apply-edit-adapter" import Convert from "../../lib/convert" @@ -9,6 +11,7 @@ const TEST_PATH1 = normalizeDriveLetterName(path.join(__dirname, "test.txt")) const TEST_PATH2 = normalizeDriveLetterName(path.join(__dirname, "test2.txt")) const TEST_PATH3 = normalizeDriveLetterName(path.join(__dirname, "test3.txt")) const TEST_PATH4 = normalizeDriveLetterName(path.join(__dirname, "test4.txt")) +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atom-languageclient-tests')) function normalizeDriveLetterName(filePath: string): string { if (process.platform === "win32") { @@ -186,5 +189,235 @@ describe("ApplyEditAdapter", () => { expect(errorCalls.length).to.equal(1) expect(errorCalls[0].args[1].detail).to.equal(`Out of range edit on ${TEST_PATH4}:1:2`) }) + + it("handles rename resource operations", async () => { + const directory = fs.mkdtempSync(tempDir) + const oldUri = path.join(directory, "test.txt") + const newUri = path.join(directory, "test-renamed.txt") + fs.writeFileSync(oldUri, 'abcd') + + const result = await ApplyEditAdapter.onApplyEdit({ + edit: { + documentChanges: [ + { + kind: "rename", + oldUri: oldUri, + newUri: newUri, + } + ] + }, + }) + + expect(result.applied).to.equal(true) + expect(fs.existsSync(newUri)).to.equal(true) + expect(fs.readFileSync(newUri).toString()).to.equal('abcd') + expect(fs.existsSync(oldUri)).to.equal(false) + }) + + it("handles rename operation with ignoreIfExists option", async () => { + const directory = fs.mkdtempSync(tempDir) + const oldUri = path.join(directory, "test.txt") + const newUri = path.join(directory, "test-renamed.txt") + fs.writeFileSync(oldUri, 'abcd') + fs.writeFileSync(newUri, 'efgh') + + const result = await ApplyEditAdapter.onApplyEdit({ + edit: { + documentChanges: [ + { + kind: "rename", + oldUri: oldUri, + newUri: newUri, + options: { + ignoreIfExists: true + } + } + ] + }, + }) + + expect(result.applied).to.equal(true) + expect(fs.existsSync(oldUri)).to.equal(true) + expect(fs.readFileSync(newUri).toString()).to.equal('efgh') + }) + + it("handles rename operation with overwrite option", async () => { + const directory = fs.mkdtempSync(tempDir) + const oldUri = path.join(directory, "test.txt") + const newUri = path.join(directory, "test-renamed.txt") + fs.writeFileSync(oldUri, 'abcd') + fs.writeFileSync(newUri, 'efgh') + + const result = await ApplyEditAdapter.onApplyEdit({ + edit: { + documentChanges: [ + { + kind: "rename", + oldUri: oldUri, + newUri: newUri, + options: { + overwrite: true, + ignoreIfExists: true // Overwrite wins over ignoreIfExists + } + } + ] + }, + }) + + expect(result.applied).to.equal(true) + expect(fs.existsSync(oldUri)).to.equal(false) + expect(fs.readFileSync(newUri).toString()).to.equal('abcd') + }) + + it("throws an error on rename operation if target exists", async () => { + const directory = fs.mkdtempSync(tempDir) + const oldUri = path.join(directory, "test.txt") + const newUri = path.join(directory, "test-renamed.txt") + fs.writeFileSync(oldUri, 'abcd') + fs.writeFileSync(newUri, 'efgh') + + const result = await ApplyEditAdapter.onApplyEdit({ + edit: { + documentChanges: [ + { + kind: "rename", + oldUri: oldUri, + newUri: newUri, + } + ] + }, + }) + + expect(result.applied).to.equal(false) + expect(fs.existsSync(oldUri)).to.equal(true) + expect(fs.readFileSync(oldUri).toString()).to.equal('abcd') + expect(fs.existsSync(newUri)).to.equal(true) + expect(fs.readFileSync(newUri).toString()).to.equal('efgh') + + expect( + (atom as any).notifications.addError.calledWith("workspace/applyEdits failed", { + description: "Failed to apply edits.", + detail: "Error during rename resource operation: Target exists.", + }) + ).to.equal(true) + }) + + it("handles delete resource operations on files", async () => { + const directory = fs.mkdtempSync(tempDir) + const uri = path.join(directory, "test.txt") + fs.writeFileSync(uri, 'abcd') + + const result = await ApplyEditAdapter.onApplyEdit({ + edit: { + documentChanges: [ + { + kind: "delete", + uri: uri + } + ] + }, + }) + + expect(result.applied).to.equal(true) + expect(fs.existsSync(uri)).to.equal(false) + }) + + it("handles delete resource operations on directories", async () => { + const directory = fs.mkdtempSync(tempDir) + const file1 = path.join(directory, '1.txt') + const file2 = path.join(directory, '2.txt') + fs.writeFileSync(file1, '1') + fs.writeFileSync(file2, '2') + + const result = await ApplyEditAdapter.onApplyEdit({ + edit: { + documentChanges: [ + { + kind: "delete", + uri: directory, + options: { + recursive: true + } + } + ] + }, + }) + + expect(result.applied).to.equal(true) + expect(fs.existsSync(directory)).to.equal(false) + expect(fs.existsSync(file1)).to.equal(false) + expect(fs.existsSync(file2)).to.equal(false) + }) + + it("throws an error when deleting a non-empty directory without recursive option", async () => { + const directory = fs.mkdtempSync(tempDir) + const file1 = path.join(directory, '1.txt') + const file2 = path.join(directory, '2.txt') + fs.writeFileSync(file1, '1') + fs.writeFileSync(file2, '2') + + const result = await ApplyEditAdapter.onApplyEdit({ + edit: { + documentChanges: [ + { + kind: "delete", + uri: directory, + options: { + recursive: false + } + } + ] + }, + }) + + expect(result.applied).to.equal(false) + expect(fs.existsSync(directory)).to.equal(true) + expect(fs.existsSync(file1)).to.equal(true) + expect(fs.existsSync(file2)).to.equal(true) + const errorCalls = (atom as any).notifications.addError.getCalls() + expect(errorCalls.length).to.equal(1) + expect(errorCalls[0].args[1].detail).to.match(/Error during delete resource operation: (.*)/) + }) + + + it("throws an error on delete operation if target doesnt exist", async () => { + const result = await ApplyEditAdapter.onApplyEdit({ + edit: { + documentChanges: [ + { + kind: "delete", + uri: path.join(tempDir, "unexisting.txt"), + } + ] + }, + }) + // + expect(result.applied).to.equal(false) + expect( + (atom as any).notifications.addError.calledWith("workspace/applyEdits failed", { + description: "Failed to apply edits.", + detail: "Error during delete resource operation: Target doesn't exist.", + }) + ).to.equal(true) + }) + + it("handles create resource operations", async () => { + const directory = fs.mkdtempSync(tempDir) + const uri = path.join(directory, "test.txt") + + const result = await ApplyEditAdapter.onApplyEdit({ + edit: { + documentChanges: [ + { + kind: "create", + uri: uri + } + ] + }, + }) + + expect(result.applied).to.equal(true) + expect(fs.existsSync(uri)).to.equal(true) + }) }) }) From 4d438efae239581c03a51756200e97a7dea3f4bd Mon Sep 17 00:00:00 2001 From: Christian Daguerre Date: Tue, 13 Apr 2021 19:29:05 +0200 Subject: [PATCH 3/6] chore: use async fs calls --- lib/adapters/apply-edit-adapter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/adapters/apply-edit-adapter.ts b/lib/adapters/apply-edit-adapter.ts index 03c0527b..3b2ec18e 100644 --- a/lib/adapters/apply-edit-adapter.ts +++ b/lib/adapters/apply-edit-adapter.ts @@ -93,7 +93,7 @@ export default class ApplyEditAdapter { private static async handleResourceOperation(edit: (CreateFile | RenameFile | DeleteFile)): Promise { if (DeleteFile.is(edit)) { const path = Convert.uriToPath(edit.uri) - const exists = fs.existsSync(path) + const exists = await fs.promises.stat(path).then(() => true).catch(() => false) const ignoreIfNotExists = edit.options?.ignoreIfNotExists if (!exists) { @@ -124,7 +124,7 @@ export default class ApplyEditAdapter { if (RenameFile.is(edit)) { const oldPath = Convert.uriToPath(edit.oldUri) const newPath = Convert.uriToPath(edit.newUri) - const exists = fs.existsSync(newPath) + const exists = await fs.promises.stat(newPath).then(() => true).catch(() => false) const ignoreIfExists = edit.options?.ignoreIfExists const overwrite = edit.options?.overwrite @@ -140,7 +140,7 @@ export default class ApplyEditAdapter { } if (CreateFile.is(edit)) { const path = Convert.uriToPath(edit.uri) - const exists = fs.existsSync(path) + const exists = await fs.promises.stat(path).then(() => true).catch(() => false) const ignoreIfExists = edit.options?.ignoreIfExists const overwrite = edit.options?.overwrite From b29e99f84ca064d42ba6fdf3f7c5a3243ee54ebd Mon Sep 17 00:00:00 2001 From: Christian Daguerre Date: Wed, 14 Apr 2021 07:05:16 +0200 Subject: [PATCH 4/6] chore: ran prettier --- lib/adapters/apply-edit-adapter.ts | 66 +++++++------ lib/auto-languageclient.ts | 2 +- test/adapters/apply-edit-adapter.test.ts | 117 +++++++++++------------ 3 files changed, 95 insertions(+), 90 deletions(-) diff --git a/lib/adapters/apply-edit-adapter.ts b/lib/adapters/apply-edit-adapter.ts index e1e08901..0623ff5e 100644 --- a/lib/adapters/apply-edit-adapter.ts +++ b/lib/adapters/apply-edit-adapter.ts @@ -9,7 +9,7 @@ import { CreateFile, RenameFile, DeleteFile, - DocumentUri + DocumentUri, } from "../languageclient" import { TextBuffer, TextEditor } from "atom" import * as fs from "fs" @@ -51,24 +51,26 @@ export default class ApplyEditAdapter { // Keep checkpoints from all successful buffer edits const checkpoints: Array<{ buffer: TextBuffer; checkpoint: number }> = [] - const promises = (workspaceEdit.documentChanges || []).map(async (edit): Promise => { - if (!TextDocumentEdit.is(edit)) { - return ApplyEditAdapter.handleResourceOperation(edit).catch((err) => { - throw Error(`Error during ${edit.kind} resource operation: ${err.message}`) - }) + const promises = (workspaceEdit.documentChanges || []).map( + async (edit): Promise => { + 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 }) } - 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) @@ -87,22 +89,20 @@ export default class ApplyEditAdapter { return { applied } } - private static async handleResourceOperation(edit: (CreateFile | RenameFile | DeleteFile)): Promise { + private static async handleResourceOperation(edit: CreateFile | RenameFile | DeleteFile): Promise { if (DeleteFile.is(edit)) { const path = Convert.uriToPath(edit.uri) - const exists = await fs.promises.stat(path).then(() => true).catch(() => false) + const stats: boolean | fs.Stats = await fs.promises.lstat(path).catch(() => false) const ignoreIfNotExists = edit.options?.ignoreIfNotExists - if (!exists) { + if (!stats) { if (ignoreIfNotExists) { return } throw Error(`Target doesn't exist.`) } - const isDirectory = fs.lstatSync(path).isDirectory() - - if (isDirectory) { + if (stats.isDirectory()) { if (edit.options?.recursive) { return new Promise((resolve, reject) => { rimraf(path, { glob: false }, (err) => { @@ -121,7 +121,10 @@ export default class ApplyEditAdapter { if (RenameFile.is(edit)) { const oldPath = Convert.uriToPath(edit.oldUri) const newPath = Convert.uriToPath(edit.newUri) - const exists = await fs.promises.stat(newPath).then(() => true).catch(() => false) + const exists = await fs.promises + .access(newPath) + .then(() => true) + .catch(() => false) const ignoreIfExists = edit.options?.ignoreIfExists const overwrite = edit.options?.overwrite @@ -137,7 +140,10 @@ export default class ApplyEditAdapter { } if (CreateFile.is(edit)) { const path = Convert.uriToPath(edit.uri) - const exists = await fs.promises.stat(path).then(() => true).catch(() => false) + const exists = await fs.promises + .access(path) + .then(() => true) + .catch(() => false) const ignoreIfExists = edit.options?.ignoreIfExists const overwrite = edit.options?.overwrite @@ -145,21 +151,21 @@ export default class ApplyEditAdapter { return } - return fs.promises.writeFile(path, '') + return fs.promises.writeFile(path, "") } } private static normalize(workspaceEdit: WorkspaceEdit): void { const documentChanges = workspaceEdit.documentChanges || [] - if (!('documentChanges' in workspaceEdit) && ('changes' in workspaceEdit)) { + if (!("documentChanges" in workspaceEdit) && "changes" in workspaceEdit) { Object.keys(workspaceEdit.changes || []).forEach((uri: DocumentUri) => { documentChanges.push({ textDocument: { version: null, - uri: uri + uri, }, - edits: workspaceEdit.changes![uri] + edits: workspaceEdit.changes![uri], }) }) } diff --git a/lib/auto-languageclient.ts b/lib/auto-languageclient.ts index 16f22ecb..13618420 100644 --- a/lib/auto-languageclient.ts +++ b/lib/auto-languageclient.ts @@ -122,7 +122,7 @@ export default class AutoLanguageClient { documentChanges: true, normalizesLineEndings: false, changeAnnotationSupport: undefined, - resourceOperations: ["create", "rename", "delete"] + resourceOperations: ["create", "rename", "delete"], }, workspaceFolders: false, didChangeConfiguration: { diff --git a/test/adapters/apply-edit-adapter.test.ts b/test/adapters/apply-edit-adapter.test.ts index 7c9f357d..e1ef311b 100644 --- a/test/adapters/apply-edit-adapter.test.ts +++ b/test/adapters/apply-edit-adapter.test.ts @@ -11,7 +11,7 @@ const TEST_PATH1 = normalizeDriveLetterName(path.join(__dirname, "test.txt")) const TEST_PATH2 = normalizeDriveLetterName(path.join(__dirname, "test2.txt")) const TEST_PATH3 = normalizeDriveLetterName(path.join(__dirname, "test3.txt")) const TEST_PATH4 = normalizeDriveLetterName(path.join(__dirname, "test4.txt")) -const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atom-languageclient-tests')) +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "atom-languageclient-tests")) function normalizeDriveLetterName(filePath: string): string { if (process.platform === "win32") { @@ -194,23 +194,23 @@ describe("ApplyEditAdapter", () => { const directory = fs.mkdtempSync(tempDir) const oldUri = path.join(directory, "test.txt") const newUri = path.join(directory, "test-renamed.txt") - fs.writeFileSync(oldUri, 'abcd') + fs.writeFileSync(oldUri, "abcd") const result = await ApplyEditAdapter.onApplyEdit({ edit: { documentChanges: [ { kind: "rename", - oldUri: oldUri, - newUri: newUri, - } - ] + oldUri, + newUri, + }, + ], }, }) expect(result.applied).to.equal(true) expect(fs.existsSync(newUri)).to.equal(true) - expect(fs.readFileSync(newUri).toString()).to.equal('abcd') + expect(fs.readFileSync(newUri).toString()).to.equal("abcd") expect(fs.existsSync(oldUri)).to.equal(false) }) @@ -218,81 +218,81 @@ describe("ApplyEditAdapter", () => { const directory = fs.mkdtempSync(tempDir) const oldUri = path.join(directory, "test.txt") const newUri = path.join(directory, "test-renamed.txt") - fs.writeFileSync(oldUri, 'abcd') - fs.writeFileSync(newUri, 'efgh') + fs.writeFileSync(oldUri, "abcd") + fs.writeFileSync(newUri, "efgh") const result = await ApplyEditAdapter.onApplyEdit({ edit: { documentChanges: [ { kind: "rename", - oldUri: oldUri, - newUri: newUri, + oldUri, + newUri, options: { - ignoreIfExists: true - } - } - ] + ignoreIfExists: true, + }, + }, + ], }, }) expect(result.applied).to.equal(true) expect(fs.existsSync(oldUri)).to.equal(true) - expect(fs.readFileSync(newUri).toString()).to.equal('efgh') + expect(fs.readFileSync(newUri).toString()).to.equal("efgh") }) it("handles rename operation with overwrite option", async () => { const directory = fs.mkdtempSync(tempDir) const oldUri = path.join(directory, "test.txt") const newUri = path.join(directory, "test-renamed.txt") - fs.writeFileSync(oldUri, 'abcd') - fs.writeFileSync(newUri, 'efgh') + fs.writeFileSync(oldUri, "abcd") + fs.writeFileSync(newUri, "efgh") const result = await ApplyEditAdapter.onApplyEdit({ edit: { documentChanges: [ { kind: "rename", - oldUri: oldUri, - newUri: newUri, + oldUri, + newUri, options: { overwrite: true, - ignoreIfExists: true // Overwrite wins over ignoreIfExists - } - } - ] + ignoreIfExists: true, // Overwrite wins over ignoreIfExists + }, + }, + ], }, }) expect(result.applied).to.equal(true) expect(fs.existsSync(oldUri)).to.equal(false) - expect(fs.readFileSync(newUri).toString()).to.equal('abcd') + expect(fs.readFileSync(newUri).toString()).to.equal("abcd") }) it("throws an error on rename operation if target exists", async () => { const directory = fs.mkdtempSync(tempDir) const oldUri = path.join(directory, "test.txt") const newUri = path.join(directory, "test-renamed.txt") - fs.writeFileSync(oldUri, 'abcd') - fs.writeFileSync(newUri, 'efgh') + fs.writeFileSync(oldUri, "abcd") + fs.writeFileSync(newUri, "efgh") const result = await ApplyEditAdapter.onApplyEdit({ edit: { documentChanges: [ { kind: "rename", - oldUri: oldUri, - newUri: newUri, - } - ] + oldUri, + newUri, + }, + ], }, }) expect(result.applied).to.equal(false) expect(fs.existsSync(oldUri)).to.equal(true) - expect(fs.readFileSync(oldUri).toString()).to.equal('abcd') + expect(fs.readFileSync(oldUri).toString()).to.equal("abcd") expect(fs.existsSync(newUri)).to.equal(true) - expect(fs.readFileSync(newUri).toString()).to.equal('efgh') + expect(fs.readFileSync(newUri).toString()).to.equal("efgh") expect( (atom as any).notifications.addError.calledWith("workspace/applyEdits failed", { @@ -305,16 +305,16 @@ describe("ApplyEditAdapter", () => { it("handles delete resource operations on files", async () => { const directory = fs.mkdtempSync(tempDir) const uri = path.join(directory, "test.txt") - fs.writeFileSync(uri, 'abcd') + fs.writeFileSync(uri, "abcd") const result = await ApplyEditAdapter.onApplyEdit({ edit: { documentChanges: [ { kind: "delete", - uri: uri - } - ] + uri, + }, + ], }, }) @@ -324,10 +324,10 @@ describe("ApplyEditAdapter", () => { it("handles delete resource operations on directories", async () => { const directory = fs.mkdtempSync(tempDir) - const file1 = path.join(directory, '1.txt') - const file2 = path.join(directory, '2.txt') - fs.writeFileSync(file1, '1') - fs.writeFileSync(file2, '2') + const file1 = path.join(directory, "1.txt") + const file2 = path.join(directory, "2.txt") + fs.writeFileSync(file1, "1") + fs.writeFileSync(file2, "2") const result = await ApplyEditAdapter.onApplyEdit({ edit: { @@ -336,10 +336,10 @@ describe("ApplyEditAdapter", () => { kind: "delete", uri: directory, options: { - recursive: true - } - } - ] + recursive: true, + }, + }, + ], }, }) @@ -351,10 +351,10 @@ describe("ApplyEditAdapter", () => { it("throws an error when deleting a non-empty directory without recursive option", async () => { const directory = fs.mkdtempSync(tempDir) - const file1 = path.join(directory, '1.txt') - const file2 = path.join(directory, '2.txt') - fs.writeFileSync(file1, '1') - fs.writeFileSync(file2, '2') + const file1 = path.join(directory, "1.txt") + const file2 = path.join(directory, "2.txt") + fs.writeFileSync(file1, "1") + fs.writeFileSync(file2, "2") const result = await ApplyEditAdapter.onApplyEdit({ edit: { @@ -363,10 +363,10 @@ describe("ApplyEditAdapter", () => { kind: "delete", uri: directory, options: { - recursive: false - } - } - ] + recursive: false, + }, + }, + ], }, }) @@ -379,7 +379,6 @@ describe("ApplyEditAdapter", () => { expect(errorCalls[0].args[1].detail).to.match(/Error during delete resource operation: (.*)/) }) - it("throws an error on delete operation if target doesnt exist", async () => { const result = await ApplyEditAdapter.onApplyEdit({ edit: { @@ -387,8 +386,8 @@ describe("ApplyEditAdapter", () => { { kind: "delete", uri: path.join(tempDir, "unexisting.txt"), - } - ] + }, + ], }, }) // @@ -410,9 +409,9 @@ describe("ApplyEditAdapter", () => { documentChanges: [ { kind: "create", - uri: uri - } - ] + uri, + }, + ], }, }) From 39f3e25674023d050813790e2026b40a8ee73160 Mon Sep 17 00:00:00 2001 From: Christian Daguerre Date: Wed, 14 Apr 2021 07:41:24 +0200 Subject: [PATCH 5/6] chore: account for @aminya s review --- lib/adapters/apply-edit-adapter.ts | 18 +++++++++--------- test/adapters/apply-edit-adapter.test.ts | 3 +++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/adapters/apply-edit-adapter.ts b/lib/adapters/apply-edit-adapter.ts index 0623ff5e..09451184 100644 --- a/lib/adapters/apply-edit-adapter.ts +++ b/lib/adapters/apply-edit-adapter.ts @@ -12,7 +12,7 @@ import { DocumentUri, } from "../languageclient" import { TextBuffer, TextEditor } from "atom" -import * as fs from "fs" +import { promises as fsp, Stats } from "fs" import * as rimraf from "rimraf" /** Public: Adapts workspace/applyEdit commands to editors. */ @@ -92,11 +92,11 @@ export default class ApplyEditAdapter { private static async handleResourceOperation(edit: CreateFile | RenameFile | DeleteFile): Promise { if (DeleteFile.is(edit)) { const path = Convert.uriToPath(edit.uri) - const stats: boolean | fs.Stats = await fs.promises.lstat(path).catch(() => false) + const stats: boolean | Stats = await fsp.lstat(path).catch(() => false) const ignoreIfNotExists = edit.options?.ignoreIfNotExists if (!stats) { - if (ignoreIfNotExists) { + if (ignoreIfNotExists !== false) { return } throw Error(`Target doesn't exist.`) @@ -113,15 +113,15 @@ export default class ApplyEditAdapter { }) }) } - return fs.promises.rmdir(path, { recursive: edit.options?.recursive }) + return fsp.rmdir(path, { recursive: edit.options?.recursive }) } - return fs.promises.unlink(path) + return fsp.unlink(path) } if (RenameFile.is(edit)) { const oldPath = Convert.uriToPath(edit.oldUri) const newPath = Convert.uriToPath(edit.newUri) - const exists = await fs.promises + const exists = await fsp .access(newPath) .then(() => true) .catch(() => false) @@ -136,11 +136,11 @@ export default class ApplyEditAdapter { throw Error(`Target exists.`) } - return fs.promises.rename(oldPath, newPath) + return fsp.rename(oldPath, newPath) } if (CreateFile.is(edit)) { const path = Convert.uriToPath(edit.uri) - const exists = await fs.promises + const exists = await fsp .access(path) .then(() => true) .catch(() => false) @@ -151,7 +151,7 @@ export default class ApplyEditAdapter { return } - return fs.promises.writeFile(path, "") + return fsp.writeFile(path, "") } } diff --git a/test/adapters/apply-edit-adapter.test.ts b/test/adapters/apply-edit-adapter.test.ts index e1ef311b..f681fb1d 100644 --- a/test/adapters/apply-edit-adapter.test.ts +++ b/test/adapters/apply-edit-adapter.test.ts @@ -386,6 +386,9 @@ describe("ApplyEditAdapter", () => { { kind: "delete", uri: path.join(tempDir, "unexisting.txt"), + options: { + ignoreIfNotExists: false, + }, }, ], }, From a699bd57ac50748a2c490de2bacc58f080423115 Mon Sep 17 00:00:00 2001 From: Christian Daguerre Date: Wed, 14 Apr 2021 08:12:03 +0200 Subject: [PATCH 6/6] chore: move private functions outside of class --- lib/adapters/apply-edit-adapter.ts | 57 +++++++++++++++--------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/lib/adapters/apply-edit-adapter.ts b/lib/adapters/apply-edit-adapter.ts index 09451184..7ae33c03 100644 --- a/lib/adapters/apply-edit-adapter.ts +++ b/lib/adapters/apply-edit-adapter.ts @@ -29,7 +29,7 @@ export default class ApplyEditAdapter { // Sort edits in reverse order to prevent edit conflicts. edits.sort((edit1, edit2) => -edit1.oldRange.compare(edit2.oldRange)) edits.reduce((previous: atomIde.TextEdit | null, current) => { - ApplyEditAdapter.validateEdit(buffer, current, previous) + validateEdit(buffer, current, previous) buffer.setTextInRange(current.oldRange, current.newText) return current }, null) @@ -46,7 +46,7 @@ export default class ApplyEditAdapter { } public static async apply(workspaceEdit: WorkspaceEdit): Promise { - ApplyEditAdapter.normalize(workspaceEdit) + normalize(workspaceEdit) // Keep checkpoints from all successful buffer edits const checkpoints: Array<{ buffer: TextBuffer; checkpoint: number }> = [] @@ -154,36 +154,35 @@ export default class ApplyEditAdapter { return fsp.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], - }) +function 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() || "" - if (prevEdit && edit.oldRange.end.compare(prevEdit.oldRange.start) > 0) { - throw Error(`Found overlapping edit ranges in ${path}`) - } - const startRow = edit.oldRange.start.row - const startCol = edit.oldRange.start.column - const lineLength = buffer.lineLengthForRow(startRow) - if (lineLength == null || startCol > lineLength) { - throw Error(`Out of range edit on ${path}:${startRow + 1}:${startCol + 1}`) - } + workspaceEdit.documentChanges = documentChanges +} + +function validateEdit(buffer: TextBuffer, edit: atomIde.TextEdit, prevEdit: atomIde.TextEdit | null): void { + const path = buffer.getPath() || "" + if (prevEdit && edit.oldRange.end.compare(prevEdit.oldRange.start) > 0) { + throw Error(`Found overlapping edit ranges in ${path}`) + } + const startRow = edit.oldRange.start.row + const startCol = edit.oldRange.start.column + const lineLength = buffer.lineLengthForRow(startRow) + if (lineLength == null || startCol > lineLength) { + throw Error(`Out of range edit on ${path}:${startRow + 1}:${startCol + 1}`) } }