collabmd 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/package.json +1 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/chunk-GFDM7YCF.js +1 -0
- package/public/assets/js/chunks/editor-session-EAUBJCHH.js +22 -0
- package/public/assets/js/chunks/{preview-render-compiler-UZ4SZDQQ.js → preview-render-compiler-6Y7TKXFJ.js} +1 -1
- package/public/assets/js/chunks/{quick-switcher-controller-J7I3CUET.js → quick-switcher-controller-A6SKH5HI.js} +1 -1
- package/public/assets/js/{excalidraw-editor-ZDYKMZOL.js → excalidraw-editor-TCIH6GJL.js} +1 -1
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +68 -68
- package/public/assets/js/preview-render-worker.js +15 -15
- package/src/client/application/app-shell/git-feature.js +57 -18
- package/src/client/application/app-shell/ui-feature.js +4 -0
- package/src/client/bootstrap/collabmd-app-shell.js +12 -0
- package/src/client/domain/vault-utils.js +2 -2
- package/src/client/excalidraw-editor.js +2 -1
- package/src/client/infrastructure/editor-session.js +4 -0
- package/src/client/infrastructure/editor-view-adapter.js +181 -1
- package/src/client/infrastructure/workspace-sync-client.js +324 -0
- package/src/client/presentation/comment-ui-controller.js +192 -5
- package/src/client/presentation/file-explorer-controller.js +15 -3
- package/src/client/presentation/file-explorer-view.js +152 -11
- package/src/client/styles/style.css +46 -2
- package/src/domain/wiki-link-resolver.js +16 -5
- package/src/domain/workspace-change.js +68 -0
- package/src/domain/workspace-room.js +3 -0
- package/src/server/create-app-server.js +41 -10
- package/src/server/domain/backlink-index.js +94 -1
- package/src/server/domain/collaboration/collaboration-room.js +191 -22
- package/src/server/domain/collaboration/room-registry.js +64 -10
- package/src/server/infrastructure/git/responses.js +12 -19
- package/src/server/infrastructure/http/create-git-api-command-handler.js +53 -43
- package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
- package/src/server/infrastructure/http/create-request-handler.js +7 -0
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +58 -14
- package/src/server/infrastructure/http/create-vault-api-handler.js +3 -0
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +3 -1
- package/src/server/infrastructure/http/http-response.js +65 -28
- package/src/server/infrastructure/persistence/vault-file-store.js +163 -52
- package/src/server/infrastructure/workspace/file-system-sync-service.js +488 -0
- package/src/server/infrastructure/workspace/workspace-mutation-coordinator.js +551 -0
- package/public/assets/js/chunks/chunk-R3DDMJHH.js +0 -1
- package/public/assets/js/chunks/editor-session-TAV2VXTA.js +0 -22
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { createWorkspaceChange } from '../../../domain/workspace-change.js';
|
|
1
2
|
import { resolveApiUrl } from '../../domain/runtime-paths.js';
|
|
2
3
|
|
|
3
4
|
function normalizeWorkspaceChange(workspaceChange = {}) {
|
|
4
|
-
return
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
return createWorkspaceChange(workspaceChange);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createWorkspaceRequestId() {
|
|
9
|
+
if (globalThis.crypto?.randomUUID) {
|
|
10
|
+
return globalThis.crypto.randomUUID();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return `workspace-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export const gitFeature = {
|
|
@@ -94,15 +96,19 @@ export const gitFeature = {
|
|
|
94
96
|
},
|
|
95
97
|
|
|
96
98
|
async postGitAction(endpoint, payload) {
|
|
99
|
+
const requestId = createWorkspaceRequestId();
|
|
100
|
+
this.pendingWorkspaceRequestIds?.add(requestId);
|
|
97
101
|
const response = await fetch(endpoint, {
|
|
98
102
|
body: JSON.stringify(payload),
|
|
99
103
|
headers: {
|
|
100
104
|
'Content-Type': 'application/json',
|
|
105
|
+
'X-CollabMD-Request-Id': requestId,
|
|
101
106
|
},
|
|
102
107
|
method: 'POST',
|
|
103
108
|
});
|
|
104
109
|
const data = await response.json();
|
|
105
110
|
if (!response.ok) {
|
|
111
|
+
this.pendingWorkspaceRequestIds?.delete(requestId);
|
|
106
112
|
const error = new Error(data.error || 'Git action failed');
|
|
107
113
|
if (typeof data?.code === 'string') {
|
|
108
114
|
error.code = data.code;
|
|
@@ -145,11 +151,6 @@ export const gitFeature = {
|
|
|
145
151
|
const workspaceChange = normalizeWorkspaceChange(result.workspaceChange);
|
|
146
152
|
await this.refreshWorkspaceAfterGitAction({ filePath, preferredScope });
|
|
147
153
|
this.handleWorkspaceChangeForCurrentFile(workspaceChange, { action, local: true, showToast: showLocalFileToast });
|
|
148
|
-
this.lobby.sendWorkspaceEvent({
|
|
149
|
-
action,
|
|
150
|
-
sourceRef: result.sourceRef ?? null,
|
|
151
|
-
workspaceChange,
|
|
152
|
-
});
|
|
153
154
|
return workspaceChange;
|
|
154
155
|
},
|
|
155
156
|
|
|
@@ -165,21 +166,27 @@ export const gitFeature = {
|
|
|
165
166
|
return false;
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
this.navigation.navigateToFile(null);
|
|
169
169
|
if (!showToast) {
|
|
170
|
+
if (renameEntry) {
|
|
171
|
+
this.navigation.navigateToFile(renameEntry.newPath);
|
|
172
|
+
} else {
|
|
173
|
+
this.navigation.navigateToFile(null);
|
|
174
|
+
}
|
|
170
175
|
return true;
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
const displayName = this.getDisplayName(currentFilePath);
|
|
174
179
|
if (renameEntry) {
|
|
180
|
+
this.navigation.navigateToFile(renameEntry.newPath);
|
|
175
181
|
this.toastController.show(
|
|
176
182
|
local
|
|
177
|
-
? `${displayName}
|
|
178
|
-
: `${displayName} moved
|
|
183
|
+
? `${displayName} moved to ${this.getDisplayName(renameEntry.newPath)}`
|
|
184
|
+
: `${displayName} moved on disk`,
|
|
179
185
|
);
|
|
180
186
|
return true;
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
this.navigation.navigateToFile(null);
|
|
183
190
|
this.toastController.show(
|
|
184
191
|
local
|
|
185
192
|
? `${displayName} was removed by ${action}`
|
|
@@ -193,13 +200,45 @@ export const gitFeature = {
|
|
|
193
200
|
return;
|
|
194
201
|
}
|
|
195
202
|
|
|
203
|
+
if (event.requestId && this.pendingWorkspaceRequestIds?.has(event.requestId)) {
|
|
204
|
+
this.pendingWorkspaceRequestIds.delete(event.requestId);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
196
208
|
const workspaceChange = normalizeWorkspaceChange(event.workspaceChange);
|
|
197
|
-
|
|
209
|
+
if (event.origin === 'git') {
|
|
210
|
+
await this.refreshGitAfterAction();
|
|
211
|
+
}
|
|
198
212
|
this.handleWorkspaceChangeForCurrentFile(workspaceChange, {
|
|
199
|
-
action: event.action || '
|
|
213
|
+
action: event.action || event.origin || 'workspace',
|
|
200
214
|
local: false,
|
|
201
215
|
showToast: true,
|
|
202
216
|
});
|
|
217
|
+
|
|
218
|
+
if (
|
|
219
|
+
event.origin === 'filesystem'
|
|
220
|
+
&& this.currentFilePath
|
|
221
|
+
&& workspaceChange.changedPaths.includes(this.currentFilePath)
|
|
222
|
+
) {
|
|
223
|
+
const canUseInlineCue = workspaceChange.changedPaths.length === 1;
|
|
224
|
+
const highlightRange = canUseInlineCue && Array.isArray(event.highlightRanges)
|
|
225
|
+
? event.highlightRanges.find((entry) => entry.path === this.currentFilePath)
|
|
226
|
+
: null;
|
|
227
|
+
const didFlash = highlightRange
|
|
228
|
+
? this.session?.flashExternalUpdate?.(highlightRange)
|
|
229
|
+
: false;
|
|
230
|
+
if (!didFlash) {
|
|
231
|
+
this.toastController.show(`${this.getDisplayName(this.currentFilePath)} updated from disk`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (
|
|
236
|
+
this.currentFilePath
|
|
237
|
+
&& Array.isArray(event.reloadRequiredPaths)
|
|
238
|
+
&& event.reloadRequiredPaths.includes(this.currentFilePath)
|
|
239
|
+
) {
|
|
240
|
+
this.toastController.show(`${this.getDisplayName(this.currentFilePath)} needs a manual reload`);
|
|
241
|
+
}
|
|
203
242
|
},
|
|
204
243
|
|
|
205
244
|
async stageGitFile(filePath, { scope = 'working-tree' } = {}) {
|
|
@@ -534,6 +534,9 @@ export const uiFeature = {
|
|
|
534
534
|
if (!this.lobby.provider) {
|
|
535
535
|
this.lobby.connect();
|
|
536
536
|
}
|
|
537
|
+
if (!this.workspaceSync.provider) {
|
|
538
|
+
this.workspaceSync.connect();
|
|
539
|
+
}
|
|
537
540
|
|
|
538
541
|
if (wasInactive) {
|
|
539
542
|
if (this.fileExplorerReady) {
|
|
@@ -554,6 +557,7 @@ export const uiFeature = {
|
|
|
554
557
|
this.elements.displayNameDialog.close();
|
|
555
558
|
}
|
|
556
559
|
this.lobby.disconnect();
|
|
560
|
+
this.workspaceSync.disconnect();
|
|
557
561
|
this.globalUsers = [];
|
|
558
562
|
this.chatMessages = [];
|
|
559
563
|
this.chatMessageIds.clear();
|
|
@@ -18,6 +18,7 @@ import { BrowserPreferencesPort } from '../infrastructure/browser-preferences-po
|
|
|
18
18
|
import { getRuntimeConfig } from '../infrastructure/runtime-config.js';
|
|
19
19
|
import { TabActivityLock } from '../infrastructure/tab-activity-lock.js';
|
|
20
20
|
import { vaultApiClient } from '../infrastructure/vault-api-client.js';
|
|
21
|
+
import { WorkspaceSyncClient } from '../infrastructure/workspace-sync-client.js';
|
|
21
22
|
import { BacklinksPanel } from '../presentation/backlinks-panel.js';
|
|
22
23
|
import { CommentUiController } from '../presentation/comment-ui-controller.js';
|
|
23
24
|
import { ExcalidrawEmbedController } from '../presentation/excalidraw-embed-controller.js';
|
|
@@ -111,11 +112,22 @@ export class CollabMdAppShell {
|
|
|
111
112
|
this.quickSwitcherModulePromise = null;
|
|
112
113
|
this.fileExplorerReadyPromise = Promise.resolve();
|
|
113
114
|
this.mobileBreakpointQuery = window.matchMedia('(max-width: 768px)');
|
|
115
|
+
this.pendingWorkspaceRequestIds = new Set();
|
|
114
116
|
|
|
115
117
|
this.lobby = new LobbyPresence({
|
|
116
118
|
preferredUserName: this.getStoredUserName(),
|
|
117
119
|
onChange: (users) => this.updateGlobalUsers(users),
|
|
118
120
|
onChatChange: (messages, meta) => this.updateChatMessages(messages, meta),
|
|
121
|
+
});
|
|
122
|
+
this.workspaceSync = new WorkspaceSyncClient({
|
|
123
|
+
onTreeChange: (tree, metadata = {}) => {
|
|
124
|
+
const wasReady = this.fileExplorerReady;
|
|
125
|
+
this.fileExplorer.setTree(tree, metadata);
|
|
126
|
+
this.fileExplorerReady = true;
|
|
127
|
+
if (!wasReady && this.isTabActive) {
|
|
128
|
+
void this.handleHashChange();
|
|
129
|
+
}
|
|
130
|
+
},
|
|
119
131
|
onWorkspaceEvent: (event) => {
|
|
120
132
|
void this.handleIncomingWorkspaceEvent(event);
|
|
121
133
|
},
|
|
@@ -25,8 +25,8 @@ export function escapeHtml(text) {
|
|
|
25
25
|
*
|
|
26
26
|
* Matching rules (in order):
|
|
27
27
|
* 1. Exact path match (with .md appended if missing)
|
|
28
|
-
* 2.
|
|
29
|
-
* 3.
|
|
28
|
+
* 2. Exact path without .md extension matches the target
|
|
29
|
+
* 3. Filename/path suffix match at any directory depth
|
|
30
30
|
*
|
|
31
31
|
* @param {string} target — the raw wiki-link target text
|
|
32
32
|
* @param {string[]} files — list of vault file paths
|
|
@@ -149,6 +149,7 @@ if (isTestMode) {
|
|
|
149
149
|
&& collabReady
|
|
150
150
|
&& roomClient.canWriteToRoom === true
|
|
151
151
|
&& roomClient.waitingForAuthoritativeSync === false
|
|
152
|
+
&& roomClient.isApplyingSharedSnapshot() === false
|
|
152
153
|
),
|
|
153
154
|
isReady: () => collabReady && Boolean(excalidrawAPI) && Boolean(getNativeHistoryButton('undo')) && Boolean(getNativeHistoryButton('redo')),
|
|
154
155
|
redoShared: () => triggerNativeHistory('redo'),
|
|
@@ -506,7 +507,7 @@ window.addEventListener('message', (event) => {
|
|
|
506
507
|
});
|
|
507
508
|
|
|
508
509
|
function scheduleSyncToRoom(elements, appState, files) {
|
|
509
|
-
if (!collabReady || suppressOnChange
|
|
510
|
+
if (!collabReady || suppressOnChange) {
|
|
510
511
|
return;
|
|
511
512
|
}
|
|
512
513
|
|
|
@@ -229,6 +229,10 @@ export class EditorSession {
|
|
|
229
229
|
return this.viewAdapter.insertText(text);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
flashExternalUpdate(range) {
|
|
233
|
+
return this.viewAdapter.flashRemoteRange(range);
|
|
234
|
+
}
|
|
235
|
+
|
|
232
236
|
waitForInitialSync(timeoutMs = 1500) {
|
|
233
237
|
return this.collaborationClient.waitForInitialSync(timeoutMs);
|
|
234
238
|
}
|
|
@@ -11,11 +11,12 @@ import {
|
|
|
11
11
|
} from '@codemirror/language';
|
|
12
12
|
import { languages } from '@codemirror/language-data';
|
|
13
13
|
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
|
|
14
|
-
import { Compartment, EditorSelection, EditorState, Prec } from '@codemirror/state';
|
|
14
|
+
import { Compartment, EditorSelection, EditorState, Prec, StateEffect, StateField } from '@codemirror/state';
|
|
15
15
|
import { oneDark } from '@codemirror/theme-one-dark';
|
|
16
16
|
import {
|
|
17
17
|
Decoration,
|
|
18
18
|
EditorView,
|
|
19
|
+
WidgetType,
|
|
19
20
|
crosshairCursor,
|
|
20
21
|
drawSelection,
|
|
21
22
|
highlightActiveLine,
|
|
@@ -36,6 +37,59 @@ import { handleImagePasteEvent } from './editor-paste-utils.js';
|
|
|
36
37
|
const markdownCodeLanguages = [...languages, plantUmlLanguageDescription];
|
|
37
38
|
const pairedMatchingBracketMark = Decoration.mark({ class: 'cm-matchingBracket cm-matchingBracket-paired' });
|
|
38
39
|
const nonmatchingBracketMark = Decoration.mark({ class: 'cm-nonmatchingBracket' });
|
|
40
|
+
const remoteUpdateMark = Decoration.mark({ class: 'cm-remoteUpdateFlash' });
|
|
41
|
+
const REMOTE_UPDATE_FLASH_DURATION_MS = 1350;
|
|
42
|
+
const REMOTE_UPDATE_CARET_MAX_LENGTH = 160;
|
|
43
|
+
const RECENT_LOCAL_INPUT_WINDOW_MS = 900;
|
|
44
|
+
|
|
45
|
+
class RemoteUpdateCaretWidget extends WidgetType {
|
|
46
|
+
toDOM() {
|
|
47
|
+
const element = document.createElement('span');
|
|
48
|
+
element.className = 'cm-remoteUpdateCaretWidget';
|
|
49
|
+
element.setAttribute('aria-hidden', 'true');
|
|
50
|
+
return element;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ignoreEvent() {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const addRemoteUpdateFlashEffect = StateEffect.define();
|
|
59
|
+
const clearRemoteUpdateFlashEffect = StateEffect.define();
|
|
60
|
+
const remoteUpdateFlashField = StateField.define({
|
|
61
|
+
create: () => Decoration.none,
|
|
62
|
+
update(decorations, transaction) {
|
|
63
|
+
let nextDecorations = decorations.map(transaction.changes);
|
|
64
|
+
|
|
65
|
+
transaction.effects.forEach((effect) => {
|
|
66
|
+
if (effect.is(clearRemoteUpdateFlashEffect)) {
|
|
67
|
+
nextDecorations = Decoration.none;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (effect.is(addRemoteUpdateFlashEffect)) {
|
|
72
|
+
const { from, showCaret, to } = effect.value;
|
|
73
|
+
const nextRanges = [];
|
|
74
|
+
if (to > from) {
|
|
75
|
+
nextRanges.push(remoteUpdateMark.range(from, to));
|
|
76
|
+
}
|
|
77
|
+
if (showCaret) {
|
|
78
|
+
nextRanges.push(Decoration.widget({
|
|
79
|
+
side: -1,
|
|
80
|
+
widget: new RemoteUpdateCaretWidget(),
|
|
81
|
+
}).range(from));
|
|
82
|
+
}
|
|
83
|
+
nextDecorations = nextRanges.length > 0
|
|
84
|
+
? Decoration.set(nextRanges, true)
|
|
85
|
+
: Decoration.none;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return nextDecorations;
|
|
90
|
+
},
|
|
91
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
92
|
+
});
|
|
39
93
|
|
|
40
94
|
function isBracketBeforeCaret(range, state) {
|
|
41
95
|
return state.selection.ranges.some((selectionRange) =>
|
|
@@ -150,6 +204,35 @@ function createEditorTheme(theme) {
|
|
|
150
204
|
'.cm-selectionMatch': {
|
|
151
205
|
backgroundColor: 'var(--color-primary-highlight)',
|
|
152
206
|
},
|
|
207
|
+
'.cm-remoteUpdateFlash': {
|
|
208
|
+
animation: 'collabmd-remote-update-flash 1.35s ease-out',
|
|
209
|
+
backgroundColor: theme === 'dark'
|
|
210
|
+
? 'oklch(from var(--color-primary) l c h / 0.22)'
|
|
211
|
+
: 'oklch(from var(--color-primary) l c h / 0.16)',
|
|
212
|
+
borderRadius: '3px',
|
|
213
|
+
boxShadow: `0 0 0 1px ${theme === 'dark'
|
|
214
|
+
? 'oklch(from var(--color-primary) calc(l + 0.05) c h / 0.26)'
|
|
215
|
+
: 'oklch(from var(--color-primary) calc(l - 0.04) c h / 0.2)'}`,
|
|
216
|
+
},
|
|
217
|
+
'.cm-remoteUpdateCaretWidget': {
|
|
218
|
+
animation: 'collabmd-remote-update-caret 1s ease-out',
|
|
219
|
+
backgroundColor: 'transparent',
|
|
220
|
+
borderLeft: `2px solid ${theme === 'dark'
|
|
221
|
+
? 'color-mix(in oklab, var(--color-primary) 78%, white)'
|
|
222
|
+
: 'color-mix(in oklab, var(--color-primary) 84%, black)'}`,
|
|
223
|
+
borderRadius: '999px',
|
|
224
|
+
boxShadow: `0 0 0 1px ${theme === 'dark'
|
|
225
|
+
? 'oklch(from var(--color-primary) calc(l + 0.06) c h / 0.22)'
|
|
226
|
+
: 'oklch(from var(--color-primary) calc(l - 0.04) c h / 0.16)'}`,
|
|
227
|
+
display: 'inline-block',
|
|
228
|
+
height: '1.2em',
|
|
229
|
+
marginLeft: '-1px',
|
|
230
|
+
marginRight: '-1px',
|
|
231
|
+
pointerEvents: 'none',
|
|
232
|
+
transformOrigin: 'center bottom',
|
|
233
|
+
verticalAlign: 'text-bottom',
|
|
234
|
+
width: '0',
|
|
235
|
+
},
|
|
153
236
|
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
|
|
154
237
|
backgroundColor: selectionBackground,
|
|
155
238
|
},
|
|
@@ -157,6 +240,42 @@ function createEditorTheme(theme) {
|
|
|
157
240
|
border: `1px solid ${selectionBorder}`,
|
|
158
241
|
borderRadius: '2px',
|
|
159
242
|
},
|
|
243
|
+
'@keyframes collabmd-remote-update-flash': {
|
|
244
|
+
'0%': {
|
|
245
|
+
backgroundColor: theme === 'dark'
|
|
246
|
+
? 'oklch(from var(--color-primary) l c h / 0.34)'
|
|
247
|
+
: 'oklch(from var(--color-primary) l c h / 0.26)',
|
|
248
|
+
},
|
|
249
|
+
'100%': {
|
|
250
|
+
backgroundColor: theme === 'dark'
|
|
251
|
+
? 'oklch(from var(--color-primary) l c h / 0)'
|
|
252
|
+
: 'oklch(from var(--color-primary) l c h / 0)',
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
'@keyframes collabmd-remote-update-caret': {
|
|
256
|
+
'0%': {
|
|
257
|
+
boxShadow: `0 0 0 1px ${theme === 'dark'
|
|
258
|
+
? 'oklch(from var(--color-primary) calc(l + 0.08) c h / 0.34)'
|
|
259
|
+
: 'oklch(from var(--color-primary) calc(l - 0.04) c h / 0.24)'}`,
|
|
260
|
+
opacity: '0',
|
|
261
|
+
transform: 'translateY(0.2em) scaleY(0.7)',
|
|
262
|
+
},
|
|
263
|
+
'18%': {
|
|
264
|
+
opacity: '1',
|
|
265
|
+
transform: 'translateY(0) scaleY(1)',
|
|
266
|
+
},
|
|
267
|
+
'72%': {
|
|
268
|
+
opacity: '1',
|
|
269
|
+
transform: 'translateY(0) scaleY(1)',
|
|
270
|
+
},
|
|
271
|
+
'100%': {
|
|
272
|
+
boxShadow: `0 0 0 1px ${theme === 'dark'
|
|
273
|
+
? 'oklch(from var(--color-primary) calc(l + 0.08) c h / 0)'
|
|
274
|
+
: 'oklch(from var(--color-primary) calc(l - 0.04) c h / 0)'}`,
|
|
275
|
+
opacity: '0',
|
|
276
|
+
transform: 'translateY(-0.05em) scaleY(0.9)',
|
|
277
|
+
},
|
|
278
|
+
},
|
|
160
279
|
}, { dark: theme === 'dark' });
|
|
161
280
|
}
|
|
162
281
|
|
|
@@ -204,6 +323,8 @@ export class EditorViewAdapter {
|
|
|
204
323
|
this.syntaxThemeCompartment = new Compartment();
|
|
205
324
|
this.lineWrappingCompartment = new Compartment();
|
|
206
325
|
this.viewportFrame = 0;
|
|
326
|
+
this.remoteUpdateFlashTimer = 0;
|
|
327
|
+
this.lastLocalInputAt = 0;
|
|
207
328
|
this.handleScroll = () => {
|
|
208
329
|
if (this.viewportFrame) {
|
|
209
330
|
return;
|
|
@@ -214,6 +335,9 @@ export class EditorViewAdapter {
|
|
|
214
335
|
this.emitViewportChange();
|
|
215
336
|
});
|
|
216
337
|
};
|
|
338
|
+
this.handleLocalInputActivity = () => {
|
|
339
|
+
this.lastLocalInputAt = Date.now();
|
|
340
|
+
};
|
|
217
341
|
}
|
|
218
342
|
|
|
219
343
|
initialize({ awareness, filePath, undoManager, ytext }) {
|
|
@@ -276,6 +400,7 @@ export class EditorViewAdapter {
|
|
|
276
400
|
this.themeCompartment.of(createEditorTheme(this.initialTheme)),
|
|
277
401
|
this.syntaxThemeCompartment.of(this.initialTheme === 'dark' ? oneDark : []),
|
|
278
402
|
this.lineWrappingCompartment.of(this.lineWrappingEnabled ? EditorView.lineWrapping : []),
|
|
403
|
+
remoteUpdateFlashField,
|
|
279
404
|
yCollab(ytext, awareness, { undoManager }),
|
|
280
405
|
updateListener,
|
|
281
406
|
],
|
|
@@ -283,6 +408,10 @@ export class EditorViewAdapter {
|
|
|
283
408
|
});
|
|
284
409
|
|
|
285
410
|
this.editorView.scrollDOM.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
411
|
+
this.editorView.contentDOM.addEventListener('beforeinput', this.handleLocalInputActivity);
|
|
412
|
+
this.editorView.contentDOM.addEventListener('keydown', this.handleLocalInputActivity);
|
|
413
|
+
this.editorView.contentDOM.addEventListener('paste', this.handleLocalInputActivity);
|
|
414
|
+
this.editorView.contentDOM.addEventListener('compositionstart', this.handleLocalInputActivity);
|
|
286
415
|
this.updateCursorInfo(this.editorView.state);
|
|
287
416
|
this.onSelectionChanged?.(this.editorView.state);
|
|
288
417
|
this.emitViewportChange();
|
|
@@ -293,6 +422,14 @@ export class EditorViewAdapter {
|
|
|
293
422
|
cancelAnimationFrame(this.viewportFrame);
|
|
294
423
|
this.viewportFrame = 0;
|
|
295
424
|
}
|
|
425
|
+
if (this.remoteUpdateFlashTimer) {
|
|
426
|
+
clearTimeout(this.remoteUpdateFlashTimer);
|
|
427
|
+
this.remoteUpdateFlashTimer = 0;
|
|
428
|
+
}
|
|
429
|
+
this.editorView?.contentDOM?.removeEventListener('beforeinput', this.handleLocalInputActivity);
|
|
430
|
+
this.editorView?.contentDOM?.removeEventListener('keydown', this.handleLocalInputActivity);
|
|
431
|
+
this.editorView?.contentDOM?.removeEventListener('paste', this.handleLocalInputActivity);
|
|
432
|
+
this.editorView?.contentDOM?.removeEventListener('compositionstart', this.handleLocalInputActivity);
|
|
296
433
|
this.editorView?.scrollDOM?.removeEventListener('scroll', this.handleScroll);
|
|
297
434
|
this.editorView?.destroy();
|
|
298
435
|
this.editorView = null;
|
|
@@ -355,6 +492,49 @@ export class EditorViewAdapter {
|
|
|
355
492
|
this.editorView?.requestMeasure();
|
|
356
493
|
}
|
|
357
494
|
|
|
495
|
+
flashRemoteRange({ from = 0, to = 0 } = {}, durationMs = REMOTE_UPDATE_FLASH_DURATION_MS) {
|
|
496
|
+
const state = this.editorView?.state;
|
|
497
|
+
if (!state || state.doc.length === 0) {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let start = Math.min(Math.max(Math.round(from), 0), state.doc.length);
|
|
502
|
+
let end = Math.min(Math.max(Math.round(to), 0), state.doc.length);
|
|
503
|
+
if (end <= start) {
|
|
504
|
+
if (start < state.doc.length) {
|
|
505
|
+
end = start + 1;
|
|
506
|
+
} else if (start > 0) {
|
|
507
|
+
start -= 1;
|
|
508
|
+
end = start + 1;
|
|
509
|
+
} else {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const rangeLength = Math.max(end - start, 0);
|
|
515
|
+
const showCaret = (
|
|
516
|
+
rangeLength <= REMOTE_UPDATE_CARET_MAX_LENGTH
|
|
517
|
+
&& (Date.now() - this.lastLocalInputAt) > RECENT_LOCAL_INPUT_WINDOW_MS
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
this.editorView.dispatch({
|
|
521
|
+
effects: addRemoteUpdateFlashEffect.of({ from: start, showCaret, to: end }),
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
if (this.remoteUpdateFlashTimer) {
|
|
525
|
+
clearTimeout(this.remoteUpdateFlashTimer);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
this.remoteUpdateFlashTimer = window.setTimeout(() => {
|
|
529
|
+
this.remoteUpdateFlashTimer = 0;
|
|
530
|
+
this.editorView?.dispatch({
|
|
531
|
+
effects: clearRemoteUpdateFlashEffect.of(null),
|
|
532
|
+
});
|
|
533
|
+
}, durationMs);
|
|
534
|
+
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
|
|
358
538
|
getViewportState(viewportRatio = 0.35) {
|
|
359
539
|
return {
|
|
360
540
|
topLine: this.getTopVisibleLineNumber(viewportRatio),
|