parent
e7d7a5b072
commit
51ba6777fc
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
Loading…
Reference in New Issue