Skip to content

Commit bbd2743

Browse files
committed
explorer.incrementalNaming
fixes microsoft#76752
1 parent 5438c9b commit bbd2743

File tree

5 files changed

+291
-52
lines changed

5 files changed

+291
-52
lines changed

src/vs/workbench/contrib/files/browser/fileActions.ts

Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage';
1515
import * as strings from 'vs/base/common/strings';
1616
import { Action } from 'vs/base/common/actions';
1717
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
18-
import { VIEWLET_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files';
18+
import { VIEWLET_ID, IExplorerService, IFilesConfiguration } from 'vs/workbench/contrib/files/common/files';
1919
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
2020
import { IFileService, AutoSaveConfiguration } from 'vs/platform/files/common/files';
2121
import { toResource, SideBySideEditor } from 'vs/workbench/common/editor';
@@ -328,7 +328,7 @@ function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean
328328
}
329329

330330

331-
export function findValidPasteFileTarget(targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean, allowOverwrite: boolean }): URI {
331+
export function findValidPasteFileTarget(targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean, allowOverwrite: boolean }, incrementalNaming: 'simple' | 'smart'): URI {
332332
let name = resources.basenameOrAuthority(fileToPaste.resource);
333333

334334
let candidate = resources.joinPath(targetFolder.resource, name);
@@ -337,37 +337,104 @@ export function findValidPasteFileTarget(targetFolder: ExplorerItem, fileToPaste
337337
break;
338338
}
339339

340-
name = incrementFileName(name, !!fileToPaste.isDirectory);
340+
name = incrementFileName(name, !!fileToPaste.isDirectory, incrementalNaming);
341341
candidate = resources.joinPath(targetFolder.resource, name);
342342
}
343343

344344
return candidate;
345345
}
346346

347-
export function incrementFileName(name: string, isFolder: boolean): string {
348-
let namePrefix = name;
349-
let extSuffix = '';
350-
if (!isFolder) {
351-
extSuffix = extname(name);
352-
namePrefix = basename(name, extSuffix);
353-
}
354-
355-
// name copy 5(.txt) => name copy 6(.txt)
356-
// name copy(.txt) => name copy 2(.txt)
357-
const suffixRegex = /^(.+ copy)( \d+)?$/;
358-
if (suffixRegex.test(namePrefix)) {
359-
return namePrefix.replace(suffixRegex, (match, g1?, g2?) => {
360-
let number = (g2 ? parseInt(g2) : 1);
361-
return number === 0
362-
? `${g1}`
363-
: (number < Constants.MAX_SAFE_SMALL_INTEGER
364-
? `${g1} ${number + 1}`
365-
: `${g1}${g2} copy`);
366-
}) + extSuffix;
367-
}
368-
369-
// name(.txt) => name copy(.txt)
370-
return `${namePrefix} copy${extSuffix}`;
347+
export function incrementFileName(name: string, isFolder: boolean, incrementalNaming: 'simple' | 'smart'): string {
348+
if (incrementalNaming === 'simple') {
349+
let namePrefix = name;
350+
let extSuffix = '';
351+
if (!isFolder) {
352+
extSuffix = extname(name);
353+
namePrefix = basename(name, extSuffix);
354+
}
355+
356+
// name copy 5(.txt) => name copy 6(.txt)
357+
// name copy(.txt) => name copy 2(.txt)
358+
const suffixRegex = /^(.+ copy)( \d+)?$/;
359+
if (suffixRegex.test(namePrefix)) {
360+
return namePrefix.replace(suffixRegex, (match, g1?, g2?) => {
361+
let number = (g2 ? parseInt(g2) : 1);
362+
return number === 0
363+
? `${g1}`
364+
: (number < Constants.MAX_SAFE_SMALL_INTEGER
365+
? `${g1} ${number + 1}`
366+
: `${g1}${g2} copy`);
367+
}) + extSuffix;
368+
}
369+
370+
// name(.txt) => name copy(.txt)
371+
return `${namePrefix} copy${extSuffix}`;
372+
}
373+
374+
const separators = '[\\.\\-_]';
375+
const maxNumber = Constants.MAX_SAFE_SMALL_INTEGER;
376+
377+
// file.1.txt=>file.2.txt
378+
let suffixFileRegex = RegExp('(.*' + separators + ')(\\d+)(\\..*)$');
379+
if (!isFolder && name.match(suffixFileRegex)) {
380+
return name.replace(suffixFileRegex, (match, g1?, g2?, g3?) => {
381+
let number = parseInt(g2);
382+
return number < maxNumber
383+
? g1 + strings.pad(number + 1, g2.length) + g3
384+
: strings.format('{0}{1}.1{2}', g1, g2, g3);
385+
});
386+
}
387+
388+
// 1.file.txt=>2.file.txt
389+
let prefixFileRegex = RegExp('(\\d+)(' + separators + '.*)(\\..*)$');
390+
if (!isFolder && name.match(prefixFileRegex)) {
391+
return name.replace(prefixFileRegex, (match, g1?, g2?, g3?) => {
392+
let number = parseInt(g1);
393+
return number < maxNumber
394+
? strings.pad(number + 1, g1.length) + g2 + g3
395+
: strings.format('{0}{1}.1{2}', g1, g2, g3);
396+
});
397+
}
398+
399+
// 1.txt=>2.txt
400+
let prefixFileNoNameRegex = RegExp('(\\d+)(\\..*)$');
401+
if (!isFolder && name.match(prefixFileNoNameRegex)) {
402+
return name.replace(prefixFileNoNameRegex, (match, g1?, g2?) => {
403+
let number = parseInt(g1);
404+
return number < maxNumber
405+
? strings.pad(number + 1, g1.length) + g2
406+
: strings.format('{0}.1{1}', g1, g2);
407+
});
408+
}
409+
410+
// file.txt=>file.1.txt
411+
const lastIndexOfDot = name.lastIndexOf('.');
412+
if (!isFolder && lastIndexOfDot >= 0) {
413+
return strings.format('{0}.1{1}', name.substr(0, lastIndexOfDot), name.substr(lastIndexOfDot));
414+
}
415+
416+
// folder.1=>folder.2
417+
if (isFolder && name.match(/(\d+)$/)) {
418+
return name.replace(/(\d+)$/, (match: string, ...groups: any[]) => {
419+
let number = parseInt(groups[0]);
420+
return number < maxNumber
421+
? strings.pad(number + 1, groups[0].length)
422+
: strings.format('{0}.1', groups[0]);
423+
});
424+
}
425+
426+
// 1.folder=>2.folder
427+
if (isFolder && name.match(/^(\d+)/)) {
428+
return name.replace(/^(\d+)(.*)$/, (match: string, ...groups: any[]) => {
429+
let number = parseInt(groups[0]);
430+
return number < maxNumber
431+
? strings.pad(number + 1, groups[0].length) + groups[1]
432+
: strings.format('{0}{1}.1', groups[0], groups[1]);
433+
});
434+
}
435+
436+
// file/folder=>file.1/folder.1
437+
return strings.format('{0}.1', name);
371438
}
372439

373440
// Global Compare with
@@ -1006,6 +1073,7 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => {
10061073
const textFileService = accessor.get(ITextFileService);
10071074
const notificationService = accessor.get(INotificationService);
10081075
const editorService = accessor.get(IEditorService);
1076+
const configurationService = accessor.get(IConfigurationService);
10091077

10101078
if (listService.lastFocusedList) {
10111079
const explorerContext = getContext(listService.lastFocusedList);
@@ -1030,7 +1098,8 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => {
10301098
target = element.isDirectory ? element : element.parent!;
10311099
}
10321100

1033-
const targetFile = findValidPasteFileTarget(target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove });
1101+
const incrementalNaming = configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
1102+
const targetFile = findValidPasteFileTarget(target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming);
10341103

10351104
// Move/Copy File
10361105
if (pasteShouldMove) {

src/vs/workbench/contrib/files/browser/files.contribution.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,15 @@ configurationRegistry.registerConfiguration({
426426
description: nls.localize('explorer.decorations.badges', "Controls whether file decorations should use badges."),
427427
default: true
428428
},
429+
'explorer.incrementalNaming': {
430+
enum: ['simple', 'smart'],
431+
enumDescriptions: [
432+
nls.localize('simple', "Appends the word \"copy\" at the end of the duplicated name potentially followed by a number"),
433+
nls.localize('smart', "Adds a number at the end of the duplicated name. If some number is already part of the name, tries to increase that number")
434+
],
435+
description: nls.localize('explorer.incrementalNaming', "Controls what naming strategy to use when a giving a new name to a duplicated explorer item on paste."),
436+
default: 'simple'
437+
}
429438
}
430439
});
431440

src/vs/workbench/contrib/files/browser/views/explorerViewer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -808,8 +808,8 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
808808
private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
809809
// Reuse duplicate action if user copies
810810
if (isCopy) {
811-
812-
return this.fileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false })).then(stat => {
811+
const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
812+
return this.fileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming)).then(stat => {
813813
if (!stat.isDirectory) {
814814
return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }).then(() => undefined);
815815
}

src/vs/workbench/contrib/files/common/files.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export interface IFilesConfiguration extends IFilesConfiguration, IWorkbenchEdit
114114
colors: boolean;
115115
badges: boolean;
116116
};
117+
incrementalNaming: 'simple' | 'smart';
117118
};
118119
editor: IEditorOptions;
119120
}

0 commit comments

Comments
 (0)