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:
parent
2cfe96f451
commit
ff5f422dda
13 changed files with 285 additions and 7 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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>;
|
||||
|
|
11
src/vs/vscode.proposed.d.ts
vendored
11
src/vs/vscode.proposed.d.ts
vendored
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -21,11 +21,13 @@ export class ExtHostDocuments implements ExtHostDocumentsShape {
|
|||
private _onDidRemoveDocument = new Emitter<vscode.TextDocument>();
|
||||
private _onDidChangeDocument = new Emitter<vscode.TextDocumentChangeEvent>();
|
||||
private _onDidSaveDocument = new Emitter<vscode.TextDocument>();
|
||||
private _onDidRenameResource = new Emitter<vscode.ResourceRenamedEvent>();
|
||||
|
||||
readonly onDidAddDocument: Event<vscode.TextDocument> = this._onDidAddDocument.event;
|
||||
readonly onDidRemoveDocument: Event<vscode.TextDocument> = this._onDidRemoveDocument.event;
|
||||
readonly onDidChangeDocument: Event<vscode.TextDocumentChangeEvent> = this._onDidChangeDocument.event;
|
||||
readonly onDidSaveDocument: Event<vscode.TextDocument> = this._onDidSaveDocument.event;
|
||||
readonly onDidRenameResource: Event<vscode.ResourceRenamedEvent> = this._onDidRenameResource.event;
|
||||
|
||||
private _toDispose: IDisposable[];
|
||||
private _proxy: MainThreadDocumentsShape;
|
||||
|
@ -148,4 +150,11 @@ export class ExtHostDocuments implements ExtHostDocumentsShape {
|
|||
public setWordDefinitionFor(modeId: string, wordDefinition: RegExp): void {
|
||||
setWordDefinitionFor(modeId, wordDefinition);
|
||||
}
|
||||
|
||||
public $onDidRename(oldURL: UriComponents, newURL: UriComponents): void {
|
||||
const oldResource = URI.revive(oldURL);
|
||||
const newResource = URI.revive(newURL);
|
||||
this._onDidRenameResource.fire({ oldResource, newResource });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import { Event } from 'vs/base/common/event';
|
|||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
|
||||
suite('MainThreadDocumentsAndEditors', () => {
|
||||
|
||||
|
@ -63,6 +64,10 @@ suite('MainThreadDocumentsAndEditors', () => {
|
|||
onEditorGroupMoved = Event.None;
|
||||
};
|
||||
|
||||
const fileService = new class extends mock<IFileService>() {
|
||||
onAfterOperation = Event.None;
|
||||
};
|
||||
|
||||
/* tslint:disable */
|
||||
new MainThreadDocumentsAndEditors(
|
||||
SingleProxyRPCProtocol(new class extends mock<ExtHostDocumentsAndEditorsShape>() {
|
||||
|
@ -73,7 +78,7 @@ suite('MainThreadDocumentsAndEditors', () => {
|
|||
workbenchEditorService,
|
||||
codeEditorService,
|
||||
null,
|
||||
null,
|
||||
fileService,
|
||||
null,
|
||||
null,
|
||||
editorGroupService,
|
||||
|
|
Loading…
Reference in a new issue