Skip to content

Commit

Permalink
feat(prettier): Format with Prettier on Cmd/Ctrl+S (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
jackhurley23 authored and markdalgleish committed Dec 6, 2018
1 parent 7564103 commit 716c3bd
Show file tree
Hide file tree
Showing 7 changed files with 1,488 additions and 196 deletions.
6 changes: 6 additions & 0 deletions .babelrc
@@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
4 changes: 4 additions & 0 deletions lib/makeWebpackConfig.js
Expand Up @@ -36,6 +36,10 @@ module.exports = (playroomConfig, options) => {
}
},
module: {
// This option fixes https://github.com/prettier/prettier/issues/4959
// Once this issue is fixed, we can remove this line:
exprContextCritical: false,

rules: [
{
test: /\.js$/,
Expand Down
12 changes: 8 additions & 4 deletions package.json
Expand Up @@ -7,7 +7,7 @@
"playroom": "bin/cli.js"
},
"scripts": {
"test": "npm run lint && npm run cypress",
"test": "npm run lint && npm run jest && npm run cypress",
"cypress": "start-server-and-test cypress:prepare http://localhost:9000 cypress:run",
"cypress:dev": "start-server-and-test cypress:prepare http://localhost:9000 cypress:open",
"cypress:prepare": "./bin/cli.js start --config cypress/projects/basic/playroom.config.js",
Expand All @@ -16,7 +16,8 @@
"commit": "git-cz",
"lint": "eslint . && prettier --list-different '**/*.{js,md,less}'",
"format": "prettier --write '**/*.{js,md,less}'",
"semantic-release": "semantic-release"
"semantic-release": "semantic-release",
"jest": "jest src"
},
"husky": {
"hooks": {
Expand Down Expand Up @@ -54,7 +55,7 @@
"homepage": "https://github.com/seek-oss/playroom#readme",
"dependencies": {
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-env": "^7.2.0",
"@babel/preset-react": "^7.0.0",
"acorn-jsx": "^4.1.1",
"babel-loader": "^8.0.2",
Expand All @@ -76,6 +77,7 @@
"mini-css-extract-plugin": "^0.4.3",
"opn": "^5.4.0",
"parse-prop-types": "^0.3.0",
"prettier": "^1.15.3",
"prop-types": "^15.6.2",
"query-string": "^6.1.0",
"re-resizable": "^4.9.3",
Expand All @@ -92,6 +94,8 @@
},
"devDependencies": {
"@commitlint/cli": "^7.2.1",
"babel-core": "^7.0.0-bridge",
"babel-jest": "^23.6.0",
"commitizen": "^3.0.4",
"commitlint-config-seek": "^1.0.0",
"cypress": "^3.1.2",
Expand All @@ -101,8 +105,8 @@
"eslint-plugin-cypress": "^2.1.2",
"extract-text-webpack-plugin": "^3.0.2",
"husky": "^1.1.3",
"jest": "^23.6.0",
"lint-staged": "^8.0.4",
"prettier": "^1.15.1",
"semantic-release": "^15.10.8",
"start-server-and-test": "^1.7.11"
}
Expand Down
49 changes: 47 additions & 2 deletions src/Playroom/Playroom.js
Expand Up @@ -13,6 +13,7 @@ import styles from './Playroom.less';
import { store } from '../index';
import WindowPortal from './WindowPortal';
import UndockSvg from '../assets/icons/NewWindowSvg';
import { formatCode } from '../utils/formatting';

import codeMirror from 'codemirror';
import ReactCodeMirror from 'react-codemirror';
Expand Down Expand Up @@ -82,7 +83,8 @@ export default class Playroom extends Component {
code: null,
renderCode: null,
height: 200,
editorUndocked: false
editorUndocked: false,
key: 0
};
}

Expand All @@ -98,6 +100,11 @@ export default class Playroom extends Component {
this.validateCode(code);
}
);
window.addEventListener('keydown', this.handleKeyPress);
}

componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyPress);
}

storeCodeMirrorRef = cmRef => {
Expand Down Expand Up @@ -155,6 +162,36 @@ export default class Playroom extends Component {
}
};

handleKeyPress = e => {
if (
e.keyCode === 83 &&
(navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey)
) {
e.preventDefault();

const { code } = this.state;

const { formattedCode, line, ch } = formatCode({
code,
cursor: this.cmRef.codeMirror.getCursor()
});

this.setState(
{
code: formattedCode,
key: Math.random()
},
() => {
this.cmRef.codeMirror.focus();
this.cmRef.codeMirror.setCursor({
line,
ch
});
}
);
}
};

updateHeight = (event, direction, ref) => {
this.setState({
height: ref.offsetHeight
Expand All @@ -176,7 +213,14 @@ export default class Playroom extends Component {

render() {
const { components, themes, widths, frameComponent } = this.props;
const { codeReady, code, renderCode, height, editorUndocked } = this.state;
const {
codeReady,
code,
renderCode,
height,
editorUndocked,
key
} = this.state;

const themeNames = Object.keys(themes);
const frames = flatMap(widths, width =>
Expand Down Expand Up @@ -219,6 +263,7 @@ export default class Playroom extends Component {

const codeMirrorEl = (
<ReactCodeMirror
key={key}
codeMirrorInstance={codeMirror}
ref={this.storeCodeMirrorRef}
value={code}
Expand Down
78 changes: 78 additions & 0 deletions src/utils/formatting.js
@@ -0,0 +1,78 @@
import prettier from 'prettier/standalone';
import babylon from 'prettier/parser-babylon';

export const runPrettier = ({ code, cursorOffset }) => {
try {
return prettier.formatWithCursor(code, {
cursorOffset,
parser: 'babylon',
plugins: [babylon]
});
} catch (e) {
// Just a formatting error so we pass
return null;
}
};

export const positionToCursorOffset = (code, { line, ch }) => {
return code.split('\n').reduce((pos, currLine, index) => {
if (index < line) {
return pos + currLine.length + 1;
} else if (index === line) {
return pos + ch;
}
return pos;
}, 0);
};

export const cursorOffsetToPosition = (code, cursorOffset) => {
const substring = code.slice(0, cursorOffset);
const line = substring.split('\n').length - 1;
const indexOfLastLine = substring.lastIndexOf('\n');

return {
line,
ch: cursorOffset - indexOfLastLine - 1
};
};

export const wrapJsx = code => `<>\n${code}\n</>`;

// Removes `<>\n` and `\n</>` and unindents the two spaces due to the wrapping
export const unwrapJsx = code => code.replace(/\n {2}/g, '\n').slice(3, -5);

// Handles running prettier, ensuring multiple root level JSX values are valid
// by wrapping the code in <>{code}</> then finally removing the layer of indentation
// all while maintaining the cursor position.
export const formatCode = ({ code, cursor }) => {
// Since we're automatically adding a line due to the wrapping we need to
// remove one
const WRAPPED_LINE_OFFSET = 1;
// Since we are wrapping we need to "unindent" the cursor one level , i.e two spaces.
const WRAPPED_INDENT_OFFSET = 2;

const wrappedCode = wrapJsx(code);

const currentCursorPosition = positionToCursorOffset(wrappedCode, {
line: cursor.line + WRAPPED_LINE_OFFSET,
ch: cursor.ch
});

const formatResult = runPrettier({
code: wrappedCode,
cursorOffset: currentCursorPosition
});

const formattedCode = unwrapJsx(formatResult.formatted);

const position = cursorOffsetToPosition(
formatResult.formatted,
formatResult.cursorOffset
);

return {
formattedCode,
line: position.line - WRAPPED_LINE_OFFSET,
ch: position.ch - WRAPPED_INDENT_OFFSET
};
};
72 changes: 72 additions & 0 deletions src/utils/formatting.spec.js
@@ -0,0 +1,72 @@
import {
positionToCursorOffset,
cursorOffsetToPosition,
formatCode
} from './formatting';

describe('cursor offset to position', () => {
it('should work for one line', () => {
const code = `<h1>Title</h1>`;
const position = 4; // Before the capital T

expect(cursorOffsetToPosition(code, position)).toEqual({ line: 0, ch: 4 });
});

it('should work across multiple lines', () => {
const code = `<div>\n<h1>Title</h1>\n</div>`;
const position = 10; // Before the capital T

expect(cursorOffsetToPosition(code, position)).toEqual({ line: 1, ch: 4 });
});
});

describe('position to cursor offset', () => {
it('should work for one line', () => {
const code = `<h1>Title</h1>`;
const offset = {
line: 0,
ch: 4
}; // Before the capital T

expect(positionToCursorOffset(code, offset)).toEqual(4);
});

it('should work across multiple lines', () => {
const code = `<div>\n<h1>Title</h1>\n</div>`;
const offset = {
line: 1,
ch: 4
};

expect(positionToCursorOffset(code, offset)).toEqual(10);
});
});

describe('formatting code', () => {
it('should handle one line', () => {
const code = `<div><h1>Title</h1></div>`;
expect(formatCode({ code, cursor: { line: 0, ch: 9 } })).toEqual({
line: 1,
ch: 6,
formattedCode: `<div>\n <h1>Title</h1>\n</div>\n`
});
});

it('should handle multiple lines', () => {
const code = `<div>\n<h1>Title</h1>\n</div>`;
expect(formatCode({ code, cursor: { line: 1, ch: 4 } })).toEqual({
line: 1,
ch: 6,
formattedCode: `<div>\n <h1>Title</h1>\n</div>\n`
});
});

it('should handle multiple root level jsx elements', () => {
const code = `<div><h1>Title</h1></div><div><h1>Title Two</h1></div>`;
expect(formatCode({ code, cursor: { line: 0, ch: 34 } })).toEqual({
line: 4,
ch: 6,
formattedCode: `<div>\n <h1>Title</h1>\n</div>\n<div>\n <h1>Title Two</h1>\n</div>\n`
});
});
});

0 comments on commit 716c3bd

Please sign in to comment.