popcode icon indicating copy to clipboard operation
popcode copied to clipboard

Working on an auto-indentation/beautify/formatting feature!

Open cricklet opened this issue 7 years ago • 3 comments

I recently started working on a code beautification feature for pop-code. In general, I'm using js-beautify to do the formatting.

My current approach is to reproduce the auto-formatting present in Cloud9's IDE (format only the current selection & retain cursor if formatting the entire page). I'm basically copying the code from C9 verbatim

In doing this, I've run into a dependency problem. Cloud9 depends on this package: c9.ide.threewaymerge. Unfortunately, that package depends on ace and we use blace.

The clearest way I can think of solving this is to simply copy the relevant code from threewaymerge and edit it to work with blace. If I do that, where should I put the code?

auto-indent

Thanks! Kenrick

cricklet avatar Nov 05 '17 22:11 cricklet

diff --git a/src/components/Editor.jsx b/src/components/Editor.jsx
index 944f6bf..fc6c67e 100644
--- a/src/components/Editor.jsx
+++ b/src/components/Editor.jsx
@@ -1,21 +1,51 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import ACE from 'brace';
 import bindAll from 'lodash/bindAll';
 import get from 'lodash/get';
 import throttle from 'lodash/throttle';
 import noop from 'lodash/noop';
 
+import Beautify from 'js-beautify';
+import merge from 'c9.ide.threewaymerge/threewaymerge';
+
+import ACE from 'brace';
 import 'brace/ext/searchbox';
 import 'brace/mode/html';
 import 'brace/mode/css';
 import 'brace/mode/javascript';
 import 'brace/theme/monokai';
 
+const {Range} = ACE.acequire('ace/range');
+
 const RESIZE_THROTTLE = 250;
 const NORMAL_FONTSIZE = 14;
 const LARGE_FONTSIZE = 20;
 
+const BEAUTIFY_SETTINGS = {
+  indent_size: 4,
+  indent_char: ' ',
+  indent_with_tabs: true,
+  eol: '\n',
+  end_with_newline: false,
+  indent_inner_html: true,
+  indent_level: 0,
+  preserve_newlines: true,
+  max_preserve_newlines: 10,
+  space_in_paren: false,
+  space_in_empty_paren: false,
+  jslint_happy: false,
+  space_after_anon_function: false,
+  brace_style: 'collapse',
+  unindent_chained_methods: false,
+  break_chained_methods: false,
+  keep_array_indentation: false,
+  unescape_strings: false,
+  wrap_line_length: 0,
+  e4x: false,
+  comma_first: false,
+  operator_position: 'before-newline',
+};
+
 function createSessionWithoutWorker(source, language) {
   const session = ACE.createEditSession(source, null);
   session.setUseWorker(false);
@@ -23,6 +53,7 @@ function createSessionWithoutWorker(source, language) {
   return session;
 }
 
+
 class Editor extends React.Component {
   constructor() {
     super();
@@ -33,7 +64,8 @@ class Editor extends React.Component {
       }
     }, RESIZE_THROTTLE);
 
-    bindAll(this, '_handleWindowResize', '_resizeEditor', '_setupEditor');
+    bindAll(this, '_handleWindowResize', '_resizeEditor', '_setupEditor',
+      '_handleKeyPress');
   }
 
   componentDidMount() {
@@ -142,11 +174,84 @@ class Editor extends React.Component {
     this._resizeEditor();
   }
 
+  _diffAndReplace(document, range, text) {
+    const start = document.positionToIndex(range.start);
+    const oldText = document.getTextRange(range);
+    merge.patchAce(oldText, text, document, {
+      offset: start,
+      method: 'quick',
+    });
+    const diff = text.replace(/\r\n|\r|\n/g, document.getNewLineCharacter());
+    return document.indexToPosition(start + diff.length);
+  }
+
+  _handleKeyPress(event) {
+    if (event.key === 'i' && (event.metaKey || event.ctrlKey) &&
+        !event.altKey && !event.ctrlKey) {
+      const {session, selection} = this._editor;
+
+      let keepSelection = false;
+      let range = selection.getRange();
+      if (range.isEmpty()) {
+        const numRows = session.getLength();
+        range = new Range(0, 0, numRows, 0);
+        keepSelection = true;
+      }
+
+      const options = Object.assign({}, BEAUTIFY_SETTINGS);
+      if (session.getUseSoftTabs()) {
+        options.indent_char = ' ';
+        options.indent_size = session.getTabSize();
+      } else {
+        options.indent_char = '\t';
+        options.indent_size = 1;
+      }
+
+      const line = session.getLine(range.start.row);
+      const [indent] = line.match(/^\s*/);
+      let trim = false;
+
+      if (range.start.column < indent.length) {
+        range.start.column = 0;
+      } else {
+        trim = true;
+      }
+
+      let value = session.getTextRange(range);
+
+      try {
+        value = Beautify.html(value, options);
+        if (trim) {
+          value = value.replace(/^/gm, indent).trim();
+        }
+        if (range.end.column === 0) {
+          value += `\n${indent}`;
+        }
+      } catch (e) {
+        console.warn('Failed to format text with error', e);
+        return;
+      }
+
+      this._diffAndReplace(session.doc, range, value);
+
+      if (!keepSelection) {
+        selection.setSelectionRange(Range.fromPoints(range.start, end));
+      }
+
+      event.preventDefault();
+    }
+  }
+
   render() {
     return (
       <div
         className="editors__editor"
         ref={this._setupEditor}
+        onKeyPress={this._handleKeyPress}
       />
     );
   }

cricklet avatar Nov 05 '17 22:11 cricklet

In general, I'm not sure where to put the code related to this change! I'm open to suggestions to match the general organization style of popcode.

cricklet avatar Nov 05 '17 22:11 cricklet

@cricklet cool!

In general, I'm not sure where to put the code related to this change! I'm open to suggestions to match the general organization style of popcode.

  • The guts should go in a module in src/util that is reasonably agnostic to the specifics of ACE, whose job is just to take a source string + selection range (or whatever makes sense) and return a modified string + cursor position (or whatever makes sense)
  • That module should be invoked by a saga (probably defined in src/sagas/projects.js), which consumes an action along the lines of BEAUTIFY_SOURCE, calls the aforementioned method, and then dispatches another action e.g. SOURCE_BEAUTIFIED which instructs the projects reducer to update the project source in question to the beautified version, and also instructs the UI reducer to move the cursor position to the right place
  • That initial BEAUTIFY_SOURCE action should be invoked by the _handleKeyPress method in the Editor component but the footprint in that component should only be a few LOC

The clearest way I can think of solving this is to simply copy the relevant code from threewaymerge and edit it to work with blace. If I do that, where should I put the code?

I just forked that repo into the popcode org; you should have write access! Let’s just push all of our modifications to a branch (maybe brace) which we can then target in Popcode’s package.json.

Finally—feel free to open a pull request even if it’s a work in progress—happy to follow along and provide help/feedback as needed.

outoftime avatar Nov 06 '17 02:11 outoftime