Skip to content

Commit 3ebd9a5

Browse files
committed
Add cell/clipboard copy & paste tracking
Copying and pasting a whole cell or from the clipboard is tracked and sent to the backend, along with the copied/pasted content.
1 parent e1c88a4 commit 3ebd9a5

File tree

5 files changed

+175
-6
lines changed

5 files changed

+175
-6
lines changed

src/PanelManager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ import { ExecutionDisposable } from './trackers/ExecutionDisposable';
1313
import { AlterationDisposable } from './trackers/AlterationDisposable';
1414
import { FocusDisposable } from './trackers/FocusDisposable';
1515
import { disabledNotebooksSignaler } from '.';
16+
import { JupyterFrontEnd } from '@jupyterlab/application';
1617

1718
export class PanelManager {
1819
constructor(
20+
app: JupyterFrontEnd,
1921
settings: ISettingRegistry.ISettings,
2022
dialogShownSettings: ISettingRegistry.ISettings
2123
) {
24+
this._app = app;
2225
this._panel = null;
2326

2427
this._isDataCollectionEnabled = settings.get(EXTENSION_SETTING_NAME)
@@ -108,6 +111,7 @@ export class PanelManager {
108111
this._isDataCollectionEnabled
109112
) {
110113
this._focusDisposable = new FocusDisposable(
114+
this._app.commands,
111115
this._panel,
112116
notebookId
113117
);
@@ -209,6 +213,7 @@ export class PanelManager {
209213
this.panel = null;
210214
}
211215

216+
private _app: JupyterFrontEnd;
212217
private _panel: NotebookPanel | null;
213218
private _ongoingContextId = '';
214219
private _websocketManager: WebsocketManager;

src/api.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
ICodeExecObject,
88
INotebookClickObject,
99
IMarkdownExecObject,
10-
PostDataObject
10+
PostDataObject,
11+
ICellCopyObject,
12+
ICellPasteObject,
13+
IClipBoardObject
1114
} from './utils/types';
1215

1316
const postRequest = (data: PostDataObject, endpoint: string): void => {
@@ -58,6 +61,22 @@ export const postCellClick = (cellClick: ICellClickObject): void => {
5861
postRequest(cellClick, 'clickevent/cell');
5962
};
6063

64+
export const postCellCopy = (cellCopy: ICellCopyObject): void => {
65+
postRequest(cellCopy, 'copyevent/cell');
66+
};
67+
68+
export const postCellPaste = (cellPaste: ICellPasteObject): void => {
69+
postRequest(cellPaste, 'pasteevent/cell');
70+
};
71+
72+
export const postClipboardCopy = (clipboardCopy: IClipBoardObject): void => {
73+
postRequest(clipboardCopy, 'copyevent/clipboard');
74+
};
75+
76+
export const postClipboardPaste = (clipboardPaste: IClipBoardObject): void => {
77+
postRequest(clipboardPaste, 'pasteevent/clipboard');
78+
};
79+
6180
export const postNotebookClick = (
6281
notebookClick: INotebookClickObject
6382
): void => {

src/dataCollectionPlugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const dataCollectionPlugin = async (
4343
onEndpointChanged(endpointSettings);
4444
endpointSettings.changed.connect(onEndpointChanged);
4545

46-
const panelManager = new PanelManager(settings, dialogShownSettings);
46+
const panelManager = new PanelManager(app, settings, dialogShownSettings);
4747

4848
const labShell = app.shell as LabShell;
4949

src/trackers/FocusDisposable.ts

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,36 @@
11
import { Signal } from '@lumino/signaling';
22
import { IDisposable } from '@lumino/disposable';
3-
import { Cell, ICellModel } from '@jupyterlab/cells';
3+
import { CommandRegistry } from '@lumino/commands';
4+
import { Cell, CodeCell, ICellModel } from '@jupyterlab/cells';
45
import { Notebook, NotebookPanel } from '@jupyterlab/notebook';
5-
import { postNotebookClick, postCellClick } from '../api';
6+
import {
7+
postNotebookClick,
8+
postCellClick,
9+
postCellCopy,
10+
postCellPaste,
11+
postClipboardCopy,
12+
postClipboardPaste
13+
} from '../api';
614
import { Selectors } from '../utils/constants';
715
import { CompatibilityManager } from '../utils/compatibility';
816

917
type ClickType = 'OFF' | 'ON';
18+
interface CellCopyData {
19+
lastCopyCellId: string;
20+
lastCopyNotebookId: string;
21+
lastCopyTime: string;
22+
lastCopyContent: string;
23+
}
24+
25+
const CELL_COPY_ID: string = 'notebook:copy-cell';
26+
const CELL_PASTE_ID: string = 'notebook:paste-cell-below';
1027

1128
export class FocusDisposable implements IDisposable {
12-
constructor(panel: NotebookPanel, notebookId: string) {
29+
constructor(
30+
commands: CommandRegistry,
31+
panel: NotebookPanel,
32+
notebookId: string
33+
) {
1334
this._panel = panel;
1435

1536
this._notebookId = notebookId;
@@ -22,6 +43,13 @@ export class FocusDisposable implements IDisposable {
2243
// connect to active cell changes
2344
panel.content.activeCellChanged.connect(this._onCellChanged, this);
2445

46+
// connect to commands executed
47+
commands.commandExecuted.connect(this._onCommandExecuted, this);
48+
49+
// Add listener to copy and paste (to clipbaord)
50+
document.addEventListener('copy', this._onClipboardCopy);
51+
document.addEventListener('paste', this._onClipboardPaste);
52+
2553
// panel.content is disposed before panel itself, so release the associated connection before
2654
panel.content.disposed.connect(this._onContentDisposed, this);
2755
}
@@ -44,10 +72,56 @@ export class FocusDisposable implements IDisposable {
4472
this._sendCellClick('ON');
4573
};
4674

75+
private _onCommandExecuted = (
76+
commandR: CommandRegistry,
77+
args: CommandRegistry.ICommandExecutedArgs
78+
) => {
79+
if (args.id === CELL_COPY_ID) {
80+
this._onCopyCommandExecuted();
81+
} else if (args.id === CELL_PASTE_ID) {
82+
this._onPasteCommandExecuted();
83+
}
84+
};
85+
86+
private _onCopyCommandExecuted = () => {
87+
console.log('A cell was copied!');
88+
if (this._lastActiveCellId && this._lastActiveCellContent) {
89+
this._cellCopyData = {
90+
lastCopyNotebookId: this._notebookId,
91+
lastCopyCellId: this._lastActiveCellId,
92+
lastCopyTime: new Date().toISOString(),
93+
lastCopyContent: this._lastActiveCellContent
94+
};
95+
this._sendCellCopy();
96+
}
97+
};
98+
99+
private _onPasteCommandExecuted = () => {
100+
console.log('A cell was pasted!');
101+
this._sendCellPaste();
102+
};
103+
104+
private _onClipboardCopy = (event: ClipboardEvent) => {
105+
const content = event.clipboardData?.getData('text');
106+
console.log('Clipboard copied ', content);
107+
if (content) {
108+
this._sendClipboardCopy(content);
109+
}
110+
};
111+
112+
private _onClipboardPaste = (event: ClipboardEvent) => {
113+
const content = event.clipboardData?.getData('text');
114+
console.log('Clipboard pasted ', content);
115+
if (content) {
116+
this._sendClipboardPaste(content);
117+
}
118+
};
119+
47120
private _setActiveCellAndOrigCellId = (
48121
activeCell: Cell<ICellModel> | null
49122
) => {
50123
this._lastActiveCellId = activeCell?.model.sharedModel.getId();
124+
this._lastActiveCellContent = activeCell?.model.toJSON().source.toString();
51125
if (this._lastActiveCellId) {
52126
this._lastOrigCellId = CompatibilityManager.getMetadataComp(
53127
this._panel?.model,
@@ -83,6 +157,53 @@ export class FocusDisposable implements IDisposable {
83157
}
84158
};
85159

160+
private _sendCellCopy = () => {
161+
if (this._cellCopyData) {
162+
postCellCopy({
163+
notebook_id: this._cellCopyData.lastCopyNotebookId,
164+
cell_id: this._cellCopyData.lastCopyCellId,
165+
time: this._cellCopyData.lastCopyTime,
166+
content: this._cellCopyData.lastCopyContent
167+
});
168+
}
169+
};
170+
171+
private _sendCellPaste = () => {
172+
if (this._cellCopyData && this._lastActiveCellId) {
173+
postCellPaste({
174+
notebook_id: this._notebookId,
175+
copied_notebook_id: this._cellCopyData.lastCopyNotebookId,
176+
copied_cell_id: this._cellCopyData.lastCopyCellId,
177+
copied_time: this._cellCopyData.lastCopyTime,
178+
cell_id: this._lastActiveCellId,
179+
time: new Date().toISOString(),
180+
content: this._cellCopyData.lastCopyContent
181+
});
182+
}
183+
};
184+
185+
private _sendClipboardCopy = (content: string) => {
186+
if (this._lastActiveCellId) {
187+
postClipboardCopy({
188+
notebook_id: this._notebookId,
189+
cell_id: this._lastActiveCellId,
190+
time: new Date().toISOString(),
191+
content: content
192+
});
193+
}
194+
};
195+
196+
private _sendClipboardPaste = (content: string) => {
197+
if (this._lastActiveCellId) {
198+
postClipboardPaste({
199+
notebook_id: this._notebookId,
200+
cell_id: this._lastActiveCellId,
201+
time: new Date().toISOString(),
202+
content: content
203+
});
204+
}
205+
};
206+
86207
private _sendNotebookClick = (clickType: ClickType) => {
87208
let notebookDurationSec: number | null = null;
88209
if (clickType === 'ON') {
@@ -116,15 +237,20 @@ export class FocusDisposable implements IDisposable {
116237

117238
this._isDisposed = true;
118239
this._lastActiveCellId = null;
240+
this._cellCopyData = null;
119241

242+
document.removeEventListener('copy', this._onClipboardCopy);
243+
document.removeEventListener('paste', this._onClipboardPaste);
120244
Signal.clearData(this);
121245
}
122246

123247
private _isDisposed = false;
124248
private _panel: NotebookPanel;
125249
private _notebookId: string;
126250
private _lastActiveCellId: string | null | undefined = null;
251+
private _lastActiveCellContent: string | null | undefined = null;
127252
private _lastOrigCellId: string | null | undefined = null;
253+
private _cellCopyData: CellCopyData | null | undefined = null;
128254

129255
private _notebookStart: Date = new Date();
130256
private _cellStart: Date = new Date();

src/utils/types.d.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,28 @@ export interface ICellAlterationObject extends IBaseEvent {
4444
time: string;
4545
}
4646

47+
interface ICopyPaste extends IBaseEvent {
48+
cell_id: string;
49+
time: string;
50+
content: string;
51+
}
52+
53+
export interface ICellCopyObject extends ICopyPaste {}
54+
55+
export interface ICellPasteObject extends ICopyPaste {
56+
copied_notebook_id: string;
57+
copied_cell_id: string;
58+
copied_time: string;
59+
}
60+
61+
export interface IClipBoardObject extends ICopyPaste {}
62+
4763
export type PostDataObject =
4864
| ICodeExecObject
4965
| IMarkdownExecObject
5066
| INotebookClickObject
5167
| ICellClickObject
52-
| ICellAlterationObject;
68+
| ICellAlterationObject
69+
| ICellCopyObject
70+
| ICellPasteObject
71+
| IClipBoardObject;

0 commit comments

Comments
 (0)