Skip to content

Commit

Permalink
Workspace: Rename files/folder is case sensitive
Browse files Browse the repository at this point in the history
This commit fixes the rename command for windows users by making possible
the case sensitive rename of files and folders. Also, it adds the test file
workspace-commands.spec.ts which verifies that the implementation works as expected.
  • Loading branch information
OmarSdt-EC committed Jul 9, 2021
1 parent bc3b5e1 commit fb824ec
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 7 deletions.
2 changes: 1 addition & 1 deletion packages/filesystem/src/browser/file-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1120,7 +1120,7 @@ export class FileService {
// if target exists get valid target
if (exists && !overwrite) {
const parent = await this.resolve(target.parent);
const name = target.path.name + '_copy';
const name = isSameResourceWithDifferentPathCase ? target.path.name : target.path.name + '_copy';
target = FileSystemUtils.generateUniqueResourceURI(target.parent, parent, name, target.path.ext);
}

Expand Down
148 changes: 148 additions & 0 deletions packages/workspace/src/browser/workspace-commands.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/********************************************************************************
* Copyright (C) 2021 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
let disableJSDOM = enableJSDOM();

import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { ApplicationProps } from '@theia/application-package/lib/application-props';
FrontendApplicationConfigProvider.set({
...ApplicationProps.DEFAULT.frontend.config
});

import { expect } from 'chai';
import URI from '@theia/core/lib/common/uri';
import { Container } from '@theia/core/shared/inversify';
import { FileDialogService } from '@theia/filesystem/lib/browser';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { LabelProvider, OpenerService, FrontendApplication } from '@theia/core/lib/browser';
import { MessageService } from '@theia/core/lib/common';
import { SelectionService } from '@theia/core/lib/common/selection-service';
import { WorkspaceCommandContribution } from './workspace-commands';
import { WorkspaceCompareHandler } from './workspace-compare-handler';
import { WorkspaceDeleteHandler } from './workspace-delete-handler';
import { WorkspaceDuplicateHandler } from './workspace-duplicate-handler';
import { WorkspacePreferences } from './workspace-preferences';
import { WorkspaceService } from './workspace-service';

disableJSDOM();

describe('workspace-commands', () => {

let commands: WorkspaceCommandContribution;

const childStat: FileStat = {
isFile: true,
isDirectory: false,
isSymbolicLink: false,
resource: new URI('foo/bar'),
name: 'bar',
};

const parent: FileStat = {
isFile: false,
isDirectory: true,
isSymbolicLink: false,
resource: new URI('foo'),
name: 'foo',
children: [
childStat
]
};

before(() => disableJSDOM = enableJSDOM());
after(() => disableJSDOM());

beforeEach(() => {
const container = new Container();

container.bind(FileDialogService).toConstantValue(<FileDialogService>{});
container.bind(FileService).toConstantValue(<FileService>{
async exists(resource: URI): Promise<boolean> {
return resource.path.base.includes('bar'); // 'bar' exists for test purposes.
}
});
container.bind(FrontendApplication).toConstantValue(<FrontendApplication>{});
container.bind(LabelProvider).toConstantValue(<LabelProvider>{});
container.bind(MessageService).toConstantValue(<MessageService>{});
container.bind(OpenerService).toConstantValue(<OpenerService>{});
container.bind(SelectionService).toConstantValue(<SelectionService>{});
container.bind(WorkspaceCommandContribution).toSelf().inSingletonScope();
container.bind(WorkspaceCompareHandler).toConstantValue(<WorkspaceCompareHandler>{});
container.bind(WorkspaceDeleteHandler).toConstantValue(<WorkspaceDeleteHandler>{});
container.bind(WorkspaceDuplicateHandler).toConstantValue(<WorkspaceDuplicateHandler>{});
container.bind(WorkspacePreferences).toConstantValue(<WorkspacePreferences>{});
container.bind(WorkspaceService).toConstantValue(<WorkspaceService>{});

commands = container.get(WorkspaceCommandContribution);
});

describe('#validateFileName', () => {

it('should not validate an empty file name', async () => {
const message = await commands['validateFileName']('', parent);
expect(message).to.equal('');
});

it('should accept the resource does not exist', async () => {
const message = await commands['validateFileName']('a.ts', parent);
expect(message).to.equal('');
});

it('should not accept if the resource exists', async () => {
const message = await commands['validateFileName']('bar', parent);
expect(message).to.not.equal(''); // a non empty message indicates an error.
});

it('should not accept invalid filenames', async () => {
let message = await commands['validateFileName']('.', parent, true); // invalid filename.
expect(message).to.not.equal('');

message = await commands['validateFileName']('/a', parent, true); // invalid starts-with `\`.
expect(message).to.not.equal('');

message = await commands['validateFileName'](' a', parent, true); // invalid leading whitespace.
expect(message).to.not.equal('');

message = await commands['validateFileName']('a ', parent, true); // invalid trailing whitespace.
expect(message).to.not.equal('');

});

});

describe('#validateFileRename', () => {

it('should accept if the resource exists case-insensitively', async () => {
const initialValue: string = 'bar';
const newValue = 'Bar';
const overwrite = parent.resource.resolve(newValue).isEqual(parent.resource.resolve(initialValue), false);
const message = await commands['validateFileRename'](newValue, parent, initialValue, overwrite);
expect(message).to.equal('');
});

it('should accept if the resource does not exist case-insensitively', async () => {
const initialValue: string = 'bar';
const newValue = 'foo';
const overwrite = parent.resource.resolve(newValue).isEqual(parent.resource.resolve(initialValue), false);
const message = await commands['validateFileRename'](newValue, parent, initialValue, overwrite);
expect(message).to.equal('');
});

});

});
31 changes: 25 additions & 6 deletions packages/workspace/src/browser/workspace-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { inject, injectable } from '@theia/core/shared/inversify';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { SelectionService } from '@theia/core/lib/common/selection-service';
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
import { MenuContribution, MenuModelRegistry } from '@theia/core/lib/common/menu';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
Expand All @@ -34,7 +35,7 @@ import { WorkspaceCompareHandler } from './workspace-compare-handler';
import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution';
import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { WorkspaceInputDialog } from './workspace-input-dialog';
import { Emitter, Event } from '@theia/core/lib/common';
import { Emitter, Event, OS } from '@theia/core/lib/common';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileStat } from '@theia/filesystem/lib/common/files';

Expand Down Expand Up @@ -196,10 +197,18 @@ export class WorkspaceCommandContribution implements CommandContribution {
@inject(WorkspaceDeleteHandler) protected readonly deleteHandler: WorkspaceDeleteHandler;
@inject(WorkspaceDuplicateHandler) protected readonly duplicateHandler: WorkspaceDuplicateHandler;
@inject(WorkspaceCompareHandler) protected readonly compareHandler: WorkspaceCompareHandler;
@inject(ApplicationServer) protected readonly applicationServer: ApplicationServer;

private readonly onDidCreateNewFileEmitter = new Emitter<DidCreateNewResourceEvent>();
private readonly onDidCreateNewFolderEmitter = new Emitter<DidCreateNewResourceEvent>();

protected _backendOS: Promise<OS.Type>;

@postConstruct()
async onStart(): Promise<void> {
this._backendOS = this.applicationServer.getBackendOS();
};

get onDidCreateNewFile(): Event<DidCreateNewResourceEvent> {
return this.onDidCreateNewFileEmitter.event;
}
Expand Down Expand Up @@ -282,11 +291,14 @@ export class WorkspaceCommandContribution implements CommandContribution {
start: 0,
end: uri.path.name.length
},
validate: (name, mode) => {
validate: async (name, mode) => {
if (initialValue === name && mode === 'preview') {
return false;
}
return this.validateFileName(name, parent, false);
const allowOverwrite = await this._backendOS === OS.Type.Windows
&& parent.resource.resolve(name).isEqual(parent.resource.resolve(initialValue), false);

return this.validateFileRename(name, parent, initialValue, allowOverwrite);
}
});
const fileName = await dialog.open();
Expand Down Expand Up @@ -368,19 +380,26 @@ export class WorkspaceCommandContribution implements CommandContribution {
return new WorkspaceRootUriAwareCommandHandler(this.workspaceService, this.selectionService, handler);
}

protected async validateFileRename(name: string, parent: FileStat, initialValue: string, allowOverwrite: boolean = false): Promise<string> {
if (allowOverwrite) {
return '';
}
return this.validateFileName(name, parent, false);
}

/**
* Returns an error message if the file name is invalid. Otherwise, an empty string.
*
* @param name the simple file name of the file to validate.
* @param parent the parent directory's file stat.
* @param recursive allow file or folder creation using recursive path
*/
protected async validateFileName(name: string, parent: FileStat, recursive: boolean = false): Promise<string> {
protected async validateFileName(name: string, parent: FileStat, allowNested: boolean = false): Promise<string> {
if (!name) {
return '';
}
// do not allow recursive rename
if (!recursive && !validFilename(name)) {
if (!allowNested && !validFilename(name)) {
return 'Invalid file or folder name';
}
if (name.startsWith('/')) {
Expand Down

0 comments on commit fb824ec

Please sign in to comment.