-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
734 additions
and
576 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
Oops, something went wrong.