Skip to content

Commit 2d40b75

Browse files
authored
Merge pull request #118 from Coding/hackape/git-blame
redesign the modeling of tab/editor/file/filetree + git blame feature
2 parents e3a93eb + 7e670e5 commit 2d40b75

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+1652
-1138
lines changed

app/backendAPI/gitAPI.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,7 @@ export function gitReadFile ({ref, path}) {
127127
export function gitHistory ({ path, page, size }) {
128128
return request.get(`/git/${config.spaceKey}/logs`, { path, page, size })
129129
}
130+
131+
export function gitBlame (path) {
132+
return request.get(`/git/${config.spaceKey}/blame`, { path })
133+
}

app/backendAPI/websocketClients.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class TtySocketClient {
106106
// manually handle all connect/reconnect behavior
107107
connectingPromise = undefined
108108
connect () {
109+
if (!config.isPlatform) return
109110
// Need to make sure EVERY ATTEMPT to connect has ensured `fsSocketConnected == true`
110111
if (this.socket.connected || this.connectingPromise) return this.connectingPromise
111112
let resolve, reject

app/commands/commandBindings/file.js

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import mobxStore from '../../mobxStore'
55
import { path as pathUtil } from '../../utils'
66
import api from '../../backendAPI'
77
import * as _Modal from '../../components/Modal/actions'
8-
import * as TabActions from 'commons/Tab/actions'
8+
import TabStore from 'components/Tab/store'
9+
import FileTreeStore from 'components/FileTree/store'
10+
import FileStore from 'commons/File/store'
911
import { notify } from '../../components/Notification/actions'
1012

1113
const Modal = bindActionCreators(_Modal, dispatch)
@@ -51,13 +53,10 @@ function createFolderAtPath (path) {
5153
}
5254

5355
function openTabOfNewFile (path) {
54-
TabActions.createTab({
55-
id: uniqueId('tab_'),
56-
type: 'editor',
56+
TabStore.createTab({
5757
title: path.split('/').pop(),
58-
path: path,
59-
content: {
60-
body: '',
58+
editor: {
59+
filePath: path,
6160
}
6261
})
6362
}
@@ -91,29 +90,30 @@ export default {
9190
'file:save': (c) => {
9291
const { EditorTabState } = mobxStore
9392
const activeTab = EditorTabState.activeTab
94-
const content = activeTab ? activeTab.editor.getValue() : ''
93+
const content = activeTab ? activeTab.editor.cm.getValue() : ''
9594

96-
if (!activeTab.path) {
95+
if (!activeTab.file) {
9796
const createFile = createFileWithContent(content)
9897
Modal.showModal('Prompt', {
9998
message: 'Enter the path for the new file.',
10099
defaultValue: '/untitled',
101100
selectionRange: [1, '/untitled'.length]
102101
})
103102
.then(createFile)
104-
.then(path => dispatch(TabActions.updateTab({
103+
.then(path => TabStore.updateTab({
105104
id: activeTab.id,
106-
path,
107-
title: path.replace(/^.*\/([^\/]+$)/, '$1')
108-
})))
109-
.then(() => dispatch(TabActions.updateTabFlags(activeTab.id, 'modified', false)))
105+
title: path.replace(/^.*\/([^\/]+$)/, '$1'),
106+
editor: { filePath: path },
107+
}))
108+
.then(() => TabStore.updateTabFlags(activeTab.id, 'modified', false))
110109
} else {
111-
api.writeFile(activeTab.path, content)
110+
api.writeFile(activeTab.file.path, content)
112111
.then(() => {
113-
dispatch(TabActions.updateTabFlags(activeTab.id, 'modified', false))
114-
dispatch(TabActions.updateTab({
115-
id: activeTab.id, content: { body: content }
116-
}))
112+
TabStore.updateTabFlags(activeTab.id, 'modified', false)
113+
FileStore.updateFile({
114+
path: activeTab.file.path,
115+
content,
116+
})
117117
})
118118
}
119119
},
@@ -160,9 +160,9 @@ export default {
160160
Modal.dismissModal()
161161
},
162162

163-
164163
'file:download': c => {
165164
api.downloadFile(c.context.path, c.context.isDir)
166-
}
165+
},
166+
167167
// 'file:unsaved_files_list':
168168
}

app/commands/commandBindings/tab.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* @flow weak */
22
import { dispatch as $d } from '../../store'
33
import store from 'mobxStore'
4-
import * as Tab from 'commons/Tab/actions'
4+
import * as Tab from 'components/Tab/actions'
55
import * as PaneActions from 'components/Pane/actions'
66

77
export default {

app/commons/File/actions.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { registerAction } from 'utils/actions'
2+
import is from 'utils/is'
3+
import { action } from 'mobx'
4+
import api from 'backendAPI'
5+
import state, { FileNode } from './state'
6+
7+
export const loadNodeData = registerAction('fs:load_node_data',
8+
(nodeConfigs) => {
9+
if (!is.array(nodeConfigs)) nodeConfigs = [nodeConfigs]
10+
return nodeConfigs.map(nodeConfig => {
11+
const curNode = state.entities.get(nodeConfig.path)
12+
if (curNode) {
13+
curNode.update(nodeConfig)
14+
return curNode
15+
}
16+
const newNode = new FileNode(nodeConfig)
17+
state.entities.set(newNode.path, newNode)
18+
return newNode
19+
})
20+
21+
}
22+
)
23+
24+
export const fetchProjectRoot = registerAction('fs:init', () =>
25+
api.fetchPath('/').then(loadNodeData)
26+
)
27+
28+
export const removeNode = registerAction('fs:remove_node', (node) => {
29+
if (is.string(node.path)) state.entities.delete(node.path)
30+
})
31+
32+
export const updateFile = registerAction('fs:update', (fileProps) => {
33+
const path = fileProps.id || fileProps.path
34+
const file = state.entities.get(path)
35+
file.update(fileProps)
36+
})

app/commons/File/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import FileState from './state'
2+
3+
export { FileState }

app/commons/File/state.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import _ from 'lodash'
2+
import { createTransformer, toJS, extendObservable, observable, computed, action } from 'mobx'
3+
import config from 'config'
4+
5+
const ROOT_PATH = ''
6+
const nodeSorter = (a, b) => {
7+
// node.isDir comes first
8+
// then sort by node.path alphabetically
9+
if (a.isDir && !b.isDir) return -1
10+
if (a.path < b.path) return -1
11+
if (a.path > b.path) return 1
12+
return 0
13+
}
14+
15+
const state = observable({
16+
entities: observable.map(),
17+
get root () {
18+
return this.entities.get(ROOT_PATH)
19+
},
20+
})
21+
22+
class FileNode {
23+
constructor (nodeConfig) {
24+
const {
25+
name,
26+
path,
27+
isDir,
28+
gitStatus,
29+
contentType,
30+
content,
31+
size,
32+
} = nodeConfig
33+
34+
extendObservable(this, {
35+
name,
36+
path,
37+
isDir,
38+
gitStatus,
39+
contentType,
40+
content,
41+
size,
42+
})
43+
44+
state.entities.set(this.path, this)
45+
}
46+
47+
@observable name
48+
@observable path
49+
@observable contentType
50+
@observable content = ''
51+
@observable isDir = false
52+
@observable gitStatus = {}
53+
@observable size = 0
54+
55+
@computed get id () {
56+
return this.path
57+
}
58+
set id (v) {
59+
this.path = v
60+
}
61+
62+
@computed get isRoot () {
63+
return this.path === ROOT_PATH
64+
}
65+
66+
@computed get depth () {
67+
var slashMatches = this.path.match(/\/(?=.)/g)
68+
return slashMatches ? slashMatches.length : 0
69+
}
70+
71+
@computed
72+
get parent () {
73+
if (this.isRoot) return null
74+
const pathComps = this.path.split('/')
75+
pathComps.pop()
76+
const parentPath = pathComps.join('/')
77+
const parent = state.entities.get(parentPath)
78+
// only rootFileNode has no parent, other case means something wrong
79+
if (!parent) { throw Error(`Missing internal node of path '${parentPath}'`) }
80+
return parent
81+
}
82+
83+
@computed
84+
get children () {
85+
const depth = this.depth
86+
return state.entities.values()
87+
.filter(node => {
88+
return node.path.startsWith(`${this.path}/`) && node.depth === depth + 1
89+
})
90+
.sort(nodeSorter)
91+
}
92+
93+
@computed
94+
get siblings () {
95+
return this.parent.children
96+
}
97+
98+
@computed
99+
get firstChild () {
100+
return this.children[0]
101+
}
102+
103+
@computed
104+
get lastChild () {
105+
return this.children.pop()
106+
}
107+
108+
@computed
109+
get prev () {
110+
const siblings = this.siblings
111+
return siblings[siblings.indexOf(this) - 1]
112+
}
113+
114+
@computed
115+
get next () {
116+
const siblings = this.siblings
117+
return siblings[siblings.indexOf(this) + 1]
118+
}
119+
120+
@action
121+
forEachDescendant (handler) {
122+
if (!this.isDir) return
123+
this.children.forEach(childNode => {
124+
handler(childNode)
125+
childNode.forEachDescendant(handler)
126+
})
127+
}
128+
129+
@action
130+
update (nodeConfig) {
131+
extendObservable(this, nodeConfig)
132+
}
133+
}
134+
135+
state.entities.set(ROOT_PATH, new FileNode({
136+
path: ROOT_PATH,
137+
name: config.projectName,
138+
isDir: true,
139+
}))
140+
141+
export default state
142+
export { state, FileNode }

app/commons/File/store.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as actions from './actions'
2+
import state, { FileNode } from './state'
3+
4+
class FileStore {
5+
constructor () {
6+
Object.assign(this, actions)
7+
}
8+
9+
getState () { return state }
10+
11+
get (path) {
12+
let file = state.entities.get(path)
13+
return file
14+
}
15+
16+
isValid (instance) {
17+
return (instance instanceof FileNode && state.entities.has(instance.id))
18+
}
19+
}
20+
21+
const store = new FileStore()
22+
export default store

app/components/FileTree/subscribeToFileChange.js renamed to app/commons/File/subscribeToFileChange.js

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { autorun } from 'mobx'
55
import { FsSocketClient } from 'backendAPI/websocketClients'
66
import store, { getState, dispatch } from 'store'
77
import mobxStore from 'mobxStore'
8-
import * as TabActions from 'commons/Tab/actions'
9-
import * as FileTreeActions from './actions'
10-
import * as GitActions from '../Git/actions'
8+
import * as TabActions from 'components/Tab/actions'
9+
import * as GitActions from 'components/Git/actions'
10+
import * as FileActions from './actions'
1111

1212
function handleGitFiles (node) {
1313
const path = node.path
@@ -18,7 +18,7 @@ function handleGitFiles (node) {
1818
const current = gitState.branches.current
1919
if (branchName === current) {
2020
const history = gitState.history
21-
const focusedNodes = Object.values(getState().FileTreeState.nodes).filter(node => node.isFocused)
21+
const focusedNodes = Object.values(mobxStore.FileTreeState.entities).filter(node => node.isFocused)
2222
const historyPath = focusedNodes[0] ? focusedNodes[0].path : '/'
2323
dispatch(GitActions.fetchHistory({
2424
path: historyPath,
@@ -35,11 +35,8 @@ function handleGitFiles (node) {
3535
gitStatus: file ? file.status : 'CLEAN',
3636
}
3737
})
38-
dispatch(
39-
FileTreeActions.loadNodeData(
40-
result
41-
)
42-
)
38+
39+
FileActions.loadNodeData(result)
4340
})
4441
}
4542
return true
@@ -63,25 +60,25 @@ export default function subscribeToFileChange () {
6360
if (handleGitFiles(node)) {
6461
break
6562
}
66-
dispatch(FileTreeActions.loadNodeData([node]))
63+
FileActions.loadNodeData([node])
6764
break
6865
case 'modify':
6966
if (handleGitFiles(node)) {
7067
break
7168
}
72-
dispatch(FileTreeActions.loadNodeData([node]))
69+
FileActions.loadNodeData([node])
7370
const tabsToUpdate = mobxStore.EditorTabState.tabs.values().filter(tab => tab.path === node.path)
7471
if (tabsToUpdate.length) {
7572
api.readFile(node.path).then(({ content }) => {
7673
dispatch(TabActions.updateTabByPath({
7774
path: node.path,
78-
content: { body: content }
75+
content,
7976
}))
8077
})
8178
}
8279
break
8380
case 'delete':
84-
dispatch(FileTreeActions.removeNode(node))
81+
dispatch(FileActions.removeNode(node))
8582
break
8683
}
8784
})

0 commit comments

Comments
 (0)