Skip to content

Commit

Permalink
track code lenses and related context keys in dedicated class (#16204)
Browse files Browse the repository at this point in the history
* track code lenses and related context keys in dedicated class

* fix test

* move tests

* make tests pass

* always track code cell ranges
  • Loading branch information
amunger authored Nov 6, 2024
1 parent eaa5d21 commit 07e9477
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 105 deletions.
104 changes: 104 additions & 0 deletions src/interactive-window/editor-integration/cellRangeCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import * as vscode from 'vscode';
import { inject, injectable } from 'inversify';
import { ICellRange, IConfigurationService } from '../../platform/common/types';
import { IDisposable } from '../../platform/common/types';
import { generateCellRangesFromDocument } from './cellFactory';
import { ConfigurationChangeEvent } from 'vscode';
import { ContextKey } from '../../platform/common/contextKey';
import {
EditorContexts,
InteractiveInputScheme,
NotebookCellScheme,
PYTHON_LANGUAGE
} from '../../platform/common/constants';
import { noop } from '../../platform/common/utils/misc';
import { logger } from '../../platform/logging';
import { ICellRangeCache } from './types';

@injectable()
export class CellRangeCache implements ICellRangeCache {
private cachedOwnsSetting: boolean;
private cache = new Map<vscode.Uri, { version: number; ranges: ICellRange[] }>();
private disposables: IDisposable[] = [];

constructor(@inject(IConfigurationService) private readonly configService: IConfigurationService) {
this.cachedOwnsSetting = this.configService.getSettings(undefined).sendSelectionToInteractiveWindow;
vscode.workspace.onDidChangeConfiguration(this.onSettingChanged, this, this.disposables);
vscode.window.onDidChangeActiveTextEditor(this.onChangedActiveTextEditor, this, this.disposables);
this.onChangedActiveTextEditor();
vscode.workspace.onDidCloseTextDocument(this.onClosedDocument, this, this.disposables);
}

public getCellRanges(document: vscode.TextDocument): ICellRange[] {
const cached = this.cache.get(document.uri);
if (cached && cached.version === document.version) {
return cached.ranges;
}

const settings = this.configService.getSettings(document.uri);
const ranges = generateCellRangesFromDocument(document, settings);
this.cache.set(document.uri, { version: document.version, ranges });

this.updateContextKeys(document);

return ranges;
}

public clear(): void {
this.cache.clear();
}

private onChangedActiveTextEditor() {
const activeEditor = vscode.window.activeTextEditor;

if (
!activeEditor ||
activeEditor.document.languageId != PYTHON_LANGUAGE ||
[NotebookCellScheme, InteractiveInputScheme].includes(activeEditor.document.uri.scheme)
) {
// set the context to false so our command doesn't run for other files
const hasCellsContext = new ContextKey(EditorContexts.HasCodeCells);
hasCellsContext.set(false).catch((ex) => logger.warn('Failed to set jupyter.HasCodeCells context', ex));
this.updateContextKeys(false);
} else {
this.updateContextKeys(activeEditor.document);
}
}

private onSettingChanged(e: ConfigurationChangeEvent) {
this.cache.clear();

if (e.affectsConfiguration('jupyter.interactiveWindow.textEditor.executeSelection')) {
const settings = this.configService.getSettings(undefined);
this.cachedOwnsSetting = settings.sendSelectionToInteractiveWindow;
this.updateContextKeys();
}
}

private updateContextKeys(documentOrOverride?: vscode.TextDocument | boolean) {
let hasCodeCells = false;
if (typeof documentOrOverride == 'boolean') {
hasCodeCells = documentOrOverride;
} else {
const document = documentOrOverride ?? vscode.window.activeTextEditor?.document;
hasCodeCells = document ? this.getCellRanges(document).length > 0 : false;
}

new ContextKey(EditorContexts.OwnsSelection).set(this.cachedOwnsSetting || hasCodeCells).catch(noop);
new ContextKey(EditorContexts.HasCodeCells).set(hasCodeCells).catch(noop);
}

private onClosedDocument(doc: vscode.TextDocument) {
this.cache.delete(doc.uri);

// Don't delete the document execution count, we need to keep track
// of it past the closing of a doc if the notebook or interactive window is still open.
}

public dispose() {
this.disposables.forEach((d) => d.dispose());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { Uri, Disposable } from 'vscode';
import { createDocument } from '../../test/datascience/editor-integration/helpers';
import * as TypeMoq from 'typemoq';
import { CodeLensFactory } from './codeLensFactory';
import { IConfigurationService } from '../../platform/common/types';
import { CellRangeCache } from './cellRangeCache';
import { IKernelProvider } from '../../kernels/types';
import { mockedVSCodeNamespaces } from '../../test/vscode-mock';
import { IGeneratedCodeStorageFactory } from './types';
import { IReplNotebookTrackerService } from '../../platform/notebooks/replNotebookTrackerService';
import { when, anything, verify } from 'ts-mockito';
import { MockJupyterSettings } from '../../test/datascience/mockJupyterSettings';
import { SystemVariables } from '../../platform/common/variables/systemVariables.node';

suite('DataScienceCodeLensProvider Unit Tests', () => {
let configService: TypeMoq.IMock<IConfigurationService>;
let jupyterSettings: MockJupyterSettings;

const storageFactory = TypeMoq.Mock.ofType<IGeneratedCodeStorageFactory>();
const kernelProvider = TypeMoq.Mock.ofType<IKernelProvider>();
const replTracker = TypeMoq.Mock.ofType<IReplNotebookTrackerService>();
const disposables: Disposable[] = [];

setup(() => {
configService = TypeMoq.Mock.ofType<IConfigurationService>();
jupyterSettings = new MockJupyterSettings(undefined, SystemVariables, 'node');
configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => jupyterSettings);
when(mockedVSCodeNamespaces.commands.executeCommand(anything(), anything(), anything())).thenResolve();
});

function createCodeLensFactory() {
return new CodeLensFactory(
configService.object,
disposables,
storageFactory.object,
kernelProvider.object,
replTracker.object,
new CellRangeCache(configService.object)
);
}

test('Having code lenses will update context keys to true', async () => {
jupyterSettings.sendSelectionToInteractiveWindow = false;

const fileName = Uri.file('test.py').fsPath;
const version = 1;
const inputText = `# %%\nprint(1)`;
const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true);

createCodeLensFactory().getCellRanges(document.object);

// verify context keys set
verify(mockedVSCodeNamespaces.commands.executeCommand('setContext', 'jupyter.ownsSelection', true)).atLeast(1);
verify(mockedVSCodeNamespaces.commands.executeCommand('setContext', 'jupyter.hascodecells', true)).atLeast(1);
});

test('Having no code lenses will set context keys to false', async () => {
jupyterSettings.sendSelectionToInteractiveWindow = false;

const fileName = Uri.file('test.py').fsPath;
const version = 1;
const inputText = `print(1)`;
const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true);

createCodeLensFactory().getCellRanges(document.object);

// verify context keys set
verify(mockedVSCodeNamespaces.commands.executeCommand('setContext', 'jupyter.ownsSelection', true)).atLeast(1);
verify(mockedVSCodeNamespaces.commands.executeCommand('setContext', 'jupyter.hascodecells', true)).atLeast(1);
});

test('Having no code lenses but ownership setting true will set context keys correctly', async () => {
jupyterSettings.sendSelectionToInteractiveWindow = true;

const fileName = Uri.file('test.py').fsPath;
const version = 1;
const inputText = `print(1)`;
const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true);

createCodeLensFactory().getCellRanges(document.object);

// verify context keys set
verify(mockedVSCodeNamespaces.commands.executeCommand('setContext', 'jupyter.ownsSelection', true)).atLeast(1);
verify(mockedVSCodeNamespaces.commands.executeCommand('setContext', 'jupyter.hascodecells', true)).atLeast(1);
});
});
15 changes: 11 additions & 4 deletions src/interactive-window/editor-integration/codeLensFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ import * as localize from '../../platform/common/utils/localize';
import { getInteractiveCellMetadata } from '../helpers';
import { IKernelProvider } from '../../kernels/types';
import { CodeLensCommands, Commands } from '../../platform/common/constants';
import { generateCellRangesFromDocument } from './cellFactory';
import { CodeLensPerfMeasures, ICodeLensFactory, IGeneratedCode, IGeneratedCodeStorageFactory } from './types';
import {
CodeLensPerfMeasures,
ICellRangeCache,
ICodeLensFactory,
IGeneratedCode,
IGeneratedCodeStorageFactory
} from './types';
import { StopWatch } from '../../platform/common/utils/stopWatch';
import {
NotebookCellExecutionState,
Expand Down Expand Up @@ -67,7 +72,8 @@ export class CodeLensFactory implements ICodeLensFactory {
@inject(IGeneratedCodeStorageFactory)
private readonly generatedCodeStorageFactory: IGeneratedCodeStorageFactory,
@inject(IKernelProvider) kernelProvider: IKernelProvider,
@inject(IReplNotebookTrackerService) private readonly replTracker: IReplNotebookTrackerService
@inject(IReplNotebookTrackerService) private readonly replTracker: IReplNotebookTrackerService,
@inject(ICellRangeCache) private readonly cellRangeCache: ICellRangeCache
) {
workspace.onDidCloseTextDocument(this.onClosedDocument, this, disposables);
workspace.onDidGrantWorkspaceTrust(() => this.codeLensCache.clear(), this, disposables);
Expand Down Expand Up @@ -132,7 +138,7 @@ export class CodeLensFactory implements ICodeLensFactory {

// If the document version doesn't match, our cell ranges are out of date
if (cache.cachedDocumentVersion !== document.version) {
cache.cellRanges = generateCellRangesFromDocument(document, this.configService.getSettings(document.uri));
cache.cellRanges = this.cellRangeCache.getCellRanges(document);

// Because we have all new ranges, we need to recompute ALL of our code lenses.
cache.documentLenses = [];
Expand Down Expand Up @@ -246,6 +252,7 @@ export class CodeLensFactory implements ICodeLensFactory {
private onChangedSettings() {
// When config settings change, refresh our code lenses.
this.codeLensCache.clear();
this.cellRangeCache.clear();

// Force an update so that code lenses are recomputed now and not during execution.
this.updateEvent.fire();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { injectable, inject } from 'inversify';
import { IExtensionSyncActivationService } from '../../platform/activation/types';
import { IDataScienceCodeLensProvider } from './types';
import { ICellRangeCache, IDataScienceCodeLensProvider } from './types';
import { languages } from 'vscode';
import { PYTHON_FILE_ANY_SCHEME } from '../../platform/common/constants';
import { IExtensionContext } from '../../platform/common/types';
Expand All @@ -12,8 +12,13 @@ import { IExtensionContext } from '../../platform/common/types';
export class CodeLensProviderActivator implements IExtensionSyncActivationService {
constructor(
@inject(IDataScienceCodeLensProvider) private dataScienceCodeLensProvider: IDataScienceCodeLensProvider,
@inject(IExtensionContext) private extensionContext: IExtensionContext
) {}
@inject(IExtensionContext) private extensionContext: IExtensionContext,
@inject(ICellRangeCache) cellRangeCache: ICellRangeCache
) {
// make sure this is tracking the cell ranges and updating context keys to be used for shift+enter,
// even when code lenses and decorations are disabled
cellRangeCache;
}

public activate() {
this.extensionContext.subscriptions.push(
Expand Down
63 changes: 0 additions & 63 deletions src/interactive-window/editor-integration/codelensprovider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,17 @@

import { inject, injectable, optional } from 'inversify';
import * as vscode from 'vscode';

import { IDebugService } from '../../platform/common/application/types';
import { ContextKey } from '../../platform/common/contextKey';
import { dispose } from '../../platform/common/utils/lifecycle';

import { IConfigurationService, IDisposable, IDisposableRegistry } from '../../platform/common/types';
import { noop } from '../../platform/common/utils/misc';
import { StopWatch } from '../../platform/common/utils/stopWatch';
import { IServiceContainer } from '../../platform/ioc/types';
import { sendTelemetryEvent } from '../../telemetry';
import { logger } from '../../platform/logging';
import {
CodeLensCommands,
EditorContexts,
InteractiveInputScheme,
NotebookCellScheme,
PYTHON_LANGUAGE,
Telemetry
} from '../../platform/common/constants';
import { IDataScienceCodeLensProvider, ICodeWatcher } from './types';
Expand All @@ -37,7 +31,6 @@ export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider
private totalGetCodeLensCalls: number = 0;
private activeCodeWatchers: ICodeWatcher[] = [];
private didChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();
private cachedOwnsSetting: boolean;

constructor(
@inject(IServiceContainer) private serviceContainer: IServiceContainer,
Expand All @@ -58,51 +51,6 @@ export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider
if (this.debugLocationTracker) {
disposableRegistry.push(this.debugLocationTracker.updated(this.onDebugLocationUpdated.bind(this)));
}

disposableRegistry.push(vscode.window.onDidChangeActiveTextEditor(() => this.onChangedActiveTextEditor()));
const settings = this.configuration.getSettings(undefined);
this.cachedOwnsSetting = settings.sendSelectionToInteractiveWindow;
this.updateOwnerContextKey();
disposableRegistry.push(vscode.workspace.onDidChangeConfiguration((e) => this.onSettingChanged(e)));
this.onChangedActiveTextEditor();
}

private onChangedActiveTextEditor() {
const activeEditor = vscode.window.activeTextEditor;

if (
!activeEditor ||
activeEditor.document.languageId != PYTHON_LANGUAGE ||
[NotebookCellScheme, InteractiveInputScheme].includes(activeEditor.document.uri.scheme)
) {
// set the context to false so our command doesn't run for other files
const hasCellsContext = new ContextKey(EditorContexts.HasCodeCells);
hasCellsContext.set(false).catch((ex) => logger.warn('Failed to set jupyter.HasCodeCells context', ex));
this.updateOwnerContextKey(false);
}
}

private onSettingChanged(e: vscode.ConfigurationChangeEvent) {
if (e.affectsConfiguration('jupyter.interactiveWindow.textEditor.executeSelection')) {
const settings = this.configuration.getSettings(undefined);
this.cachedOwnsSetting = settings.sendSelectionToInteractiveWindow;
this.updateOwnerContextKey();
}
}

private updateOwnerContextKey(hasCodeCells?: boolean) {
const editorContext = new ContextKey(EditorContexts.OwnsSelection);
if (this.cachedOwnsSetting) {
editorContext.set(true).catch(noop);
return;
}

if (hasCodeCells === undefined) {
const hasCellsContext = new ContextKey(EditorContexts.HasCodeCells);
hasCodeCells = hasCellsContext.value ?? false;
}

editorContext.set(hasCodeCells).catch(noop);
}

public dispose() {
Expand All @@ -113,9 +61,6 @@ export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider
});
}

const editorContext = new ContextKey(EditorContexts.HasCodeCells);
editorContext.set(false).catch(noop);

dispose(this.activeCodeWatchers);
}

Expand Down Expand Up @@ -160,14 +105,6 @@ export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider
this.totalExecutionTimeInMs += stopWatch.elapsedTime;
this.totalGetCodeLensCalls += 1;

// Update the hasCodeCells context at the same time we are asked for codelens as VS code will
// ask whenever a change occurs. Do this regardless of if we have code lens turned on or not as
// shift+enter relies on this code context.
const hasCellsContext = new ContextKey(EditorContexts.HasCodeCells);
const hasCodeCells = codeLenses && codeLenses.length > 0;
hasCellsContext.set(hasCodeCells).catch((ex) => logger.debug('Failed to set jupyter.HasCodeCells context', ex));
this.updateOwnerContextKey(hasCodeCells);

// Don't provide any code lenses if we have not enabled data science
const settings = this.configuration.getSettings(document.uri);
if (!settings.enableCellCodeLens) {
Expand Down
Loading

0 comments on commit 07e9477

Please sign in to comment.