From c469455184f363671bb4fb69fae24ef737a5b606 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 6 Sep 2023 19:37:00 +0200 Subject: [PATCH] Improves playground --- website/package.json | 2 +- website/src/shared.ts | 2 +- .../website/pages/playground/BisectModel.ts | 161 +++++ .../website/pages/playground/LocationModel.ts | 211 +++++++ .../pages/playground/PlaygroundModel.ts | 556 ++---------------- .../pages/playground/PlaygroundPage.tsx | 5 +- .../playground/PlaygroundPageContent.tsx | 158 +++-- .../src/website/pages/playground/Preview.tsx | 68 ++- .../src/website/pages/playground/Source.ts | 107 ++++ website/src/website/pages/playground/utils.ts | 29 + website/src/website/style.scss | 3 + website/yarn.lock | 8 +- 12 files changed, 734 insertions(+), 576 deletions(-) create mode 100644 website/src/website/pages/playground/BisectModel.ts create mode 100644 website/src/website/pages/playground/LocationModel.ts create mode 100644 website/src/website/pages/playground/Source.ts create mode 100644 website/src/website/pages/playground/utils.ts diff --git a/website/package.json b/website/package.json index 99b36bd34c..657513c6cd 100644 --- a/website/package.json +++ b/website/package.json @@ -24,7 +24,7 @@ "mini-css-extract-plugin": "^2.6.1", "mobx": "^5.15.4", "mobx-react": "^6.2.2", - "monaco-editor": "^0.41.0", + "monaco-editor": "^0.42.0-dev-20230906", "react": "^17.0.2", "react-bootstrap": "^2.4.0", "react-dom": "^17.0.2", diff --git a/website/src/shared.ts b/website/src/shared.ts index 807b30b747..0165733fc2 100644 --- a/website/src/shared.ts +++ b/website/src/shared.ts @@ -32,6 +32,6 @@ export interface IPlaygroundProject { } export interface IPreviewState extends IPlaygroundProject { - key: number; + reloadKey: number; monacoSetup: IMonacoSetup; } diff --git a/website/src/website/pages/playground/BisectModel.ts b/website/src/website/pages/playground/BisectModel.ts new file mode 100644 index 0000000000..a2f400d9a1 --- /dev/null +++ b/website/src/website/pages/playground/BisectModel.ts @@ -0,0 +1,161 @@ +import { action, ObservableMap } from "mobx"; +import { + getNpmVersions, + getNpmVersionsSync, + getVsCodeCommitId, +} from "./getNpmVersionsSync"; +import { PlaygroundModel } from "./PlaygroundModel"; +import { findLastIndex } from "./utils"; + +export class BisectModel { + private readonly map = new ObservableMap(); + + constructor(private readonly model: PlaygroundModel) {} + + public getState(version: string): boolean | undefined { + return this.map.get(version); + } + + public get isActive() { + return [...this.map.values()].some((e) => e !== undefined); + } + + public reset(): void { + this.map.clear(); + } + + public async toggleState(version: string, state: boolean): Promise { + const currentState = this.getState(version); + await this.setState( + version, + currentState === state ? undefined : state + ); + } + + @action + public async setState( + version: string, + state: boolean | undefined + ): Promise { + if (state === undefined) { + this.map.delete(version); + } else { + this.map.set(version, state); + } + + const nextVersion = await this.getNextVersion(); + if (!nextVersion) { + return; + } + this.model.settings.setSettings({ + ...this.model.settings.settings, + npmVersion: nextVersion, + }); + } + + private get versions() { + return getNpmVersionsSync(undefined); + } + + private get indexOfLastBadVersion() { + return findLastIndex(this.versions, (v) => this.map.get(v) === false); + } + private get indexOfFirstGoodVersion() { + return this.versions.findIndex((v) => this.map.get(v) === true); + } + + public get steps() { + const indexOfFirstGoodVersion = this.indexOfFirstGoodVersion; + const indexOfLastBadVersion = this.indexOfLastBadVersion; + + if (indexOfFirstGoodVersion === -1 && indexOfLastBadVersion === -1) { + return -1; + } + if (indexOfFirstGoodVersion === -1) { + return Math.ceil( + Math.log2(this.versions.length - indexOfLastBadVersion) + ); + } else if (indexOfLastBadVersion === -1) { + return Math.ceil(Math.log2(indexOfFirstGoodVersion + 1)); + } else { + return Math.ceil( + Math.log2(indexOfFirstGoodVersion - indexOfLastBadVersion) + ); + } + } + + public get isFinished() { + if ( + this.indexOfFirstGoodVersion !== -1 && + this.indexOfLastBadVersion + 1 === this.indexOfFirstGoodVersion + ) { + return true; + } + return false; + } + + public async openGithub() { + const versions = await getNpmVersions(); + const indexOfFirstGoodVersion = + this.indexOfFirstGoodVersion === -1 + ? versions.length - 1 + : this.indexOfFirstGoodVersion; + const indexOfLastBadVersion = + this.indexOfLastBadVersion === -1 ? 0 : this.indexOfLastBadVersion; + const goodCommitId = await getVsCodeCommitId( + versions[indexOfFirstGoodVersion] + ); + const badCommitId = await getVsCodeCommitId( + versions[indexOfLastBadVersion] + ); + window.open( + `https://github.com/microsoft/vscode/compare/${goodCommitId}...${badCommitId}`, + "_blank" + ); + } + + private async getNextVersion(): Promise { + const versions = await getNpmVersions(); + + const indexOfFirstGoodVersion = this.indexOfFirstGoodVersion; + const indexOfLastBadVersion = this.indexOfLastBadVersion; + + if ( + indexOfFirstGoodVersion !== -1 && + indexOfLastBadVersion + 1 === indexOfFirstGoodVersion + ) { + // Finished + return; + } + + if (indexOfLastBadVersion === -1 && indexOfFirstGoodVersion === -1) { + return versions[0]; + } + if (indexOfLastBadVersion === -1) { + // try first (newest) version that hasn't been tested + const indexOfFirstUntestedVersion = versions.findIndex( + (v) => this.map.get(v) === undefined + ); + if (indexOfFirstUntestedVersion === -1) { + return undefined; + } + return versions[indexOfFirstUntestedVersion]; + } + + if (indexOfFirstGoodVersion === -1) { + /*// exponential back off, might be good for recent regressions, but ruins step counter + const candidate = Math.min( + indexOfLastBadVersion * 2 + 1, + versions.length - 1 + );*/ + const candidate = Math.floor( + (indexOfLastBadVersion + versions.length) / 2 + ); + return versions[candidate]; + } + + return versions[ + Math.floor((indexOfLastBadVersion + indexOfFirstGoodVersion) / 2) + ]; + } +} diff --git a/website/src/website/pages/playground/LocationModel.ts b/website/src/website/pages/playground/LocationModel.ts new file mode 100644 index 0000000000..abc96ad18b --- /dev/null +++ b/website/src/website/pages/playground/LocationModel.ts @@ -0,0 +1,211 @@ +import { action, observable } from "mobx"; +import { IPlaygroundProject } from "../../../shared"; +import { monacoEditorVersion } from "../../monacoEditorVersion"; +import { LzmaCompressor } from "../../utils/lzmaCompressor"; +import { + HistoryController, + IHistoryModel, + ILocation, +} from "../../utils/ObservableHistory"; +import { debouncedComputed, Disposable } from "../../utils/utils"; +import { getPlaygroundExamples, PlaygroundExample } from "./playgroundExamples"; +import { Source } from "./Source"; +import { PlaygroundModel } from "./PlaygroundModel"; +import { projectEquals } from "./utils"; + +export class LocationModel implements IHistoryModel { + public readonly dispose = Disposable.fn(); + + private readonly compressor = new LzmaCompressor(); + + private cachedState: + | { state: IPlaygroundProject; hash: string } + | undefined = undefined; + + @observable private _sourceOverride: Source | undefined; + get sourceOverride(): Source | undefined { + return this._sourceOverride; + } + + @observable private _compareWith: Source | undefined; + get compareWith(): Source | undefined { + return this._compareWith; + } + + /** + * This is used to control replace/push state. + * Replace is used if the history id does not change. + */ + @observable historyId: number = 0; + + constructor(private readonly model: PlaygroundModel) { + this.dispose.track( + new HistoryController((initialLocation) => { + this.updateLocation(initialLocation); + return this; + }) + ); + } + + get location(): ILocation { + const source = this._sourceOverride || this.sourceFromSettings; + return { + hashValue: this.computedHashValue.value || this.cachedState?.hash, + searchParams: { + source: source?.sourceToString(), + sourceLanguages: source?.sourceLanguagesToString(), + compareWith: this._compareWith?.sourceToString(), + }, + }; + } + + @action + updateLocation(currentLocation: ILocation): void { + const hashValue = currentLocation.hashValue; + const sourceStr = currentLocation.searchParams.source; + const sourceLanguages = currentLocation.searchParams.sourceLanguages; + const source = + sourceStr || sourceLanguages + ? Source.parse(sourceStr, sourceLanguages) + : undefined; + + if (this.sourceFromSettings?.equals(source)) { + this._sourceOverride = undefined; + } else { + this._sourceOverride = source; + } + + const compareWithStr = currentLocation.searchParams.compareWith; + const compareWith = compareWithStr + ? Source.parse(compareWithStr, undefined) + : undefined; + this._compareWith = compareWith; + + function findExample(hashValue: string): PlaygroundExample | undefined { + if (hashValue.startsWith("example-")) { + hashValue = hashValue.substring("example-".length); + } + return getPlaygroundExamples() + .flatMap((e) => e.examples) + .find((e) => e.id === hashValue); + } + + let example: PlaygroundExample | undefined; + + if (!hashValue) { + this.model.selectedExample = getPlaygroundExamples()[0].examples[0]; + } else if ((example = findExample(hashValue))) { + this.model.selectedExample = example; + } else { + let p: IPlaygroundProject | undefined = undefined; + if (this.cachedState?.hash === hashValue) { + p = this.cachedState.state; + } + if (!p) { + try { + p = + this.compressor.decodeData( + hashValue + ); + } catch (e) { + console.log("Could not deserialize from hash value", e); + } + } + if (p) { + this.cachedState = { state: p, hash: hashValue }; + this.model.setState(p); + } + } + } + + private readonly computedHashValue = debouncedComputed( + 500, + () => ({ + state: this.model.playgroundProject, + selectedExampleProject: this.model.selectedExampleProject, + }), + ({ state, selectedExampleProject }) => { + if ( + selectedExampleProject && + projectEquals(state, selectedExampleProject.project) + ) { + return "example-" + selectedExampleProject.example.id; + } + if ( + this.cachedState && + projectEquals(this.cachedState.state, state) + ) { + return this.cachedState.hash; + } + return this.compressor.encodeData(state); + } + ); + + private get sourceFromSettings(): Source | undefined { + const settings = this.model.settings.settings; + if (settings.monacoSource === "npm") { + return new Source(settings.npmVersion, undefined, undefined); + } else if ( + settings.monacoSource === "independent" && + ((settings.coreSource === "url" && + (settings.languagesSource === "latest" || + settings.languagesSource === "url")) || + (settings.coreSource === "latest" && + settings.languagesSource === "url")) + ) { + return new Source( + undefined, + settings.coreSource === "url" ? settings.coreUrl : undefined, + settings.languagesSource === "latest" + ? undefined + : settings.languagesUrl + ); + } else if (settings.monacoSource === "latest") { + return new Source(monacoEditorVersion, undefined, undefined); + } + return undefined; + } + + @action + exitCompare(): void { + this._compareWith = undefined; + this.historyId++; + } + + @action + disableSourceOverride(): void { + this._sourceOverride = undefined; + this.historyId++; + } + + @action + compareWithLatestDev(): void { + this._compareWith = Source.useLatestDev(); + this.historyId++; + } + + @action + saveCompareWith(): void { + if (this._compareWith) { + this.model.settings.setSettings({ + ...this.model.settings.settings, + ...this._compareWith.toPartialSettings(), + }); + this.historyId++; + this._compareWith = undefined; + this._sourceOverride = undefined; + } + } + + @action + saveSourceOverride(): void { + if (this._sourceOverride) { + this.model.settings.setSettings({ + ...this.model.settings.settings, + ...this._sourceOverride.toPartialSettings(), + }); + this.historyId++; + this._sourceOverride = undefined; + } + } +} diff --git a/website/src/website/pages/playground/PlaygroundModel.ts b/website/src/website/pages/playground/PlaygroundModel.ts index db067c6614..46ea286caf 100644 --- a/website/src/website/pages/playground/PlaygroundModel.ts +++ b/website/src/website/pages/playground/PlaygroundModel.ts @@ -8,7 +8,6 @@ import { autorun, computed, observable, - ObservableMap, reaction, runInAction, } from "mobx"; @@ -18,22 +17,10 @@ import { waitForLoadedMonaco, } from "../../../monaco-loader"; import { IPlaygroundProject, IPreviewState } from "../../../shared"; -import { monacoEditorVersion } from "../../monacoEditorVersion"; import { Debouncer } from "../../utils/Debouncer"; -import { LzmaCompressor } from "../../utils/lzmaCompressor"; -import { - HistoryController, - IHistoryModel, - ILocation, -} from "../../utils/ObservableHistory"; import { ObservablePromise } from "../../utils/ObservablePromise"; -import { debouncedComputed, Disposable } from "../../utils/utils"; -import { - getNpmVersions, - getNpmVersionsSync, - getVsCodeCommitId, -} from "./getNpmVersionsSync"; -import { getPlaygroundExamples, PlaygroundExample } from "./playgroundExamples"; +import { Disposable } from "../../utils/utils"; +import { PlaygroundExample } from "./playgroundExamples"; import { getDefaultSettings, JsonString, @@ -41,6 +28,8 @@ import { SettingsModel, toLoaderConfig, } from "./SettingsModel"; +import { BisectModel } from "./BisectModel"; +import { LocationModel } from "./LocationModel"; export class PlaygroundModel { public readonly dispose = Disposable.fn(); @@ -58,16 +47,18 @@ export class PlaygroundModel { @observable public reloadKey = 0; - public readonly serializer = new StateUrlSerializer(this); + public readonly historyModel = new LocationModel(this); public reload(): void { this.reloadKey++; } - private readonly _previewHandlers = new Set(); + public get previewShouldBeFullScreen(): boolean { + return this.settings.previewFullScreen; + } private _wasEverNonFullScreen = false; - public get wasEverNonFullScreen() { + public get wasEverNonFullScreen(): boolean { if (this._wasEverNonFullScreen) { return true; } @@ -79,7 +70,7 @@ export class PlaygroundModel { @computed.struct get monacoSetup(): IMonacoSetup { - const sourceOverride = this.serializer.sourceOverride; + const sourceOverride = this.historyModel.sourceOverride; if (sourceOverride) { return toLoaderConfig({ ...getDefaultSettings(), @@ -105,10 +96,33 @@ export class PlaygroundModel { return { ...this.playgroundProject, monacoSetup: this.monacoSetup, - key: this.reloadKey, + reloadKey: this.reloadKey, }; } + @observable.ref + private _previewState: IPreviewState | undefined = undefined; + + public readonly getPreviewState = (): IPreviewState | undefined => { + return this._previewState; + }; + + public readonly getCompareWithPreviewState = (): + | IPreviewState + | undefined => { + const previewState = this.getPreviewState(); + if (!previewState) { + return undefined; + } + return { + ...previewState, + monacoSetup: toLoaderConfig({ + ...getDefaultSettings(), + ...this.historyModel.compareWith!.toPartialSettings(), + }), + }; + }; + @observable public settingsDialogModel: SettingsDialogModel | undefined = undefined; @@ -134,6 +148,7 @@ export class PlaygroundModel { example: value, project: p, }; + this.reloadKey++; this.setState(p); }); }); @@ -146,37 +161,37 @@ export class PlaygroundModel { public isDirty = false; constructor() { - let lastState = this.state; + let lastState: IPreviewState | undefined = undefined; this.dispose.track({ dispose: reaction( () => ({ state: this.state }), - ({ state }) => { + () => { + const state = this.state; if (!this.settings.autoReload) { if ( - JSON.stringify(state.monacoSetup) === - JSON.stringify(lastState.monacoSetup) && - state.key === lastState.key + (!lastState || + JSON.stringify(state.monacoSetup) === + JSON.stringify(lastState.monacoSetup)) && + state.reloadKey === (lastState?.reloadKey ?? 0) ) { this.isDirty = true; return; } } - const action = () => { + const updatePreviewState = () => { this.isDirty = false; - lastState = state; - for (const handler of this._previewHandlers) { - handler.handlePreview(state); - } + this._previewState = state; + lastState = this._previewState; }; - if (state.key !== lastState.key) { - action(); // sync update + if (state.reloadKey !== lastState?.reloadKey) { + updatePreviewState(); } else { - this.debouncer.run(action); + this.debouncer.run(updatePreviewState); } }, - { name: "update preview" } + { name: "update preview", fireImmediately: true } ), }); @@ -284,21 +299,13 @@ export class PlaygroundModel { this.css = state.css; } - public setPreviewHandler(handler: IPreviewHandler): monaco.IDisposable { - this._previewHandlers.add(handler); - handler.handlePreview(this.state); - return { - dispose: () => { - this._previewHandlers.delete(handler); - }, - }; - } - public readonly bisectModel = new BisectModel(this); -} -export interface IPreviewHandler { - handlePreview(state: IPreviewState): void; + @action + compareWithLatestDev(): void { + this.settings.previewFullScreen = true; + this.historyModel.compareWithLatestDev(); + } } export class SettingsDialogModel { @@ -316,458 +323,3 @@ export class SettingsDialogModel { this.settings = Object.assign({}, settings); } } - -function projectEquals( - project1: IPlaygroundProject, - project2: IPlaygroundProject -): boolean { - return ( - normalizeLineEnding(project1.css) === - normalizeLineEnding(project2.css) && - normalizeLineEnding(project1.html) === - normalizeLineEnding(project2.html) && - normalizeLineEnding(project1.js) === normalizeLineEnding(project2.js) - ); -} - -function normalizeLineEnding(str: string): string { - return str.replace(/\r\n/g, "\n"); -} - -class StateUrlSerializer implements IHistoryModel { - public readonly dispose = Disposable.fn(); - - private readonly compressor = new LzmaCompressor(); - - private cachedState: - | { state: IPlaygroundProject; hash: string } - | undefined = undefined; - - private readonly computedHashValue = debouncedComputed( - 500, - () => ({ - state: this.model.playgroundProject, - selectedExampleProject: this.model.selectedExampleProject, - }), - ({ state, selectedExampleProject }) => { - if ( - selectedExampleProject && - projectEquals(state, selectedExampleProject.project) - ) { - return "example-" + selectedExampleProject.example.id; - } - if ( - this.cachedState && - projectEquals(this.cachedState.state, state) - ) { - return this.cachedState.hash; - } - return this.compressor.encodeData(state); - } - ); - - private get sourceFromSettings(): Source | undefined { - const settings = this.model.settings.settings; - if (settings.monacoSource === "npm") { - return new Source(settings.npmVersion, undefined, undefined); - } else if ( - settings.monacoSource === "independent" && - ((settings.coreSource === "url" && - (settings.languagesSource === "latest" || - settings.languagesSource === "url")) || - (settings.coreSource === "latest" && - settings.languagesSource === "url")) - ) { - return new Source( - undefined, - settings.coreSource === "url" ? settings.coreUrl : undefined, - settings.languagesSource === "latest" - ? undefined - : settings.languagesUrl - ); - } else if (settings.monacoSource === "latest") { - return new Source(monacoEditorVersion, undefined, undefined); - } - return undefined; - } - - @observable - private _sourceOverride: Source | undefined; - - get sourceOverride(): Source | undefined { - return this._sourceOverride; - } - - @action - disableSourceOverride(): void { - this._sourceOverride = undefined; - this.historyId++; - } - - @action - useLatestDev(): void { - this._sourceOverride = undefined; - this.model.settings.setSettings({ - ...this.model.settings.settings, - ...Source.useLatestDev().toPartialSettings(), - }); - this.historyId++; - } - - @action - saveSourceOverride(): void { - if (this._sourceOverride) { - this.model.settings.setSettings({ - ...this.model.settings.settings, - ...this._sourceOverride.toPartialSettings(), - }); - this.historyId++; - this._sourceOverride = undefined; - } - } - - /** - * This is used to control replace/push state. - * Replace is used if the history id does not change. - */ - @observable historyId: number = 0; - - get location(): ILocation { - const source = this._sourceOverride || this.sourceFromSettings; - return { - hashValue: this.computedHashValue.value || this.cachedState?.hash, - searchParams: { - source: source?.sourceToString(), - sourceLanguages: source?.sourceLanguagesToString(), - }, - }; - } - - @action - updateLocation(currentLocation: ILocation): void { - const hashValue = currentLocation.hashValue; - const sourceStr = currentLocation.searchParams.source; - const sourceLanguages = currentLocation.searchParams.sourceLanguages; - const source = - sourceStr || sourceLanguages - ? Source.parse(sourceStr, sourceLanguages) - : undefined; - - if (this.sourceFromSettings?.equals(source)) { - this._sourceOverride = undefined; - } else { - this._sourceOverride = source; - } - - function findExample(hashValue: string): PlaygroundExample | undefined { - if (hashValue.startsWith("example-")) { - hashValue = hashValue.substring("example-".length); - } - return getPlaygroundExamples() - .flatMap((e) => e.examples) - .find((e) => e.id === hashValue); - } - - let example: PlaygroundExample | undefined; - - if (!hashValue) { - this.model.selectedExample = getPlaygroundExamples()[0].examples[0]; - } else if ((example = findExample(hashValue))) { - this.model.selectedExample = example; - } else { - let p: IPlaygroundProject | undefined = undefined; - if (this.cachedState?.hash === hashValue) { - p = this.cachedState.state; - } - if (!p) { - try { - p = - this.compressor.decodeData( - hashValue - ); - } catch (e) { - console.log("Could not deserialize from hash value", e); - } - } - if (p) { - this.cachedState = { state: p, hash: hashValue }; - this.model.setState(p); - } - } - } - - private readonly historyController = this.dispose.track( - new HistoryController((initialLocation) => { - this.updateLocation(initialLocation); - return this; - }) - ); - - constructor(private readonly model: PlaygroundModel) {} -} - -class BisectModel { - private readonly map = new ObservableMap(); - - constructor(private readonly model: PlaygroundModel) {} - - public getState(version: string): boolean | undefined { - return this.map.get(version); - } - - public get isActive() { - return [...this.map.values()].some((e) => e !== undefined); - } - - public reset(): void { - this.map.clear(); - } - - public async toggleState(version: string, state: boolean): Promise { - const currentState = this.getState(version); - await this.setState( - version, - currentState === state ? undefined : state - ); - } - - @action - public async setState( - version: string, - state: boolean | undefined - ): Promise { - if (state === undefined) { - this.map.delete(version); - } else { - this.map.set(version, state); - } - - const nextVersion = await this.getNextVersion(); - if (!nextVersion) { - return; - } - this.model.settings.setSettings({ - ...this.model.settings.settings, - npmVersion: nextVersion, - }); - } - - private get versions() { - return getNpmVersionsSync(undefined); - } - - private get indexOfLastBadVersion() { - return findLastIndex(this.versions, (v) => this.map.get(v) === false); - } - private get indexOfFirstGoodVersion() { - return this.versions.findIndex((v) => this.map.get(v) === true); - } - - public get steps() { - const indexOfFirstGoodVersion = this.indexOfFirstGoodVersion; - const indexOfLastBadVersion = this.indexOfLastBadVersion; - - if (indexOfFirstGoodVersion === -1 && indexOfLastBadVersion === -1) { - return -1; - } - if (indexOfFirstGoodVersion === -1) { - return Math.ceil( - Math.log2(this.versions.length - indexOfLastBadVersion) - ); - } else if (indexOfLastBadVersion === -1) { - return Math.ceil(Math.log2(indexOfFirstGoodVersion + 1)); - } else { - return Math.ceil( - Math.log2(indexOfFirstGoodVersion - indexOfLastBadVersion) - ); - } - } - - public get isFinished() { - if ( - this.indexOfFirstGoodVersion !== -1 && - this.indexOfLastBadVersion + 1 === this.indexOfFirstGoodVersion - ) { - return true; - } - return false; - } - - public async openGithub() { - const versions = await getNpmVersions(); - const indexOfFirstGoodVersion = - this.indexOfFirstGoodVersion === -1 - ? versions.length - 1 - : this.indexOfFirstGoodVersion; - const indexOfLastBadVersion = - this.indexOfLastBadVersion === -1 ? 0 : this.indexOfLastBadVersion; - const goodCommitId = await getVsCodeCommitId( - versions[indexOfFirstGoodVersion] - ); - const badCommitId = await getVsCodeCommitId( - versions[indexOfLastBadVersion] - ); - window.open( - `https://github.com/microsoft/vscode/compare/${goodCommitId}...${badCommitId}`, - "_blank" - ); - } - - private async getNextVersion(): Promise { - const versions = await getNpmVersions(); - - const indexOfFirstGoodVersion = this.indexOfFirstGoodVersion; - const indexOfLastBadVersion = this.indexOfLastBadVersion; - - if ( - indexOfFirstGoodVersion !== -1 && - indexOfLastBadVersion + 1 === indexOfFirstGoodVersion - ) { - // Finished - return; - } - - if (indexOfLastBadVersion === -1 && indexOfFirstGoodVersion === -1) { - return versions[0]; - } - if (indexOfLastBadVersion === -1) { - // try first (newest) version that hasn't been tested - const indexOfFirstUntestedVersion = versions.findIndex( - (v) => this.map.get(v) === undefined - ); - if (indexOfFirstUntestedVersion === -1) { - return undefined; - } - return versions[indexOfFirstUntestedVersion]; - } - - if (indexOfFirstGoodVersion === -1) { - /*// exponential back off, might be good for recent regressions, but ruins step counter - const candidate = Math.min( - indexOfLastBadVersion * 2 + 1, - versions.length - 1 - );*/ - const candidate = Math.floor( - (indexOfLastBadVersion + versions.length) / 2 - ); - return versions[candidate]; - } - - return versions[ - Math.floor((indexOfLastBadVersion + indexOfFirstGoodVersion) / 2) - ]; - } -} - -function findLastIndex( - array: T[], - predicate: (value: T) => boolean -): number { - for (let i = array.length - 1; i >= 0; i--) { - if (predicate(array[i])) { - return i; - } - } - return -1; -} - -class Source { - public static useLatestDev(sourceLanguagesStr?: string): Source { - // Assume the versions are already loaded - const versions = getNpmVersionsSync(undefined); - const version = versions.find((v) => v.indexOf("-dev-") !== -1); - return new Source(version, undefined, sourceLanguagesStr); - } - - public static useLatest(sourceLanguagesStr?: string): Source { - return new Source(monacoEditorVersion, undefined, sourceLanguagesStr); - } - - public static parse( - sourceStr: string | undefined, - sourceLanguagesStr: string | undefined - ): Source { - if (sourceStr === "latest-dev") { - return Source.useLatestDev(sourceLanguagesStr); - } - if (sourceStr === "latest") { - return Source.useLatest(sourceLanguagesStr); - } - - if (sourceStr && sourceStr.startsWith("v")) { - return new Source( - sourceStr.substring(1), - undefined, - sourceLanguagesStr - ); - } - return new Source(undefined, sourceStr, sourceLanguagesStr); - } - - public equals(other: Source | undefined): boolean { - if (!other) { - return false; - } - return other.toString() === this.toString(); - } - - constructor( - public readonly version: string | undefined, - public readonly url: string | undefined, - public readonly sourceLanguagesStr: string | undefined - ) { - if ( - version === undefined && - url === undefined && - sourceLanguagesStr === undefined - ) { - throw new Error("one parameter must be defined"); - } - } - - sourceToString(): string | undefined { - if (this.url) { - return this.url; - } - if (this.version) { - return `v${this.version}`; - } - return undefined; - } - - sourceLanguagesToString(): string | undefined { - return this.sourceLanguagesStr; - } - - toString() { - return `${this.sourceToString()};${this.sourceLanguagesToString()}`; - } - - public toPartialSettings(): Partial { - const languagesSettings: Partial = { - languagesSource: - this.sourceLanguagesStr === undefined ? "latest" : "url", - languagesUrl: this.sourceLanguagesStr, - }; - - if (this.version) { - return { - monacoSource: "npm", - npmVersion: this.version, - }; - } else if (this.url) { - return { - monacoSource: "independent", - coreSource: "url", - coreUrl: this.url, - ...languagesSettings, - }; - } else { - return { - monacoSource: "independent", - coreSource: "latest", - ...languagesSettings, - }; - } - } -} diff --git a/website/src/website/pages/playground/PlaygroundPage.tsx b/website/src/website/pages/playground/PlaygroundPage.tsx index fc6f6fbf96..fccfa6f1bc 100644 --- a/website/src/website/pages/playground/PlaygroundPage.tsx +++ b/website/src/website/pages/playground/PlaygroundPage.tsx @@ -7,9 +7,10 @@ import { withLoader } from "../../components/Loader"; import { getNpmVersions } from "./getNpmVersionsSync"; @withLoader(async () => { + const search = new URLSearchParams(window.location.search); if ( - new URLSearchParams(window.location.search).get("source") === - "latest-dev" + search.get("source") === "latest-dev" || + search.get("compareWith") === "latest-dev" ) { // So that the source class can resolve that value await getNpmVersions(); diff --git a/website/src/website/pages/playground/PlaygroundPageContent.tsx b/website/src/website/pages/playground/PlaygroundPageContent.tsx index ac5129cf6e..4681b0fb8b 100644 --- a/website/src/website/pages/playground/PlaygroundPageContent.tsx +++ b/website/src/website/pages/playground/PlaygroundPageContent.tsx @@ -19,6 +19,7 @@ import { Preview } from "./Preview"; import { SettingsDialog } from "./SettingsDialog"; import { getNpmVersionsSync } from "./getNpmVersionsSync"; import { PlaygroundExample, getPlaygroundExamples } from "./playgroundExamples"; +import { getDefaultSettings, toLoaderConfig } from "./SettingsModel"; @hotComponent(module) @observer @@ -41,7 +42,7 @@ export class PlaygroundPageContent extends React.Component< )} - + - {model.settings.previewFullScreen || ( + {model.previewShouldBeFullScreen || ( - {model.serializer.sourceOverride ? ( + {!model.historyModel.compareWith ? ( + model.historyModel + .sourceOverride ? ( + + + + + + ) : ( + <> + + + - - - ) : ( - <> - - - + + + } + > + + + + )} diff --git a/website/src/website/pages/playground/Preview.tsx b/website/src/website/pages/playground/Preview.tsx index 07ee853ca3..538b8d803f 100644 --- a/website/src/website/pages/playground/Preview.tsx +++ b/website/src/website/pages/playground/Preview.tsx @@ -1,27 +1,53 @@ import * as React from "react"; -import { IPreviewHandler, PlaygroundModel } from "./PlaygroundModel"; +import { PlaygroundModel } from "./PlaygroundModel"; import { observer } from "mobx-react"; -import { observable } from "mobx"; +import { autorun, observable, reaction } from "mobx"; import { IMessageFromRunner, IMessageToRunner, IPreviewState, } from "../../../shared"; +import { Button } from "react-bootstrap"; @observer -export class Preview - extends React.Component<{ model: PlaygroundModel }> - implements IPreviewHandler -{ +export class Preview extends React.Component<{ + model: PlaygroundModel; + getPreviewState: () => IPreviewState | undefined; +}> { private disposables: monaco.IDisposable[] = []; - @observable - private counter = 0; - private currentState: IPreviewState | undefined; + @observable private counter = 0; + @observable.ref private currentState: IPreviewState | undefined; private iframe: HTMLIFrameElement | null = null; render() { return (
+ {this.currentState ? null : ( +
+
+ Load{" "} +
+
+ )}