Verified Commit 24373b42 authored by Paul Slaughter's avatar Paul Slaughter 2️⃣ Committed by Enrique Alcántara
Browse files

feat: Code suggestions gutter icon status

- This introduces CodeSuggestionsGutterIcon that
  follows the CodeSuggestionsStatusBar pattern
- gitlab#418999
parent c0e61309
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ module.exports = {
    tabGroups: {
      activeTabGroup: {},
    },
    createTextEditorDecorationType: jest.fn(),
  },
  commands: {
    executeCommand: jest.fn(),
+1 −1
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@ export const activate = async (context: vscode.ExtensionContext) => {
      dependencyContainer.gitLabPlatformManager,
    );
  } else {
    const codeSuggestions = new CodeSuggestions(dependencyContainer.gitLabPlatformManager);
    const codeSuggestions = new CodeSuggestions(context, dependencyContainer.gitLabPlatformManager);
    await codeSuggestions.init();
    context.subscriptions.push(codeSuggestions);
  }
+4 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@ jest.mock('./code_suggestions_state_manager', () => ({
  },
}));

jest.mock('./code_suggestions_gutter_icon');
jest.mock('./code_suggestions_provider');
jest.mock('./code_suggestions_status_bar_item');

@@ -41,11 +42,13 @@ const manager: GitLabPlatformManager = createFakePartial<GitLabPlatformManager>(
  getForAllAccounts: jest.fn(),
});

const context = createFakePartial<vscode.ExtensionContext>({});

describe('CodeSuggestions', () => {
  let codeSuggestions: CodeSuggestions;

  beforeEach(() => {
    codeSuggestions = new CodeSuggestions(manager);
    codeSuggestions = new CodeSuggestions(context, manager);
  });

  afterEach(() => {
+6 −1
Original line number Diff line number Diff line
@@ -8,12 +8,15 @@ import { CodeSuggestionsStatusBarItem } from './code_suggestions_status_bar_item
import { COMMAND_TOGGLE_CODE_SUGGESTIONS, toggleCodeSuggestions } from './commands/toggle';
import { LegacyApiFallbackConfig } from './legacy_api_fallback_config';
import { GitLabPlatformManagerForCodeSuggestions } from './gitlab_platform_manager_for_code_suggestions';
import { CodeSuggestionsGutterIcon } from './code_suggestions_gutter_icon';

export class CodeSuggestions {
  stateManager: CodeSuggestionsStateManager;

  statusBarItem: CodeSuggestionsStatusBarItem;

  #gutterIcon: CodeSuggestionsGutterIcon;

  toggleSuggestionsCommand?: vscode.Disposable;

  providerDisposable?: vscode.Disposable;
@@ -22,7 +25,7 @@ export class CodeSuggestions {

  legacyApiFallbackConfig: LegacyApiFallbackConfig;

  constructor(manager: GitLabPlatformManager) {
  constructor(context: vscode.ExtensionContext, manager: GitLabPlatformManager) {
    this.stateManager = new CodeSuggestionsStateManager(manager);
    this.toggleSuggestionsCommand = vscode.commands.registerCommand(
      COMMAND_TOGGLE_CODE_SUGGESTIONS,
@@ -32,6 +35,7 @@ export class CodeSuggestions {
    const platformManagerForCodeSuggestions = new GitLabPlatformManagerForCodeSuggestions(manager);

    this.statusBarItem = new CodeSuggestionsStatusBarItem(this.stateManager);
    this.#gutterIcon = new CodeSuggestionsGutterIcon(context, this.stateManager);

    this.legacyApiFallbackConfig = new LegacyApiFallbackConfig(platformManagerForCodeSuggestions);

@@ -80,6 +84,7 @@ export class CodeSuggestions {

  dispose() {
    this.statusBarItem?.dispose();
    this.#gutterIcon.dispose();
    this.providerDisposable?.dispose();
    this.activeTextEditorChangeDisposable?.dispose();
    this.toggleSuggestionsCommand?.dispose();
+159 −0
Original line number Diff line number Diff line
import * as vscode from 'vscode';
import { CodeSuggestionsGutterIcon } from './code_suggestions_gutter_icon';
import {
  CodeSuggestionsStateManager,
  VisibleCodeSuggestionsState,
} from './code_suggestions_state_manager';
import { createFakePartial } from '../test_utils/create_fake_partial';
import { createExtensionContext } from '../test_utils/entities';

describe('CodeSuggestionsGutterIcon', () => {
  let codeSuggestionsStateManager: CodeSuggestionsStateManager;
  let context: vscode.ExtensionContext;
  let mockStateChangeEmitter: vscode.EventEmitter<VisibleCodeSuggestionsState>;
  let setDecorationsSpy: jest.Mock;
  let subject: CodeSuggestionsGutterIcon;
  let textEditorDecorationTypeMap: Map<string, vscode.TextEditorDecorationType>;
  let activeTextEditorSelection: { start: { line: number } };

  const triggerTextEditorSelectionChange = (e: vscode.TextEditorSelectionChangeEvent) => {
    jest
      .mocked(vscode.window.onDidChangeTextEditorSelection)
      .mock.calls.forEach(([listener]) => listener(e));
  };

  beforeEach(() => {
    textEditorDecorationTypeMap = new Map<string, vscode.TextEditorDecorationType>();
    setDecorationsSpy = jest.fn();
    mockStateChangeEmitter = new vscode.EventEmitter<VisibleCodeSuggestionsState>();

    activeTextEditorSelection = {
      start: {
        line: 7,
      },
    };
    vscode.window.activeTextEditor = createFakePartial<vscode.TextEditor>({
      setDecorations: setDecorationsSpy,
      selection: activeTextEditorSelection,
      document: {},
    });
    jest
      .mocked(vscode.window.createTextEditorDecorationType)
      .mockImplementation(({ gutterIconPath = '' }) => {
        const textEditorDecorationType: vscode.TextEditorDecorationType = {
          dispose: jest.fn(),
          key: gutterIconPath.toString(),
        };

        textEditorDecorationTypeMap.set(textEditorDecorationType.key, textEditorDecorationType);

        return textEditorDecorationType;
      });
    jest.mocked(vscode.window.onDidChangeTextEditorSelection).mockReturnValue({
      dispose: jest.fn(),
    });

    context = createExtensionContext();
    codeSuggestionsStateManager = createFakePartial<CodeSuggestionsStateManager>({
      onDidChangeVisibleState: mockStateChangeEmitter.event,
    });
  });

  afterEach(() => {
    jest.resetAllMocks();
    subject.dispose();
  });

  describe('default', () => {
    beforeEach(() => {
      subject = new CodeSuggestionsGutterIcon(context, codeSuggestionsStateManager);
    });

    it.each`
      state                                  | iconPath
      ${VisibleCodeSuggestionsState.LOADING} | ${'/test/abs/assets/icons/gitlab-code-suggestions-loading.svg'}
      ${VisibleCodeSuggestionsState.ERROR}   | ${'/test/abs/assets/icons/gitlab-code-suggestions-error.svg'}
    `(
      'when manager state changes to $state, updates decorations to $iconPath',
      ({ state, iconPath }) => {
        expect(setDecorationsSpy).not.toHaveBeenCalled();

        mockStateChangeEmitter.fire(state);

        expect(setDecorationsSpy).toHaveBeenCalledTimes(1);
        expect(setDecorationsSpy).toHaveBeenCalledWith(textEditorDecorationTypeMap.get(iconPath), [
          {
            range: {
              start: { line: 7 },
              end: { line: 7 },
            },
          },
        ]);
      },
    );

    it('when manager state changes to unhandled state, nothing happens', () => {
      mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.READY);

      expect(setDecorationsSpy).not.toHaveBeenCalled();
    });

    it('when manager state changes then changes again, clears old decoration', () => {
      mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.LOADING);
      setDecorationsSpy.mockClear();

      mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.READY);

      const loadingDecoration = textEditorDecorationTypeMap.get(
        '/test/abs/assets/icons/gitlab-code-suggestions-loading.svg',
      );

      expect(setDecorationsSpy).toHaveBeenCalledTimes(1);
      expect(setDecorationsSpy).toHaveBeenCalledWith(loadingDecoration, []);
    });

    it('when manager state changes then text editor selection changes, clears and reapplies old decoration', () => {
      mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.LOADING);
      setDecorationsSpy.mockClear();

      activeTextEditorSelection.start.line = 10;
      triggerTextEditorSelectionChange(
        createFakePartial<vscode.TextEditorSelectionChangeEvent>({}),
      );

      const loadingDecoration = textEditorDecorationTypeMap.get(
        '/test/abs/assets/icons/gitlab-code-suggestions-loading.svg',
      );

      expect(setDecorationsSpy).toHaveBeenCalledTimes(2);
      expect(setDecorationsSpy).toHaveBeenCalledWith(loadingDecoration, []);
      expect(setDecorationsSpy).toHaveBeenCalledWith(loadingDecoration, [
        {
          range: {
            start: { line: 10 },
            end: { line: 10 },
          },
        },
      ]);
    });

    it('with not active editor, when manager state changes, nothing happens', () => {
      vscode.window.activeTextEditor = undefined;
      mockStateChangeEmitter.fire(VisibleCodeSuggestionsState.LOADING);

      expect(setDecorationsSpy).not.toHaveBeenCalled();
    });
  });

  describe('dispose', () => {
    it('disposes subscriptions', () => {
      const icon = new CodeSuggestionsGutterIcon(context, codeSuggestionsStateManager);
      icon.dispose();

      const disposeTextEditorSelection = jest.mocked(vscode.window.onDidChangeTextEditorSelection)
        .mock.results[0].value.dispose;

      expect(disposeTextEditorSelection).toHaveBeenCalled();
    });
  });
});
Loading