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)