undergroundwires/privacy.sexy

View on GitHub
src/presentation/components/Code/Ace/AceCodeEditorFactory.ts

Summary

Maintainability
A
1 hr
Test Coverage
import ace from './ace-importer';
import type { CodeEditorFactory, SupportedSyntaxLanguage } from '../CodeEditorFactory';

const CodeEditorTheme = 'xcode';

export const initializeAceEditor: CodeEditorFactory = (options) => {
  const editor = ace.edit(options.editorContainerElementId);
  const mode = getAceModeName(options.language);
  editor.getSession().setMode(`ace/mode/${mode}`);
  editor.setTheme(`ace/theme/${CodeEditorTheme}`);
  editor.setReadOnly(true);
  editor.setAutoScrollEditorIntoView(true);
  editor.setShowPrintMargin(false); // Hide the vertical line
  editor.getSession().setUseWrapMode(true); // Make code readable on mobile
  hideActiveLineAndCursorUntilInteraction(editor);
  return {
    setContent: (content) => editor.setValue(content, 1),
    destroy: () => editor.destroy(),
    scrollToLine: (lineNumber) => {
      const column = editor.session.getLine(lineNumber).length;
      if (column === undefined) {
        return;
      }
      editor.gotoLine(lineNumber, column, true);
    },
    updateSize: () => editor?.resize(),
    applyStyleToLineRange: (start, end, className) => {
      const AceRange = ace.require('ace/range').Range;
      const markerId = editor.session.addMarker(
        new AceRange(start, 0, end, 0),
        className,
        'fullLine',
      );
      return {
        clearStyle: () => {
          editor.session.removeMarker(markerId);
        },
      };
    },
  };
};

function getAceModeName(language: SupportedSyntaxLanguage): string {
  switch (language) {
    case 'batchfile': return 'batchfile';
    case 'shellscript': return 'sh';
    default:
      throw new Error(`Language not supported: ${language}`);
  }
}

function hideActiveLineAndCursorUntilInteraction(editor: ace.Ace.Editor) {
  hideActiveLineAndCursor(editor);
  editor.session.on('change', () => {
    editor.session.selection.clearSelection();
    hideActiveLineAndCursor(editor);
  });
  editor.session.selection.on('changeSelection', () => {
    showActiveLineAndCursor(editor);
  });
}

function hideActiveLineAndCursor(editor: ace.Ace.Editor): void {
  editor.setHighlightGutterLine(false); // Remove highlighting on line number column
  editor.setHighlightActiveLine(false); // Remove highlighting throughout the line
  setCursorVisibility(false, editor);
}

function showActiveLineAndCursor(editor: ace.Ace.Editor): void {
  editor.setHighlightGutterLine(true); // Show highlighting on line number column
  editor.setHighlightActiveLine(true); // Show highlighting throughout the line
  setCursorVisibility(true, editor);
}

// Shows/removes vertical line after focused character
function setCursorVisibility(
  isVisible: boolean,
  editor: ace.Ace.Editor,
) {
  const cursor = editor.renderer.container.querySelector('.ace_cursor-layer') as HTMLElement;
  if (!cursor) {
    throw new Error('Cannot find Ace cursor, did Ace change its rendering?');
  }
  cursor.style.display = isVisible ? '' : 'none';
  // Implementation options for cursor visibility:
  //   ❌ editor.renderer.showCursor() and hideCursor(): Not functioning as expected
  //   ❌ editor.renderer.#cursorLayer: No longer part of the public API
  //   ✅ .ace_hidden-cursors { opacity: 0; }: Hides cursor when not focused
  //      Pros: Works more automatically
  //      Cons: Provides less control over visibility toggling
}