Skip to content

Commit

Permalink
Prototype update import paths on file rename/move for JS/TS (#50074)
Browse files Browse the repository at this point in the history
* 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'
  • Loading branch information
mjbvz committed May 21, 2018
1 parent 2cfe96f commit ff5f422
Show file tree
Hide file tree
Showing 13 changed files with 285 additions and 7 deletions.
22 changes: 22 additions & 0 deletions extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,28 @@
"default": true,
"description": "%typescript.showUnused.enabled%",
"scope": "resource"
},
"typescript.updateImportsOnFileMove.enabled": {
"type": "string",
"enum": [
"prompt",
"always",
"never"
],
"default": "prompt",
"description": "%typescript.updateImportsOnFileMove.enabled%",
"scope": "resource"
},
"javascript.updateImportsOnFileMove.enabled": {
"type": "string",
"enum": [
"prompt",
"always",
"never"
],
"default": "prompt",
"description": "%typescript.updateImportsOnFileMove.enabled%",
"scope": "resource"
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@
"typescript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for TypeScript files in the editor. Requires TypeScript >= 2.8",
"typescript.preferences.quoteStyle": "Preferred quote style to use for quick fixes: 'single' quotes, 'double' quotes, or 'auto' infer quote type from existing imports. Requires TS >= 2.9",
"typescript.preferences.importModuleSpecifier": "Preferred path style for auto imports: 'relative' paths, 'non-relative' paths, or 'auto' infer the shortest path type. Requires TS >= 2.9",
"typescript.showUnused.enabled": "Enable/disable highlighting of unused variables in code. Requires TypeScript >= 2.9"
"typescript.showUnused.enabled": "Enable/disable highlighting of unused variables in code. Requires TypeScript >= 2.9",
"typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code. Possible values are: 'prompt' on each rename, 'always' update paths automatically, and 'never' rename paths and don't prompt me. Requires TypeScript >= 2.9"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
12 changes: 12 additions & 0 deletions extensions/typescript-language-features/src/languageProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -73,6 +84,7 @@ export default class LanguageProvider {
this.diagnosticsManager.dispose();
this.bufferSyncSupport.dispose();
this.fileConfigurationManager.dispose();
this.renameHandler.dispose();
}

@memoize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
11 changes: 11 additions & 0 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,4 +640,15 @@ declare module 'vscode' {
}

//#endregion

//#region mjbvz: File rename events
export interface ResourceRenamedEvent {
readonly oldResource: Uri;
readonly newResource: Uri;
}

export namespace workspace {
export const onDidRenameResource: Event<ResourceRenamedEvent>;
}
//#endregion
}
8 changes: 7 additions & 1 deletion src/vs/workbench/api/electron-browser/mainThreadDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { IModelService, shouldSynchronizeModel } from 'vs/editor/common/services
import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle';
import { TextFileModelChangeEvent, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { TPromise } from 'vs/base/common/winjs.base';
import { IFileService } from 'vs/platform/files/common/files';
import { IFileService, FileOperation } from 'vs/platform/files/common/files';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { ExtHostContext, MainThreadDocumentsShape, ExtHostDocumentsShape, IExtHostContext } from '../node/extHost.protocol';
Expand Down Expand Up @@ -119,6 +119,12 @@ export class MainThreadDocuments implements MainThreadDocumentsShape {
}
}));

this._toDispose.push(fileService.onAfterOperation(e => {
if (e.operation === FileOperation.MOVE) {
this._proxy.$onDidRename(e.resource, e.target.resource);
}
}));

this._modelToDisposeMap = Object.create(null);
}

Expand Down
5 changes: 4 additions & 1 deletion src/vs/workbench/api/node/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,10 @@ export function createApiFactory(
}),
registerSearchProvider: proposedApiFunction(extension, (scheme, provider) => {
return extHostSearch.registerSearchProvider(scheme, provider);
})
}),
onDidRenameResource: proposedApiFunction(extension, (listener, thisArg?, disposables?) => {
return extHostDocuments.onDidRenameResource(listener, thisArg, disposables);
}),
};

// namespace: scm
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/node/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ export interface ExtHostDocumentsShape {
$acceptModelSaved(strURL: UriComponents): void;
$acceptDirtyStateChanged(strURL: UriComponents, isDirty: boolean): void;
$acceptModelChanged(strURL: UriComponents, e: IModelChangedEvent, isDirty: boolean): void;
$onDidRename(oldURL: UriComponents, newURL: UriComponents): void;
}

export interface ExtHostDocumentSaveParticipantShape {
Expand Down

0 comments on commit ff5f422

Please sign in to comment.