Skip to content

Commit 7852d1a

Browse files
fix(editor): prevent md-engine text overwrite during rapid typing (#679)
1 parent 645295a commit 7852d1a

3 files changed

Lines changed: 131 additions & 28 deletions

File tree

src/renderer/components/editor/Editor.vue

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ const {
4343
} = useApp()
4444
const { editorThemeName } = useTheme()
4545
46-
const { addToUpdateContentQueue } = useSnippetUpdate()
46+
const {
47+
addToUpdateContentQueue,
48+
getPendingContentUpdate,
49+
isContentUpdateBusy,
50+
} = useSnippetUpdate()
4751
4852
let editor: CodeMirror.Editor | null = null
4953
let currentSearchOverlay: any = null
@@ -223,8 +227,29 @@ async function init() {
223227
watch(selectedSnippetContent, (v, oldV) => {
224228
nextTick(() => {
225229
const isNewValue = v?.id !== oldV?.id
230+
const isSameContent = v?.id === oldV?.id
231+
const snippetId = selectedSnippet.value?.id
232+
const contentId = v?.id
233+
let nextValue = v?.value || ''
234+
235+
if (snippetId && contentId) {
236+
const pendingUpdate = getPendingContentUpdate(snippetId, contentId)
237+
if (pendingUpdate) {
238+
nextValue = pendingUpdate.value || ''
239+
}
240+
241+
if (
242+
isSameContent
243+
&& isContentUpdateBusy(snippetId, contentId)
244+
&& editor
245+
&& editor.getValue() !== nextValue
246+
) {
247+
return
248+
}
249+
}
250+
226251
// Не сохраняем вьюпорт при смене фрагмента/сниппета
227-
setValue(v?.value || '', true, !isNewValue)
252+
setValue(nextValue, true, !isNewValue)
228253
nextTick(() => {
229254
if (searchQuery.value) {
230255
updateSearchOverlay()

src/renderer/composables/useSnippetUpdate.ts

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ const { updateSnippetContent, updateSnippet } = useSnippets()
2222

2323
const updateQueue = ref<Map<string, UpdateQueueItem>>(new Map())
2424
const updateContentQueue = ref<Map<string, UpdateContentQueueItem>>(new Map())
25+
const contentUpdateTimers = ref<Map<string, ReturnType<typeof setTimeout>>>(
26+
new Map(),
27+
)
28+
const inFlightContentKeys = ref<Set<string>>(new Set())
2529

2630
const updateDebounced = useDebounceFn((snippetId: number) => {
2731
const key = `${snippetId}`
@@ -33,18 +37,47 @@ const updateDebounced = useDebounceFn((snippetId: number) => {
3337
}
3438
}, UPDATE_DEBOUNCE_TIME)
3539

36-
const updateContentDebounced = useDebounceFn(
37-
(snippetId: number, contentId: number) => {
38-
const key = `${snippetId}-${contentId}`
39-
const update = updateContentQueue.value.get(key)
40+
function getContentUpdateKey(snippetId: number, contentId: number) {
41+
return `${snippetId}-${contentId}`
42+
}
43+
44+
async function flushContentUpdate(key: string) {
45+
const update = updateContentQueue.value.get(key)
46+
if (!update) {
47+
return
48+
}
4049

41-
if (update) {
42-
updateSnippetContent(update.snippetId, update.contentId, update.data)
43-
updateContentQueue.value.delete(key)
50+
updateContentQueue.value.delete(key)
51+
inFlightContentKeys.value.add(key)
52+
53+
try {
54+
await updateSnippetContent(update.snippetId, update.contentId, update.data)
55+
}
56+
catch (error) {
57+
console.error(error)
58+
}
59+
finally {
60+
inFlightContentKeys.value.delete(key)
61+
62+
if (updateContentQueue.value.has(key)) {
63+
scheduleContentUpdate(key)
4464
}
45-
},
46-
UPDATE_DEBOUNCE_TIME,
47-
)
65+
}
66+
}
67+
68+
function scheduleContentUpdate(key: string) {
69+
const pendingTimer = contentUpdateTimers.value.get(key)
70+
if (pendingTimer) {
71+
clearTimeout(pendingTimer)
72+
}
73+
74+
const timer = setTimeout(() => {
75+
contentUpdateTimers.value.delete(key)
76+
void flushContentUpdate(key)
77+
}, UPDATE_DEBOUNCE_TIME)
78+
79+
contentUpdateTimers.value.set(key, timer)
80+
}
4881

4982
function addToUpdateQueue(snippetId: number, data: SnippetsUpdate) {
5083
const key = `${snippetId}`
@@ -57,14 +90,40 @@ function addToUpdateContentQueue(
5790
contentId: number,
5891
data: SnippetContentsAdd,
5992
) {
60-
const key = `${snippetId}-${contentId}`
93+
const key = getContentUpdateKey(snippetId, contentId)
6194
updateContentQueue.value.set(key, { snippetId, contentId, data })
62-
updateContentDebounced(snippetId, contentId)
95+
96+
if (inFlightContentKeys.value.has(key)) {
97+
return
98+
}
99+
100+
scheduleContentUpdate(key)
101+
}
102+
103+
function getPendingContentUpdate(snippetId: number, contentId: number) {
104+
const key = getContentUpdateKey(snippetId, contentId)
105+
return updateContentQueue.value.get(key)?.data
106+
}
107+
108+
function isContentUpdateBusy(snippetId: number, contentId: number) {
109+
const key = getContentUpdateKey(snippetId, contentId)
110+
return (
111+
updateContentQueue.value.has(key) || inFlightContentKeys.value.has(key)
112+
)
113+
}
114+
115+
function hasBusyContentUpdates() {
116+
return (
117+
updateContentQueue.value.size > 0 || inFlightContentKeys.value.size > 0
118+
)
63119
}
64120

65121
export function useSnippetUpdate() {
66122
return {
67123
addToUpdateContentQueue,
68124
addToUpdateQueue,
125+
getPendingContentUpdate,
126+
hasBusyContentUpdates,
127+
isContentUpdateBusy,
69128
}
70129
}

src/renderer/ipc/listeners/system.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { useApp, useFolders, useSnippets, useSonner } from '@/composables'
1+
import {
2+
useApp,
3+
useFolders,
4+
useSnippets,
5+
useSnippetUpdate,
6+
useSonner,
7+
} from '@/composables'
28
import { i18n, ipc } from '@/electron'
39
import { router, RouterName } from '@/router'
410
import { repository } from '../../../../package.json'
@@ -13,6 +19,7 @@ const {
1319
const { selectFolder, getFolders } = useFolders()
1420
const { selectSnippet, getSnippets, selectFirstSnippet, displayedSnippets }
1521
= useSnippets()
22+
const { hasBusyContentUpdates } = useSnippetUpdate()
1623
const { sonner } = useSonner()
1724
let storageSyncDebounceTimer: ReturnType<typeof setTimeout> | null = null
1825

@@ -39,6 +46,24 @@ async function refreshAfterStorageSync() {
3946
}
4047
}
4148

49+
function scheduleStorageSyncRefresh() {
50+
if (storageSyncDebounceTimer) {
51+
clearTimeout(storageSyncDebounceTimer)
52+
storageSyncDebounceTimer = null
53+
}
54+
55+
storageSyncDebounceTimer = setTimeout(() => {
56+
if (hasBusyContentUpdates()) {
57+
scheduleStorageSyncRefresh()
58+
return
59+
}
60+
61+
refreshAfterStorageSync().catch((error) => {
62+
console.error('Failed to refresh after storage sync:', error)
63+
})
64+
}, 300)
65+
}
66+
4267
export function registerSystemListeners() {
4368
ipc.on('system:deep-link', async (_, url: string) => {
4469
try {
@@ -47,15 +72,18 @@ export function registerSystemListeners() {
4772
const snippetId = u.searchParams.get('snippetId')
4873

4974
if (folderId && snippetId) {
75+
const nextFolderId = Number(folderId)
76+
const nextSnippetId = Number(snippetId)
77+
5078
highlightedFolderIds.value.clear()
5179
highlightedSnippetIds.value.clear()
5280
focusedSnippetId.value = undefined
5381
focusedFolderId.value = undefined
5482

55-
await getSnippets({ folderId })
83+
await getSnippets({ folderId: nextFolderId })
5684

57-
await selectFolder(Number(folderId))
58-
selectSnippet(Number(snippetId))
85+
await selectFolder(nextFolderId)
86+
selectSnippet(nextSnippetId)
5987
}
6088
}
6189
catch (error) {
@@ -92,16 +120,7 @@ export function registerSystemListeners() {
92120
})
93121

94122
ipc.on('system:storage-synced', () => {
95-
if (storageSyncDebounceTimer) {
96-
clearTimeout(storageSyncDebounceTimer)
97-
storageSyncDebounceTimer = null
98-
}
99-
100-
storageSyncDebounceTimer = setTimeout(() => {
101-
refreshAfterStorageSync().catch((error) => {
102-
console.error('Failed to refresh after storage sync:', error)
103-
})
104-
}, 300)
123+
scheduleStorageSyncRefresh()
105124
})
106125

107126
ipc.on('system:error', (_, payload) => {

0 commit comments

Comments
 (0)