Standardise types for ranges (#16927)

GitOrigin-RevId: 28dd0eb67e1684e6bd0e452d15315ce1f9e3481a
This commit is contained in:
Alf Eaton 2024-02-08 10:10:46 +00:00 committed by Copybot
parent c997d1dc2b
commit 7b3ffb9fae
13 changed files with 171 additions and 185 deletions

View file

@ -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,

View file

@ -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

View file

@ -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 },

View file

@ -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')
}

View file

@ -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`

View file

@ -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,18 +247,7 @@ export const createChangeManager = (
const changesToDispatch = changes.map(change => {
const { op } = change
const opType = 'i' in op ? 'i' : 'c' in op ? 'c' : 'd'
switch (opType) {
case 'd': {
return {
from: op.p,
to: op.p,
insert: op.d,
}
}
case 'i': {
if (isInsertOperation(op)) {
const from = op.p
const content = op.i
const to = from + content.length
@ -269,11 +263,14 @@ export const createChangeManager = (
}
return { from, to, insert: '' }
} else if (isDeleteOperation(op)) {
return {
from: op.p,
to: op.p,
insert: op.d,
}
default: {
throw new Error(`unknown change: ${JSON.stringify(change)}`)
}
} else {
throw new Error(`unknown change type: ${JSON.stringify(change)}`)
}
})

View file

@ -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 === '') {

View file

@ -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 => {

View file

@ -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

View file

@ -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')

View file

@ -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

View file

@ -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

View file

@ -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
}