Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Substitute ~ for home directory in file dialog #9416

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
[1.14.0 Milestone](https://github.com/eclipse-theia/theia/milestone/20)

- [debug] Fix behavior of `Add Configurations` command when empty `launch.json` present. [#9467](https://github.com/eclipse-theia/theia/pull/9467)
- [filesystem] Allow for ~ substitution in browser's file dialog [#9416](https://github.com/eclipse-theia/theia/pull/9416)

<a name="breaking_changes_1.14.0">[Breaking Changes:](#breaking_changes_1.14.0)</a>

- [debug] `DebugConfigurationManager` no longer `@injects()` the `FileService` and now uses `MonacoTextModelService` instead. [#9467](https://github.com/eclipse-theia/theia/pull/9467)
- [filesystem] ReactRenderer, LocationListRenderer, and FileDialogTreeFiltersRenderer have been made injectable/factoritized. FileDialog and its children have been updated to use property injection where appropriate and initialization inside constructor has been moved to postConstruct. [#9416](https://github.com/eclipse-theia/theia/pull/9416).

## v1.13.0 - 4/29/2021

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/browser/widgets/react-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable } from 'inversify';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Disposable } from '../../common';

@injectable()
export class ReactRenderer implements Disposable {
readonly host: HTMLElement;
constructor(
Expand Down
36 changes: 36 additions & 0 deletions packages/core/src/common/path.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,30 @@ describe('Path', () => {
const expected = `${linuxHome}/a/b/theia`;
expect(Path.tildify(path, '')).eq(expected);
});

it('should expand ~ on Linux when path begins with ~', async () => {
const path = '~/a/b/theia';
const expected = `${linuxHome}/a/b/theia`;
expect(Path.untildify(path, linuxHome)).eq(expected);
});

it('should expand ~ on Linux when path starts with ~ duplication', async () => {
const path = '~/~/a/b/theia';
const expected = `${linuxHome}/~/a/b/theia`;
expect(Path.untildify(path, linuxHome)).eq(expected);
});

it('should not expand ~ on Linux when path does not start with ~', async () => {
const path = '/test/~/a/b/theia';
const expected = '/test/~/a/b/theia';
expect(Path.untildify(path, linuxHome)).eq(expected);
});

it('should not expand ~ on Linux when home is empty', async () => {
const path = '~/a/b/theia';
const expected = '~/a/b/theia';
expect(Path.untildify(path, '')).eq(expected);
});
});

describe('Windows', () => {
Expand All @@ -304,5 +328,17 @@ describe('Path', () => {
const expected = `${windowsHome}/a/b/theia`;
expect(Path.tildify(path, '')).eq(expected);
});

it('should not expand ~ on Windows', async () => {
const path = '~/a/b/theia';
const expected = '~/a/b/theia';
expect(Path.untildify(path, windowsHome)).eq(expected);
});

it('should not expand ~ on Windows when home is empty', async () => {
const path = '~/a/b/theia';
const expected = '~/a/b/theia';
expect(Path.untildify(path, '')).eq(expected);
});
});
});
19 changes: 19 additions & 0 deletions packages/core/src/common/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@ export class Path {
return resourcePath;
}

/**
* Untildify path, replacing `~` with `home` if `~` present at the beginning of the path.
* This is a non-operation for Windows.
*
* @param resourcePath
* @param home
*/
static untildify(resourcePath: string, home: string): string {
if (resourcePath.startsWith('~')) {
const untildifiedResource = resourcePath.replace(/^~/, home);
const untildifiedPath = new Path(untildifiedResource);
const isWindows = untildifiedPath.root && Path.isDrive(untildifiedPath.root.base);
if (!isWindows && home && untildifiedResource.startsWith(`${home}`)) {
return untildifiedResource;
}
}
return resourcePath;
}

readonly isAbsolute: boolean;
readonly isRoot: boolean;
readonly root: Path | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,22 @@
********************************************************************************/

import { ContainerModule } from '@theia/core/shared/inversify';
import { LocationListRenderer, LocationListRendererFactory, LocationListRendererOptions } from '../location';
import { DefaultFileDialogService, FileDialogService } from './file-dialog-service';

import { FileDialogTreeFiltersRenderer, FileDialogTreeFiltersRendererFactory, FileDialogTreeFiltersRendererOptions } from './file-dialog-tree-filters-renderer';
export default new ContainerModule(bind => {
bind(DefaultFileDialogService).toSelf().inSingletonScope();
bind(FileDialogService).toService(DefaultFileDialogService);
bind(LocationListRendererFactory).toFactory(context => (options: LocationListRendererOptions) => {
const childContainer = context.container.createChild();
childContainer.bind(LocationListRendererOptions).toConstantValue(options);
childContainer.bind(LocationListRenderer).toSelf().inSingletonScope();
return childContainer.get(LocationListRenderer);
});
bind(FileDialogTreeFiltersRendererFactory).toFactory(context => (options: FileDialogTreeFiltersRendererOptions) => {
const childContainer = context.container.createChild();
childContainer.bind(FileDialogTreeFiltersRendererOptions).toConstantValue(options);
childContainer.bind(FileDialogTreeFiltersRenderer).toSelf().inSingletonScope();
return childContainer.get(FileDialogTreeFiltersRenderer);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { ReactRenderer } from '@theia/core/lib/browser/widgets/react-renderer';
import { FileDialogTree } from './file-dialog-tree';
import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';

export const FILE_TREE_FILTERS_LIST_CLASS = 'theia-FileTreeFiltersList';

Expand All @@ -34,16 +35,31 @@ export class FileDialogTreeFilters {
[name: string]: string[];
}

export const FileDialogTreeFiltersRendererFactory = Symbol('FileDialogTreeFiltersRendererFactory');
export interface FileDialogTreeFiltersRendererFactory {
(options: FileDialogTreeFiltersRendererOptions): FileDialogTreeFiltersRenderer;
}

export const FileDialogTreeFiltersRendererOptions = Symbol('FileDialogTreeFiltersRendererOptions');
export interface FileDialogTreeFiltersRendererOptions {
suppliedFilters: FileDialogTreeFilters;
fileDialogTree: FileDialogTree;
}

@injectable()
export class FileDialogTreeFiltersRenderer extends ReactRenderer {

readonly appliedFilters: FileDialogTreeFilters;
readonly suppliedFilters: FileDialogTreeFilters;
readonly fileDialogTree: FileDialogTree;

constructor(
readonly suppliedFilters: FileDialogTreeFilters,
readonly fileDialogTree: FileDialogTree
@inject(FileDialogTreeFiltersRendererOptions) readonly options: FileDialogTreeFiltersRendererOptions
) {
super();
this.appliedFilters = { 'All Files': [], ...suppliedFilters, };
this.suppliedFilters = options.suppliedFilters;
this.fileDialogTree = options.fileDialogTree;
this.appliedFilters = { 'All Files': [], ...this.suppliedFilters, };
}

protected readonly handleFilterChanged = (e: React.ChangeEvent<HTMLSelectElement>) => this.onFilterChanged(e);
Expand Down
79 changes: 39 additions & 40 deletions packages/filesystem/src/browser/file-dialog/file-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,18 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable, inject } from '@theia/core/shared/inversify';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { Disposable, MaybeArray } from '@theia/core/lib/common';
import { Key, LabelProvider } from '@theia/core/lib/browser';
import { AbstractDialog, DialogProps, setEnabled, createIconButton, Widget } from '@theia/core/lib/browser';
import { FileStatNode } from '../file-tree';
import { LocationListRenderer } from '../location';
import { LocationListRenderer, LocationListRendererFactory } from '../location';
import { FileDialogModel } from './file-dialog-model';
import { FileDialogWidget } from './file-dialog-widget';
import { FileDialogTreeFiltersRenderer, FileDialogTreeFilters } from './file-dialog-tree-filters-renderer';
import { FileDialogTreeFiltersRenderer, FileDialogTreeFilters, FileDialogTreeFiltersRendererFactory } from './file-dialog-tree-filters-renderer';
import URI from '@theia/core/lib/common/uri';
import { Panel } from '@theia/core/shared/@phosphor/widgets';
import { FileService } from '../file-service';

export const OpenFileDialogFactory = Symbol('OpenFileDialogFactory');
export interface OpenFileDialogFactory {
Expand Down Expand Up @@ -116,20 +115,26 @@ export class SaveFileDialogProps extends FileDialogProps {

export abstract class FileDialog<T> extends AbstractDialog<T> {

protected readonly back: HTMLSpanElement;
protected readonly forward: HTMLSpanElement;
protected readonly home: HTMLSpanElement;
protected readonly up: HTMLSpanElement;
protected readonly locationListRenderer: LocationListRenderer;
protected readonly treeFiltersRenderer: FileDialogTreeFiltersRenderer | undefined;
protected readonly treePanel: Panel;
protected back: HTMLSpanElement;
protected forward: HTMLSpanElement;
protected home: HTMLSpanElement;
protected up: HTMLSpanElement;
protected locationListRenderer: LocationListRenderer;
protected treeFiltersRenderer: FileDialogTreeFiltersRenderer | undefined;
protected treePanel: Panel;

@inject(FileDialogWidget) readonly widget: FileDialogWidget;
@inject(LocationListRendererFactory) readonly locationListFactory: LocationListRendererFactory;
@inject(FileDialogTreeFiltersRendererFactory) readonly treeFiltersFactory: FileDialogTreeFiltersRendererFactory;

constructor(
@inject(FileDialogProps) readonly props: FileDialogProps,
@inject(FileDialogWidget) readonly widget: FileDialogWidget,
@inject(FileService) readonly fileService: FileService
@inject(FileDialogProps) readonly props: FileDialogProps
) {
super(props);
}

@postConstruct()
init(): void {
this.treePanel = new Panel();
this.treePanel.addWidget(this.widget);
this.toDispose.push(this.treePanel);
Expand All @@ -155,30 +160,20 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
this.up.title = 'Navigate Up One Directory';

const locationListRendererHost = document.createElement('div');
this.locationListRenderer = this.createLocationListRenderer(locationListRendererHost);
this.locationListRenderer = this.locationListFactory({ model: this.model, host: locationListRendererHost });
this.toDispose.push(this.locationListRenderer);
this.locationListRenderer.host.classList.add(NAVIGATION_LOCATION_LIST_PANEL_CLASS);
navigationPanel.appendChild(this.locationListRenderer.host);

this.treeFiltersRenderer = this.createFileTreeFiltersRenderer();
if (this.props.filters) {
this.treeFiltersRenderer = this.treeFiltersFactory({ suppliedFilters: this.props.filters, fileDialogTree: this.widget.model.tree });
}
}

get model(): FileDialogModel {
return this.widget.model;
}

protected createLocationListRenderer(host?: HTMLElement): LocationListRenderer {
return new LocationListRenderer(this.model, this.fileService, host);
}

protected createFileTreeFiltersRenderer(): FileDialogTreeFiltersRenderer | undefined {
if (this.props.filters) {
return new FileDialogTreeFiltersRenderer(this.props.filters, this.widget.model.tree);
}

return undefined;
}

protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
setEnabled(this.back, this.model.canNavigateBackward());
Expand Down Expand Up @@ -290,12 +285,14 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
@injectable()
export class OpenFileDialog extends FileDialog<MaybeArray<FileStatNode>> {

constructor(
@inject(OpenFileDialogProps) readonly props: OpenFileDialogProps,
@inject(FileDialogWidget) readonly widget: FileDialogWidget,
@inject(FileService) readonly fileService: FileService
) {
super(props, widget, fileService);
constructor(@inject(OpenFileDialogProps) readonly props: OpenFileDialogProps) {
super(props);
}

@postConstruct()
init(): void {
super.init();
const { props } = this;
if (props.canSelectFiles !== undefined) {
this.widget.disableFileSelection = !props.canSelectFiles;
}
Expand Down Expand Up @@ -340,12 +337,14 @@ export class SaveFileDialog extends FileDialog<URI | undefined> {
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;

constructor(
@inject(SaveFileDialogProps) readonly props: SaveFileDialogProps,
@inject(FileDialogWidget) readonly widget: FileDialogWidget,
@inject(FileService) readonly fileService: FileService
) {
super(props, widget, fileService);
constructor(@inject(SaveFileDialogProps) readonly props: SaveFileDialogProps) {
super(props);
}

@postConstruct()
init(): void {
super.init();
const { widget } = this;
widget.addClass(SAVE_DIALOG_CLASS);
}

Expand Down
Loading