packages/line-ending-selector/lib/main.js

Summary

Maintainability
B
4 hrs
Test Coverage
'use babel';

import _ from 'underscore-plus';
import { CompositeDisposable, Disposable } from 'atom';
import { Selector } from './selector';
import StatusBarItem from './status-bar-item';
import helpers from './helpers';

const LineEndingRegExp = /\r\n|\n/g;

// the following regular expression is executed natively via the `substring` package,
// where `\A` corresponds to the beginning of the string.
// More info: https://github.com/atom/line-ending-selector/pull/56
// eslint-disable-next-line no-useless-escape
const LFRegExp = /(\A|[^\r])\n/g;
const CRLFRegExp = /\r\n/g;

let disposables = null;

export function activate() {
  disposables = new CompositeDisposable();
  let selectorDisposable;
  let selector;

  disposables.add(
    atom.commands.add('atom-text-editor', {
      'line-ending-selector:show': () => {
        // Initiating Selector object - called only once when `line-ending-selector:show` is called
        if (!selectorDisposable) {
          // make a Selector object
          selector = new Selector([
            { name: 'LF', value: '\n' },
            { name: 'CRLF', value: '\r\n' }
          ]);
          // Add disposable for selector
          selectorDisposable = new Disposable(() => selector.dispose());
          disposables.add(selectorDisposable);
        }

        selector.show();
      },

      'line-ending-selector:convert-to-LF': event => {
        const editorElement = event.target.closest('atom-text-editor');
        setLineEnding(editorElement.getModel(), '\n');
      },

      'line-ending-selector:convert-to-CRLF': event => {
        const editorElement = event.target.closest('atom-text-editor');
        setLineEnding(editorElement.getModel(), '\r\n');
      }
    })
  );
}

export function deactivate() {
  disposables.dispose();
}

export function consumeStatusBar(statusBar) {
  let statusBarItem = new StatusBarItem();
  let currentBufferDisposable = null;
  let tooltipDisposable = null;

  const updateTile = _.debounce(buffer => {
    getLineEndings(buffer).then(lineEndings => {
      if (lineEndings.size === 0) {
        let defaultLineEnding = getDefaultLineEnding();
        buffer.setPreferredLineEnding(defaultLineEnding);
        lineEndings = new Set().add(defaultLineEnding);
      }
      statusBarItem.setLineEndings(lineEndings);
    });
  }, 0);

  disposables.add(
    atom.workspace.observeActiveTextEditor(editor => {
      if (currentBufferDisposable) currentBufferDisposable.dispose();

      if (editor && editor.getBuffer) {
        let buffer = editor.getBuffer();
        updateTile(buffer);
        currentBufferDisposable = buffer.onDidChange(({ oldText, newText }) => {
          if (!statusBarItem.hasLineEnding('\n')) {
            if (newText.indexOf('\n') >= 0) {
              updateTile(buffer);
            }
          } else if (!statusBarItem.hasLineEnding('\r\n')) {
            if (newText.indexOf('\r\n') >= 0) {
              updateTile(buffer);
            }
          } else if (oldText.indexOf('\n')) {
            updateTile(buffer);
          }
        });
      } else {
        statusBarItem.setLineEndings(new Set());
        currentBufferDisposable = null;
      }

      if (tooltipDisposable) {
        disposables.remove(tooltipDisposable);
        tooltipDisposable.dispose();
      }
      tooltipDisposable = atom.tooltips.add(statusBarItem.element, {
        title() {
          return `File uses ${statusBarItem.description()} line endings`;
        }
      });
      disposables.add(tooltipDisposable);
    })
  );

  disposables.add(
    new Disposable(() => {
      if (currentBufferDisposable) currentBufferDisposable.dispose();
    })
  );

  statusBarItem.onClick(() => {
    const editor = atom.workspace.getActiveTextEditor();
    atom.commands.dispatch(
      atom.views.getView(editor),
      'line-ending-selector:show'
    );
  });

  let tile = statusBar.addRightTile({ item: statusBarItem, priority: 200 });
  disposables.add(new Disposable(() => tile.destroy()));
}

function getDefaultLineEnding() {
  switch (atom.config.get('line-ending-selector.defaultLineEnding')) {
    case 'LF':
      return '\n';
    case 'CRLF':
      return '\r\n';
    case 'OS Default':
    default:
      return helpers.getProcessPlatform() === 'win32' ? '\r\n' : '\n';
  }
}

function getLineEndings(buffer) {
  if (typeof buffer.find === 'function') {
    return Promise.all([buffer.find(LFRegExp), buffer.find(CRLFRegExp)]).then(
      ([hasLF, hasCRLF]) => {
        const result = new Set();
        if (hasLF) result.add('\n');
        if (hasCRLF) result.add('\r\n');
        return result;
      }
    );
  } else {
    return new Promise(resolve => {
      const result = new Set();
      for (let i = 0; i < buffer.getLineCount() - 1; i++) {
        result.add(buffer.lineEndingForRow(i));
      }
      resolve(result);
    });
  }
}

export function setLineEnding(item, lineEnding) {
  if (item && item.getBuffer) {
    let buffer = item.getBuffer();
    buffer.setPreferredLineEnding(lineEnding);
    buffer.setText(buffer.getText().replace(LineEndingRegExp, lineEnding));
  }
}