diff --git a/app/commons/File/subscribeToFileChange.js b/app/commons/File/subscribeToFileChange.js index 863f0274..8526d33e 100644 --- a/app/commons/File/subscribeToFileChange.js +++ b/app/commons/File/subscribeToFileChange.js @@ -43,6 +43,12 @@ function handleGitFiles (node) { return false } +// fixme: maybe we should make this a standard method of File model +function fileIsOpened (filePath) { + const openedFilePaths = mobxStore.EditorState.entities.values().map(editor => editor.filePath) + return openedFilePaths.includes(filePath) +} + export default function subscribeToFileChange () { autorun(() => { if (!config.fsSocketConnected) return @@ -51,27 +57,22 @@ export default function subscribeToFileChange () { client.subscribe(`/topic/ws/${config.spaceKey}/change`, (frame) => { const data = JSON.parse(frame.body) const node = data.fileInfo + switch (data.changeType) { case 'create': - if (handleGitFiles(node)) { - break - } - FileActions.loadNodeData([node]) - break case 'modify': if (handleGitFiles(node)) { break } - FileActions.loadNodeData([node]) - const tabsToUpdate = mobxStore.EditorTabState.tabs.values().filter(tab => tab.path === node.path) - if (tabsToUpdate.length) { + if (!node.isDir && fileIsOpened(node.path)) { api.readFile(node.path).then(({ content }) => { - TabActions.updateTabByPath({ - path: node.path, - content, - }) + node.content = content + FileActions.loadNodeData([node]) }) + } else { + FileActions.loadNodeData([node]) } + break case 'delete': FileActions.removeNode(node) diff --git a/app/commons/Tree/state.js b/app/commons/Tree/state.js index 0737f538..05223f57 100644 --- a/app/commons/Tree/state.js +++ b/app/commons/Tree/state.js @@ -1,5 +1,6 @@ import _ from 'lodash' import { observable, computed, action, autorun } from 'mobx' +import { protectedObservable } from 'utils/decorators' function TreeNodeScope () { const SHADOW_ROOT_NODE = 'SHADOW_ROOT_NODE' @@ -28,17 +29,13 @@ function TreeNodeScope () { state.entities.set(this.id, this) } - @observable _isDir = false - @observable _name = '' - @computed get name () { return this._name } - set name (v) { return this._name = v } - @computed get isDir () { return this._isDir } - set isDir (v) { return this._isDir = v } + @protectedObservable _name = '' + @protectedObservable _isDir = false + @protectedObservable _parentId = undefined @observable isFolded = true @observable isFocused = false @observable isHighlighted = false - @observable parentId = undefined @observable index = 0 @computed get isShadowRoot () { diff --git a/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx b/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx index 1b9e2500..53c2824a 100644 --- a/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx +++ b/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx @@ -30,6 +30,10 @@ class BaseCodeEditor extends Component {
this.dom = r} style={{ width: '100%', height: '100%' }} /> ) } + + componentWillUnmount () { + this.editor.destroy() + } } BaseCodeEditor.propTypes = { diff --git a/app/components/FileTree/actions.js b/app/components/FileTree/actions.js index 9c824507..f5776c2c 100644 --- a/app/components/FileTree/actions.js +++ b/app/components/FileTree/actions.js @@ -67,9 +67,9 @@ export const toggleNodeFold = registerAction('filetree:toggle_node_fold', } ) -export const removeNode = registerAction('filetree:remove_node', - node => state.entities.delete(node.id) -) +export const removeNode = registerAction('filetree:remove_node', (node) => { + state.entities.delete(node.id || node.path) +}) export const openContextMenu = contextMenuStore.openContextMenuFactory(FileTreeContextMenuItems) export const closeContextMenu = contextMenuStore.closeContextMenu diff --git a/app/components/FileTree/state.js b/app/components/FileTree/state.js index 7329432c..738ae651 100644 --- a/app/components/FileTree/state.js +++ b/app/components/FileTree/state.js @@ -42,29 +42,29 @@ class FileTreeNode extends TreeNode { if (this.path === ROOT_PATH) this.isFolded = false } - @observable path = null - - // override default name / isDir behavior + /* override base class */ @computed get name () { - return this.file.name + return this.file ? this.file.name : '' } @computed get isDir () { - return this.file.isDir - } - - @computed get file () { - return FileState.entities.get(this.path) + return this.file ? this.file.isDir : false } - @computed get parent () { + @computed get parentId () { // prioritize corresponding file's tree node - if (this.file) { - const parentFile = this.file && this.file.parent - if (parentFile === null) return state.shadowRoot - return state.entities.get(parentFile.path) + if (this.file && this.file.parent) { + return this.file.parent.path } - return null + return this._parentId + } + /* end override */ + + /* extend base class */ + @observable path = null + + @computed get file () { + return FileState.entities.get(this.path) } @computed get children () { @@ -84,6 +84,7 @@ class FileTreeNode extends TreeNode { @computed get size () { if (this.file) return this.file.size } + /* end extend */ } export default state diff --git a/app/config.js b/app/config.js index d84e2b38..8ea52817 100644 --- a/app/config.js +++ b/app/config.js @@ -14,6 +14,7 @@ const config = observable({ fsSocketConnected: false, ttySocketConnected: false, fileExcludePatterns: ['/.git', '/.coding-ide'], + preventAccidentalClose: false, }) window.config = config diff --git a/app/initialize/state.js b/app/initialize/state.js index eb309525..82d2e730 100644 --- a/app/initialize/state.js +++ b/app/initialize/state.js @@ -63,6 +63,18 @@ const stepCache = observable.map({ func: () => api.connectWebsocketClient() }, + preventAccidentalClose: { + desc: 'Prevent accidental close', + func: () => { + window.onbeforeunload = function () { + if (config.preventAccidentalClose) { + return 'Do you really want to leave this site? Changes you made may not be saved.' + } + return void 0 + } + return true + } + } }) stepCache.insert = function (key, value, referKey, before = false) { diff --git a/app/settings.js b/app/settings.js index 82a5ec2b..1d1d2663 100644 --- a/app/settings.js +++ b/app/settings.js @@ -1,7 +1,8 @@ import isObject from 'lodash/isObject' +import { observable, reaction, extendObservable, computed, action } from 'mobx' +import config from 'config' import emitter, { THEME_CHANGED } from 'utils/emitter' import is from 'utils/is' -import { observable, reaction, extendObservable, computed, action, autorunAsync } from 'mobx' import dynamicStyle from 'utils/dynamicStyle' let EditorState @@ -14,7 +15,7 @@ export const UIThemeOptions = [ ] export const SyntaxThemeOptions = ['default', 'neo', 'eclipse', 'monokai', 'material'] -const changeTheme = (nextThemeId) => { +const changeUITheme = (nextThemeId) => { if (!window.themes) window.themes = {} if (UIThemeOptions.map(option => option.value).includes(nextThemeId)) { import(`!!style-loader/useable!css-loader!stylus-loader!./styles/${nextThemeId}/index.styl`) @@ -26,9 +27,10 @@ const changeTheme = (nextThemeId) => { }) } - if (nextThemeId === 'dark' && EditorState.options.theme === 'default') { + const editorTheme = EditorState.options.theme + if (nextThemeId === 'dark' && (editorTheme === 'default' || editorTheme === 'neo' || editorTheme === 'eclipse')) { settings.theme.syntax_theme.value = 'material' - } else if (nextThemeId === 'base-theme' && (EditorState.options.theme === 'monokai' || EditorState.options.theme === 'material')) { + } else if (nextThemeId === 'base-theme' && (editorTheme === 'monokai' || editorTheme === 'material')) { settings.theme.syntax_theme.value = 'default' } emitter.emit(THEME_CHANGED, nextThemeId) @@ -144,22 +146,21 @@ const settings = observable({ ui_theme: { name: 'settings.theme.uiTheme', value: 'base-theme', - options: UIThemeOptions + options: UIThemeOptions, + reaction: changeUITheme, }, syntax_theme: { name: 'settings.theme.syntaxTheme', value: 'default', options: SyntaxThemeOptions, - reaction (value) { - changeSyntaxTheme(value) - } + reaction: changeSyntaxTheme, } }), extensions: new DomainSetting({}), general: new DomainSetting({ - _keys: ['language', 'hide_files'], + _keys: ['language', 'exclude_files'], requireConfirm: true, language: { name: 'settings.general.language', @@ -169,9 +170,12 @@ const settings = observable({ { name: 'settings.general.languageOption.chinese', value: 'Chinese' }, ] }, - hide_files: { + exclude_files: { name: 'settings.general.hideFiles', - value: '/.git,/.coding-ide' + value: config.fileExcludePatterns.join(','), + reaction (value) { + config.fileExcludePatterns = value.split(',') + } } }), @@ -275,7 +279,3 @@ const settings = observable({ }) export default settings - -autorunAsync('changeTheme', () => { - changeTheme(settings.theme.ui_theme.value) -}) diff --git a/app/utils/decorators/index.js b/app/utils/decorators/index.js index d2aef227..d2984e5b 100644 --- a/app/utils/decorators/index.js +++ b/app/utils/decorators/index.js @@ -1,4 +1,5 @@ import mapEntityFactory from './mapEntity' import defaultProps from './defaultProps' +import protectedObservable from './protectedObservable' -export { mapEntityFactory, defaultProps } +export { mapEntityFactory, defaultProps, protectedObservable } diff --git a/app/utils/decorators/protectedObservable.js b/app/utils/decorators/protectedObservable.js new file mode 100644 index 00000000..56d7fbd7 --- /dev/null +++ b/app/utils/decorators/protectedObservable.js @@ -0,0 +1,40 @@ +import { observable, computed } from 'mobx' + +/* + * This decorator enforce a pattern that's widely used in this project, + * + * @protectedObservable _foo = 'bar' + * + * is a short hand for: + * + * @observable _foo = 'bar' + * @computed + * get foo () { return this._foo } + * set foo (value) { return this._foo = value } + * + * you can specify publicKey explicitly by calling: + * @protectedObservable('publicFoo') _foo = 'bar' + */ +function _protectedObservableDecorator (target, privateKey, descriptor, publicKey) { + if (!publicKey) publicKey = privateKey.replace(/^_/, '') + + const computedDescriptor = computed(target, publicKey, { + get () { return this[privateKey] }, + set (v) { return this[privateKey] = v }, + }) + + Object.defineProperty(target, publicKey, computedDescriptor) + + return observable(target, privateKey, descriptor) +} + +function protectedObservable (optionalPublicKey) { + if (typeof optionalPublicKey === 'string') { + return function protectedObservableDecorator (target, key, descriptor) { + return _protectedObservableDecorator(target, key, descriptor, optionalPublicKey) + } + } else { + return _protectedObservableDecorator.apply(null, arguments) + } +} +export default protectedObservable