Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(prettier): Format with Prettier on Cmd/Ctrl+S #36

Merged
merged 3 commits into from Dec 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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`
});
});
});