@codingame/monaco-vscode-welcome-service-override 34.1.3 → 35.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +6 -2
- package/package.json +2 -2
- package/vscode/src/vs/workbench/contrib/onboarding/browser/media/onboardingTarget.css +28 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/media/spotlight.css +147 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/onboarding.contribution.d.ts +1 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/onboarding.contribution.js +107 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/onboardingService.d.ts +122 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/onboardingService.js +378 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/spotlight/onboardingTarget.d.ts +31 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/spotlight/onboardingTarget.js +25 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/spotlight/spotlightOverlay.d.ts +96 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/spotlight/spotlightOverlay.js +381 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/spotlight/spotlightPresentation.d.ts +24 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/spotlight/spotlightPresentation.js +205 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/spotlight/spotlightTypes.d.ts +49 -0
- package/vscode/src/vs/workbench/contrib/onboarding/browser/spotlight/spotlightTypes.js +5 -0
- package/vscode/src/vs/workbench/contrib/onboarding/common/onboardingPresentation.d.ts +47 -0
- package/vscode/src/vs/workbench/contrib/onboarding/common/onboardingPresentation.js +27 -0
- package/vscode/src/vs/workbench/contrib/onboarding/common/onboardingRegistry.d.ts +21 -0
- package/vscode/src/vs/workbench/contrib/onboarding/common/onboardingRegistry.js +35 -0
- package/vscode/src/vs/workbench/contrib/onboarding/common/onboardingScenario.d.ts +171 -0
- package/vscode/src/vs/workbench/contrib/onboarding/common/onboardingScenario.js +20 -0
- package/vscode/src/vs/workbench/contrib/onboarding/common/onboardingScenarioService.d.ts +41 -0
- package/vscode/src/vs/workbench/contrib/onboarding/common/onboardingScenarioService.js +10 -0
- package/vscode/src/vs/workbench/contrib/welcomeOnboarding/browser/onboardingVariationA.js +64 -64
- package/vscode/src/vs/workbench/contrib/welcomeOnboarding/browser/welcomeOnboarding.contribution.js +1 -1
- package/vscode/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.js +10 -10
- package/vscode/src/vs/workbench/contrib/welcomeViews/common/viewsWelcomeContribution.js +1 -1
- package/vscode/src/vs/workbench/contrib/welcomeViews/common/viewsWelcomeExtensionPoint.js +8 -8
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
|
|
2
|
+
import { __decorate, __param } from '@codingame/monaco-vscode-api/external/tslib/tslib.es6';
|
|
3
|
+
import { DeferredPromise } from '@codingame/monaco-vscode-api/vscode/vs/base/common/async';
|
|
4
|
+
import { onUnexpectedError } from '@codingame/monaco-vscode-api/vscode/vs/base/common/errors';
|
|
5
|
+
import { Emitter } from '@codingame/monaco-vscode-api/vscode/vs/base/common/event';
|
|
6
|
+
import { Disposable, DisposableStore } from '@codingame/monaco-vscode-api/vscode/vs/base/common/lifecycle';
|
|
7
|
+
import '@codingame/monaco-vscode-api/vscode/vs/base/common/observableInternal/index';
|
|
8
|
+
import { mainWindow } from '@codingame/monaco-vscode-api/vscode/vs/base/browser/window';
|
|
9
|
+
import { IConfigurationService } from '@codingame/monaco-vscode-api/vscode/vs/platform/configuration/common/configuration.service';
|
|
10
|
+
import { IContextKeyService } from '@codingame/monaco-vscode-api/vscode/vs/platform/contextkey/common/contextkey.service';
|
|
11
|
+
import { ITelemetryService } from '@codingame/monaco-vscode-api/vscode/vs/platform/telemetry/common/telemetry.service';
|
|
12
|
+
import { ILifecycleService } from '@codingame/monaco-vscode-api/vscode/vs/workbench/services/lifecycle/common/lifecycle.service';
|
|
13
|
+
import { StorageScope, StorageTarget } from '@codingame/monaco-vscode-api/vscode/vs/platform/storage/common/storage';
|
|
14
|
+
import { IStorageService } from '@codingame/monaco-vscode-api/vscode/vs/platform/storage/common/storage.service';
|
|
15
|
+
import { IWorkbenchAssignmentService } from '@codingame/monaco-vscode-api/vscode/vs/workbench/services/assignment/common/assignmentService.service';
|
|
16
|
+
import { Memento } from '@codingame/monaco-vscode-api/vscode/vs/workbench/common/memento';
|
|
17
|
+
import { onboardingPresentationRegistry } from '../common/onboardingPresentation.js';
|
|
18
|
+
import { onboardingScenarioRegistry } from '../common/onboardingRegistry.js';
|
|
19
|
+
import { ONBOARDING_ASSIGNMENT_CONTEXT_PREFIX, OnboardingOutcome } from '../common/onboardingScenario.js';
|
|
20
|
+
import { ONBOARDING_ENABLED_CONFIG, ONBOARDING_DEVELOPER_MODE_CONFIG, isOnboardingDeveloperModeEnabled } from '../common/onboardingScenarioService.js';
|
|
21
|
+
import { autorun } from '@codingame/monaco-vscode-api/vscode/vs/base/common/observableInternal/reactions/autorun';
|
|
22
|
+
|
|
23
|
+
var OnboardingScenarioService_1;
|
|
24
|
+
let OnboardingScenarioService = class OnboardingScenarioService extends Disposable {
|
|
25
|
+
static {
|
|
26
|
+
OnboardingScenarioService_1 = this;
|
|
27
|
+
}
|
|
28
|
+
static {
|
|
29
|
+
this.MEMENTO_ID = "onboarding";
|
|
30
|
+
}
|
|
31
|
+
static {
|
|
32
|
+
this.OPENED_IDS_STORAGE_KEY = "onboarding.openedAssignmentContextIds";
|
|
33
|
+
}
|
|
34
|
+
constructor(
|
|
35
|
+
storageService,
|
|
36
|
+
contextKeyService,
|
|
37
|
+
configurationService,
|
|
38
|
+
lifecycleService,
|
|
39
|
+
assignmentService,
|
|
40
|
+
telemetryService
|
|
41
|
+
) {
|
|
42
|
+
super();
|
|
43
|
+
this.storageService = storageService;
|
|
44
|
+
this.contextKeyService = contextKeyService;
|
|
45
|
+
this.configurationService = configurationService;
|
|
46
|
+
this.lifecycleService = lifecycleService;
|
|
47
|
+
this.assignmentService = assignmentService;
|
|
48
|
+
this.telemetryService = telemetryService;
|
|
49
|
+
this._triggerListeners = this._register(( new DisposableStore()));
|
|
50
|
+
this._pending = ( new Set());
|
|
51
|
+
this._queue = [];
|
|
52
|
+
this._inflight = ( new Map());
|
|
53
|
+
this._pumping = false;
|
|
54
|
+
this._experimentStates = ( new Map());
|
|
55
|
+
this._onDidChangeOpenedIds = this._register(( new Emitter()));
|
|
56
|
+
this._started = false;
|
|
57
|
+
this._stopped = false;
|
|
58
|
+
this._shownSinceStart = ( new Set());
|
|
59
|
+
this._memento = ( new Memento(OnboardingScenarioService_1.MEMENTO_ID, this.storageService));
|
|
60
|
+
this._state = this._memento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE);
|
|
61
|
+
this._openedAssignmentContextIds = this._loadOpenedIds();
|
|
62
|
+
this.assignmentService.addTelemetryAssignmentFilter({
|
|
63
|
+
id: "onboarding",
|
|
64
|
+
exclude: assignment => assignment.startsWith(ONBOARDING_ASSIGNMENT_CONTEXT_PREFIX) && !( this._openedAssignmentContextIds.has(assignment)),
|
|
65
|
+
onDidChange: this._onDidChangeOpenedIds.event
|
|
66
|
+
});
|
|
67
|
+
this._register(this.lifecycleService.onWillShutdown(() => this._stop()));
|
|
68
|
+
}
|
|
69
|
+
_stop() {
|
|
70
|
+
this._stopped = true;
|
|
71
|
+
this._activeAbort?.fire();
|
|
72
|
+
let entry;
|
|
73
|
+
while ((entry = this._queue.shift())) {
|
|
74
|
+
this._pending.delete(entry.scenario.id);
|
|
75
|
+
entry.deferred.complete(OnboardingOutcome.Aborted);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
start() {
|
|
79
|
+
if (this._started) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this._started = true;
|
|
83
|
+
this._register(onboardingScenarioRegistry.onDidChange(() => {
|
|
84
|
+
this._registerTriggerListeners();
|
|
85
|
+
this._resolveExperiments();
|
|
86
|
+
this._evaluate();
|
|
87
|
+
}));
|
|
88
|
+
this._register(this.contextKeyService.onDidChangeContext(() => this._evaluate()));
|
|
89
|
+
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
|
90
|
+
if (e.affectsConfiguration(ONBOARDING_ENABLED_CONFIG) || e.affectsConfiguration(ONBOARDING_DEVELOPER_MODE_CONFIG)) {
|
|
91
|
+
this._evaluate();
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
this._registerTriggerListeners();
|
|
95
|
+
this._resolveExperiments();
|
|
96
|
+
this._evaluate();
|
|
97
|
+
}
|
|
98
|
+
getScenarios() {
|
|
99
|
+
return onboardingScenarioRegistry.getScenarios();
|
|
100
|
+
}
|
|
101
|
+
async runScenario(id) {
|
|
102
|
+
const scenario = onboardingScenarioRegistry.getScenario(id);
|
|
103
|
+
if (!scenario) {
|
|
104
|
+
throw ( new Error(`Unknown onboarding scenario '${id}'.`));
|
|
105
|
+
}
|
|
106
|
+
return this._enqueue(scenario);
|
|
107
|
+
}
|
|
108
|
+
hasBeenShown(id) {
|
|
109
|
+
const scenario = onboardingScenarioRegistry.getScenario(id);
|
|
110
|
+
return this._hasBeenShownKey(scenario ? this._seenKey(scenario) : id, id);
|
|
111
|
+
}
|
|
112
|
+
reset(id) {
|
|
113
|
+
const scenario = onboardingScenarioRegistry.getScenario(id);
|
|
114
|
+
delete this._state[scenario ? this._seenKey(scenario) : id];
|
|
115
|
+
this._memento.saveMemento();
|
|
116
|
+
}
|
|
117
|
+
resetAll() {
|
|
118
|
+
for (const key of ( Object.keys(this._state))) {
|
|
119
|
+
delete this._state[key];
|
|
120
|
+
}
|
|
121
|
+
this._memento.saveMemento();
|
|
122
|
+
}
|
|
123
|
+
get _enabled() {
|
|
124
|
+
return this.configurationService.getValue(ONBOARDING_ENABLED_CONFIG) !== false;
|
|
125
|
+
}
|
|
126
|
+
_isDeveloperMode(scenarioId) {
|
|
127
|
+
return isOnboardingDeveloperModeEnabled(this.configurationService, scenarioId);
|
|
128
|
+
}
|
|
129
|
+
_evaluate() {
|
|
130
|
+
if (!this._enabled || this._stopped) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const claimedSeenKeys = ( new Set());
|
|
134
|
+
for (const scenario of onboardingScenarioRegistry.getScenarios()) {
|
|
135
|
+
if (!scenario.repeatable && ( this._pending.has(scenario.id))) {
|
|
136
|
+
claimedSeenKeys.add(this._seenKey(scenario));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const scenario of onboardingScenarioRegistry.getScenarios()) {
|
|
140
|
+
if (!this._isAutoEligible(scenario)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const seenKey = this._seenKey(scenario);
|
|
144
|
+
if (!scenario.repeatable && ( claimedSeenKeys.has(seenKey))) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const experiment = scenario.experiment ? this._experimentStates.get(scenario.id) : undefined;
|
|
148
|
+
if (experiment?.active && !this._isDeveloperMode(scenario.id)) {
|
|
149
|
+
this._openGate(experiment.assignmentContextId);
|
|
150
|
+
if (!experiment.behavior) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
this._enqueue(scenario);
|
|
155
|
+
if (!scenario.repeatable) {
|
|
156
|
+
claimedSeenKeys.add(seenKey);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
_isAutoEligible(scenario) {
|
|
161
|
+
if (scenario.trigger.kind === "command") {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if (( this._pending.has(scenario.id))) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (!scenario.repeatable && this._hasBeenShownKey(this._seenKey(scenario), scenario.id)) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
if (scenario.when && !this.contextKeyService.contextMatchesRules(scenario.when)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (scenario.experiment && this._experimentStates.get(scenario.id)?.active !== true && !this._isDeveloperMode(scenario.id)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
if (scenario.trigger.kind === "observable" && scenario.trigger.signal.get() !== true) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
_enqueue(scenario) {
|
|
182
|
+
if (this._stopped) {
|
|
183
|
+
return Promise.resolve(OnboardingOutcome.Aborted);
|
|
184
|
+
}
|
|
185
|
+
const queued = this._queue.find(entry => entry.scenario.id === scenario.id);
|
|
186
|
+
if (queued) {
|
|
187
|
+
return queued.deferred.p;
|
|
188
|
+
}
|
|
189
|
+
const inflight = this._inflight.get(scenario.id);
|
|
190
|
+
if (inflight) {
|
|
191
|
+
return inflight.p;
|
|
192
|
+
}
|
|
193
|
+
const deferred = ( new DeferredPromise());
|
|
194
|
+
this._pending.add(scenario.id);
|
|
195
|
+
this._queue.push({
|
|
196
|
+
scenario,
|
|
197
|
+
deferred
|
|
198
|
+
});
|
|
199
|
+
this._queue.sort((a, b) => (b.scenario.priority ?? 0) - (a.scenario.priority ?? 0));
|
|
200
|
+
this._pump();
|
|
201
|
+
return deferred.p;
|
|
202
|
+
}
|
|
203
|
+
_pump() {
|
|
204
|
+
if (this._pumping) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this._pumping = true;
|
|
208
|
+
this._doPump();
|
|
209
|
+
}
|
|
210
|
+
async _doPump() {
|
|
211
|
+
await Promise.resolve();
|
|
212
|
+
try {
|
|
213
|
+
let entry;
|
|
214
|
+
while (!this._stopped && (entry = this._queue.shift())) {
|
|
215
|
+
const {
|
|
216
|
+
scenario,
|
|
217
|
+
deferred
|
|
218
|
+
} = entry;
|
|
219
|
+
this._inflight.set(scenario.id, deferred);
|
|
220
|
+
let outcome;
|
|
221
|
+
try {
|
|
222
|
+
outcome = await this._runPresentation(scenario);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
onUnexpectedError(error);
|
|
225
|
+
outcome = OnboardingOutcome.Aborted;
|
|
226
|
+
} finally {
|
|
227
|
+
this._inflight.delete(scenario.id);
|
|
228
|
+
this._pending.delete(scenario.id);
|
|
229
|
+
}
|
|
230
|
+
deferred.complete(outcome);
|
|
231
|
+
}
|
|
232
|
+
} finally {
|
|
233
|
+
this._pumping = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async _runPresentation(scenario) {
|
|
237
|
+
const presentation = onboardingPresentationRegistry.get(scenario.presentation.kind);
|
|
238
|
+
if (!presentation) {
|
|
239
|
+
return OnboardingOutcome.Aborted;
|
|
240
|
+
}
|
|
241
|
+
this._markShown(this._seenKey(scenario));
|
|
242
|
+
const abort = ( new Emitter());
|
|
243
|
+
this._activeAbort = abort;
|
|
244
|
+
const startTime = Date.now();
|
|
245
|
+
try {
|
|
246
|
+
const result = await presentation.run(scenario, {
|
|
247
|
+
targetWindow: mainWindow,
|
|
248
|
+
onAbort: abort.event
|
|
249
|
+
});
|
|
250
|
+
this._recordOutcome(this._seenKey(scenario), result.outcome);
|
|
251
|
+
if (result.shown) {
|
|
252
|
+
this._reportOutcome(scenario, result, Date.now() - startTime);
|
|
253
|
+
}
|
|
254
|
+
return result.outcome;
|
|
255
|
+
} finally {
|
|
256
|
+
this._activeAbort = undefined;
|
|
257
|
+
abort.dispose();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
_reportOutcome(scenario, result, durationMs) {
|
|
261
|
+
const experimentActive = !!scenario.experiment && this._experimentStates.get(scenario.id)?.active === true;
|
|
262
|
+
this.telemetryService.publicLog2("onboarding.scenarioOutcome", {
|
|
263
|
+
scenarioId: scenario.id,
|
|
264
|
+
outcome: result.outcome,
|
|
265
|
+
dismissReason: result.dismissReason,
|
|
266
|
+
lastStepIndex: result.lastStepIndex,
|
|
267
|
+
stepCount: result.stepCount,
|
|
268
|
+
durationMs,
|
|
269
|
+
experimentActive
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
_registerTriggerListeners() {
|
|
273
|
+
this._triggerListeners.clear();
|
|
274
|
+
for (const scenario of onboardingScenarioRegistry.getScenarios()) {
|
|
275
|
+
if (scenario.trigger.kind === "observable") {
|
|
276
|
+
const signal = scenario.trigger.signal;
|
|
277
|
+
this._triggerListeners.add(autorun(reader => {
|
|
278
|
+
signal.read(reader);
|
|
279
|
+
this._evaluate();
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
_resolveExperiments() {
|
|
285
|
+
for (const scenario of onboardingScenarioRegistry.getScenarios()) {
|
|
286
|
+
const experiment = scenario.experiment;
|
|
287
|
+
if (!experiment || ( this._experimentStates.has(scenario.id))) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
this._experimentStates.set(scenario.id, {
|
|
291
|
+
active: false,
|
|
292
|
+
behavior: false,
|
|
293
|
+
assignmentContextId: ""
|
|
294
|
+
});
|
|
295
|
+
Promise.all([
|
|
296
|
+
this.assignmentService.getTreatment(experiment.behaviorFlag),
|
|
297
|
+
this.assignmentService.getTreatment(experiment.assignmentContextIdFlag)
|
|
298
|
+
]).then(([behavior, assignmentContextId]) => {
|
|
299
|
+
const hasBehavior = typeof behavior === "boolean";
|
|
300
|
+
const hasId = typeof assignmentContextId === "string" && assignmentContextId.length > 0;
|
|
301
|
+
const hasValidId = hasId && assignmentContextId.startsWith(ONBOARDING_ASSIGNMENT_CONTEXT_PREFIX);
|
|
302
|
+
if (hasId && !hasValidId) {
|
|
303
|
+
onUnexpectedError(( new Error(
|
|
304
|
+
`Onboarding experiment for scenario '${scenario.id}' resolved an assignment-context id '${assignmentContextId}' that does not start with the required '${ONBOARDING_ASSIGNMENT_CONTEXT_PREFIX}' prefix; treating the experiment as inactive.`
|
|
305
|
+
)));
|
|
306
|
+
}
|
|
307
|
+
const active = hasBehavior && hasValidId;
|
|
308
|
+
this._experimentStates.set(scenario.id, {
|
|
309
|
+
active,
|
|
310
|
+
behavior: behavior === true,
|
|
311
|
+
assignmentContextId: active ? assignmentContextId : ""
|
|
312
|
+
});
|
|
313
|
+
if (active) {
|
|
314
|
+
this._evaluate();
|
|
315
|
+
}
|
|
316
|
+
}, error => onUnexpectedError(error));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
_loadOpenedIds() {
|
|
320
|
+
const raw = this.storageService.get(
|
|
321
|
+
OnboardingScenarioService_1.OPENED_IDS_STORAGE_KEY,
|
|
322
|
+
StorageScope.APPLICATION
|
|
323
|
+
);
|
|
324
|
+
if (!raw) {
|
|
325
|
+
return ( new Set());
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
const parsed = JSON.parse(raw);
|
|
329
|
+
return Array.isArray(parsed) ? ( new Set(parsed.filter(id => typeof id === "string"))) : ( new Set());
|
|
330
|
+
} catch (error) {
|
|
331
|
+
onUnexpectedError(error);
|
|
332
|
+
return ( new Set());
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
_openGate(assignmentContextId) {
|
|
336
|
+
if (!assignmentContextId || ( this._openedAssignmentContextIds.has(assignmentContextId))) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
this._openedAssignmentContextIds.add(assignmentContextId);
|
|
340
|
+
this.storageService.store(
|
|
341
|
+
OnboardingScenarioService_1.OPENED_IDS_STORAGE_KEY,
|
|
342
|
+
JSON.stringify(Array.from(this._openedAssignmentContextIds)),
|
|
343
|
+
StorageScope.APPLICATION,
|
|
344
|
+
StorageTarget.MACHINE
|
|
345
|
+
);
|
|
346
|
+
this._onDidChangeOpenedIds.fire();
|
|
347
|
+
}
|
|
348
|
+
_seenKey(scenario) {
|
|
349
|
+
return scenario.seenKey ?? scenario.id;
|
|
350
|
+
}
|
|
351
|
+
_hasBeenShownKey(key, scenarioId) {
|
|
352
|
+
if (this._isDeveloperMode(scenarioId)) {
|
|
353
|
+
return ( this._shownSinceStart.has(key));
|
|
354
|
+
}
|
|
355
|
+
return !!this._state[key]?.shownAt;
|
|
356
|
+
}
|
|
357
|
+
_markShown(id) {
|
|
358
|
+
this._shownSinceStart.add(id);
|
|
359
|
+
const previous = this._state[id];
|
|
360
|
+
const next = {
|
|
361
|
+
shownAt: Date.now(),
|
|
362
|
+
outcome: previous?.outcome,
|
|
363
|
+
seenCount: (previous?.seenCount ?? 0) + 1
|
|
364
|
+
};
|
|
365
|
+
this._state[id] = next;
|
|
366
|
+
this._memento.saveMemento();
|
|
367
|
+
}
|
|
368
|
+
_recordOutcome(id, outcome) {
|
|
369
|
+
const state = this._state[id];
|
|
370
|
+
if (state) {
|
|
371
|
+
state.outcome = outcome;
|
|
372
|
+
this._memento.saveMemento();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
OnboardingScenarioService = OnboardingScenarioService_1 = ( __decorate([( __param(0, IStorageService)), ( __param(1, IContextKeyService)), ( __param(2, IConfigurationService)), ( __param(3, ILifecycleService)), ( __param(4, IWorkbenchAssignmentService)), ( __param(5, ITelemetryService))], OnboardingScenarioService));
|
|
377
|
+
|
|
378
|
+
export { OnboardingScenarioService };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { IDisposable } from "@codingame/monaco-vscode-api/vscode/vs/base/common/lifecycle";
|
|
2
|
+
/**
|
|
3
|
+
* Attribute used to mark a DOM element as an onboarding spotlight target.
|
|
4
|
+
*
|
|
5
|
+
* Components opt in by tagging their *own* element with this attribute. Tours
|
|
6
|
+
* reference the id; they never reach into another component's markup or CSS
|
|
7
|
+
* classes to locate a target (see the sessions "DOM Traversal & Intent" rule).
|
|
8
|
+
*/
|
|
9
|
+
export declare const ONBOARDING_TARGET_ATTR = "data-onboarding-id";
|
|
10
|
+
export declare const ONBOARDING_TARGET_PULSE_CLASS = "onboarding-target-pulse";
|
|
11
|
+
/**
|
|
12
|
+
* Marks `element` as the onboarding target identified by `id`.
|
|
13
|
+
*
|
|
14
|
+
* @returns A disposable that removes the attribute again.
|
|
15
|
+
*/
|
|
16
|
+
export declare function markOnboardingTarget(element: HTMLElement, id: string): IDisposable;
|
|
17
|
+
/**
|
|
18
|
+
* Applies the standard onboarding pulse treatment to `element`.
|
|
19
|
+
*
|
|
20
|
+
* @returns A disposable that removes the pulse again.
|
|
21
|
+
*/
|
|
22
|
+
export declare function pulseOnboardingTarget(element: HTMLElement): IDisposable;
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the element marked with the given onboarding target id within the
|
|
25
|
+
* provided window's document. Returns `undefined` if no such element exists
|
|
26
|
+
* (e.g. the feature is not currently rendered).
|
|
27
|
+
*
|
|
28
|
+
* This is the *only* place onboarding queries the DOM, and it matches solely on
|
|
29
|
+
* the onboarding attribute — never on foreign classes or structure.
|
|
30
|
+
*/
|
|
31
|
+
export declare function findOnboardingTarget(targetWindow: Window, id: string): HTMLElement | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
import { registerCss } from '@codingame/monaco-vscode-api/css';
|
|
3
|
+
import '@codingame/monaco-vscode-api/vscode/vs/base/common/lifecycle';
|
|
4
|
+
import * as onboardingTarget from '../media/onboardingTarget.css';
|
|
5
|
+
|
|
6
|
+
registerCss(onboardingTarget);
|
|
7
|
+
const ONBOARDING_TARGET_ATTR = "data-onboarding-id";
|
|
8
|
+
function findOnboardingTarget(targetWindow, id) {
|
|
9
|
+
const selector = `[${ONBOARDING_TARGET_ATTR}="${CSS.escape(id)}"]`;
|
|
10
|
+
const targets = Array.from(targetWindow.document.querySelectorAll(selector));
|
|
11
|
+
return targets.find(target => isVisibleOnboardingTarget(targetWindow, target));
|
|
12
|
+
}
|
|
13
|
+
function isVisibleOnboardingTarget(targetWindow, target) {
|
|
14
|
+
if (!target.isConnected) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const style = targetWindow.getComputedStyle(target);
|
|
18
|
+
if (style.display === "none" || style.visibility === "hidden") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const rect = target.getBoundingClientRect();
|
|
22
|
+
return rect.width > 0 && rect.height > 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { ONBOARDING_TARGET_ATTR, findOnboardingTarget };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Disposable } from "@codingame/monaco-vscode-api/vscode/vs/base/common/lifecycle";
|
|
2
|
+
import { Event } from "@codingame/monaco-vscode-api/vscode/vs/base/common/event";
|
|
3
|
+
import { IMarkdownString } from "@codingame/monaco-vscode-api/vscode/vs/base/common/htmlContent";
|
|
4
|
+
import { SpotlightPlacement } from "./spotlightTypes.js";
|
|
5
|
+
import { OnboardingDismissReason } from "../../common/onboardingScenario.js";
|
|
6
|
+
/** How the user advanced to the next step. */
|
|
7
|
+
export type SpotlightAdvanceSource = "button" | "target";
|
|
8
|
+
/** Why a step ended in a skip: the Skip button or the Escape key. */
|
|
9
|
+
export type SpotlightSkipReason = OnboardingDismissReason.SkipButton | OnboardingDismissReason.EscapeKey;
|
|
10
|
+
/** Content rendered inside the spotlight callout for a single step. */
|
|
11
|
+
export interface ISpotlightContent {
|
|
12
|
+
readonly title: string;
|
|
13
|
+
readonly description: string | IMarkdownString;
|
|
14
|
+
/** Zero-based index of the current step. */
|
|
15
|
+
readonly stepIndex: number;
|
|
16
|
+
/** Total number of steps in the tour. */
|
|
17
|
+
readonly stepCount: number;
|
|
18
|
+
/** Whether a "Back" action should be offered. */
|
|
19
|
+
readonly canGoBack: boolean;
|
|
20
|
+
/** Whether this is the final step (the primary button becomes "Done"). */
|
|
21
|
+
readonly isLastStep: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Options controlling how a step is shown. */
|
|
24
|
+
export interface ISpotlightShowOptions {
|
|
25
|
+
readonly placement?: SpotlightPlacement;
|
|
26
|
+
readonly allowTargetInteraction?: boolean;
|
|
27
|
+
readonly padding?: number;
|
|
28
|
+
/**
|
|
29
|
+
* When set, the step advances (fires `onDidClickNext`) when the user clicks
|
|
30
|
+
* the spotlighted target itself. The "Next" button is hidden and the target
|
|
31
|
+
* is kept interactive so the user can press it to continue.
|
|
32
|
+
*/
|
|
33
|
+
readonly advanceOnTargetClick?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A pure-DOM spotlight overlay: dims the window, cuts a highlight hole around a
|
|
37
|
+
* target element and shows an anchored callout. It owns no VS Code services so
|
|
38
|
+
* it can be unit-tested and reused. Scheduling and content come from the
|
|
39
|
+
* spotlight presentation.
|
|
40
|
+
*/
|
|
41
|
+
export declare class SpotlightOverlay extends Disposable {
|
|
42
|
+
private readonly _container;
|
|
43
|
+
private readonly _resizeObserverCtor;
|
|
44
|
+
private readonly _root;
|
|
45
|
+
private readonly _blockers;
|
|
46
|
+
private readonly _hole;
|
|
47
|
+
private readonly _pointer;
|
|
48
|
+
private readonly _callout;
|
|
49
|
+
private readonly _title;
|
|
50
|
+
private readonly _description;
|
|
51
|
+
private readonly _counter;
|
|
52
|
+
private readonly _descriptionRenderStore;
|
|
53
|
+
private readonly _backButton;
|
|
54
|
+
private readonly _nextButton;
|
|
55
|
+
private readonly _skipButton;
|
|
56
|
+
/** Listeners scoped to the currently shown step (re-layout sources). */
|
|
57
|
+
private readonly _stepListeners;
|
|
58
|
+
private readonly _onDidClickNext;
|
|
59
|
+
readonly onDidClickNext: Event<SpotlightAdvanceSource>;
|
|
60
|
+
private readonly _onDidClickPrevious;
|
|
61
|
+
readonly onDidClickPrevious: Event<void>;
|
|
62
|
+
private readonly _onDidSkip;
|
|
63
|
+
readonly onDidSkip: Event<SpotlightSkipReason>;
|
|
64
|
+
private _target;
|
|
65
|
+
private _options;
|
|
66
|
+
private _previousFocus;
|
|
67
|
+
private _scheduledLayout;
|
|
68
|
+
constructor(_container: HTMLElement, _resizeObserverCtor?: typeof ResizeObserver);
|
|
69
|
+
/** Show `content` spotlighting `target`. */
|
|
70
|
+
show(target: HTMLElement, content: ISpotlightContent, options?: ISpotlightShowOptions): void;
|
|
71
|
+
/** Recompute the hole and callout positions for the current target. */
|
|
72
|
+
layout(): void;
|
|
73
|
+
private _layoutBlocker;
|
|
74
|
+
private _layoutCallout;
|
|
75
|
+
private _centerCallout;
|
|
76
|
+
private _getPointerSide;
|
|
77
|
+
private _offsetCalloutForPointer;
|
|
78
|
+
private _layoutPointer;
|
|
79
|
+
private _clamp;
|
|
80
|
+
private _resolvePlacement;
|
|
81
|
+
private _renderContent;
|
|
82
|
+
private _onKeyDown;
|
|
83
|
+
private _trapFocus;
|
|
84
|
+
/**
|
|
85
|
+
* The focusable elements participating in the focus trap, in DOM order: the
|
|
86
|
+
* spotlighted target (when the step advances by pressing it), then any
|
|
87
|
+
* interactive content in the (possibly markdown) description, then the visible
|
|
88
|
+
* action buttons. Including the target keeps the spotlighted control
|
|
89
|
+
* keyboard-reachable, and querying the description keeps markdown links
|
|
90
|
+
* reachable despite `aria-modal`.
|
|
91
|
+
*/
|
|
92
|
+
private _collectFocusable;
|
|
93
|
+
scheduleLayout(): void;
|
|
94
|
+
private _restoreFocus;
|
|
95
|
+
dispose(): void;
|
|
96
|
+
}
|