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