Prototype update import paths on file rename/move for JS/TS (#50074)

* Prototype of updating paths on rename file

* Fix apply edits

* Hook up to normal rename

* Fix unit test

* Remove timeout

* Adding prompt

* Bail early if user has set 'never'
This commit is contained in:
Matt Bierner
2018-05-21 13:26:24 -07:00
committed by GitHub
parent 2cfe96f451
commit ff5f422dda
13 changed files with 285 additions and 7 deletions

View File

@@ -172,10 +172,10 @@ export default class BufferSyncSupport {
}
public listen(): void {
workspace.onDidOpenTextDocument(this.onDidOpenTextDocument, this, this.disposables);
workspace.onDidOpenTextDocument(this.openTextDocument, this, this.disposables);
workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this.disposables);
workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables);
workspace.textDocuments.forEach(this.onDidOpenTextDocument, this);
workspace.textDocuments.forEach(this.openTextDocument, this);
}
public set validate(value: boolean) {
@@ -196,7 +196,7 @@ export default class BufferSyncSupport {
disposeAll(this.disposables);
}
private onDidOpenTextDocument(document: TextDocument): void {
public openTextDocument(document: TextDocument): void {
if (!this.modeIds.has(document.languageId)) {
return;
}

View File

@@ -0,0 +1,206 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import * as languageIds from '../utils/languageModeIds';
import * as typeConverters from '../utils/typeConverters';
import BufferSyncSupport from './bufferSyncSupport';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
const updateImportsOnFileMoveName = 'updateImportsOnFileMove.enabled';
enum UpdateImportsOnFileMoveSetting {
Prompt = 'prompt',
Always = 'always',
Never = 'never',
}
export class UpdateImportsOnFileRenameHandler {
private readonly _onDidRenameSub: vscode.Disposable;
public constructor(
private readonly client: ITypeScriptServiceClient,
private readonly bufferSyncSupport: BufferSyncSupport,
private readonly fileConfigurationManager: FileConfigurationManager,
private readonly handles: (uri: vscode.Uri) => Promise<boolean>,
) {
this._onDidRenameSub = vscode.workspace.onDidRenameResource(e => {
this.doRename(e.oldResource, e.newResource);
});
}
public dispose() {
this._onDidRenameSub.dispose();
}
private async doRename(
oldResource: vscode.Uri,
newResource: vscode.Uri,
): Promise<void> {
if (!this.client.apiVersion.has290Features) {
return;
}
if (!await this.handles(newResource)) {
return;
}
const newFile = this.client.normalizePath(newResource);
if (!newFile) {
return;
}
const oldFile = this.client.normalizePath(oldResource);
if (!oldFile) {
return;
}
const document = await vscode.workspace.openTextDocument(newResource);
const config = this.getConfiguration(document);
const setting = config.get<UpdateImportsOnFileMoveSetting>(updateImportsOnFileMoveName);
if (setting === UpdateImportsOnFileMoveSetting.Never) {
return;
}
// Make sure TS knows about file
this.bufferSyncSupport.openTextDocument(document);
const edits = await this.getEditsForFileRename(document, oldFile, newFile);
if (!edits || !edits.size) {
return;
}
if (await this.confirmActionWithUser(document)) {
await vscode.workspace.applyEdit(edits);
}
}
private async confirmActionWithUser(
newDocument: vscode.TextDocument
): Promise<boolean> {
const config = this.getConfiguration(newDocument);
const setting = config.get<UpdateImportsOnFileMoveSetting>(updateImportsOnFileMoveName);
switch (setting) {
case UpdateImportsOnFileMoveSetting.Always:
return true;
case UpdateImportsOnFileMoveSetting.Never:
return false;
case UpdateImportsOnFileMoveSetting.Prompt:
default:
return this.promptUser(newDocument);
}
}
private getConfiguration(newDocument: vscode.TextDocument) {
return vscode.workspace.getConfiguration(isTypeScriptDocument(newDocument) ? 'typescript' : 'javascript', newDocument.uri);
}
private async promptUser(
newDocument: vscode.TextDocument
): Promise<boolean> {
enum Choice {
None = 0,
Accept = 1,
Reject = 2,
Always = 3,
Never = 4,
}
interface Item extends vscode.QuickPickItem {
choice: Choice;
}
const response = await vscode.window.showQuickPick<Item>([
{
label: localize('accept.label', "Yes"),
description: localize('accept.description', "Update imports."),
choice: Choice.Accept,
},
{
label: localize('reject.label', "No"),
description: localize('reject.description', "Do not update imports."),
choice: Choice.Reject,
},
{
label: localize('always.label', "Always"),
description: localize('always.description', "Yes, and always automatically update imports."),
choice: Choice.Always,
},
{
label: localize('never.label', "Never"),
description: localize('never.description', "No, and do not prompt me again."),
choice: Choice.Never,
},
], {
placeHolder: localize('prompt', "Update import paths?"),
ignoreFocusOut: true,
});
if (!response) {
return false;
}
switch (response.choice) {
case Choice.Accept:
{
return true;
}
case Choice.Reject:
{
return false;
}
case Choice.Always:
{
const config = this.getConfiguration(newDocument);
config.update(
updateImportsOnFileMoveName,
UpdateImportsOnFileMoveSetting.Always,
vscode.ConfigurationTarget.Global);
return true;
}
case Choice.Never:
{
const config = this.getConfiguration(newDocument);
config.update(
updateImportsOnFileMoveName,
UpdateImportsOnFileMoveSetting.Never,
vscode.ConfigurationTarget.Global);
return false;
}
}
return false;
}
private async getEditsForFileRename(
document: vscode.TextDocument,
oldFile: string,
newFile: string,
) {
await this.fileConfigurationManager.ensureConfigurationForDocument(document, undefined);
const args: Proto.GetEditsForFileRenameRequestArgs = {
file: newFile,
oldFilePath: oldFile,
newFilePath: newFile,
};
const response = await this.client.execute('getEditsForFileRename', args);
if (!response || !response.body) {
return;
}
return typeConverters.WorkspaceEdit.fromFromFileCodeEdits(this.client, response.body);
}
}
function isTypeScriptDocument(document: vscode.TextDocument) {
return document.languageId === languageIds.typescript || document.languageId === languageIds.typescriptreact;
}

View File

@@ -21,6 +21,7 @@ import { CachedNavTreeResponse } from './features/baseCodeLensProvider';
import { memoize } from './utils/memoize';
import { disposeAll } from './utils/dispose';
import TelemetryReporter from './utils/telemetry';
import { UpdateImportsOnFileRenameHandler } from './features/updatePathsOnRename';
const validateSetting = 'validate.enable';
const suggestionSetting = 'suggestionActions.enabled';
@@ -40,6 +41,7 @@ export default class LanguageProvider {
private readonly versionDependentDisposables: Disposable[] = [];
private foldingProviderRegistration: Disposable | undefined = void 0;
private readonly renameHandler: UpdateImportsOnFileRenameHandler;
constructor(
private readonly client: TypeScriptServiceClient,
@@ -64,6 +66,15 @@ export default class LanguageProvider {
await this.registerProviders(client, commandManager, typingsStatus);
this.bufferSyncSupport.listen();
});
this.renameHandler = new UpdateImportsOnFileRenameHandler(this.client, this.bufferSyncSupport, this.fileConfigurationManager, async uri => {
try {
const doc = await workspace.openTextDocument(uri);
return this.handles(uri, doc);
} catch {
return false;
}
});
}
public dispose(): void {
@@ -73,6 +84,7 @@ export default class LanguageProvider {
this.diagnosticsManager.dispose();
this.bufferSyncSupport.dispose();
this.fileConfigurationManager.dispose();
this.renameHandler.dispose();
}
@memoize

View File

@@ -25,6 +25,7 @@ import { LanguageDescription } from './utils/languageDescription';
import LogDirectoryProvider from './utils/logDirectoryProvider';
import { disposeAll } from './utils/dispose';
import { DiagnosticKind } from './features/diagnostics';
import { UpdateImportsOnFileRenameHandler } from './features/updatePathsOnRename';
// Style check diagnostics that can be reported as warnings
const styleCheckDiagnostics = [

View File

@@ -58,6 +58,7 @@ export interface ITypeScriptServiceClient {
execute(command: 'applyCodeActionCommand', args: Proto.ApplyCodeActionCommandRequestArgs, token?: CancellationToken): Promise<Proto.ApplyCodeActionCommandResponse>;
execute(command: 'organizeImports', args: Proto.OrganizeImportsRequestArgs, token?: CancellationToken): Promise<Proto.OrganizeImportsResponse>;
execute(command: 'getOutliningSpans', args: Proto.FileRequestArgs, token: CancellationToken): Promise<Proto.OutliningSpansResponse>;
execute(command: 'getEditsForFileRename', args: Proto.GetEditsForFileRenameRequestArgs): Promise<Proto.GetEditsForFileRenameResponse>;
execute(command: string, args: any, expectedResult: boolean | CancellationToken, token?: CancellationToken): Promise<any>;
executeAsync(command: 'geterr', args: Proto.GeterrRequestArgs, token: CancellationToken): Promise<any>;