viddo/atom-textual-velocity

View on GitHub
lib/previewEditor.js

Summary

Maintainability
B
5 hrs
Test Coverage
/* @flow */

import { Point, Range } from "atom";

import type { PreviewEditor } from "../flow-types/PreviewEditor";

export const PREVIEW_SCHEMA_PREFIX = "tv://";
export const PREVIEW_EDITOR_TITLE = "Textual Velocity Preview";
const FULL_RANGE = new Range(Point.ZERO, Point.INFINITY);

const findFirstRow = (content: string, searchRegexp?: RegExp) => {
  if (searchRegexp) {
    const searchMatch = content.match(searchRegexp);
    if (searchMatch) {
      // $FlowFixMe str.match returns an array according to flow core definitions (v0.78)
      const matchStart = searchMatch.index;
      const newLines = content.slice(0, matchStart).match(/\n/g);
      if (newLines) {
        return newLines.length;
      }
    }
  }

  return 0;
};

const previewEditor = (): PreviewEditor => {
  const editor: PreviewEditor = (atom.workspace.buildTextEditor({
    // autoHeight is necessay for scrolling to work as expected.
    // It's also done internally when opening a new text editor:
    // https://github.com/atom/atom/blob/3e97867f3e3ffd1b04a3b25978a40eb1d377f52f/src/workspace.js#L1266
    autoHeight: false,
    readOnly: true
  }): any);
  let previewPath = "";

  let onDidChangeCursorPosition = null;
  const disposeOnDidChangeCursorPosition = () => {
    if (onDidChangeCursorPosition) {
      onDidChangeCursorPosition.dispose();
      onDidChangeCursorPosition = null;
    }
  };
  const maybeReplaceWithRealEditor = async (event: any) => {
    if (!event.textChanged) {
      // if the cursor is changed without any text change it's because the user did some interaction
      // in that case open a normal text editor in the same place
      const notePath = previewPath.slice(PREVIEW_SCHEMA_PREFIX.length);
      await atom.workspace.open(notePath, {
        initialLine: event.newBufferPosition.row,
        initialColumn: event.newBufferPosition.column
      });
      editor.destroy();
    }
  };

  const highlightMarkerLayer = editor.addMarkerLayer({
    maintainHistory: false
  });
  const highlightMarkers: atom$DisplayMarker[] = [];
  const destroyMarkers = () => {
    highlightMarkers.forEach(marker => {
      marker.destroy();
    });
    highlightMarkers.length = 0;
    highlightMarkerLayer.clear();
  };
  const layerDecoration = editor.decorateMarkerLayer(highlightMarkerLayer, {
    type: "highlight",
    class: "find-result"
  });

  // Some overrides of the default text editor behavior for the purpose of being a reusable preview:
  editor.getTitle = editor.getLongTitle = () => PREVIEW_EDITOR_TITLE;
  editor.getPath = editor.getURI = () => undefined;
  editor.setPath = (notePath: string) => {
    previewPath = PREVIEW_SCHEMA_PREFIX + notePath;
  };
  editor.isModified = () => false; // don't indicate changes to UI
  editor.shouldPromptToSave = () => false;

  // Open a preview; handles necessary state changes and setup/teardowns
  editor.openPreview = async (
    notePath: string,
    content: ?string,
    searchRegexps: RegExp[]
  ) => {
    content = content || "";
    disposeOnDidChangeCursorPosition();

    // setPath must be called before setText so the state is what's expected in the onDidChangeCursorPosition,
    // which is triggered on setText
    editor.setPath(notePath);

    const oldText = editor.getText();
    editor.setText(content, { bypassReadOnly: true });

    // Update markers, if need be
    if (searchRegexps.length && oldText !== editor.getText()) {
      destroyMarkers();
      searchRegexps.forEach(regex => {
        editor.scanInBufferRange(regex, FULL_RANGE, ({ range }) => {
          const marker = highlightMarkerLayer.markBufferRange(range);
          highlightMarkers.push(marker);
        });
      });
    }

    const openPromise = await atom.workspace.open(previewPath, {
      activatePane: false, // to keep focus on search/keyboard navigation
      initialLine: findFirstRow(content, searchRegexps[0]), // scroll to first search match (if any)
      initialColumn: 0,
      searchAllPanes: true // will open existing texteditor (if any)
    });

    onDidChangeCursorPosition = editor.onDidChangeCursorPosition(
      maybeReplaceWithRealEditor
    );

    return openPromise;
  };

  const opener = atom.workspace.addOpener(uri => {
    if (uri.startsWith(PREVIEW_SCHEMA_PREFIX)) {
      return editor;
    }
  });

  // prevent user inserts from ever happening
  const onWillInsertText = editor.onWillInsertText(event => {
    event.cancel();
  });

  const onDidDestroy = editor.onDidDestroy(() => {
    destroyMarkers();
    highlightMarkerLayer.destroy();
    layerDecoration.destroy();
    disposeOnDidChangeCursorPosition();
    onWillInsertText.dispose();
    onDidDestroy.dispose();
    opener.dispose();
  });

  return editor;
};

export default previewEditor;