From 48a515005b835dec07b9786c91d80086d80e3216 Mon Sep 17 00:00:00 2001 From: chemzqm <chemzqm@gmail.com> Date: Sun, 14 Oct 2018 11:15:08 +0800 Subject: [PATCH] support rename for import path. --- src/server/features/rename.ts | 120 ++++++++++++++++++++++++++------ src/server/typescriptService.ts | 14 ++++ src/server/utils/api.ts | 2 + 3 files changed, 114 insertions(+), 22 deletions(-) diff --git a/src/server/features/rename.ts b/src/server/features/rename.ts index de51af2..ce0a842 100644 --- a/src/server/features/rename.ts +++ b/src/server/features/rename.ts @@ -2,50 +2,87 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, Position, TextDocument, TextEdit, WorkspaceEdit } from 'vscode-languageserver-protocol' import { RenameProvider } from 'coc.nvim/lib/provider' +import path from 'path' +import { CancellationToken, Position, Range, TextDocument, TextEdit, WorkspaceEdit } from 'vscode-languageserver-protocol' import * as Proto from '../protocol' -import { ITypeScriptServiceClient } from '../typescriptService' +import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService' +import API from '../utils/api' import * as typeConverters from '../utils/typeConverters' +import Uri from 'vscode-uri' export default class TypeScriptRenameProvider implements RenameProvider { public constructor(private readonly client: ITypeScriptServiceClient) { } + public async prepareRename( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise<Range | null> { + const response = await this.execRename(document, position, token) + if (!response || response.type !== 'response' || !response.body) { + return null + } + + const renameInfo = response.body.info + if (!renameInfo.canRename) { + return Promise.reject(new Error('Invalid location for rename.')) + } + + if (this.client.apiVersion.gte(API.v310)) { + const triggerSpan = (renameInfo as any).triggerSpan + if (triggerSpan) { + const range = typeConverters.Range.fromTextSpan(triggerSpan) + return range + } + } + return null + } + public async provideRenameEdits( document: TextDocument, position: Position, newName: string, token: CancellationToken ): Promise<WorkspaceEdit | null> { - const file = this.client.toPath(document.uri) - if (!file) { + const response = await this.execRename(document, position, token) + if (!response || response.type !== 'response' || !response.body) { return null } + const renameInfo = response.body.info + if (!renameInfo.canRename) { + return Promise.reject(new Error('Invalid location for rename.')) + } + + if (this.client.apiVersion.gte(API.v310)) { + if ((renameInfo as any).fileToRename) { + const edits = await this.renameFile((renameInfo as any).fileToRename, newName, token) + if (edits) { + return edits + } else { + return Promise.reject(new Error('An error occurred while renaming file')) + } + } + } + return this.toWorkspaceEdit(response.body.locs, newName) + } + + public async execRename( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise<ServerResponse<Proto.RenameResponse> | undefined> { + const file = this.client.toPath(document.uri) + if (!file) return undefined + const args: Proto.RenameRequestArgs = { ...typeConverters.Position.toFileLocationRequestArgs(file, position), findInStrings: false, findInComments: false } - try { - const response = await this.client.execute('rename', args, token) - if (!response.body) { - return null - } - - const renameInfo = response.body.info - if (!renameInfo.canRename) { - return Promise.reject<WorkspaceEdit>( - renameInfo.localizedErrorMessage - ) - } - - return this.toWorkspaceEdit(response.body.locs, newName) - } catch { - // noop - } - return null + return this.client.execute('rename', args, token) } private toWorkspaceEdit( @@ -67,4 +104,43 @@ export default class TypeScriptRenameProvider implements RenameProvider { } return { changes } } + + private async renameFile( + fileToRename: string, + newName: string, + token: CancellationToken, + ): Promise<WorkspaceEdit | undefined> { + // Make sure we preserve file exension if none provided + if (!path.extname(newName)) { + newName += path.extname(fileToRename) + } + + const dirname = path.dirname(fileToRename) + const newFilePath = path.join(dirname, newName) + + const args: Proto.GetEditsForFileRenameRequestArgs & { file: string } = { + file: fileToRename, + oldFilePath: fileToRename, + newFilePath + } + const response = await this.client.execute('getEditsForFileRename', args, token) + if (response.type !== 'response' || !response.body) { + return undefined + } + + const edits = typeConverters.WorkspaceEdit.fromFileCodeEdits(this.client, response.body) + + edits.documentChanges = edits.documentChanges || [] + edits.documentChanges.push({ + kind: 'rename', + oldUri: Uri.file(fileToRename).toString(), + newUri: Uri.file(newFilePath).toString(), + options: { + overwrite: false, + ignoreIfExists: true + } + }) + return edits + } + } diff --git a/src/server/typescriptService.ts b/src/server/typescriptService.ts index a400012..dc67151 100644 --- a/src/server/typescriptService.ts +++ b/src/server/typescriptService.ts @@ -9,6 +9,20 @@ import API from './utils/api' import { TypeScriptServiceConfiguration } from './utils/configuration' import Logger from './utils/logger' +export class CancelledResponse { + public readonly type: 'cancelled' = 'cancelled' + + constructor( + public readonly reason: string + ) { } +} + +export class NoContentResponse { + public readonly type: 'noContent' = 'noContent' +} + +export type ServerResponse<T extends Proto.Response> = T | CancelledResponse | NoContentResponse + export interface TypeScriptServerPlugin { readonly path: string readonly name: string diff --git a/src/server/utils/api.ts b/src/server/utils/api.ts index a5746ee..31dd07a 100644 --- a/src/server/utils/api.ts +++ b/src/server/utils/api.ts @@ -27,6 +27,8 @@ export default class API { public static readonly v291 = API.fromSimpleString('2.9.1') public static readonly v292 = API.fromSimpleString('2.9.2') public static readonly v300 = API.fromSimpleString('3.0.0') + public static readonly v310 = API.fromSimpleString('3.1.0') + public static readonly v320 = API.fromSimpleString('3.2.0') public static fromVersionString(versionString: string): API { let version = semver.valid(versionString)