feeles-ide
Version:
The hackable and serializable IDE to make learning material
692 lines (623 loc) • 24 kB
JavaScript
import _String$raw from 'babel-runtime/core-js/string/raw';
import _taggedTemplateLiteral from 'babel-runtime/helpers/taggedTemplateLiteral';
import _Promise from 'babel-runtime/core-js/promise';
import _extends from 'babel-runtime/helpers/extends';
import _toConsumableArray from 'babel-runtime/helpers/toConsumableArray';
import _slicedToArray from 'babel-runtime/helpers/slicedToArray';
import _getIterator from 'babel-runtime/core-js/get-iterator';
import _regeneratorRuntime from 'babel-runtime/regenerator';
import _asyncToGenerator from 'babel-runtime/helpers/asyncToGenerator';
import _Map from 'babel-runtime/core-js/map';
import _Object$getPrototypeOf from 'babel-runtime/core-js/object/get-prototype-of';
import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
import _createClass from 'babel-runtime/helpers/createClass';
import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
import _inherits from 'babel-runtime/helpers/inherits';
import _WeakMap from 'babel-runtime/core-js/weak-map';
var _templateObject = _taggedTemplateLiteral(['(const|let|var)s', '(d+)s'], ['(const|let|var)\\s', '(\\d+)\\s']);
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import FlatButton from 'material-ui/FlatButton';
import LinearProgress from 'material-ui/LinearProgress';
import HardwareKeyboardBackspace from 'material-ui/svg-icons/hardware/keyboard-backspace';
import ContentSave from 'material-ui/svg-icons/content/save';
import { Pos } from 'codemirror';
import beautify from 'js-beautify';
import includes from 'lodash/includes';
import Editor from './Editor';
import CreditBar from './CreditBar';
import PlayMenu from './PlayMenu';
import AssetPane from './AssetPane';
import ErrorPane from './ErrorPane';
import zenkakuToHankaku from './zenkakuToHankaku';
var getStyle = function getStyle(props, state, context) {
var palette = context.muiTheme.palette;
return {
root: {
position: 'absolute',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch'
},
editorContainer: {
flex: '1 1 auto',
position: 'relative'
},
menuBar: {
display: 'flex',
backgroundColor: palette.canvasColor,
borderBottom: '1px solid ' + palette.primary1Color,
zIndex: 3
},
barButton: {
padding: 0,
lineHeight: 2
},
barButtonLabel: {
fontSize: '.5rem'
},
progressColor: palette.primary1Color,
progress: {
borderRadius: 0
}
};
};
// file -> prevFile を参照する
var prevFiles = new _WeakMap();
var SourceEditor = function (_PureComponent) {
_inherits(SourceEditor, _PureComponent);
function SourceEditor() {
var _ref,
_this2 = this;
var _temp, _this, _ret;
_classCallCheck(this, SourceEditor);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = SourceEditor.__proto__ || _Object$getPrototypeOf(SourceEditor)).call.apply(_ref, [this].concat(args))), _this), _this.state = {
showHint: !_this.props.file.is('json'),
hasHistory: false,
hasChanged: false,
loading: false,
snippets: [],
assetFileName: null,
assetLineNumber: 0,
assetScope: null,
appendToHead: true,
classNameStyles: []
}, _this._widgets = new _Map(), _this.handleSave = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
var text, prevFile, file, babelrc;
return _regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
if (_this.codemirror) {
_context.next = 2;
break;
}
return _context.abrupt('return');
case 2:
_this.beautify(_this.codemirror); // Auto beautify
text = _this.codemirror.getValue();
if (!(text === _this.props.file.text)) {
_context.next = 6;
break;
}
return _context.abrupt('return');
case 6:
_this.setState({ hasChanged: false, loading: true });
prevFile = _this.props.file;
_context.next = 10;
return _this.props.putFile(_this.props.file, _this.props.file.set({ text: text }));
case 10:
file = _context.sent;
prevFiles.set(file, prevFile);
// Like a watching
babelrc = _this.props.getConfig('babelrc');
file.babel(babelrc, function (e) {
_this.props.selectTabFromFile(file);
// あらたな Babel Error が発生したときを検知して,
// ダイアログを表示させる (エラーの詳細は file.error を参照する)
_this.forceUpdate(); // 再描画
console.info(e);
});
_this.setState({ loading: false });
case 15:
case 'end':
return _context.stop();
}
}
}, _callee, _this2);
})), _this.handleUndo = function () {
_this.codemirror.undo();
}, _this.handleUpdateWidget = function (cm) {
_this._widgets.clear();
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = _getIterator(cm.getValue('\n').split('\n').entries()), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var _step$value = _slicedToArray(_step.value, 2),
line = _step$value[0],
text = _step$value[1];
_this.updateWidget(cm, line, text);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
}, _this.updateWidget = function (cm, line, text) {
// Syntax: /*+ モンスター アイテム */
var asset = /^(.*)(\/\*)(\+[^*]+)(\*\/)/.exec(text);
if (asset) {
var _asset$map = asset.map(function (t) {
return t.replace(/\t/g, ' ');
}),
_asset$map2 = _slicedToArray(_asset$map, 5),
_prefix = _asset$map2[1],
_left = _asset$map2[2],
_label = _asset$map2[3],
_right = _asset$map2[4];
var prefix = document.createElement('span');
prefix.textContent = _prefix;
prefix.classList.add('Feeles-asset-blank');
var left = document.createElement('span');
left.textContent = _left;
left.classList.add('Feeles-asset-blank');
var label = document.createElement('span');
label.textContent = _label;
var right = document.createElement('span');
right.textContent = _right;
right.classList.add('Feeles-asset-blank');
var button = document.createElement('span');
button.classList.add('Feeles-asset-button');
button.onclick = function () {
_this.setState({
assetScope: _label.substr(1).trim(),
assetLineNumber: line,
appendToHead: false
});
};
button.appendChild(left);
button.appendChild(label);
button.appendChild(right);
var parent = document.createElement('div');
parent.classList.add('Feeles-widget', 'Feeles-asset');
parent.appendChild(prefix);
parent.appendChild(button);
_this._widgets.set(line, parent);
}
}, _this.handleValueClick = function (event) {
// Put cursor into editor
if (_this.codemirror) {
var locate = { left: event.x, top: event.y };
var pos = _this.codemirror.coordsChar(locate);
_this.codemirror.focus();
_this.codemirror.setCursor(pos);
}
}, _this.handleRenderWidget = function (cm) {
// remove old widgets
var _arr = [].concat(_toConsumableArray(document.querySelectorAll('.Feeles-asset')));
for (var _i = 0; _i < _arr.length; _i++) {
var widget = _arr[_i];
if (widget.parentNode) {
widget.parentNode.removeChild(widget);
}
}
// render new widgets
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = _getIterator(_this._widgets.entries()), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var _step2$value = _slicedToArray(_step2.value, 2),
i = _step2$value[0],
element = _step2$value[1];
cm.addWidget(new Pos(i, 0), element);
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
}, _this.handleIndexReplacement = function (cm, change) {
if (!includes(['asset', 'paste'], change.origin)) return;
var _arr2 = ['item', 'map'];
var _loop = function _loop() {
var keyword = _arr2[_i2];
// すでに使われている item{N} のような変数を探す.
// 戻り値は Array<Number>
// e.g. From "const item1 = 'hello';", into [1]
var usedIndexes = searchItemIndexes(cm.getValue('\n'), keyword);
if (usedIndexes.length < 1) return 'continue';
var sourceText = change.text.join('\n');
if (usedIndexes.some(function (i) {
return includes(sourceText, keyword + i);
})) {
// もし名前が競合していたら…
var max = Math.max.apply(null, usedIndexes);
var regExp = new RegExp(keyword + '(\\d+)', 'g');
var text = sourceText.replace(regExp, function (match, n) {
// item{n} => item{n+max}
n = n >> 0;
return keyword + (n + max);
});
change.update(change.from, change.to, text.split('\n'));
}
};
for (var _i2 = 0; _i2 < _arr2.length; _i2++) {
var _ret2 = _loop();
if (_ret2 === 'continue') continue;
}
}, _this.handleAssetClose = function () {
_this.setState({
assetFileName: null,
assetScope: null
});
}, _this.setLocation = function () {
var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(href) {
return _regeneratorRuntime.wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
_context2.next = 2;
return _this.handleSave();
case 2:
return _context2.abrupt('return', _this.props.setLocation(href));
case 3:
case 'end':
return _context2.stop();
}
}
}, _callee2, _this2);
}));
return function (_x) {
return _ref3.apply(this, arguments);
};
}(), _this.handleSaveAndRun = function () {
return _this.setLocation();
}, _this.handleAssetInsert = function (_ref4) {
var code = _ref4.code;
var assetLineNumber = _this.state.assetLineNumber;
var pos = new Pos(assetLineNumber, 0);
var end = new Pos(pos.line + code.split('\n').length, 0);
code = _this.state.appendToHead ? '\n' + code : code + '\n';
_this.codemirror.replaceRange(code, pos, pos, 'asset');
// トランジション(フェードイン)
var fadeInMarker = _this.codemirror.markText(pos, end, {
className: 'emphasize-' + Date.now(),
clearOnEnter: true
});
_this.emphasizeTextMarker(fadeInMarker);
// スクロール
_this.codemirror.scrollIntoView({
from: pos,
to: end
}, 10);
// カーソル (挿入直後に undo したときスクロールが上に戻るのを防ぐ)
_this.codemirror.focus();
_this.codemirror.setCursor(end);
// Pane をとじる
_this.handleAssetClose();
// 実行 (UIが固まらないように時間をおいている)
setTimeout(_this.setLocation, 1000);
}, _this.emphasizeTextMarker = function () {
var _ref5 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(textMarker) {
var transitions, begin, end;
return _regeneratorRuntime.wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
transitions = _this.context.muiTheme.transitions;
begin = {
className: textMarker.className,
style: 'opacity: 0; background-color: rgba(0,0,0,1)'
};
end = {
className: textMarker.className,
style: 'opacity: 1; background-color: rgba(0,0,0,0.1); transition: ' + transitions.easeOut()
};
textMarker.on('clear', function () {
_this.setState(function (prevState) {
return {
classNameStyles: prevState.classNameStyles.filter(function (item) {
return begin !== item && end !== item;
})
};
});
});
_this.setState(function (prevState) {
return {
classNameStyles: prevState.classNameStyles.concat(begin)
};
});
_context3.next = 7;
return wait(500);
case 7:
_this.setState(function (prevState) {
return {
classNameStyles: prevState.classNameStyles.map(function (item) {
return item === begin ? end : item;
})
};
});
case 8:
case 'end':
return _context3.stop();
}
}
}, _callee3, _this2);
}));
return function (_x2) {
return _ref5.apply(this, arguments);
};
}(), _this.handleIndentLine = function (cm, change) {
if (!includes(['asset', 'paste'], change.origin)) return;
var from = change.from;
var to = new Pos(from.line + change.text.length, 0);
// インデント
for (var line = from.line; line < to.line; line++) {
cm.indentLine(line);
}
}, _this.handleRestore = function () {
// 保存する前の状態に戻す
var prevFile = prevFiles.get(_this.props.file);
if (prevFile) {
_this.setValue(prevFile.text);
_this.setLocation();
}
}, _this.beautify = function () {
var _this$props = _this.props,
file = _this$props.file,
fileView = _this$props.fileView;
var prevValue = _this.codemirror.getValue();
var setValueWithoutHistory = function setValueWithoutHistory(replacement) {
// undo => beautify => setValue することで history を 1 つに
var _this$codemirror$getS = _this.codemirror.getScrollInfo(),
left = _this$codemirror$getS.left,
top = _this$codemirror$getS.top;
_this.codemirror.undo();
_this.codemirror.setValue(replacement);
_this.codemirror.scrollTo(left, top);
};
// import .jsbeautifyrc
var configs = {};
try {
var runCommand = fileView.getFileByFullPath('.jsbeautifyrc');
if (runCommand) {
configs = JSON.parse(runCommand.text);
}
} catch (error) {
console.info(error);
}
if (file.is('javascript') || file.is('json')) {
setValueWithoutHistory(beautify(prevValue, configs.js || {}));
} else if (file.is('html')) {
setValueWithoutHistory(beautify.html(prevValue, configs.html || {}));
} else if (file.is('css')) {
setValueWithoutHistory(beautify.css(prevValue, configs.css || {}));
}
}, _temp), _possibleConstructorReturn(_this, _ret);
}
_createClass(SourceEditor, [{
key: 'componentWillMount',
value: function componentWillMount() {
this.setState({
snippets: this.props.getConfig('snippets')(this.props.file)
});
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
if (this.props.fileView !== nextProps.fileView) {
this.setState({
snippets: nextProps.getConfig('snippets')(nextProps.file)
});
}
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
var _this3 = this;
if (this.codemirror) {
this.codemirror.on('beforeChange', zenkakuToHankaku);
this.codemirror.on('beforeChange', this.handleIndexReplacement);
this.codemirror.on('change', this.handleIndentLine);
var onChange = function onChange(cm) {
_this3.setState({
hasHistory: cm.historySize().undo > 0,
hasChanged: cm.getValue('\n') !== _this3.props.file.text
});
};
this.codemirror.on('change', onChange);
this.codemirror.on('swapDoc', onChange);
this.codemirror.on('change', this.handleUpdateWidget);
this.codemirror.on('swapDoc', this.handleUpdateWidget);
this.codemirror.on('update', this.handleRenderWidget);
this.handleUpdateWidget(this.codemirror);
this.handleRenderWidget(this.codemirror);
}
}
}, {
key: 'setValue',
value: function setValue(value) {
var _codemirror$getScroll = this.codemirror.getScrollInfo(),
left = _codemirror$getScroll.left,
top = _codemirror$getScroll.top;
this.codemirror.setValue(value);
this.codemirror.scrollTo(left, top);
}
}, {
key: 'render',
value: function render() {
var _this4 = this;
var _props = this.props,
file = _props.file,
localization = _props.localization;
var showHint = this.state.showHint;
var styles = getStyle(this.props, this.state, this.context);
// const snippets = this.props.getConfig('snippets')(file);
var extraKeys = {
'Ctrl-Enter': function CtrlEnter() {
// Key Binding された操作の直後にカーソルが先頭に戻ってしまう(?)ため,
// それをやり過ごしてから実行する
window.setTimeout(_this4.handleSaveAndRun, 10);
},
'Ctrl-Alt-B': function CtrlAltB() {
// Key Binding された操作の直後にカーソルが先頭に戻ってしまう(?)ため,
// それをやり過ごしてから実行する
window.setTimeout(_this4.beautify, 10);
}
};
var foldOptions = {
widget: ' 📦 ',
minFoldSize: 1,
scanUp: false
};
return React.createElement(
'div',
{ style: styles.root },
React.createElement(
'style',
null,
this.state.classNameStyles.map(function (item) {
return '.' + item.className + ' { ' + item.style + ' } ';
})
),
React.createElement(
'div',
{ style: styles.menuBar },
React.createElement(FlatButton, {
label: localization.editorCard.undo,
disabled: !this.state.hasHistory,
style: styles.barButton,
labelStyle: styles.barButtonLabel,
icon: React.createElement(HardwareKeyboardBackspace, null),
onClick: this.handleUndo
}),
React.createElement(FlatButton, {
label: localization.editorCard.save,
disabled: !this.state.hasChanged,
style: styles.barButton,
labelStyle: styles.barButtonLabel,
icon: React.createElement(ContentSave, null),
onClick: this.handleSaveAndRun
}),
React.createElement('div', {
style: {
flex: '1 1 auto'
}
}),
React.createElement(PlayMenu, {
getFiles: this.props.getFiles,
setLocation: this.setLocation,
href: this.props.href,
localization: this.props.localization
})
),
this.state.loading ? React.createElement(LinearProgress, {
color: styles.progressColor,
style: styles.progress
}) : null,
React.createElement(
'div',
{ style: styles.editorContainer },
React.createElement(AssetPane, {
fileView: this.props.fileView,
open: !!this.state.assetScope,
scope: this.state.assetScope,
loadConfig: this.props.loadConfig,
findFile: this.props.findFile,
handleClose: this.handleAssetClose,
handleAssetInsert: this.handleAssetInsert,
localization: this.props.localization
}),
React.createElement(Editor, _extends({}, this.props, {
showHint: showHint,
snippets: this.state.snippets,
codemirrorRef: function codemirrorRef(ref) {
return _this4.codemirror = ref;
},
onDocChanged: this.props.onDocChanged,
extraKeys: extraKeys,
foldOptions: foldOptions,
loadConfig: this.props.loadConfig,
fileView: this.props.fileView
}))
),
React.createElement(ErrorPane, {
error: file.error,
localization: localization,
onRestore: this.handleRestore,
canRestore: prevFiles.has(file)
}),
React.createElement(CreditBar, {
file: file,
openFileDialog: this.props.openFileDialog,
putFile: this.props.putFile,
localization: localization,
getFiles: this.props.getFiles
})
);
}
}]);
return SourceEditor;
}(PureComponent);
SourceEditor.propTypes = {
fileView: PropTypes.object.isRequired,
file: PropTypes.object.isRequired,
files: PropTypes.array.isRequired,
getFiles: PropTypes.func.isRequired,
setLocation: PropTypes.func.isRequired,
href: PropTypes.string.isRequired,
getConfig: PropTypes.func.isRequired,
loadConfig: PropTypes.func.isRequired,
findFile: PropTypes.func.isRequired,
reboot: PropTypes.bool.isRequired,
localization: PropTypes.object.isRequired,
openFileDialog: PropTypes.func.isRequired,
putFile: PropTypes.func.isRequired,
closeSelectedTab: PropTypes.func.isRequired,
selectTabFromFile: PropTypes.func.isRequired,
onDocChanged: PropTypes.func.isRequired
};
SourceEditor.contextTypes = {
muiTheme: PropTypes.object.isRequired
};
export default SourceEditor;
function wait(millisec) {
return new _Promise(function (resolve) {
setTimeout(resolve, millisec);
});
}
function searchItemIndexes(text, keyword) {
var limit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1000;
var regExp = new RegExp(_String$raw(_templateObject, keyword), 'g');
text = beautify(text);
var indexes = [];
for (var i = 0, result = null; (result = regExp.exec(text)) && i < limit; i++) {
indexes.push(+result[2]);
}
return indexes;
}