diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index de82ea26de3d2..118a9e66da401 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -548,7 +548,7 @@ class WordHighlighter { // ignore typing & other // need to check if the model is a notebook cell, should not stop if nb - if (e.source !== 'api' && e.reason !== CursorChangeReason.Explicit && this.editor.getModel()?.uri.scheme !== Schemas.vscodeNotebookCell) { + if (e.source !== 'api' && e.reason !== CursorChangeReason.Explicit) { this._stopAll(); return; } diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 69edcaa8ecd97..16cd23d554b67 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -116,12 +116,12 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } $registerDebugVisualizerTree(treeId: string, canEdit: boolean): void { - this.visualizerService.registerTree(treeId, { + this._visualizerTreeHandles.set(treeId, this.visualizerService.registerTree(treeId, { disposeItem: id => this._proxy.$disposeVisualizedTree(id), getChildren: e => this._proxy.$getVisualizerTreeItemChildren(treeId, e), getTreeItem: e => this._proxy.$getVisualizerTreeItem(treeId, e), editItem: canEdit ? ((e, v) => this._proxy.$editVisualizerTreeItem(e, v)) : undefined - }); + })); } $unregisterDebugVisualizerTree(treeId: string): void { diff --git a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts index 8ce32d172039c..da5d68df44472 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts @@ -525,6 +525,10 @@ async function getResourceAttachContext(resource: URI, isDirectory: boolean, tex isOmitted = true; } + if (/\.(svg)$/i.test(resource.path)) { + isOmitted = true; + } + return { value: resource, id: resource.toString(), diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 3aade02fa2cb1..94405a7465c4b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -80,7 +80,7 @@ import { revealInSideBarCommand } from '../../files/browser/fileActions.contribu import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; -import { IChatRequestVariableEntry, isLinkVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; +import { IChatRequestVariableEntry, isImageVariableEntry, isLinkVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; import { IChatFollowup } from '../common/chatService.js'; import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; @@ -101,6 +101,7 @@ import { ChatFollowups } from './chatFollowups.js'; import { IChatViewState } from './chatWidget.js'; import { ChatFileReference } from './contrib/chatDynamicVariables/chatFileReference.js'; import { ChatImplicitContext } from './contrib/chatImplicitContext.js'; +import { resizeImage } from './imageUtils.js'; const $ = dom.$; @@ -538,12 +539,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this.container; } - showPreviousValue(): void { + async showPreviousValue(): Promise { const inputState = this.getInputState(); if (this.history.isAtEnd()) { this.saveCurrentValue(inputState); } else { - if (!this.history.has({ text: this._inputEditor.getValue(), state: inputState })) { + const currentEntry = this.getFilteredEntry(this._inputEditor.getValue(), inputState); + if (!this.history.has(currentEntry)) { this.saveCurrentValue(inputState); this.history.resetCursor(); } @@ -552,12 +554,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.navigateHistory(true); } - showNextValue(): void { + async showNextValue(): Promise { const inputState = this.getInputState(); if (this.history.isAtEnd()) { return; } else { - if (!this.history.has({ text: this._inputEditor.getValue(), state: inputState })) { + const currentEntry = this.getFilteredEntry(this._inputEditor.getValue(), inputState); + if (!this.history.has(currentEntry)) { this.saveCurrentValue(inputState); this.history.resetCursor(); } @@ -566,11 +569,30 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.navigateHistory(false); } - private navigateHistory(previous: boolean): void { + private async navigateHistory(previous: boolean): Promise { const historyEntry = previous ? this.history.previous() : this.history.next(); - const historyAttachments = historyEntry.state?.chatContextAttachments ?? []; + let historyAttachments = historyEntry.state?.chatContextAttachments ?? []; + + // Check for images in history to restore the value. + if (historyAttachments.length > 0) { + historyAttachments = (await Promise.all(historyAttachments.map(async (attachment) => { + if (attachment.isImage && attachment.references?.length && URI.isUri(attachment.references[0].reference)) { + try { + const buffer = await this.fileService.readFile(attachment.references[0].reference); + const newAttachment = { ...attachment }; + newAttachment.value = (isImageVariableEntry(attachment) && attachment.isPasted) ? buffer.value.buffer : await resizeImage(buffer.value.buffer); // if pasted image, we do not need to resize. + return newAttachment; + } catch (err) { + this.logService.error('Failed to restore image from history', err); + return undefined; + } + } + return attachment; + }))).filter(attachment => attachment !== undefined); + } + this._attachmentModel.clearAndSetContext(...historyAttachments); aria.status(historyEntry.text); @@ -610,8 +632,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } private saveCurrentValue(inputState: IChatInputState): void { - inputState.chatContextAttachments = inputState.chatContextAttachments?.filter(attachment => !attachment.isImage); - const newEntry = { text: this._inputEditor.getValue(), state: inputState }; + const newEntry = this.getFilteredEntry(this._inputEditor.getValue(), inputState); this.history.replaceLast(newEntry); } @@ -631,8 +652,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (isUserQuery) { const userQuery = this._inputEditor.getValue(); const inputState = this.getInputState(); - inputState.chatContextAttachments = inputState.chatContextAttachments?.filter(attachment => !attachment.isImage); - const entry: IChatHistoryEntry = { text: userQuery, state: inputState }; + const entry = this.getFilteredEntry(userQuery, inputState); this.history.replaceLast(entry); this.history.add({ text: '' }); } @@ -648,6 +668,26 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } + // A funtion that filters out specifically the `value` property of the attachment. + private getFilteredEntry(query: string, inputState: IChatInputState): IChatHistoryEntry { + const attachmentsWithoutImageValues = inputState.chatContextAttachments?.map(attachment => { + if (attachment.isImage && attachment.references?.length && attachment.value) { + const newAttachment = { ...attachment }; + newAttachment.value = undefined; + return newAttachment; + } + return attachment; + }); + + inputState.chatContextAttachments = attachmentsWithoutImageValues; + const newEntry = { + text: query, + state: inputState, + }; + + return newEntry; + } + private _acceptInputForVoiceover(): void { const domNode = this._inputEditor.getDomNode(); if (!domNode) { diff --git a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts index fc71eeb1bdc82..72911840c5f3f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts @@ -2,25 +2,28 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - +import { VSBuffer } from '../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Mimes } from '../../../../base/common/mime.js'; +import { basename, joinPath } from '../../../../base/common/resources.js'; +import { URI, UriComponents } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { ChatInputPart } from './chatInputPart.js'; -import { IChatWidgetService } from './chat.js'; -import { Codicon } from '../../../../base/common/codicons.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; import { localize } from '../../../../nls.js'; -import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../common/chatModel.js'; +import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; -import { Mimes } from '../../../../base/common/mime.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { URI, UriComponents } from '../../../../base/common/uri.js'; -import { basename } from '../../../../base/common/resources.js'; +import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../common/chatModel.js'; +import { IChatWidgetService } from './chat.js'; +import { ChatInputPart } from './chatInputPart.js'; import { resizeImage } from './imageUtils.js'; const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data'; @@ -31,6 +34,7 @@ interface SerializedCopyData { } export class PasteImageProvider implements DocumentPasteEditProvider { + private readonly imagesFolder: URI; public readonly kind = new HierarchicalKind('chat.attach.image'); public readonly providedPasteEditKinds = [this.kind]; @@ -41,7 +45,13 @@ export class PasteImageProvider implements DocumentPasteEditProvider { constructor( private readonly chatWidgetService: IChatWidgetService, private readonly extensionService: IExtensionService, - ) { } + @IFileService private readonly fileService: IFileService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @ILogService private readonly logService: ILogService, + ) { + this.imagesFolder = joinPath(this.environmentService.workspaceStorageHome, 'vscode-chat-images'); + this.cleanupOldImages(); + } async provideDocumentPasteEdits(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { if (!this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) { @@ -90,12 +100,17 @@ export class PasteImageProvider implements DocumentPasteEditProvider { tempDisplayName = `${displayName} ${appendValue}`; } + const fileReference = await this.createFileForMedia(currClipboard, mimeType); + if (token.isCancellationRequested || !fileReference) { + return; + } + const scaledImageData = await resizeImage(currClipboard); if (token.isCancellationRequested || !scaledImageData) { return; } - const scaledImageContext = await getImageAttachContext(scaledImageData, mimeType, token, tempDisplayName); + const scaledImageContext = await getImageAttachContext(scaledImageData, mimeType, token, tempDisplayName, fileReference); if (token.isCancellationRequested || !scaledImageContext) { return; } @@ -111,21 +126,75 @@ export class PasteImageProvider implements DocumentPasteEditProvider { const edit = createCustomPasteEdit(model, scaledImageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService); return createEditSession(edit); } + + private async createFileForMedia( + dataTransfer: Uint8Array, + mimeType: string, + ): Promise { + const exists = await this.fileService.exists(this.imagesFolder); + if (!exists) { + await this.fileService.createFolder(this.imagesFolder); + } + + const ext = mimeType.split('/')[1] || 'png'; + const filename = `image-${Date.now()}.${ext}`; + const fileUri = joinPath(this.imagesFolder, filename); + + const buffer = VSBuffer.wrap(dataTransfer); + await this.fileService.writeFile(fileUri, buffer); + + return fileUri; + } + + private async cleanupOldImages(): Promise { + const exists = await this.fileService.exists(this.imagesFolder); + if (!exists) { + return; + } + + const duration = 7 * 24 * 60 * 60 * 1000; // 7 days + const files = await this.fileService.resolve(this.imagesFolder); + if (!files.children) { + return; + } + + await Promise.all(files.children.map(async (file) => { + try { + const timestamp = this.getTimestampFromFilename(file.name); + if (timestamp && (Date.now() - timestamp > duration)) { + await this.fileService.del(file.resource); + } + } catch (err) { + this.logService.error('Failed to clean up old images', err); + } + })); + } + + private getTimestampFromFilename(filename: string): number | undefined { + const match = filename.match(/image-(\d+)\./); + if (match) { + return parseInt(match[1], 10); + } + return undefined; + } } -async function getImageAttachContext(data: Uint8Array, mimeType: string, token: CancellationToken, displayName: string): Promise { +async function getImageAttachContext(data: Uint8Array, mimeType: string, token: CancellationToken, displayName: string, resource: URI): Promise { const imageHash = await imageToHash(data); if (token.isCancellationRequested) { return undefined; } return { + kind: 'image', value: data, id: imageHash, name: displayName, isImage: true, icon: Codicon.fileMedia, - mimeType + mimeType, + isPasted: true, + references: [{ reference: resource, kind: 'reference' }] }; } @@ -308,10 +377,13 @@ export class ChatPasteProvidersFeature extends Disposable { @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService chatWidgetService: IChatWidgetService, @IExtensionService extensionService: IExtensionService, - @IModelService modelService: IModelService + @IFileService fileService: IFileService, + @IModelService modelService: IModelService, + @IEnvironmentService environmentService: IEnvironmentService, + @ILogService logService: ILogService, ) { super(); - this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteImageProvider(chatWidgetService, extensionService))); + this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteImageProvider(chatWidgetService, extensionService, fileService, environmentService, logService))); this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteTextProvider(chatWidgetService, modelService))); this._register(languageFeaturesService.documentPasteEditProvider.register('*', new CopyTextProvider())); } diff --git a/src/vs/workbench/contrib/chat/browser/imageUtils.ts b/src/vs/workbench/contrib/chat/browser/imageUtils.ts index 428bfd135dd8b..e336a86b0f201 100644 --- a/src/vs/workbench/contrib/chat/browser/imageUtils.ts +++ b/src/vs/workbench/contrib/chat/browser/imageUtils.ts @@ -27,7 +27,7 @@ export async function resizeImage(data: Uint8Array | string): Promise { + readonly kind: 'image'; + readonly isPasted?: boolean; +} + export interface IDiagnosticVariableEntryFilterData { readonly filterUri?: URI; readonly filterSeverity?: MarkerSeverity; @@ -120,7 +125,7 @@ export interface IDiagnosticVariableEntry extends Omit { - if (this.isVisible()) { - this.onDidFocusSession(session); - } + this.onDidFocusSession(session); })); this._register(this.debugService.getViewModel().onDidEvaluateLazyExpression(async e => { if (e instanceof Variable && this.tree?.hasNode(e)) { @@ -213,8 +211,6 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { const focusedSession = this.debugService.getViewModel().focusedSession; if (this.tree && this.tree.getInput() !== focusedSession) { this.onDidFocusSession(focusedSession); - } else { - this.selectSession(); } this.setMode(); diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 117e4757eca8d..55398607cd36a 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -13,24 +13,29 @@ import { ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, ITreeMo import { RunOnceScheduler } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { localize } from '../../../../nls.js'; import { getContextMenuActions, getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { Action2, IMenu, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; +import { FocusedViewContext } from '../../../common/contextkeys.js'; import { IViewDescriptorService } from '../../../common/views.js'; -import { CONTEXT_CAN_VIEW_MEMORY, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_WATCH_ITEM_TYPE, IDebugConfiguration, IDebugService, IDebugViewWithVariables, IExpression, WATCH_VIEW_ID } from '../common/debug.js'; +import { CONTEXT_CAN_VIEW_MEMORY, CONTEXT_EXPRESSION_SELECTED, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_WATCH_ITEM_TYPE, IDebugConfiguration, IDebugService, IDebugViewWithVariables, IExpression, WATCH_VIEW_ID } from '../common/debug.js'; import { Expression, Variable, VisualizedExpression } from '../common/debugModel.js'; import { AbstractExpressionDataSource, AbstractExpressionsRenderer, expressionAndScopeLabelProvider, IExpressionTemplateData, IInputBoxOptions, renderViewTree } from './baseDebugView.js'; +import { COPY_WATCH_EXPRESSION_COMMAND_ID } from './debugCommands.js'; import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; import { watchExpressionsAdd, watchExpressionsRemoveAll } from './debugIcons.js'; import { VariablesRenderer, VisualizedVariableRenderer } from './variablesView.js'; @@ -570,3 +575,39 @@ registerAction2(class RemoveAllWatchExpressionsAction extends Action2 { debugService.removeWatchExpressions(); } }); + +registerAction2(class CopyExpression extends ViewAction { + constructor() { + super({ + id: COPY_WATCH_EXPRESSION_COMMAND_ID, + title: localize('copyWatchExpression', "Copy Expression"), + f1: false, + viewId: WATCH_VIEW_ID, + precondition: CONTEXT_WATCH_EXPRESSIONS_EXIST, + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and( + FocusedViewContext.isEqualTo(WATCH_VIEW_ID), + CONTEXT_EXPRESSION_SELECTED.negate(), + ), + }, + menu: { + id: MenuId.DebugWatchContext, + order: 20, + group: '3_modification', + when: CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression') + } + }); + } + + runInView(accessor: ServicesAccessor, view: WatchExpressionsView, value?: IExpression): void { + const clipboardService = accessor.get(IClipboardService); + if (!value) { + value = view.treeSelection.at(-1); + } + if (value) { + clipboardService.writeText(value.name); + } + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts index 8d0938fceac9a..8ce231cbd2499 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts @@ -31,7 +31,6 @@ import { indentOfLine } from '../../../../../../editor/common/model/textModel.js import { ITextModelService } from '../../../../../../editor/common/services/resolverService.js'; import { ICoordinatesConverter } from '../../../../../../editor/common/viewModel.js'; import { ViewModelEventsCollector } from '../../../../../../editor/common/viewModelEventDispatcher.js'; -import { WordHighlighterContribution } from '../../../../../../editor/contrib/wordHighlighter/browser/wordHighlighter.js'; import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; import { MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; @@ -886,21 +885,6 @@ export class NotebookMultiCursorController extends Disposable implements INotebo cell.decorationIds, newDecorations ); - - /** - * TODO: @Yoyokrazy debt - * goal: draw decorations for occurrence higlight on the cursor blink cycle - * - * Trigger WH with delay: x ms (x = cursor blink cycle) - * -> start = Date() - * -> WordHighlighter -> compute - * -> end = Date() - * -> delay = x - ((end - start) % x) - */ - const matchingEditor = this.notebookEditor.codeEditors.find(cellEditor => cellEditor[0] === cell.cellViewModel); - if (matchingEditor) { - WordHighlighterContribution.get(matchingEditor[1])?.wordHighlighter?.trigger(); - } }); }