Improves playground
This commit is contained in:
Родитель
e7d7a5b072
Коммит
51ba6777fc
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче