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

Remove dependency on code/browser/workbench/workbench #137

Merged
merged 3 commits into from
Sep 4, 2024
Merged
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
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/out/index.js",
"program": "${workspaceFolder}/out/server/index.js",
"args": [
"--browserType=chromium",
"--extensionDevelopmentPath=${workspaceFolder}/sample",
@@ -58,7 +58,7 @@
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/out/index.js",
"program": "${workspaceFolder}/out/server/index.js",
"args": [
"--browserType=chromium",
"--extensionDevelopmentPath=${workspaceFolder}/sample",
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@
"version": "0.0.57",
"scripts": {
"install-extensions": "npm i --prefix=fs-provider && npm i --prefix=sample",
"compile": "tsc -p ./ && npm run compile-fs-provider",
"watch": "tsc -w -p ./",
"compile": "tsc -b ./ && npm run compile-fs-provider",
"watch": "tsc -b -w ./",
"prepack": "npm run compile",
"test": "eslint src && tsc --noEmit",
"preversion": "npm test",
@@ -15,9 +15,9 @@
"sample-tests": "npm run compile && npm run compile-sample && node . --extensionDevelopmentPath=sample --extensionTestsPath=sample/dist/web/test/suite/index.js --headless=true sample/test-workspace",
"empty": "npm run compile && node ."
},
"main": "./out/index.js",
"main": "./out/server/index.js",
"bin": {
"vscode-test-web": "./out/index.js"
"vscode-test-web": "./out/server/index.js"
},
"engines": {
"node": ">=16"
272 changes: 272 additions & 0 deletions src/browser/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
///<amd-module name='vscode-web-browser-main'/>

import { create, IWorkspaceProvider, IWorkbenchConstructionOptions, UriComponents, IWorkspace, URI, IURLCallbackProvider, Emitter, IDisposable} from './workbench.api';

class WorkspaceProvider implements IWorkspaceProvider {

private static QUERY_PARAM_EMPTY_WINDOW = 'ew';
private static QUERY_PARAM_FOLDER = 'folder';
private static QUERY_PARAM_WORKSPACE = 'workspace';

private static QUERY_PARAM_PAYLOAD = 'payload';

static create(config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents }) {
let foundWorkspace = false;
let workspace: IWorkspace;
let payload = Object.create(null);

const query = new URL(document.location.href).searchParams;
query.forEach((value, key) => {
switch (key) {

// Folder
case WorkspaceProvider.QUERY_PARAM_FOLDER:
workspace = { folderUri: URI.parse(value) };
foundWorkspace = true;
break;

// Workspace
case WorkspaceProvider.QUERY_PARAM_WORKSPACE:
workspace = { workspaceUri: URI.parse(value) };
foundWorkspace = true;
break;

// Empty
case WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW:
workspace = undefined;
foundWorkspace = true;
break;

// Payload
case WorkspaceProvider.QUERY_PARAM_PAYLOAD:
try {
payload = JSON.parse(value);
} catch (error) {
console.error(error); // possible invalid JSON
}
break;
}
});

// If no workspace is provided through the URL, check for config
// attribute from server
if (!foundWorkspace) {
if (config.folderUri) {
workspace = { folderUri: URI.revive(config.folderUri) };
} else if (config.workspaceUri) {
workspace = { workspaceUri: URI.revive(config.workspaceUri) };
}
}

return new WorkspaceProvider(workspace, payload);
}

readonly trusted = true;

private constructor(
readonly workspace: IWorkspace,
readonly payload: object,
) {
}

async open(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): Promise<boolean> {
if (options?.reuse && !options.payload && this.isSame(this.workspace, workspace)) {
return true; // return early if workspace and environment is not changing and we are reusing window
}

const targetHref = this.createTargetUrl(workspace, options);
if (targetHref) {
if (options?.reuse) {
window.location.href = targetHref;
return true;
} else {
return !!window.open(targetHref);
}
}
return false;
}

private createTargetUrl(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): string | undefined {

// Empty
let targetHref: string | undefined = undefined;
if (!workspace) {
targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW}=true`;
}

// Folder
else if ('folderUri' in workspace) {
const queryParamFolder = encodeURIComponent(workspace.folderUri.toString(true));
targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${queryParamFolder}`;
}

// Workspace
else if ('workspaceUri' in workspace) {
const queryParamWorkspace = encodeURIComponent(workspace.workspaceUri.toString(true));
targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${queryParamWorkspace}`;
}

// Append payload if any
if (options?.payload) {
targetHref += `&${WorkspaceProvider.QUERY_PARAM_PAYLOAD}=${encodeURIComponent(JSON.stringify(options.payload))}`;
}

return targetHref;
}

private isSame(workspaceA: IWorkspace, workspaceB: IWorkspace): boolean {
if (!workspaceA || !workspaceB) {
return workspaceA === workspaceB; // both empty
}

if ('folderUri' in workspaceA && 'folderUri' in workspaceB) {
return isEqual(workspaceA.folderUri, workspaceB.folderUri); // same workspace
}

if ('workspaceUri' in workspaceA && 'workspaceUri' in workspaceB) {
return isEqual(workspaceA.workspaceUri, workspaceB.workspaceUri); // same workspace
}

return false;
}

}

class LocalStorageURLCallbackProvider implements IURLCallbackProvider, IDisposable {

private static REQUEST_ID = 0;

private static QUERY_KEYS: ('scheme' | 'authority' | 'path' | 'query' | 'fragment')[] = [
'scheme',
'authority',
'path',
'query',
'fragment'
];

private readonly _onCallback = new Emitter<URI>();
readonly onCallback = this._onCallback.event;

private pendingCallbacks = new Set<number>();
private lastTimeChecked = Date.now();
private checkCallbacksTimeout: unknown | undefined = undefined;
private onDidChangeLocalStorageDisposable: IDisposable | undefined;

constructor(private readonly _callbackRoute: string) {
}

create(options: Partial<UriComponents> = {}): URI {
const id = ++LocalStorageURLCallbackProvider.REQUEST_ID;
const queryParams: string[] = [`vscode-reqid=${id}`];

for (const key of LocalStorageURLCallbackProvider.QUERY_KEYS) {
const value = options[key];

if (value) {
queryParams.push(`vscode-${key}=${encodeURIComponent(value)}`);
}
}

// TODO@joao remove eventually
// https://github.com/microsoft/vscode-dev/issues/62
// https://github.com/microsoft/vscode/blob/159479eb5ae451a66b5dac3c12d564f32f454796/extensions/github-authentication/src/githubServer.ts#L50-L50
if (!(options.authority === 'vscode.github-authentication' && options.path === '/dummy')) {
const key = `vscode-web.url-callbacks[${id}]`;
localStorage.removeItem(key);

this.pendingCallbacks.add(id);
this.startListening();
}

return URI.parse(window.location.href).with({ path: this._callbackRoute, query: queryParams.join('&') });
}

private startListening(): void {
if (this.onDidChangeLocalStorageDisposable) {
return;
}

const fn = () => this.onDidChangeLocalStorage();
window.addEventListener('storage', fn);
this.onDidChangeLocalStorageDisposable = { dispose: () => window.removeEventListener('storage', fn) };
}

private stopListening(): void {
this.onDidChangeLocalStorageDisposable?.dispose();
this.onDidChangeLocalStorageDisposable = undefined;
}

// this fires every time local storage changes, but we
// don't want to check more often than once a second
private async onDidChangeLocalStorage(): Promise<void> {
const ellapsed = Date.now() - this.lastTimeChecked;

if (ellapsed > 1000) {
this.checkCallbacks();
} else if (this.checkCallbacksTimeout === undefined) {
this.checkCallbacksTimeout = setTimeout(() => {
this.checkCallbacksTimeout = undefined;
this.checkCallbacks();
}, 1000 - ellapsed);
}
}

private checkCallbacks(): void {
let pendingCallbacks: Set<number> | undefined;

for (const id of this.pendingCallbacks) {
const key = `vscode-web.url-callbacks[${id}]`;
const result = localStorage.getItem(key);

if (result !== null) {
try {
this._onCallback.fire(URI.revive(JSON.parse(result)));
} catch (error) {
console.error(error);
}

pendingCallbacks = pendingCallbacks ?? new Set(this.pendingCallbacks);
pendingCallbacks.delete(id);
localStorage.removeItem(key);
}
}

if (pendingCallbacks) {
this.pendingCallbacks = pendingCallbacks;

if (this.pendingCallbacks.size === 0) {
this.stopListening();
}
}

this.lastTimeChecked = Date.now();
}

dispose(): void {
this._onCallback.dispose();
}
}

function isEqual(a: UriComponents, b: UriComponents): boolean {
return a.scheme === b.scheme && a.authority === b.authority && a.path === b.path;
}

(function () {
const configElement = window.document.getElementById('vscode-workbench-web-configuration');
const configElementAttribute = configElement ? configElement.getAttribute('data-settings') : undefined;
if (!configElement || !configElementAttribute) {
throw new Error('Missing web configuration element');
}
const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents; callbackRoute: string } = JSON.parse(configElementAttribute);

create(window.document.body, {
...config,
workspaceProvider: WorkspaceProvider.create(config),
urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute)
});

})();
22 changes: 22 additions & 0 deletions src/browser/tsconfig-amd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "AMD",
"lib": [
"ES2022",
"DOM",
],
"outDir": "../../out/browser/amd",
"declaration": true,
"strict": true,
"noImplicitAny": false,
"noImplicitThis": true,
"noUnusedLocals": true,
"alwaysStrict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": false,
"newLine": "lf",
"removeComments": true
}
}
22 changes: 22 additions & 0 deletions src/browser/tsconfig-esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022",
"DOM",
],
"outDir": "../../out/browser/esm",
"declaration": true,
"strict": true,
"noImplicitAny": false,
"noImplicitThis": true,
"noUnusedLocals": true,
"alwaysStrict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": false,
"newLine": "lf",
"removeComments": true
}
}
Loading