Skip to content

Commit d57049e

Browse files
committed
fbshipit-source-id: a4da583765e9992780a6f7bdeca3adaee783fc9f
1 parent 406d90b commit d57049e

8 files changed

Lines changed: 3876 additions & 6 deletions

File tree

vscode/core/src/index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
/**
2-
* (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
36
*
47
* @format
58
*/
69

7-
export * from '../../../../../xplat/vscode/modules/tintype-vscode-core/src';
10+
export * from './snapshot';
11+
export * from './snappoint';
Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,142 @@
11
/**
2-
* (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
36
*
47
* @format
58
*/
69

7-
export * from '../../../../../../xplat/vscode/modules/tintype-vscode-core/src/snappoint';
10+
import vscode from 'vscode';
11+
import {SnappointManager} from './snappoint-manager';
12+
import type {SnapshotProvider} from '../snapshot/snapshot-provider';
13+
14+
export * from './snappoint-manager';
15+
16+
export type SnappointProviderOptions = {
17+
/**
18+
* Debug type or types of the parent live sessions snappoints fire against.
19+
* The owning host is responsible for preparing its capture runtime.
20+
*/
21+
injectionDebugType: string | readonly string[];
22+
commandPrefix: string;
23+
/** Absolute path to the owning extension's root. */
24+
extensionPath: string;
25+
/**
26+
* Companion snapshot provider. The DAP processor calls
27+
* :meth:`SnapshotProvider.ensureSnapshotting` before forwarding a
28+
* snappoint-containing ``setBreakpoints`` and
29+
* :meth:`SnapshotProvider.refreshSnapshotList` when a
30+
* ``tintypeSnapshotAdded`` event fires.
31+
*/
32+
snapshotProvider: SnapshotProvider;
33+
/** Register authoring commands and gutter decorations for this host. */
34+
enableAuthoringUI?: boolean;
35+
};
36+
37+
let currentManager: SnappointManager | null = null;
38+
let currentSnapshotProvider: SnapshotProvider | null = null;
39+
let currentInjectionDebugType: string | readonly string[] | null = null;
40+
41+
/**
42+
* Get the active SnappointManager. Returns ``null`` before
43+
* :func:`registerSnappointProvider` has run. Read by the
44+
* ``createSnappointProcessors`` factory in the extension's DAP
45+
* processor pipeline.
46+
*/
47+
export function getSnappointManager(): SnappointManager | null {
48+
return currentManager;
49+
}
50+
51+
/**
52+
* Get the SnapshotProvider the snappoint module is bound to. Used by
53+
* the DAP processor to ensure injection and refresh viewers.
54+
*/
55+
export function getSnappointSnapshotProvider(): SnapshotProvider | null {
56+
return currentSnapshotProvider;
57+
}
58+
59+
/**
60+
* Debug type or types snappoints rewrite against. The DAP processor uses this
61+
* to skip the snappoint rewrite for sessions of a different type.
62+
*/
63+
export function getSnappointInjectionDebugType(): string | readonly string[] | null {
64+
return currentInjectionDebugType;
65+
}
66+
67+
export function registerSnappointProvider({
68+
injectionDebugType,
69+
commandPrefix,
70+
extensionPath,
71+
snapshotProvider,
72+
enableAuthoringUI = true,
73+
}: SnappointProviderOptions): vscode.Disposable {
74+
const manager = new SnappointManager({
75+
extensionPath,
76+
enableDecorations: enableAuthoringUI,
77+
});
78+
currentManager = manager;
79+
currentSnapshotProvider = snapshotProvider;
80+
currentInjectionDebugType = injectionDebugType;
81+
82+
const toggleCommand = `${commandPrefix}.snappoint.toggle`;
83+
const addCommand = `${commandPrefix}.snappoint.add`;
84+
const removeCommand = `${commandPrefix}.snappoint.remove`;
85+
86+
function resolveTarget(arg: unknown): {uri: vscode.Uri; line: number} | undefined {
87+
// ``editor/lineNumber/context`` invokes the command with a single
88+
// ``{uri, lineNumber}``-shaped arg (lineNumber is 1-based). Keybinding /
89+
// command-palette invocations have no arg, so fall back to the active
90+
// editor's selection.
91+
if (arg != null && typeof arg === 'object') {
92+
const candidate = arg as {uri?: vscode.Uri; lineNumber?: number};
93+
if (candidate.uri instanceof vscode.Uri && typeof candidate.lineNumber === 'number') {
94+
return {uri: candidate.uri, line: candidate.lineNumber - 1};
95+
}
96+
}
97+
const editor = vscode.window.activeTextEditor;
98+
if (editor == null) {
99+
return undefined;
100+
}
101+
return {uri: editor.document.uri, line: editor.selection.active.line};
102+
}
103+
104+
const disposables: vscode.Disposable[] = [
105+
manager,
106+
{
107+
dispose: () => {
108+
if (currentManager === manager) {
109+
currentManager = null;
110+
currentSnapshotProvider = null;
111+
currentInjectionDebugType = null;
112+
}
113+
},
114+
},
115+
];
116+
if (enableAuthoringUI) {
117+
disposables.push(
118+
vscode.commands.registerCommand(toggleCommand, (arg?: unknown) => {
119+
const target = resolveTarget(arg);
120+
if (target == null) {
121+
return;
122+
}
123+
manager.toggle(target.uri, target.line);
124+
}),
125+
vscode.commands.registerCommand(addCommand, (arg?: unknown) => {
126+
const target = resolveTarget(arg);
127+
if (target == null) {
128+
return;
129+
}
130+
manager.add(target.uri, target.line);
131+
}),
132+
vscode.commands.registerCommand(removeCommand, (arg?: unknown) => {
133+
const target = resolveTarget(arg);
134+
if (target == null) {
135+
return;
136+
}
137+
manager.remove(target.uri, target.line);
138+
}),
139+
);
140+
}
141+
return vscode.Disposable.from(...disposables);
142+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
/**
11+
* Snappoints — a "snapshot-and-continue" breakpoint variant.
12+
*
13+
* A snappoint is a real ``vscode.SourceBreakpoint`` whose ``logMessage``
14+
* field carries a fixed marker string (:data:`SNAPPOINT_LOG_MESSAGE_MARKER`).
15+
* VS Code renders it with its built-in logpoint diamond and lists it in
16+
* the Breakpoints view automatically; the snappoint module overlays a
17+
* camera icon in the gutter so users can distinguish snappoints from
18+
* regular logpoints at a glance.
19+
*
20+
* Each host intercepts the marker before ``setBreakpoints`` reaches its
21+
* adapter and prepares its capture runtime. Hosts preserve logpoint
22+
* semantics: evaluate, capture, and continue.
23+
*/
24+
25+
import path from 'path';
26+
import * as vscode from 'vscode';
27+
28+
/**
29+
* Fixed literal stored in ``SourceBreakpoint.logMessage`` to identify a
30+
* snappoint. Picked to be visibly distinct in the Breakpoints view
31+
* label and unlikely to collide with a user's own logpoint text.
32+
*/
33+
export const SNAPPOINT_LOG_MESSAGE_MARKER = '__tintype_snappoint__';
34+
35+
export function isSnappoint(bp: vscode.Breakpoint): bp is vscode.SourceBreakpoint {
36+
if (!(bp instanceof vscode.SourceBreakpoint)) {
37+
return false;
38+
}
39+
return bp.logMessage === SNAPPOINT_LOG_MESSAGE_MARKER;
40+
}
41+
42+
export type SnappointManagerOptions = {
43+
/**
44+
* Absolute path to the extension root. Used to resolve the
45+
* ``resources/{light,dark}/snappoint.svg`` gutter icon paths.
46+
*/
47+
extensionPath: string;
48+
/**
49+
* Languages the toggle command applies to. Snappoints created on
50+
* other languages still round-trip through the marker — this is only
51+
* the authoring filter for the right-click menu invocation.
52+
*/
53+
supportedLanguages?: ReadonlySet<string>;
54+
/** Whether this host should paint snappoint gutter decorations. */
55+
enableDecorations?: boolean;
56+
};
57+
58+
/**
59+
* Owns snappoint authoring (toggle/add/remove) and the gutter
60+
* decoration that overlays a camera icon on snappoint lines.
61+
*
62+
* Persistence is delegated entirely to VS Code's built-in breakpoint
63+
* store — snappoints survive reload because they are real
64+
* ``SourceBreakpoint`` instances. This class is stateless beyond the
65+
* decoration cache.
66+
*/
67+
export class SnappointManager implements vscode.Disposable {
68+
private readonly decorationType: vscode.TextEditorDecorationType | null;
69+
private readonly supportedLanguages: ReadonlySet<string>;
70+
private readonly disposables: vscode.Disposable[] = [];
71+
72+
constructor(options: SnappointManagerOptions) {
73+
this.supportedLanguages = options.supportedLanguages ?? new Set(['python']);
74+
this.decorationType =
75+
options.enableDecorations === false
76+
? null
77+
: vscode.window.createTextEditorDecorationType({
78+
gutterIconPath: vscode.Uri.file(
79+
path.join(options.extensionPath, 'resources', 'light', 'snappoint.svg'),
80+
),
81+
gutterIconSize: 'contain',
82+
dark: {
83+
gutterIconPath: vscode.Uri.file(
84+
path.join(options.extensionPath, 'resources', 'dark', 'snappoint.svg'),
85+
),
86+
},
87+
});
88+
89+
if (this.decorationType != null) {
90+
this.disposables.push(
91+
this.decorationType,
92+
vscode.debug.onDidChangeBreakpoints(() => this.refreshDecorations()),
93+
vscode.window.onDidChangeVisibleTextEditors(() => this.refreshDecorations()),
94+
);
95+
96+
// Initial paint for any editors open at extension activation time.
97+
this.refreshDecorations();
98+
}
99+
}
100+
101+
public dispose(): void {
102+
for (const d of this.disposables) {
103+
d.dispose();
104+
}
105+
}
106+
107+
/**
108+
* Find the existing snappoint at ``(uri, line)`` if any. ``line`` is
109+
* 0-based — same as ``vscode.Position`` / ``vscode.SourceBreakpoint``.
110+
*/
111+
public findAt(uri: vscode.Uri, line: number): vscode.SourceBreakpoint | undefined {
112+
for (const bp of vscode.debug.breakpoints) {
113+
if (!isSnappoint(bp)) {
114+
continue;
115+
}
116+
if (bp.location.uri.toString() === uri.toString() && bp.location.range.start.line === line) {
117+
return bp;
118+
}
119+
}
120+
return undefined;
121+
}
122+
123+
public hasSnappointAt(uri: vscode.Uri, line: number): boolean {
124+
return this.findAt(uri, line) != null;
125+
}
126+
127+
/**
128+
* Add a snappoint at ``(uri, line)``. No-op if one already exists.
129+
* Returns the new (or pre-existing) ``SourceBreakpoint``.
130+
*/
131+
public add(uri: vscode.Uri, line: number): vscode.SourceBreakpoint {
132+
const existing = this.findAt(uri, line);
133+
if (existing != null) {
134+
return existing;
135+
}
136+
const bp = new vscode.SourceBreakpoint(
137+
new vscode.Location(uri, new vscode.Position(line, 0)),
138+
/* enabled */ true,
139+
/* condition */ undefined,
140+
/* hitCondition */ undefined,
141+
SNAPPOINT_LOG_MESSAGE_MARKER,
142+
);
143+
vscode.debug.addBreakpoints([bp]);
144+
return bp;
145+
}
146+
147+
/**
148+
* Remove the snappoint at ``(uri, line)``. No-op if none exists.
149+
* Returns ``true`` when a snappoint was actually removed.
150+
*/
151+
public remove(uri: vscode.Uri, line: number): boolean {
152+
const existing = this.findAt(uri, line);
153+
if (existing == null) {
154+
return false;
155+
}
156+
vscode.debug.removeBreakpoints([existing]);
157+
return true;
158+
}
159+
160+
/**
161+
* Toggle the snappoint at ``(uri, line)``. Returns ``'added'`` or
162+
* ``'removed'`` for the resulting state so callers can log telemetry.
163+
*/
164+
public toggle(uri: vscode.Uri, line: number): 'added' | 'removed' {
165+
if (this.remove(uri, line)) {
166+
return 'removed';
167+
}
168+
this.add(uri, line);
169+
return 'added';
170+
}
171+
172+
/**
173+
* Languages the right-click "Add Snappoint" entry should apply to.
174+
* Used by callers that want to gate UI on document language.
175+
*/
176+
public isSupportedLanguage(languageId: string): boolean {
177+
return this.supportedLanguages.has(languageId);
178+
}
179+
180+
/**
181+
* All snappoints currently registered (across all files). Used by
182+
* the DAP processor to decide whether it needs to wait on
183+
* ``ensureSnapshotting`` before forwarding a ``setBreakpoints``.
184+
*/
185+
public getActiveSnappoints(): vscode.SourceBreakpoint[] {
186+
const out: vscode.SourceBreakpoint[] = [];
187+
for (const bp of vscode.debug.breakpoints) {
188+
if (isSnappoint(bp)) {
189+
out.push(bp);
190+
}
191+
}
192+
return out;
193+
}
194+
195+
private refreshDecorations(): void {
196+
if (this.decorationType == null) {
197+
return;
198+
}
199+
const byUri = new Map<string, vscode.Range[]>();
200+
for (const bp of vscode.debug.breakpoints) {
201+
if (!isSnappoint(bp)) {
202+
continue;
203+
}
204+
const key = bp.location.uri.toString();
205+
const ranges = byUri.get(key) ?? [];
206+
ranges.push(bp.location.range);
207+
byUri.set(key, ranges);
208+
}
209+
for (const editor of vscode.window.visibleTextEditors) {
210+
const ranges = byUri.get(editor.document.uri.toString()) ?? [];
211+
editor.setDecorations(this.decorationType, ranges);
212+
}
213+
}
214+
}

0 commit comments

Comments
 (0)