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

open local workspaces, files and folders in remote #14607

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ import { injectable } from 'inversify';
import { Endpoint } from '../../browser/endpoint';
import { WebSocketConnectionSource } from '../../browser/messaging/ws-connection-source';

export const LOCAL_PORT_PARAM = 'localPort';
export function getLocalPort(): string | undefined {
const params = new URLSearchParams(location.search);
return params.get('localPort') ?? undefined;
return params.get(LOCAL_PORT_PARAM) ?? undefined;
}

export const CURRENT_PORT_PARAM = 'port';
export function getCurrentPort(): string | undefined {
const params = new URLSearchParams(location.search);
return params.get('port') ?? undefined;
return params.get(CURRENT_PORT_PARAM) ?? undefined;
}

@injectable()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import { inject, injectable } from '@theia/core/shared/inversify';
import { AbstractRemoteRegistryContribution, RemoteRegistry } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
import { DevContainerFile, LastContainerInfo, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
import { RemotePreferences } from '@theia/remote/lib/electron-browser/remote-preferences';
import { WorkspaceStorageService } from '@theia/workspace/lib/browser/workspace-storage-service';
import { Command, MaybePromise, QuickInputService, URI } from '@theia/core';
import { WorkspaceInput, WorkspaceOpenHandlerContribution, WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ContainerOutputProvider } from './container-output-provider';
import { WorkspaceServer } from '@theia/workspace/lib/common';
import { DEV_CONTAINER_PATH_QUERY, DEV_CONTAINER_WORKSPACE_SCHEME } from '../electron-common/dev-container-workspaces';
import { LocalStorageService, StorageService } from '@theia/core/lib/browser';

export namespace RemoteContainerCommands {
export const REOPEN_IN_CONTAINER = Command.toLocalizedCommand({
Expand All @@ -43,8 +43,8 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
@inject(RemotePreferences)
protected readonly remotePreferences: RemotePreferences;

@inject(WorkspaceStorageService)
protected readonly workspaceStorageService: WorkspaceStorageService;
@inject(LocalStorageService)
protected readonly storageService: StorageService;

@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
Expand Down Expand Up @@ -105,7 +105,7 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr

async doOpenInContainer(devcontainerFile: DevContainerFile, workspaceUri?: string): Promise<void> {
const lastContainerInfoKey = `${LAST_USED_CONTAINER}:${devcontainerFile.path}`;
const lastContainerInfo = await this.workspaceStorageService.getData<LastContainerInfo | undefined>(lastContainerInfoKey);
const lastContainerInfo = await this.storageService.getData<LastContainerInfo | undefined>(lastContainerInfoKey);

this.containerOutputProvider.openChannel();

Expand All @@ -116,7 +116,7 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
workspaceUri
});

this.workspaceStorageService.setData<LastContainerInfo>(lastContainerInfoKey, {
this.storageService.setData<LastContainerInfo>(lastContainerInfoKey, {
id: connectionResult.containerId,
lastUsed: Date.now()
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class DefaultFileDialogService implements FileDialogService {
const value = await dialog.open();
if (value) {
if (!Array.isArray(value)) {
return value.uri;
return props.fileScheme ? value.uri.withScheme(props.fileScheme) : value.uri;
}
return value.map(node => node.uri);
}
Expand Down
23 changes: 23 additions & 0 deletions packages/filesystem/src/browser/file-dialog/file-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export const FILENAME_TEXTFIELD_CLASS = 'theia-FileNameTextField';
export const CONTROL_PANEL_CLASS = 'theia-ControlPanel';
export const TOOLBAR_ITEM_TRANSFORM_TIMEOUT = 100;

export interface AdditionalButtonDefinition<T> {
label: string;
onClick: (resolve: (v: T | undefined) => void, reject: (v: unknown) => void) => void;
}
export class FileDialogProps extends DialogProps {

/**
Expand All @@ -78,6 +82,16 @@ export class FileDialogProps extends DialogProps {
*/
modal?: boolean;

/**
* scheme of the fileUri. Defaults to `file`.
*/
fileScheme?: string;

/**
* Additional buttons to show beside the close and accept buttons.
*/
additionalButtons?: AdditionalButtonDefinition<unknown>[];

}

@injectable()
Expand Down Expand Up @@ -180,6 +194,15 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
this.hiddenFilesToggleRenderer = this.hiddenFilesToggleFactory(this.widget.model.tree);
this.contentNode.appendChild(this.hiddenFilesToggleRenderer.host);

this.props.additionalButtons?.forEach(({ label, onClick }) => {
const button = this.appendButton(label, false);
button.onclick = () => {
if (this.resolve && this.reject) {
onClick(this.resolve, this.reject);
}
};
});

if (this.props.filters) {
this.treeFiltersRenderer = this.treeFiltersFactory({ suppliedFilters: this.props.filters, fileDialogTree: this.widget.model.tree });
const filters = Object.keys(this.props.filters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ export class ElectronFileDialogService extends DefaultFileDialogService {
return undefined;
}

const uris = filePaths.map(path => FileUri.create(path));
const uris = filePaths.map(path => {
let uri = FileUri.create(path);
if (props.fileScheme) {
uri = uri.withScheme(props.fileScheme);
}
return uri;
});
const canAccess = await this.canRead(uris);
const result = canAccess ? uris.length === 1 ? uris[0] : uris : undefined;
return result;
Expand All @@ -70,6 +76,9 @@ export class ElectronFileDialogService extends DefaultFileDialogService {
}

const uri = FileUri.create(filePath);
if (props.fileScheme) {
uri.withScheme(props.fileScheme);
}
const exists = await this.fileService.exists(uri);
if (!exists) {
return uri;
Expand Down
1 change: 1 addition & 0 deletions packages/remote/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"@theia/core": "1.56.0",
"@theia/filesystem": "1.56.0",
"@theia/userstorage": "1.56.0",
"@theia/workspace": "1.56.0",
"archiver": "^5.3.1",
"decompress": "^4.2.1",
"decompress-tar": "^4.0.0",
Expand Down
37 changes: 36 additions & 1 deletion packages/remote/src/electron-browser/local-backend-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,53 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { RpcProxy } from '@theia/core';
import { RpcProxy, URI } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { RemoteFileSystemProvider, RemoteFileSystemServer } from '@theia/filesystem/lib/common/remote-file-system-provider';
import { FileService, FileServiceContribution } from '@theia/filesystem/lib/browser/file-service';

export const LocalEnvVariablesServer = Symbol('LocalEnviromentVariableServer');
export const LocalRemoteFileSytemServer = Symbol('LocalRemoteFileSytemServer');

export const LOCAL_FILE_SCHEME = 'localfile';
/**
* provide file access to local files while connected to a remote workspace or dev container.
*/
@injectable()
export class LocalRemoteFileSystemProvider extends RemoteFileSystemProvider {
@inject(LocalRemoteFileSytemServer)
protected override readonly server: RpcProxy<RemoteFileSystemServer>;

}

@injectable()
export class LocalRemoteFileSystemContribution implements FileServiceContribution {
@inject(LocalRemoteFileSystemProvider)
protected readonly provider: LocalRemoteFileSystemProvider;

registerFileSystemProviders(service: FileService): void {
service.onWillActivateFileSystemProvider(event => {
if (event.scheme === LOCAL_FILE_SCHEME) {
service.registerProvider(LOCAL_FILE_SCHEME, new Proxy(this.provider, {
get(target, prop): unknown {
const member = target[prop as keyof LocalRemoteFileSystemProvider];

if (typeof member === 'function') {
return (...args: unknown[]) => {
const mappedArgs = args.map(arg => {
if (arg instanceof URI && arg.scheme === LOCAL_FILE_SCHEME) {
return arg.withScheme('file');
}
return arg;
});
return member.apply(target, mappedArgs);
};
}
return member;
}
}));
}
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { MaybeArray, URI } from '@theia/core';
import { MaybeArray, URI, nls } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { OpenFileDialogProps, SaveFileDialogProps } from '@theia/filesystem/lib/browser/file-dialog';
import { AdditionalButtonDefinition, OpenFileDialogProps, SaveFileDialogProps } from '@theia/filesystem/lib/browser/file-dialog';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { DefaultFileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service';
import { ElectronFileDialogService } from '@theia/filesystem/lib/electron-browser/file-dialog/electron-file-dialog-service';
import { RemoteService } from './remote-service';
import { LOCAL_FILE_SCHEME } from './local-backend-services';

@injectable()
export class RemoteElectronFileDialogService extends ElectronFileDialogService {
Expand All @@ -31,6 +32,7 @@ export class RemoteElectronFileDialogService extends ElectronFileDialogService {
override showOpenDialog(props: OpenFileDialogProps, folder?: FileStat | undefined): Promise<URI | undefined>;
override showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise<MaybeArray<URI> | undefined> | Promise<URI | undefined> {
if (this.remoteService.isConnected()) {
this.addLocalFilesButton(props);
return DefaultFileDialogService.prototype.showOpenDialog.call(this, props, folder);
} else {
return super.showOpenDialog(props, folder);
Expand All @@ -44,4 +46,21 @@ export class RemoteElectronFileDialogService extends ElectronFileDialogService {
return super.showSaveDialog(props, folder);
}
}

protected addLocalFilesButton(props: OpenFileDialogProps): void {
const localFilesButton: AdditionalButtonDefinition<{ uri: URI }> = {
label: nls.localizeByDefault('Show Local'),
onClick: async resolve => {
const localFile = await super.showOpenDialog({ ...props, fileScheme: LOCAL_FILE_SCHEME });
if (localFile) {
resolve({ uri: localFile });
}
}
};
if (props.additionalButtons) {
props.additionalButtons.push(localFilesButton);
} else {
props.additionalButtons = [localFilesButton];
}
}
}
11 changes: 10 additions & 1 deletion packages/remote/src/electron-browser/remote-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ import '../../src/electron-browser/style/port-forwarding-widget.css';
import { UserStorageContribution } from '@theia/userstorage/lib/browser/user-storage-contribution';
import { RemoteUserStorageContribution } from './remote-user-storage-provider';
import { remoteFileSystemPath, RemoteFileSystemProxyFactory, RemoteFileSystemServer } from '@theia/filesystem/lib/common/remote-file-system-provider';
import { LocalEnvVariablesServer, LocalRemoteFileSystemProvider, LocalRemoteFileSytemServer } from './local-backend-services';
import { LocalEnvVariablesServer, LocalRemoteFileSystemContribution, LocalRemoteFileSystemProvider, LocalRemoteFileSytemServer } from './local-backend-services';
import { envVariablesPath, EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { RemoteWorkspaceService } from './remote-workspace-service';
import { FileServiceContribution } from '@theia/filesystem/lib/browser/file-service';

export default new ContainerModule((bind, _, __, rebind) => {
bind(RemoteFrontendContribution).toSelf().inSingletonScope();
Expand Down Expand Up @@ -81,5 +84,11 @@ export default new ContainerModule((bind, _, __, rebind) => {
bind(LocalRemoteFileSystemProvider).toSelf().inSingletonScope();
rebind(UserStorageContribution).to(RemoteUserStorageContribution);

if (isRemote) {
rebind(WorkspaceService).to(RemoteWorkspaceService).inSingletonScope();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: This should rather be a contribution, instead of a rebinding. See also the existing rebinding in the collaboration package

rebind(WorkspaceService).toService(CollaborationWorkspaceService);
and the related issue #14080.

bind(LocalRemoteFileSystemContribution).toSelf().inSingletonScope();
bind(FileServiceContribution).toService(LocalRemoteFileSystemContribution);
}

});

66 changes: 66 additions & 0 deletions packages/remote/src/electron-browser/remote-workspace-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// *****************************************************************************
// Copyright (C) 2024 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************

import { URI } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceInput, WorkspaceService } from '@theia/workspace/lib/browser';
import { LOCAL_FILE_SCHEME } from './local-backend-services';
import { CURRENT_PORT_PARAM, LOCAL_PORT_PARAM, getCurrentPort, getLocalPort } from '@theia/core/lib/electron-browser/messaging/electron-local-ws-connection-source';
import { RemoteStatusService } from '../electron-common/remote-status-service';

@injectable()
export class RemoteWorkspaceService extends WorkspaceService {

@inject(RemoteStatusService)
protected readonly remoteStatusService: RemoteStatusService;

override canHandle(uri: URI): boolean {
return super.canHandle(uri) || uri.scheme === LOCAL_FILE_SCHEME;
}

override async recentWorkspaces(): Promise<string[]> {
const workspaces = await super.recentWorkspaces();
return workspaces.map(workspace => {
const uri = new URI(workspace);
if (uri.scheme === 'file') {
return uri.withScheme(LOCAL_FILE_SCHEME).toString();
}
// possible check as well if a remote/dev-container worksace is from the connected remote and therefore change it to the 'file' scheme
return workspace;
});
}

protected override reloadWindow(options?: WorkspaceInput): void {
const currentPort = getCurrentPort();
const url = this.getModifiedUrl();
history.replaceState(undefined, '', url.toString());
this.remoteStatusService.connectionClosed(parseInt(currentPort ?? '0'));
super.reloadWindow(options);
}

protected override openNewWindow(workspacePath: string, options?: WorkspaceInput): void {
const url = this.getModifiedUrl();
url.hash = encodeURI(workspacePath);
this.windowService.openNewWindow(url.toString());
}

protected getModifiedUrl(): URL {
const url = new URL(window.location.href);
url.searchParams.set(CURRENT_PORT_PARAM, getLocalPort() ?? '');
url.searchParams.delete(LOCAL_PORT_PARAM);
return url;
}
}
3 changes: 3 additions & 0 deletions packages/remote/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
},
{
"path": "../userstorage"
},
{
"path": "../workspace"
}
]
}
9 changes: 4 additions & 5 deletions packages/workspace/src/browser/workspace-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { CommandContribution, MenuContribution, bindContributionProvider } from '@theia/core/lib/common';
import { WebSocketConnectionProvider, FrontendApplicationContribution, KeybindingContribution } from '@theia/core/lib/browser';
import { FrontendApplicationContribution, KeybindingContribution, ServiceConnectionProvider } from '@theia/core/lib/browser';
import {
OpenFileDialogFactory,
SaveFileDialogFactory,
Expand Down Expand Up @@ -65,10 +65,9 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
bind(FrontendApplicationContribution).toService(WorkspaceService);

bind(CanonicalUriService).toSelf().inSingletonScope();
bind(WorkspaceServer).toDynamicValue(ctx => {
const provider = ctx.container.get(WebSocketConnectionProvider);
return provider.createProxy<WorkspaceServer>(workspacePath);
}).inSingletonScope();
bind(WorkspaceServer).toDynamicValue(ctx =>
ServiceConnectionProvider.createLocalProxy<WorkspaceServer>(ctx.container, workspacePath)
).inSingletonScope();

bind(WorkspaceFrontendContribution).toSelf().inSingletonScope();
for (const identifier of [FrontendApplicationContribution, CommandContribution, KeybindingContribution, MenuContribution]) {
Expand Down
2 changes: 1 addition & 1 deletion packages/workspace/src/browser/workspace-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ export class WorkspaceService implements FrontendApplicationContribution, Worksp
throw new Error(`Could not find a handler to open the workspace with uri ${uri.toString()}.`);
}

async canHandle(uri: URI): Promise<boolean> {
canHandle(uri: URI): boolean {
return uri.scheme === 'file';
}

Expand Down
Loading