mirror of
https://github.com/microsoft/monaco-editor.git
synced 2025-12-22 07:00:11 +01:00
Merge branch 'main' into fix/noSuggestionDiagnostics
This commit is contained in:
commit
42ccebaa1a
13 changed files with 736 additions and 578 deletions
|
|
@ -6,8 +6,8 @@
|
||||||
'npm/dev': 'node_modules/monaco-editor-core/dev/vs',
|
'npm/dev': 'node_modules/monaco-editor-core/dev/vs',
|
||||||
'npm/min': 'node_modules/monaco-editor-core/min/vs',
|
'npm/min': 'node_modules/monaco-editor-core/min/vs',
|
||||||
built: '/vscode/out-monaco-editor-core/min/vs',
|
built: '/vscode/out-monaco-editor-core/min/vs',
|
||||||
releaseDev: 'release/dev/vs',
|
releaseDev: 'out/monaco-editor/dev/vs',
|
||||||
releaseMin: 'release/min/vs'
|
releaseMin: 'out/monaco-editor/min/vs'
|
||||||
};
|
};
|
||||||
const pluginPaths = {
|
const pluginPaths = {
|
||||||
src: 'out/amd',
|
src: 'out/amd',
|
||||||
|
|
|
||||||
|
|
@ -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.35.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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
161
website/src/website/pages/playground/BisectModel.ts
Normal file
161
website/src/website/pages/playground/BisectModel.ts
Normal file
|
|
@ -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)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
211
website/src/website/pages/playground/LocationModel.ts
Normal file
211
website/src/website/pages/playground/LocationModel.ts
Normal file
|
|
@ -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 (
|
||||||
JSON.stringify(state.monacoSetup) ===
|
(!lastState ||
|
||||||
JSON.stringify(lastState.monacoSetup) &&
|
JSON.stringify(state.monacoSetup) ===
|
||||||
state.key === lastState.key
|
JSON.stringify(lastState.monacoSetup)) &&
|
||||||
|
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,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>
|
<ButtonGroup>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
model.serializer.disableSourceOverride()
|
model.historyModel.exitCompare()
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Disable{" "}
|
Exit Compare
|
||||||
{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
|
|
||||||
</button>
|
</button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<VersionSelector
|
|
||||||
model={model}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-light settings bi-gear"
|
|
||||||
style={{
|
|
||||||
fontSize: 20,
|
|
||||||
padding: "0px 4px",
|
|
||||||
}}
|
|
||||||
onClick={() =>
|
|
||||||
model.showSettingsDialog()
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Preview model={model} />
|
<Preview
|
||||||
|
model={model}
|
||||||
|
getPreviewState={model.getPreviewState}
|
||||||
|
/>
|
||||||
</LabeledEditor>
|
</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>
|
</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 {
|
||||||
|
|
|
||||||
107
website/src/website/pages/playground/Source.ts
Normal file
107
website/src/website/pages/playground/Source.ts
Normal file
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
website/src/website/pages/playground/utils.ts
Normal file
29
website/src/website/pages/playground/utils.ts
Normal file
|
|
@ -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.35.0:
|
monaco-editor@^0.42.0-dev-20230906:
|
||||||
version "0.35.0"
|
version "0.42.0-dev-20230906"
|
||||||
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.35.0.tgz#49c4220c815262a900dacf0ae8a59bef66efab8b"
|
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.42.0-dev-20230906.tgz#612a41fcbed662d3873a94ad5f558e6893da2c7d"
|
||||||
integrity sha512-BJfkAZ0EJ7JgrgWzqjfBNP9hPSS8NlfECEDMEIIiozV2UaPq22yeuOjgbd3TwMh3anH0krWZirXZfn8KUSxiOA==
|
integrity sha512-UICbxxHu0jYovjOKcwSJkmnJbokiSefro1wDqVJ4OpyzXmS0dYZol+lYPJLIdfb0oUtUTf8840VMAPo5jC+B1Q==
|
||||||
|
|
||||||
mrmime@^1.0.0:
|
mrmime@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue