mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Standardise types for ranges
(#16927)
GitOrigin-RevId: 28dd0eb67e1684e6bd0e452d15315ce1f9e3481a
This commit is contained in:
parent
c997d1dc2b
commit
7b3ffb9fae
13 changed files with 171 additions and 185 deletions
|
@ -2,8 +2,10 @@
|
|||
"name": "@overleaf/ranges-tracker",
|
||||
"description": "Shared logic for syncing comments and tracked changes with operational transforms",
|
||||
"main": "index.cjs",
|
||||
"types": "types/index.d.cts",
|
||||
"files": [
|
||||
"index.cjs"
|
||||
"index.cjs",
|
||||
"types"
|
||||
],
|
||||
"author": "Overleaf (https://www.overleaf.com)",
|
||||
"private": true,
|
||||
|
|
|
@ -16,7 +16,7 @@ import EditorWatchdogManager from '@/features/ide-react/connection/editor-watchd
|
|||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { Document } from '@/features/ide-react/editor/document'
|
||||
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import { GotoLineOptions } from '@/features/ide-react/types/goto-line-options'
|
||||
import { Doc } from '../../../../../types/doc'
|
||||
|
@ -45,7 +45,7 @@ interface OpenDocOptions
|
|||
export type EditorManager = {
|
||||
getEditorType: () => EditorType | null
|
||||
showSymbolPalette: boolean
|
||||
currentDocument: Document
|
||||
currentDocument: DocumentContainer
|
||||
currentDocumentId: DocId | null
|
||||
getCurrentDocValue: () => string | null
|
||||
getCurrentDocId: () => DocId | null
|
||||
|
@ -73,7 +73,7 @@ function hasGotoOffset(options: OpenDocOptions): options is GotoOffsetOptions {
|
|||
export type EditorScopeValue = {
|
||||
showSymbolPalette: false
|
||||
toggleSymbolPalette: () => void
|
||||
sharejs_doc: Document | null
|
||||
sharejs_doc: DocumentContainer | null
|
||||
open_doc_id: string | null
|
||||
open_doc_name: string | null
|
||||
opening: boolean
|
||||
|
@ -101,7 +101,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
)
|
||||
const [showVisual] = useScopeValue<boolean>('editor.showVisual')
|
||||
const [currentDocument, setCurrentDocument] =
|
||||
useScopeValue<Document>('editor.sharejs_doc')
|
||||
useScopeValue<DocumentContainer>('editor.sharejs_doc')
|
||||
const [openDocId, setOpenDocId] = useScopeValue<DocId | null>(
|
||||
'editor.open_doc_id'
|
||||
)
|
||||
|
@ -140,7 +140,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
// prevents circular dependencies in useCallbacks
|
||||
const [docError, setDocError] = useState<{
|
||||
doc: Doc
|
||||
document: Document
|
||||
document: DocumentContainer
|
||||
error: Error | string
|
||||
meta?: Record<string, any>
|
||||
editorContent?: string
|
||||
|
@ -225,12 +225,12 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
[goToLineEmitter]
|
||||
)
|
||||
|
||||
const unbindFromDocumentEvents = (document: Document) => {
|
||||
const unbindFromDocumentEvents = (document: DocumentContainer) => {
|
||||
document.off()
|
||||
}
|
||||
|
||||
const attachErrorHandlerToDocument = useCallback(
|
||||
(doc: Doc, document: Document) => {
|
||||
(doc: Doc, document: DocumentContainer) => {
|
||||
document.on(
|
||||
'error',
|
||||
(
|
||||
|
@ -246,7 +246,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
)
|
||||
|
||||
const bindToDocumentEvents = useCallback(
|
||||
(doc: Doc, document: Document) => {
|
||||
(doc: Doc, document: DocumentContainer) => {
|
||||
attachErrorHandlerToDocument(doc, document)
|
||||
|
||||
document.on('externalUpdate', (update: Update) => {
|
||||
|
@ -276,7 +276,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
const syncTimeoutRef = useRef<number | null>(null)
|
||||
|
||||
const syncTrackChangesState = useCallback(
|
||||
(doc: Document) => {
|
||||
(doc: DocumentContainer) => {
|
||||
if (!doc) {
|
||||
return
|
||||
}
|
||||
|
@ -310,7 +310,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
|
||||
const doOpenNewDocument = useCallback(
|
||||
(doc: Doc) =>
|
||||
new Promise<Document>((resolve, reject) => {
|
||||
new Promise<DocumentContainer>((resolve, reject) => {
|
||||
debugConsole.log('[doOpenNewDocument] Opening...')
|
||||
const newDocument = openDocs.getDocument(doc._id)
|
||||
if (!newDocument) {
|
||||
|
@ -344,7 +344,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
|
|||
)
|
||||
|
||||
const openNewDocument = useCallback(
|
||||
async (doc: Doc): Promise<Document> => {
|
||||
async (doc: Doc): Promise<DocumentContainer> => {
|
||||
// Leave the current document
|
||||
// - when we are opening a different new one, to avoid race conditions
|
||||
// between leaving and joining the same document
|
||||
|
|
|
@ -27,7 +27,6 @@ import { debugConsole } from '@/utils/debugging'
|
|||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { deleteJSON, getJSON, postJSON } from '@/infrastructure/fetch-json'
|
||||
import ColorManager from '@/ide/colors/ColorManager'
|
||||
// @ts-ignore
|
||||
import RangesTracker from '@overleaf/ranges-tracker'
|
||||
import * as ReviewPanel from '../types/review-panel-state'
|
||||
import {
|
||||
|
@ -61,6 +60,12 @@ import {
|
|||
ReviewPanelCommentThreadsApi,
|
||||
} from '../../../../../../../types/review-panel/api'
|
||||
import { DateString } from '../../../../../../../types/helpers/date'
|
||||
import {
|
||||
Change,
|
||||
CommentOperation,
|
||||
EditOperation,
|
||||
} from '../../../../../../../types/change'
|
||||
import { RangesTrackerWithResolvedThreadIds } from '@/features/ide-react/editor/document-container'
|
||||
|
||||
const dispatchReviewPanelEvent = (type: string, payload?: any) => {
|
||||
window.dispatchEvent(
|
||||
|
@ -251,7 +256,9 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
})
|
||||
}, [loadThreadsController.signal, projectId, setLoadingThreads])
|
||||
|
||||
const rangesTrackers = useRef<Record<DocId, RangesTracker>>({})
|
||||
const rangesTrackers = useRef<
|
||||
Record<DocId, RangesTrackerWithResolvedThreadIds>
|
||||
>({})
|
||||
const refreshingRangeUsers = useRef(false)
|
||||
const refreshedForUserIds = useRef(new Set<UserId>())
|
||||
const refreshChangeUsers = useCallback(
|
||||
|
@ -299,12 +306,14 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
const getChangeTracker = useCallback(
|
||||
(docId: DocId) => {
|
||||
if (!rangesTrackers.current[docId]) {
|
||||
rangesTrackers.current[docId] = new RangesTracker()
|
||||
rangesTrackers.current[docId].resolvedThreadIds = {
|
||||
...resolvedThreadIds,
|
||||
}
|
||||
const rangesTracker = new RangesTracker([], [])
|
||||
;(
|
||||
rangesTracker as RangesTrackerWithResolvedThreadIds
|
||||
).resolvedThreadIds = { ...resolvedThreadIds }
|
||||
rangesTrackers.current[docId] =
|
||||
rangesTracker as RangesTrackerWithResolvedThreadIds
|
||||
}
|
||||
return rangesTrackers.current[docId]
|
||||
return rangesTrackers.current[docId]!
|
||||
},
|
||||
[resolvedThreadIds]
|
||||
)
|
||||
|
@ -435,17 +444,18 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
|
||||
if (!loadingThreadsInProgressRef.current) {
|
||||
for (const comment of rangesTracker.comments) {
|
||||
deleteChanges.delete(comment.id)
|
||||
const commentId = comment.id as ThreadId
|
||||
deleteChanges.delete(commentId)
|
||||
|
||||
let newComment: any
|
||||
if (localResolvedThreadIds[comment.op.t]) {
|
||||
docResolvedComments[comment.id] ??= {} as ReviewPanelCommentEntry
|
||||
newComment = docResolvedComments[comment.id]
|
||||
delete docEntries[comment.id]
|
||||
docResolvedComments[commentId] ??= {} as ReviewPanelCommentEntry
|
||||
newComment = docResolvedComments[commentId]
|
||||
delete docEntries[commentId]
|
||||
} else {
|
||||
docEntries[comment.id] ??= {} as ReviewPanelEntry
|
||||
newComment = docEntries[comment.id]
|
||||
delete docResolvedComments[comment.id]
|
||||
docEntries[commentId] ??= {} as ReviewPanelEntry
|
||||
newComment = docEntries[commentId]
|
||||
delete docResolvedComments[commentId]
|
||||
}
|
||||
|
||||
newComment.type = 'comment'
|
||||
|
@ -505,10 +515,12 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
}
|
||||
// The open doc range tracker is kept up to date in real-time so
|
||||
// replace any outdated info with this
|
||||
const rangesTracker = currentDocument.ranges!
|
||||
;(rangesTracker as RangesTrackerWithResolvedThreadIds).resolvedThreadIds = {
|
||||
...resolvedThreadIds,
|
||||
}
|
||||
rangesTrackers.current[currentDocument.doc_id as DocId] =
|
||||
currentDocument.ranges
|
||||
rangesTrackers.current[currentDocument.doc_id as DocId].resolvedThreadIds =
|
||||
{ ...resolvedThreadIds }
|
||||
rangesTracker as RangesTrackerWithResolvedThreadIds
|
||||
currentDocument.on('flipped_pending_to_inflight', () =>
|
||||
regenerateTrackChangesId(currentDocument)
|
||||
)
|
||||
|
@ -1034,8 +1046,8 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
type Doc = {
|
||||
id: DocId
|
||||
ranges: {
|
||||
comments?: unknown[]
|
||||
changes?: unknown[]
|
||||
comments?: Change<CommentOperation>[]
|
||||
changes?: Change<EditOperation>[]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1119,7 +1131,7 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
}
|
||||
|
||||
const { offset, length } = addCommentEntry
|
||||
const threadId = RangesTracker.generateId()
|
||||
const threadId = RangesTracker.generateId() as ThreadId
|
||||
setCommentThreads(prevState => ({
|
||||
...prevState,
|
||||
[threadId]: { ...getThread(threadId), submitting: true },
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
n/handle-callback-err,
|
||||
max-len,
|
||||
*/
|
||||
/* eslint-disable camelcase */
|
||||
// Migrated from services/web/frontend/js/ide/editor/Document.js
|
||||
|
||||
// @ts-ignore
|
||||
import RangesTracker from '@overleaf/ranges-tracker'
|
||||
import { ShareJsDoc } from './share-js-doc'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
@ -19,6 +14,7 @@ import {
|
|||
AnyOperation,
|
||||
Change,
|
||||
CommentOperation,
|
||||
EditOperation,
|
||||
} from '../../../../../types/change'
|
||||
import {
|
||||
isCommentOperation,
|
||||
|
@ -31,6 +27,7 @@ import {
|
|||
TrackChangesIdSeeds,
|
||||
Version,
|
||||
} from '@/features/ide-react/editor/types/document'
|
||||
import { ThreadId } from '../../../../../types/review-panel/review-panel'
|
||||
|
||||
const MAX_PENDING_OP_SIZE = 64
|
||||
|
||||
|
@ -79,11 +76,22 @@ function getShareJsOpSize(shareJsOp: ShareJsOperation) {
|
|||
return shareJsOp.reduce((total, op) => total + getOpSize(op), 0)
|
||||
}
|
||||
|
||||
export class Document extends EventEmitter {
|
||||
// TODO: define these in RangesTracker
|
||||
type _RangesTracker = Omit<RangesTracker, 'changes' | 'comments'> & {
|
||||
changes: Change<EditOperation>[]
|
||||
comments: Change<CommentOperation>[]
|
||||
track_changes?: boolean
|
||||
}
|
||||
|
||||
export type RangesTrackerWithResolvedThreadIds = _RangesTracker & {
|
||||
resolvedThreadIds: Record<ThreadId, boolean>
|
||||
}
|
||||
|
||||
export class DocumentContainer extends EventEmitter {
|
||||
private connected: boolean
|
||||
private wantToBeJoined = false
|
||||
private chaosMonkeyTimer: number | null = null
|
||||
private track_changes_as: string | null = null
|
||||
public track_changes_as: string | null = null
|
||||
|
||||
private joinCallbacks: JoinCallback[] = []
|
||||
private leaveCallbacks: LeaveCallback[] = []
|
||||
|
@ -91,7 +99,9 @@ export class Document extends EventEmitter {
|
|||
doc?: ShareJsDoc
|
||||
cm6?: EditorFacade
|
||||
oldInflightOp?: ShareJsOperation
|
||||
ranges: RangesTracker
|
||||
|
||||
ranges?: _RangesTracker | RangesTrackerWithResolvedThreadIds
|
||||
|
||||
joined = false
|
||||
|
||||
// This is set and read in useCodeMirrorScope
|
||||
|
@ -103,7 +113,7 @@ export class Document extends EventEmitter {
|
|||
private readonly globalEditorWatchdogManager: EditorWatchdogManager,
|
||||
private readonly ideEventEmitter: IdeEventEmitter,
|
||||
private readonly eventLog: EventLog,
|
||||
private readonly detachDoc: (docId: string, doc: Document) => void
|
||||
private readonly detachDoc: (docId: string, doc: DocumentContainer) => void
|
||||
) {
|
||||
super()
|
||||
this.connected = this.socket.socket.connected
|
||||
|
@ -675,18 +685,18 @@ export class Document extends EventEmitter {
|
|||
let track_changes_as = null
|
||||
const remote_op = msg != null
|
||||
if (remote_op && msg?.meta.tc) {
|
||||
old_id_seed = this.ranges.getIdSeed()
|
||||
this.ranges.setIdSeed(msg.meta.tc)
|
||||
old_id_seed = this.ranges!.getIdSeed()
|
||||
this.ranges!.setIdSeed(msg.meta.tc)
|
||||
track_changes_as = msg.meta.user_id
|
||||
} else if (!remote_op && this.track_changes_as != null) {
|
||||
track_changes_as = this.track_changes_as
|
||||
}
|
||||
this.ranges.track_changes = track_changes_as != null
|
||||
this.ranges!.track_changes = track_changes_as != null
|
||||
for (const op of this.filterOps(ops)) {
|
||||
this.ranges.applyOp(op, { user_id: track_changes_as })
|
||||
this.ranges!.applyOp(op, { user_id: track_changes_as })
|
||||
}
|
||||
if (old_id_seed != null) {
|
||||
this.ranges.setIdSeed(old_id_seed)
|
||||
this.ranges!.setIdSeed(old_id_seed)
|
||||
}
|
||||
if (remote_op) {
|
||||
// With remote ops, the editor hasn't been updated when we receive this
|
||||
|
@ -697,7 +707,10 @@ export class Document extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
private catchUpRanges(changes: Change[], comments: CommentOperation[]) {
|
||||
private catchUpRanges(
|
||||
changes: Change<EditOperation>[],
|
||||
comments: Change<CommentOperation>[]
|
||||
) {
|
||||
// We've just been given the current server's ranges, but need to apply any local ops we have.
|
||||
// Reset to the server state then apply our local ops again.
|
||||
if (changes == null) {
|
||||
|
@ -707,16 +720,16 @@ export class Document extends EventEmitter {
|
|||
comments = []
|
||||
}
|
||||
this.emit('ranges:clear')
|
||||
this.ranges.changes = changes
|
||||
this.ranges.comments = comments
|
||||
this.ranges.track_changes = this.doc?.track_changes
|
||||
this.ranges!.changes = changes
|
||||
this.ranges!.comments = comments
|
||||
this.ranges!.track_changes = this.doc?.track_changes
|
||||
for (const op of this.filterOps(this.doc?.getInflightOp() || [])) {
|
||||
this.ranges.setIdSeed(this.doc?.track_changes_id_seeds?.inflight)
|
||||
this.ranges.applyOp(op, { user_id: this.track_changes_as })
|
||||
this.ranges!.setIdSeed(this.doc?.track_changes_id_seeds?.inflight)
|
||||
this.ranges!.applyOp(op, { user_id: this.track_changes_as })
|
||||
}
|
||||
for (const op of this.filterOps(this.doc?.getPendingOp() || [])) {
|
||||
this.ranges.setIdSeed(this.doc?.track_changes_id_seeds?.pending)
|
||||
this.ranges.applyOp(op, { user_id: this.track_changes_as })
|
||||
this.ranges!.setIdSeed(this.doc?.track_changes_id_seeds?.pending)
|
||||
this.ranges!.applyOp(op, { user_id: this.track_changes_as })
|
||||
}
|
||||
return this.emit('ranges:redraw')
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
// Migrated from static methods of Document in Document.js
|
||||
|
||||
import { Document } from '@/features/ide-react/editor/document'
|
||||
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { Socket } from '@/features/ide-react/connection/types/socket'
|
||||
import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
|
||||
|
@ -8,7 +8,7 @@ import { EventLog } from '@/features/ide-react/editor/event-log'
|
|||
import EditorWatchdogManager from '@/features/ide-react/connection/editor-watchdog-manager'
|
||||
|
||||
export class OpenDocuments {
|
||||
private openDocs = new Map<string, Document>()
|
||||
private openDocs = new Map<string, DocumentContainer>()
|
||||
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(
|
||||
|
@ -44,7 +44,7 @@ export class OpenDocuments {
|
|||
}
|
||||
|
||||
private createDoc(docId: string) {
|
||||
const doc = new Document(
|
||||
const doc = new DocumentContainer(
|
||||
docId,
|
||||
this.socket,
|
||||
this.globalEditorWatchdogManager,
|
||||
|
@ -55,7 +55,7 @@ export class OpenDocuments {
|
|||
this.openDocs.set(docId, doc)
|
||||
}
|
||||
|
||||
detachDoc(docId: string, doc: Document) {
|
||||
detachDoc(docId: string, doc: DocumentContainer) {
|
||||
if (this.openDocs.get(docId) === doc) {
|
||||
debugConsole.log(
|
||||
`[detach] Removing document with ID (${docId}) from openDocs`
|
||||
|
|
|
@ -8,9 +8,12 @@ import {
|
|||
} from '../vertical-overflow'
|
||||
import { EditorSelection, EditorState } from '@codemirror/state'
|
||||
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||
import { CurrentDoc } from '../../../../../../types/current-doc'
|
||||
import { fullHeightCoordsAtPos } from '../../utils/layer'
|
||||
import { debounce } from 'lodash'
|
||||
import { Change, EditOperation } from '../../../../../../types/change'
|
||||
import { ThreadId } from '../../../../../../types/review-panel/review-panel'
|
||||
import { isDeleteOperation, isInsertOperation } from '@/utils/operations'
|
||||
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||
|
||||
// With less than this number of entries, don't bother culling to avoid
|
||||
// little UI jumps when scrolling.
|
||||
|
@ -75,7 +78,7 @@ export type UpdateType =
|
|||
|
||||
export const createChangeManager = (
|
||||
view: EditorView,
|
||||
currentDoc: CurrentDoc
|
||||
currentDoc: DocumentContainer
|
||||
): ChangeManager => {
|
||||
/**
|
||||
* Calculate the screen coordinates of each entry (change or comment),
|
||||
|
@ -152,7 +155,7 @@ export const createChangeManager = (
|
|||
/**
|
||||
* Add a comment (thread) to the ShareJS doc when it's created
|
||||
*/
|
||||
const addComment = (offset: number, length: number, threadId: string) => {
|
||||
const addComment = (offset: number, length: number, threadId: ThreadId) => {
|
||||
currentDoc.submitOp({
|
||||
c: view.state.doc.sliceString(offset, offset + length),
|
||||
p: offset,
|
||||
|
@ -164,14 +167,14 @@ export const createChangeManager = (
|
|||
* Remove a comment (thread) from the range tracker when it's deleted
|
||||
*/
|
||||
const removeComment = (commentId: string) => {
|
||||
currentDoc.ranges.removeCommentId(commentId)
|
||||
currentDoc.ranges!.removeCommentId(commentId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove tracked changes from the range tracker when they're accepted
|
||||
*/
|
||||
const acceptChanges = (changeIds: string[]) => {
|
||||
currentDoc.ranges.removeChangeIds(changeIds)
|
||||
currentDoc.ranges!.removeChangeIds(changeIds)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -179,7 +182,9 @@ export const createChangeManager = (
|
|||
* and restore the original content
|
||||
*/
|
||||
const rejectChanges = (changeIds: string[]) => {
|
||||
const changes: any[] = currentDoc.ranges.getChanges(changeIds)
|
||||
const changes = currentDoc.ranges!.getChanges(
|
||||
changeIds
|
||||
) as Change<EditOperation>[]
|
||||
|
||||
if (changes.length === 0) {
|
||||
return {}
|
||||
|
@ -242,38 +247,30 @@ export const createChangeManager = (
|
|||
const changesToDispatch = changes.map(change => {
|
||||
const { op } = change
|
||||
|
||||
const opType = 'i' in op ? 'i' : 'c' in op ? 'c' : 'd'
|
||||
if (isInsertOperation(op)) {
|
||||
const from = op.p
|
||||
const content = op.i
|
||||
const to = from + content.length
|
||||
|
||||
switch (opType) {
|
||||
case 'd': {
|
||||
return {
|
||||
from: op.p,
|
||||
to: op.p,
|
||||
insert: op.d,
|
||||
}
|
||||
const text = view.state.doc.sliceString(from, to)
|
||||
|
||||
if (text !== content) {
|
||||
throw new Error(
|
||||
`Op to be removed (${JSON.stringify(
|
||||
change.op
|
||||
)}) does not match editor text '${text}'`
|
||||
)
|
||||
}
|
||||
|
||||
case 'i': {
|
||||
const from = op.p
|
||||
const content = op.i
|
||||
const to = from + content.length
|
||||
|
||||
const text = view.state.doc.sliceString(from, to)
|
||||
|
||||
if (text !== content) {
|
||||
throw new Error(
|
||||
`Op to be removed (${JSON.stringify(
|
||||
change.op
|
||||
)}) does not match editor text '${text}'`
|
||||
)
|
||||
}
|
||||
|
||||
return { from, to, insert: '' }
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`unknown change: ${JSON.stringify(change)}`)
|
||||
return { from, to, insert: '' }
|
||||
} else if (isDeleteOperation(op)) {
|
||||
return {
|
||||
from: op.p,
|
||||
to: op.p,
|
||||
insert: op.d,
|
||||
}
|
||||
} else {
|
||||
throw new Error(`unknown change type: ${JSON.stringify(change)}`)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Range, RangeSet, RangeValue, Transaction } from '@codemirror/state'
|
||||
import { CurrentDoc } from '../../../../../../types/current-doc'
|
||||
import {
|
||||
AnyOperation,
|
||||
Change,
|
||||
ChangeOperation,
|
||||
CommentOperation,
|
||||
} from '../../../../../../types/change'
|
||||
import { ThreadId } from '../../../../../../types/review-panel/review-panel'
|
||||
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||
|
||||
export type StoredComment = {
|
||||
text: string
|
||||
|
@ -20,14 +20,14 @@ export type StoredComment = {
|
|||
* Find tracked comments within the range of the current transaction's changes
|
||||
*/
|
||||
export const findCommentsInCut = (
|
||||
currentDoc: CurrentDoc,
|
||||
currentDoc: DocumentContainer,
|
||||
transaction: Transaction
|
||||
) => {
|
||||
const items: StoredComment[] = []
|
||||
|
||||
transaction.changes.iterChanges((fromA, toA) => {
|
||||
const comments = currentDoc.ranges.comments
|
||||
.filter(
|
||||
const comments = currentDoc
|
||||
.ranges!.comments.filter(
|
||||
comment =>
|
||||
fromA <= comment.op.p && comment.op.p + comment.op.c.length <= toA
|
||||
)
|
||||
|
@ -55,7 +55,7 @@ export const findCommentsInPaste = (
|
|||
storedComments: StoredComment[],
|
||||
transaction: Transaction
|
||||
) => {
|
||||
const ops: ChangeOperation[] = []
|
||||
const ops: CommentOperation[] = []
|
||||
|
||||
transaction.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
||||
const insertedText = inserted.toString()
|
||||
|
@ -71,7 +71,7 @@ export const findCommentsInPaste = (
|
|||
ops.push({
|
||||
c: text,
|
||||
p: fromB + offset,
|
||||
t: comment.id,
|
||||
t: comment.id as ThreadId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -93,13 +93,13 @@ class CommentRangeValue extends RangeValue {
|
|||
* Find tracked comments with no content with the ranges of a transaction's changes
|
||||
*/
|
||||
export const findDetachedCommentsInChanges = (
|
||||
currentDoc: CurrentDoc,
|
||||
currentDoc: DocumentContainer,
|
||||
transaction: Transaction
|
||||
) => {
|
||||
const items: Range<CommentRangeValue>[] = []
|
||||
|
||||
transaction.changes.iterChanges((fromA, toA) => {
|
||||
for (const comment of currentDoc.ranges.comments) {
|
||||
for (const comment of currentDoc.ranges!.comments) {
|
||||
const content = comment.op.c
|
||||
|
||||
// TODO: handle comments that were never attached
|
||||
|
@ -124,7 +124,7 @@ export const findDetachedCommentsInChanges = (
|
|||
* (used when restoring comments on paste)
|
||||
*/
|
||||
const submitOps = (
|
||||
currentDoc: CurrentDoc,
|
||||
currentDoc: DocumentContainer,
|
||||
ops: AnyOperation[],
|
||||
transaction: Transaction
|
||||
) => {
|
||||
|
@ -133,14 +133,14 @@ const submitOps = (
|
|||
}
|
||||
|
||||
// Check that comments still match text. Will throw error if not.
|
||||
currentDoc.ranges.validate(transaction.state.doc.toString())
|
||||
currentDoc.ranges!.validate(transaction.state.doc.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the ShareJS doc to fire an event, then submit the operations.
|
||||
*/
|
||||
const submitOpsAfterEvent = (
|
||||
currentDoc: CurrentDoc,
|
||||
currentDoc: DocumentContainer,
|
||||
eventName: string,
|
||||
ops: AnyOperation[],
|
||||
transaction: Transaction
|
||||
|
@ -161,7 +161,7 @@ const submitOpsAfterEvent = (
|
|||
* Look through the comments stored on cut, and restore those in text that matches the pasted text.
|
||||
*/
|
||||
export const restoreCommentsOnPaste = (
|
||||
currentDoc: CurrentDoc,
|
||||
currentDoc: DocumentContainer,
|
||||
transaction: Transaction,
|
||||
storedComments: StoredComment[]
|
||||
) => {
|
||||
|
@ -183,18 +183,18 @@ export const restoreCommentsOnPaste = (
|
|||
* When undoing a change, find comments from the original content and restore them.
|
||||
*/
|
||||
export const restoreDetachedComments = (
|
||||
currentDoc: CurrentDoc,
|
||||
currentDoc: DocumentContainer,
|
||||
transaction: Transaction,
|
||||
storedComments: RangeSet<any>
|
||||
) => {
|
||||
const ops: ChangeOperation[] = []
|
||||
const ops: CommentOperation[] = []
|
||||
|
||||
const cursor = storedComments.iter()
|
||||
|
||||
while (cursor.value) {
|
||||
const { id } = cursor.value.comment
|
||||
|
||||
const comment = currentDoc.ranges.comments.find(item => item.id === id)
|
||||
const comment = currentDoc.ranges!.comments.find(item => item.id === id)
|
||||
|
||||
// check that the comment still exists and is detached
|
||||
if (comment && comment.op.c === '') {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Prec, Transaction, Annotation, ChangeSpec } from '@codemirror/state'
|
||||
import { EditorView, ViewPlugin } from '@codemirror/view'
|
||||
import { EventEmitter } from 'events'
|
||||
import { CurrentDoc } from '../../../../../types/current-doc'
|
||||
import { ShareDoc } from '../../../../../types/share-doc'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||
|
||||
/*
|
||||
* Integrate CodeMirror 6 with the real-time system, via ShareJS.
|
||||
|
@ -34,7 +34,7 @@ export type ChangeDescription = {
|
|||
* A custom extension that connects the CodeMirror 6 editor to the currently open ShareJS document.
|
||||
*/
|
||||
export const realtime = (
|
||||
{ currentDoc }: { currentDoc: CurrentDoc },
|
||||
{ currentDoc }: { currentDoc: DocumentContainer },
|
||||
handleError: (error: Error) => void
|
||||
) => {
|
||||
const realtimePlugin = ViewPlugin.define(view => {
|
||||
|
|
|
@ -21,15 +21,14 @@ import {
|
|||
StoredComment,
|
||||
} from './changes/comments'
|
||||
import { invertedEffects } from '@codemirror/commands'
|
||||
import { CurrentDoc } from '../../../../../types/current-doc'
|
||||
import { Change, DeleteOperation } from '../../../../../types/change'
|
||||
import { ChangeManager } from './changes/change-manager'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { isCommentOperation, isDeleteOperation } from '@/utils/operations'
|
||||
import {
|
||||
isChangeOperation,
|
||||
isCommentOperation,
|
||||
isDeleteOperation,
|
||||
} from '@/utils/operations'
|
||||
DocumentContainer,
|
||||
RangesTrackerWithResolvedThreadIds,
|
||||
} from '@/features/ide-react/editor/document-container'
|
||||
|
||||
const clearChangesEffect = StateEffect.define()
|
||||
const buildChangesEffect = StateEffect.define()
|
||||
|
@ -46,7 +45,7 @@ const restoreDetachedCommentsEffect = StateEffect.define<RangeSet<any>>({
|
|||
})
|
||||
|
||||
type Options = {
|
||||
currentDoc: CurrentDoc
|
||||
currentDoc: DocumentContainer
|
||||
loadingThreads: boolean
|
||||
}
|
||||
|
||||
|
@ -182,7 +181,11 @@ export const buildChangeMarkers = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const buildChangeDecorations = (currentDoc: CurrentDoc) => {
|
||||
const buildChangeDecorations = (currentDoc: DocumentContainer) => {
|
||||
if (!currentDoc.ranges) {
|
||||
return Decoration.none
|
||||
}
|
||||
|
||||
const changes = [...currentDoc.ranges.changes, ...currentDoc.ranges.comments]
|
||||
|
||||
const decorations = []
|
||||
|
@ -245,7 +248,7 @@ class ChangeCalloutWidget extends WidgetType {
|
|||
}
|
||||
}
|
||||
|
||||
const createChangeRange = (change: Change, currentDoc: CurrentDoc) => {
|
||||
const createChangeRange = (change: Change, currentDoc: DocumentContainer) => {
|
||||
const { id, metadata, op } = change
|
||||
|
||||
const from = op.p
|
||||
|
@ -273,14 +276,18 @@ const createChangeRange = (change: Change, currentDoc: CurrentDoc) => {
|
|||
return [calloutWidget.range(from, from), changeWidget.range(from, from)]
|
||||
}
|
||||
|
||||
if (isChangeOperation(op) && currentDoc.ranges.resolvedThreadIds[op.t]) {
|
||||
const _isCommentOperation = isCommentOperation(op)
|
||||
|
||||
if (
|
||||
_isCommentOperation &&
|
||||
(currentDoc.ranges as RangesTrackerWithResolvedThreadIds)
|
||||
.resolvedThreadIds![op.t]
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
const isChangeOrCommentOperation =
|
||||
isChangeOperation(op) || isCommentOperation(op)
|
||||
const opType = isChangeOrCommentOperation ? 'c' : 'i'
|
||||
const changedText = isChangeOrCommentOperation ? op.c : op.i
|
||||
const opType = _isCommentOperation ? 'c' : 'i'
|
||||
const changedText = _isCommentOperation ? op.c : op.i
|
||||
const to = from + changedText.length
|
||||
|
||||
// Mark decorations must not be empty
|
||||
|
|
|
@ -47,7 +47,6 @@ import {
|
|||
import { setKeybindings } from '../extensions/keybindings'
|
||||
import { Highlight } from '../../../../../types/highlight'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { CurrentDoc } from '../../../../../types/current-doc'
|
||||
import { useErrorHandler } from 'react-error-boundary'
|
||||
import { setVisual } from '../extensions/visual/visual'
|
||||
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
|
@ -56,6 +55,7 @@ import { setDocName } from '@/features/source-editor/extensions/doc-name'
|
|||
import isValidTexFile from '@/main/is-valid-tex-file'
|
||||
import { captureException } from '@/infrastructure/error-reporter'
|
||||
import grammarlyExtensionPresent from '@/shared/utils/grammarly'
|
||||
import { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||
|
||||
function useCodeMirrorScope(view: EditorView) {
|
||||
const ide = useIdeContext()
|
||||
|
@ -71,7 +71,9 @@ function useCodeMirrorScope(view: EditorView) {
|
|||
|
||||
const [loadingThreads] = useScopeValue<boolean>('loadingThreads')
|
||||
|
||||
const [currentDoc] = useScopeValue<CurrentDoc | null>('editor.sharejs_doc')
|
||||
const [currentDoc] = useScopeValue<DocumentContainer | null>(
|
||||
'editor.sharejs_doc'
|
||||
)
|
||||
const [docName] = useScopeValue<string>('editor.open_doc_name')
|
||||
const [trackChanges] = useScopeValue<boolean>('editor.trackChanges')
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
ChangeOperation,
|
||||
CommentOperation,
|
||||
DeleteOperation,
|
||||
InsertOperation,
|
||||
|
@ -8,9 +7,7 @@ import {
|
|||
|
||||
export const isInsertOperation = (op: Operation): op is InsertOperation =>
|
||||
'i' in op
|
||||
export const isChangeOperation = (op: Operation): op is ChangeOperation =>
|
||||
'c' in op && 't' in op
|
||||
export const isCommentOperation = (op: Operation): op is CommentOperation =>
|
||||
'c' in op && !('t' in op)
|
||||
'c' in op
|
||||
export const isDeleteOperation = (op: Operation): op is DeleteOperation =>
|
||||
'd' in op
|
||||
|
|
|
@ -1,36 +1,31 @@
|
|||
import { ThreadId } from './review-panel/review-panel'
|
||||
import { UserId } from './user'
|
||||
|
||||
export interface Operation {
|
||||
p: number
|
||||
p: number // position
|
||||
}
|
||||
|
||||
export interface InsertOperation extends Operation {
|
||||
i: string
|
||||
t: string
|
||||
}
|
||||
|
||||
export interface ChangeOperation extends Operation {
|
||||
c: string
|
||||
t: string
|
||||
i: string // inserted text
|
||||
}
|
||||
|
||||
export interface DeleteOperation extends Operation {
|
||||
d: string
|
||||
d: string // deleted text
|
||||
}
|
||||
|
||||
export interface CommentOperation extends Operation {
|
||||
c: string
|
||||
c: string // comment text
|
||||
t: ThreadId // thread/comment id
|
||||
}
|
||||
|
||||
export type NonCommentOperation =
|
||||
| InsertOperation
|
||||
| ChangeOperation
|
||||
| DeleteOperation
|
||||
export type EditOperation = InsertOperation | DeleteOperation
|
||||
|
||||
export type AnyOperation = NonCommentOperation | CommentOperation
|
||||
export type AnyOperation = EditOperation | CommentOperation
|
||||
|
||||
export type Change<T extends AnyOperation = AnyOperation> = {
|
||||
id: string
|
||||
metadata?: {
|
||||
user_id: string
|
||||
user_id: UserId | null
|
||||
ts: Date
|
||||
}
|
||||
op: T
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import EventEmitter from '../frontend/js/utils/EventEmitter'
|
||||
import { ShareDoc } from './share-doc'
|
||||
import { EditorFacade } from '../frontend/js/features/source-editor/extensions/realtime'
|
||||
import {
|
||||
AnyOperation,
|
||||
Change,
|
||||
ChangeOperation,
|
||||
CommentOperation,
|
||||
DeleteOperation,
|
||||
InsertOperation,
|
||||
} from './change'
|
||||
|
||||
// type for the Document class in ide/editor/Document.js
|
||||
// note: this is a custom EventEmitter class
|
||||
|
||||
// TODO: MIGRATION: This doesn't match the type for
|
||||
// ide-react/editor/document.ts, which has a nullable `ranges` property and some
|
||||
// other quirks. They should match.
|
||||
export interface CurrentDoc extends EventEmitter {
|
||||
doc_id: string
|
||||
docName: string
|
||||
doc: ShareDoc | null
|
||||
track_changes_as: string | null
|
||||
ranges: {
|
||||
changes: Change<InsertOperation | ChangeOperation | DeleteOperation>[]
|
||||
comments: Change<CommentOperation>[]
|
||||
resolvedThreadIds: Record<string, any>
|
||||
removeCommentId: (id: string) => void
|
||||
removeChangeIds: (ids: string[]) => void
|
||||
getChanges: (
|
||||
ids: string[]
|
||||
) => Change<InsertOperation | ChangeOperation | DeleteOperation>[]
|
||||
validate: (text: string) => void
|
||||
}
|
||||
attachToCM6: (editor: EditorFacade) => void
|
||||
detachFromCM6: () => void
|
||||
submitOp: (op: AnyOperation) => void
|
||||
getSnapshot: () => string
|
||||
}
|
Loading…
Reference in a new issue