Improves playground

pull/4151/head^2 v0.42.0-dev-20230908
Henning Dieterichs 1 year ago committed by Henning Dieterichs
parent e7d7a5b072
commit 51ba6777fc

@ -24,7 +24,7 @@
"mini-css-extract-plugin": "^2.6.1", "mini-css-extract-plugin": "^2.6.1",
"mobx": "^5.15.4", "mobx": "^5.15.4",
"mobx-react": "^6.2.2", "mobx-react": "^6.2.2",
"monaco-editor": "^0.41.0", "monaco-editor": "^0.42.0-dev-20230906",
"react": "^17.0.2", "react": "^17.0.2",
"react-bootstrap": "^2.4.0", "react-bootstrap": "^2.4.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",

@ -32,6 +32,6 @@ export interface IPlaygroundProject {
} }
export interface IPreviewState extends IPlaygroundProject { export interface IPreviewState extends IPlaygroundProject {
key: number; reloadKey: number;
monacoSetup: IMonacoSetup; monacoSetup: IMonacoSetup;
} }

@ -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<string, boolean>();
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<void> {
const currentState = this.getState(version);
await this.setState(
version,
currentState === state ? undefined : state
);
}
@action
public async setState(
version: string,
state: boolean | undefined
): Promise<void> {
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<string | undefined> {
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)
];
}
}

@ -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<IPlaygroundProject>();
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<IPlaygroundProject>(
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;
}
}
}

@ -8,7 +8,6 @@ import {
autorun, autorun,
computed, computed,
observable, observable,
ObservableMap,
reaction, reaction,
runInAction, runInAction,
} from "mobx"; } from "mobx";
@ -18,22 +17,10 @@ import {
waitForLoadedMonaco, waitForLoadedMonaco,
} from "../../../monaco-loader"; } from "../../../monaco-loader";
import { IPlaygroundProject, IPreviewState } from "../../../shared"; import { IPlaygroundProject, IPreviewState } from "../../../shared";
import { monacoEditorVersion } from "../../monacoEditorVersion";
import { Debouncer } from "../../utils/Debouncer"; import { Debouncer } from "../../utils/Debouncer";
import { LzmaCompressor } from "../../utils/lzmaCompressor";
import {
HistoryController,
IHistoryModel,
ILocation,
} from "../../utils/ObservableHistory";
import { ObservablePromise } from "../../utils/ObservablePromise"; import { ObservablePromise } from "../../utils/ObservablePromise";
import { debouncedComputed, Disposable } from "../../utils/utils"; import { Disposable } from "../../utils/utils";
import { import { PlaygroundExample } from "./playgroundExamples";
getNpmVersions,
getNpmVersionsSync,
getVsCodeCommitId,
} from "./getNpmVersionsSync";
import { getPlaygroundExamples, PlaygroundExample } from "./playgroundExamples";
import { import {
getDefaultSettings, getDefaultSettings,
JsonString, JsonString,
@ -41,6 +28,8 @@ import {
SettingsModel, SettingsModel,
toLoaderConfig, toLoaderConfig,
} from "./SettingsModel"; } from "./SettingsModel";
import { BisectModel } from "./BisectModel";
import { LocationModel } from "./LocationModel";
export class PlaygroundModel { export class PlaygroundModel {
public readonly dispose = Disposable.fn(); public readonly dispose = Disposable.fn();
@ -58,16 +47,18 @@ export class PlaygroundModel {
@observable @observable
public reloadKey = 0; public reloadKey = 0;
public readonly serializer = new StateUrlSerializer(this); public readonly historyModel = new LocationModel(this);
public reload(): void { public reload(): void {
this.reloadKey++; this.reloadKey++;
} }
private readonly _previewHandlers = new Set<IPreviewHandler>(); public get previewShouldBeFullScreen(): boolean {
return this.settings.previewFullScreen;
}
private _wasEverNonFullScreen = false; private _wasEverNonFullScreen = false;
public get wasEverNonFullScreen() { public get wasEverNonFullScreen(): boolean {
if (this._wasEverNonFullScreen) { if (this._wasEverNonFullScreen) {
return true; return true;
} }
@ -79,7 +70,7 @@ export class PlaygroundModel {
@computed.struct @computed.struct
get monacoSetup(): IMonacoSetup { get monacoSetup(): IMonacoSetup {
const sourceOverride = this.serializer.sourceOverride; const sourceOverride = this.historyModel.sourceOverride;
if (sourceOverride) { if (sourceOverride) {
return toLoaderConfig({ return toLoaderConfig({
...getDefaultSettings(), ...getDefaultSettings(),
@ -105,10 +96,33 @@ export class PlaygroundModel {
return { return {
...this.playgroundProject, ...this.playgroundProject,
monacoSetup: this.monacoSetup, 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 @observable
public settingsDialogModel: SettingsDialogModel | undefined = undefined; public settingsDialogModel: SettingsDialogModel | undefined = undefined;
@ -134,6 +148,7 @@ export class PlaygroundModel {
example: value, example: value,
project: p, project: p,
}; };
this.reloadKey++;
this.setState(p); this.setState(p);
}); });
}); });
@ -146,37 +161,37 @@ export class PlaygroundModel {
public isDirty = false; public isDirty = false;
constructor() { constructor() {
let lastState = this.state; let lastState: IPreviewState | undefined = undefined;
this.dispose.track({ this.dispose.track({
dispose: reaction( dispose: reaction(
() => ({ state: this.state }), () => ({ state: this.state }),
({ state }) => { () => {
const state = this.state;
if (!this.settings.autoReload) { if (!this.settings.autoReload) {
if ( if (
(!lastState ||
JSON.stringify(state.monacoSetup) === JSON.stringify(state.monacoSetup) ===
JSON.stringify(lastState.monacoSetup) && JSON.stringify(lastState.monacoSetup)) &&
state.key === lastState.key state.reloadKey === (lastState?.reloadKey ?? 0)
) { ) {
this.isDirty = true; this.isDirty = true;
return; return;
} }
} }
const action = () => { const updatePreviewState = () => {
this.isDirty = false; this.isDirty = false;
lastState = state; this._previewState = state;
for (const handler of this._previewHandlers) { lastState = this._previewState;
handler.handlePreview(state);
}
}; };
if (state.key !== lastState.key) { if (state.reloadKey !== lastState?.reloadKey) {
action(); // sync update updatePreviewState();
} else { } 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; 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); public readonly bisectModel = new BisectModel(this);
}
export interface IPreviewHandler { @action
handlePreview(state: IPreviewState): void; compareWithLatestDev(): void {
this.settings.previewFullScreen = true;
this.historyModel.compareWithLatestDev();
}
} }
export class SettingsDialogModel { export class SettingsDialogModel {
@ -316,458 +323,3 @@ export class SettingsDialogModel {
this.settings = Object.assign({}, settings); 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<IPlaygroundProject>();
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<IPlaygroundProject>(
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<string, boolean>();
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<void> {
const currentState = this.getState(version);
await this.setState(
version,
currentState === state ? undefined : state
);
}
@action
public async setState(
version: string,
state: boolean | undefined
): Promise<void> {
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<string | undefined> {
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<T>(
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<Settings> {
const languagesSettings: Partial<Settings> = {
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,
};
}
}
}

@ -7,9 +7,10 @@ import { withLoader } from "../../components/Loader";
import { getNpmVersions } from "./getNpmVersionsSync"; import { getNpmVersions } from "./getNpmVersionsSync";
@withLoader(async () => { @withLoader(async () => {
const search = new URLSearchParams(window.location.search);
if ( if (
new URLSearchParams(window.location.search).get("source") === search.get("source") === "latest-dev" ||
"latest-dev" search.get("compareWith") === "latest-dev"
) { ) {
// So that the source class can resolve that value // So that the source class can resolve that value
await getNpmVersions(); await getNpmVersions();

@ -19,6 +19,7 @@ import { Preview } from "./Preview";
import { SettingsDialog } from "./SettingsDialog"; import { SettingsDialog } from "./SettingsDialog";
import { getNpmVersionsSync } from "./getNpmVersionsSync"; import { getNpmVersionsSync } from "./getNpmVersionsSync";
import { PlaygroundExample, getPlaygroundExamples } from "./playgroundExamples"; import { PlaygroundExample, getPlaygroundExamples } from "./playgroundExamples";
import { getDefaultSettings, toLoaderConfig } from "./SettingsModel";
@hotComponent(module) @hotComponent(module)
@observer @observer
@ -41,7 +42,7 @@ export class PlaygroundPageContent extends React.Component<
<Col <Col
md md
className={ className={
model.settings.previewFullScreen model.previewShouldBeFullScreen
? "d-none" ? "d-none"
: "" : ""
} }
@ -118,15 +119,24 @@ export class PlaygroundPageContent extends React.Component<
</Vertical> </Vertical>
</Col> </Col>
)} )}
<Col md> <Col
md
style={{ display: "flex", flexDirection: "column" }}
>
<LabeledEditor <LabeledEditor
label="Preview" label={`Preview${
model.historyModel.compareWith &&
model.historyModel.sourceOverride
? " " +
model.historyModel.sourceOverride.toString()
: ""
}:`}
titleBarItems={ titleBarItems={
<div <div
style={{ marginLeft: "auto" }} style={{ marginLeft: "auto" }}
className="d-flex gap-2 align-items-center" className="d-flex gap-2 align-items-center"
> >
{model.settings.previewFullScreen || ( {model.previewShouldBeFullScreen || (
<FormCheck <FormCheck
label="Auto-Reload" label="Auto-Reload"
className="text-nowrap" className="text-nowrap"
@ -177,35 +187,38 @@ export class PlaygroundPageContent extends React.Component<
} }
/> />
{model.serializer.sourceOverride ? ( {!model.historyModel.compareWith ? (
model.historyModel
.sourceOverride ? (
<ButtonGroup> <ButtonGroup>
<button <button
type="button" type="button"
className="btn btn-primary" className="btn btn-primary"
onClick={() => onClick={() =>
model.serializer.disableSourceOverride() model.historyModel.disableSourceOverride()
} }
> >
Disable{" "} Disable{" "}
{model.serializer {model.historyModel
.sourceOverride .sourceOverride
.version ?? "url"}{" "} .version ??
"url"}{" "}
override override
</button> </button>
<button <button
type="button" type="button"
className="btn btn-secondary" className="btn btn-secondary"
onClick={() => onClick={() =>
model.serializer.useLatestDev() model.compareWithLatestDev()
} }
> >
Use latest dev Compare with latest dev
</button> </button>
<button <button
type="button" type="button"
className="btn btn-secondary" className="btn btn-secondary"
onClick={() => onClick={() =>
model.serializer.saveSourceOverride() model.historyModel.saveSourceOverride()
} }
> >
Save Save
@ -229,12 +242,61 @@ export class PlaygroundPageContent extends React.Component<
} }
/> />
</> </>
)
) : (
<ButtonGroup>
<button
type="button"
className="btn btn-primary"
onClick={() =>
model.historyModel.exitCompare()
}
>
Exit Compare
</button>
</ButtonGroup>
)} )}
</div> </div>
} }
> >
<Preview model={model} /> <Preview
model={model}
getPreviewState={model.getPreviewState}
/>
</LabeledEditor>
{model.historyModel.compareWith && (
<>
<div style={{ height: "10px" }} />
<LabeledEditor
label={`Preview ${model.historyModel.compareWith.toString()}:`}
titleBarItems={
<div
style={{ marginLeft: "auto" }}
className="d-flex gap-2 align-items-center"
>
<ButtonGroup>
<button
type="button"
className="btn btn-primary"
onClick={() =>
model.historyModel.saveCompareWith()
}
>
Save
</button>
</ButtonGroup>
</div>
}
>
<Preview
model={model}
getPreviewState={
model.getCompareWithPreviewState
}
/>
</LabeledEditor> </LabeledEditor>
</>
)}
</Col> </Col>
</Row> </Row>
</div> </div>

@ -1,27 +1,53 @@
import * as React from "react"; import * as React from "react";
import { IPreviewHandler, PlaygroundModel } from "./PlaygroundModel"; import { PlaygroundModel } from "./PlaygroundModel";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { observable } from "mobx"; import { autorun, observable, reaction } from "mobx";
import { import {
IMessageFromRunner, IMessageFromRunner,
IMessageToRunner, IMessageToRunner,
IPreviewState, IPreviewState,
} from "../../../shared"; } from "../../../shared";
import { Button } from "react-bootstrap";
@observer @observer
export class Preview export class Preview extends React.Component<{
extends React.Component<{ model: PlaygroundModel }> model: PlaygroundModel;
implements IPreviewHandler getPreviewState: () => IPreviewState | undefined;
{ }> {
private disposables: monaco.IDisposable[] = []; private disposables: monaco.IDisposable[] = [];
@observable @observable private counter = 0;
private counter = 0; @observable.ref private currentState: IPreviewState | undefined;
private currentState: IPreviewState | undefined;
private iframe: HTMLIFrameElement | null = null; private iframe: HTMLIFrameElement | null = null;
render() { render() {
return ( return (
<div className="preview"> <div className="preview">
{this.currentState ? null : (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<div>
Load{" "}
<Button
type="button"
className={
"btn settings bi-arrow-clockwise btn-primary"
}
style={{
fontSize: 20,
padding: "0px 4px",
}}
onClick={() => this.props.model.reload()}
/>
</div>
</div>
)}
<iframe <iframe
className="full-iframe" className="full-iframe"
key={this.counter} key={this.counter}
@ -66,27 +92,33 @@ export class Preview
}; };
componentDidMount() { componentDidMount() {
this.disposables.push(this.props.model.setPreviewHandler(this)); this.disposables.push({
dispose: reaction(
() => this.props.getPreviewState(),
(state) => {
if (state) {
console.log("handlePreview", state);
this.handlePreview(state);
}
},
{ fireImmediately: true }
),
});
} }
componentWillUnmount() { componentWillUnmount() {
this.disposables.forEach((d) => d.dispose()); this.disposables.forEach((d) => d.dispose());
} }
handlePreview(state: IPreviewState): void { private handlePreview(state: IPreviewState): void {
if ( if (
JSON.stringify({ ...state, css: "" }) === JSON.stringify({ ...state, css: "" }) ===
JSON.stringify({ ...this.currentState, css: "" }) JSON.stringify({ ...this.currentState, css: "" })
) { ) {
// only css changed // only css changed
this.iframe?.contentWindow!.postMessage( this.iframe?.contentWindow!.postMessage(
{ { kind: "update-css", css: state.css } as IMessageToRunner,
kind: "update-css", { targetOrigin: "*" }
css: state.css,
} as IMessageToRunner,
{
targetOrigin: "*",
}
); );
this.currentState = state; this.currentState = state;
} else { } else {

@ -0,0 +1,107 @@
import { monacoEditorVersion } from "../../monacoEditorVersion";
import { getNpmVersionsSync } from "./getNpmVersionsSync";
import { Settings } from "./SettingsModel";
export 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() {
const sourceLangToStr = this.sourceLanguagesToString();
return `${this.sourceToString()}${
sourceLangToStr ? `;${sourceLangToStr}` : ""
}`;
}
public toPartialSettings(): Partial<Settings> {
const languagesSettings: Partial<Settings> = {
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,
};
}
}
}

@ -0,0 +1,29 @@
import { normalizeLineEnding } from "./utils";
import { IPlaygroundProject } from "../../../shared";
export function findLastIndex<T>(
array: T[],
predicate: (value: T) => boolean
): number {
for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i])) {
return i;
}
}
return -1;
}
export 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)
);
}
export function normalizeLineEnding(str: string): string {
return str.replace(/\r\n/g, "\n");
}

@ -74,6 +74,9 @@ body,
.monaco-editor { .monaco-editor {
position: absolute !important; position: absolute !important;
a {
text-decoration: none;
}
} }
button.settings { button.settings {

@ -2147,10 +2147,10 @@ mobx@^5.15.4:
resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.7.tgz#b9a5f2b6251f5d96980d13c78e9b5d8d4ce22665" resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.7.tgz#b9a5f2b6251f5d96980d13c78e9b5d8d4ce22665"
integrity sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw== integrity sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==
monaco-editor@^0.41.0: monaco-editor@^0.42.0-dev-20230906:
version "0.41.0" version "0.42.0-dev-20230906"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.41.0.tgz#2ba31e5af7e3ae93ac5d7467ec2772ef9b3d967f" resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.42.0-dev-20230906.tgz#612a41fcbed662d3873a94ad5f558e6893da2c7d"
integrity sha512-1o4olnZJsiLmv5pwLEAmzHTE/5geLKQ07BrGxlF4Ri/AXAc2yyDGZwHjiTqD8D/ROKUZmwMA28A+yEowLNOEcA== integrity sha512-UICbxxHu0jYovjOKcwSJkmnJbokiSefro1wDqVJ4OpyzXmS0dYZol+lYPJLIdfb0oUtUTf8840VMAPo5jC+B1Q==
mrmime@^1.0.0: mrmime@^1.0.0:
version "1.0.1" version "1.0.1"

Loading…
Cancel
Save