Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/linkify/vscode-node/notebookCellLinkifier.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { NotebookCell, NotebookDocument } from 'vscode';
7
import { ILogService } from '../../../platform/log/common/logService';
8
import { CellIdPatternRe, getCellIdMap } from '../../../platform/notebook/common/helpers';
9
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
10
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
11
import { LinkifiedPart, LinkifiedText, LinkifyLocationAnchor } from '../common/linkifiedText';
12
import { IContributedLinkifier, LinkifierContext } from '../common/linkifyService';
13
import { Disposable, IDisposable } from '../../../util/vs/base/common/lifecycle';
14
15
/**
16
* Linkifies notebook cell IDs in chat responses.
17
* The linkified text will show as "<Cell ID> (Cell <number>)" where number is the cell's index + 1.
18
*/
19
export class NotebookCellLinkifier extends Disposable implements IDisposable, IContributedLinkifier {
20
private cells = new Map<string, WeakRef<NotebookCell>>();
21
private notebookCellIds = new WeakMap<NotebookDocument, Set<string>>();
22
private initialized = false;
23
constructor(
24
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
25
@ILogService private readonly logger: ILogService,
26
) {
27
super();
28
}
29
30
async linkify(text: string, context: LinkifierContext, token: CancellationToken): Promise<LinkifiedText> {
31
const parts: LinkifiedPart[] = [];
32
33
// Safety check
34
if (!text || !this.workspaceService?.notebookDocuments) {
35
return { parts: [text] };
36
}
37
38
// Early bail if no notebook documents are open
39
const notebookDocuments = this.workspaceService.notebookDocuments;
40
if (!notebookDocuments || notebookDocuments.length === 0) {
41
return { parts: [text] };
42
}
43
44
let lastIndex = 0;
45
for (const match of text.matchAll(CellIdPatternRe)) {
46
const fullMatch = match[0];
47
const cellId = match[2];
48
const index = match.index!;
49
50
// Add text before the match
51
if (index > lastIndex) {
52
parts.push(text.slice(lastIndex, index));
53
}
54
55
// Try to resolve the cell ID to a linkable cell
56
const resolved = this.resolveCellId(cellId);
57
if (resolved) {
58
parts.push(fullMatch.slice(0, fullMatch.indexOf(cellId) + cellId.length));
59
parts.push(' ');
60
parts.push(resolved);
61
parts.push(fullMatch.slice(fullMatch.indexOf(cellId) + cellId.length));
62
} else {
63
parts.push(fullMatch);
64
}
65
lastIndex = index + fullMatch.length;
66
}
67
68
// Add remaining text after the last match
69
if (lastIndex < text.length) {
70
parts.push(text.slice(lastIndex));
71
}
72
73
return { parts };
74
}
75
76
private resolveCellId(cellId: string): LinkifyLocationAnchor | undefined {
77
try {
78
this.initializeCellIds();
79
const cell = this.cells.get(cellId)?.deref();
80
if (!cell) {
81
return;
82
}
83
return new LinkifyLocationAnchor(cell.document.uri, `Cell ${cell.index + 1}`);
84
} catch (error) {
85
this.logger.error(error, `Error resolving cell ID: ${cellId}`);
86
return undefined;
87
}
88
}
89
90
private initializeCellIds() {
91
if (this.initialized) {
92
return;
93
}
94
const updateNbCellIds = (notebook: NotebookDocument) => {
95
const ids = this.notebookCellIds.get(notebook) ?? new Set<string>();
96
ids.forEach(id => this.cells.delete(id));
97
getCellIdMap(notebook).forEach((cell, cellId) => {
98
this.cells.set(cellId, new WeakRef(cell));
99
ids.add(cellId);
100
});
101
this.notebookCellIds.set(notebook, ids);
102
};
103
104
this._register(this.workspaceService.onDidOpenNotebookDocument(notebook => updateNbCellIds(notebook)));
105
this._register(this.workspaceService.onDidCloseNotebookDocument(notebook => {
106
if (this.workspaceService.notebookDocuments.length === 0) {
107
this.cells.clear();
108
return;
109
}
110
const ids = this.notebookCellIds.get(notebook) ?? new Set<string>();
111
ids.forEach(id => this.cells.delete(id));
112
}));
113
this._register(this.workspaceService.onDidChangeNotebookDocument(e => {
114
if (e.contentChanges.length) {
115
updateNbCellIds(e.notebook);
116
}
117
}));
118
this.workspaceService.notebookDocuments.forEach(notebook => updateNbCellIds(notebook));
119
}
120
}
121