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