This commit is contained in:
Henning Dieterichs 2023-09-06 19:37:00 +02:00 коммит произвёл Henning Dieterichs
Родитель e7d7a5b072
Коммит 51ba6777fc
12 изменённых файлов: 734 добавлений и 576 удалений

Просмотреть файл

@ -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",

Просмотреть файл

@ -32,6 +32,6 @@ export interface IPlaygroundProject {
}
export interface IPreviewState extends IPlaygroundProject {
key: number;
reloadKey: number;
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,
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<IPreviewHandler>();
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<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";
@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();

Просмотреть файл

@ -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<
<Col
md
className={
model.settings.previewFullScreen
model.previewShouldBeFullScreen
? "d-none"
: ""
}
@ -118,15 +119,24 @@ export class PlaygroundPageContent extends React.Component<
</Vertical>
</Col>
)}
<Col md>
<Col
md
style={{ display: "flex", flexDirection: "column" }}
>
<LabeledEditor
label="Preview"
label={`Preview${
model.historyModel.compareWith &&
model.historyModel.sourceOverride
? " " +
model.historyModel.sourceOverride.toString()
: ""
}:`}
titleBarItems={
<div
style={{ marginLeft: "auto" }}
className="d-flex gap-2 align-items-center"
>
{model.settings.previewFullScreen || (
{model.previewShouldBeFullScreen || (
<FormCheck
label="Auto-Reload"
className="text-nowrap"
@ -177,64 +187,116 @@ export class PlaygroundPageContent extends React.Component<
}
/>
{model.serializer.sourceOverride ? (
{!model.historyModel.compareWith ? (
model.historyModel
.sourceOverride ? (
<ButtonGroup>
<button
type="button"
className="btn btn-primary"
onClick={() =>
model.historyModel.disableSourceOverride()
}
>
Disable{" "}
{model.historyModel
.sourceOverride
.version ??
"url"}{" "}
override
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() =>
model.compareWithLatestDev()
}
>
Compare with latest dev
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() =>
model.historyModel.saveSourceOverride()
}
>
Save
</button>
</ButtonGroup>
) : (
<>
<VersionSelector
model={model}
/>
<button
type="button"
className="btn btn-light settings bi-gear"
style={{
fontSize: 20,
padding: "0px 4px",
}}
onClick={() =>
model.showSettingsDialog()
}
/>
</>
)
) : (
<ButtonGroup>
<button
type="button"
className="btn btn-primary"
onClick={() =>
model.serializer.disableSourceOverride()
model.historyModel.exitCompare()
}
>
Disable{" "}
{model.serializer
.sourceOverride
.version ?? "url"}{" "}
override
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() =>
model.serializer.useLatestDev()
}
>
Use latest dev
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() =>
model.serializer.saveSourceOverride()
}
>
Save
Exit Compare
</button>
</ButtonGroup>
) : (
<>
<VersionSelector
model={model}
/>
<button
type="button"
className="btn btn-light settings bi-gear"
style={{
fontSize: 20,
padding: "0px 4px",
}}
onClick={() =>
model.showSettingsDialog()
}
/>
</>
)}
</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>
</>
)}
</Col>
</Row>
</div>

Просмотреть файл

@ -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 (
<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
className="full-iframe"
key={this.counter}
@ -66,27 +92,33 @@ export class Preview
};
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() {
this.disposables.forEach((d) => d.dispose());
}
handlePreview(state: IPreviewState): void {
private handlePreview(state: IPreviewState): void {
if (
JSON.stringify({ ...state, css: "" }) ===
JSON.stringify({ ...this.currentState, css: "" })
) {
// only css changed
this.iframe?.contentWindow!.postMessage(
{
kind: "update-css",
css: state.css,
} as IMessageToRunner,
{
targetOrigin: "*",
}
{ kind: "update-css", css: state.css } as IMessageToRunner,
{ targetOrigin: "*" }
);
this.currentState = state;
} 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 {
position: absolute !important;
a {
text-decoration: none;
}
}
button.settings {

Просмотреть файл

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