diff --git a/vscode/src/services/CharactersLogger.test.ts b/vscode/src/services/CharactersLogger.test.ts index 5a2723e90fb..66a59aa7b3f 100644 --- a/vscode/src/services/CharactersLogger.test.ts +++ b/vscode/src/services/CharactersLogger.test.ts @@ -14,7 +14,6 @@ import { LOG_INTERVAL, RAPID_CHANGE_TIMEOUT, SELECTION_TIMEOUT, - changeBoundaries, } from './CharactersLogger' const testDocument = document('foo') @@ -24,15 +23,13 @@ describe('CharactersLogger', () => { let tracker: CharactersLogger let onDidChangeActiveTextEditor: (event: vscode.TextEditor | undefined) => void - let onDidChangeTextEditorVisibleRanges: (event: vscode.TextEditorVisibleRangesChangeEvent) => void let onDidChangeTextDocument: (event: vscode.TextDocumentChangeEvent) => void let onDidCloseTextDocument: (document: vscode.TextDocument) => void let onDidChangeWindowState: (state: vscode.WindowState) => void - let onDidChangeVisibleTextEditors: (editors: vscode.TextEditor[]) => void let onDidChangeTextEditorSelection: (event: vscode.TextEditorSelectionChangeEvent) => void let mockWindowState: Writable - let mockVisibleTextEditors: vscode.TextEditor[] + let mockActiveTextEditor: Writable | undefined let mockTextEditorSelectionEvent: vscode.TextEditorSelectionChangeEvent beforeEach(() => { @@ -41,7 +38,10 @@ describe('CharactersLogger', () => { recordSpy = vi.spyOn(telemetryRecorder, 'recordEvent') mockWindowState = { focused: true } - mockVisibleTextEditors = [{ document: testDocument } as vscode.TextEditor] + mockActiveTextEditor = { + document: testDocument, + visibleRanges: [range(0, 0, 1000, 1000)], + } as unknown as vscode.TextEditor mockTextEditorSelectionEvent = { textEditor: { document: testDocument } as vscode.TextEditor, @@ -61,31 +61,22 @@ describe('CharactersLogger', () => { }, }, { - activeTextEditor: {} as any, - onDidChangeTextEditorVisibleRanges(listener) { - onDidChangeTextEditorVisibleRanges = listener - return { dispose: () => {} } - }, - onDidChangeActiveTextEditor(listener) { - onDidChangeActiveTextEditor = listener - return { dispose: () => {} } - }, + state: mockWindowState, + activeTextEditor: mockActiveTextEditor, onDidChangeWindowState(listener) { onDidChangeWindowState = listener return { dispose: () => {} } }, - onDidChangeVisibleTextEditors(listener) { - onDidChangeVisibleTextEditors = listener + onDidChangeActiveTextEditor(listener) { + onDidChangeActiveTextEditor = listener return { dispose: () => {} } }, onDidChangeTextEditorSelection(listener) { onDidChangeTextEditorSelection = listener return { dispose: () => {} } }, - visibleTextEditors: mockVisibleTextEditors, } ) - console.log({ onDidChangeActiveTextEditor, onDidChangeTextEditorVisibleRanges }) }) afterEach(() => { @@ -121,400 +112,174 @@ describe('CharactersLogger', () => { } } - // Helper function to create default metadata counters with expected values + // Helper function to create expected counters function expectedCharCounters(expected: Partial): Record { return { ...DEFAULT_COUNTERS, ...expected } } - function advanceTimerToPreventRapidChange() { - vi.advanceTimersByTime(RAPID_CHANGE_TIMEOUT + 1) - } - - it('logs inserted and deleted characters for user edits', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - onDidChangeTextDocument(createChange({ text: 'foo', range: range(0, 0, 0, 0), rangeLength: 0 })) - - advanceTimerToPreventRapidChange() - + it('should handle insertions, deletions, rapid and stale changes, and changes outside of visible range', () => { + // Simulate user typing in the active text editor + onDidChangeActiveTextEditor(mockActiveTextEditor) onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - onDidChangeTextDocument(createChange({ text: 'bar', range: range(0, 3, 0, 3), rangeLength: 0 })) - vi.advanceTimersByTime(LOG_INTERVAL) - - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'char-counts', { - metadata: expectedCharCounters({ - xxs_change_inserted: 6, // 'foo' + 'bar' - }), - }) - }) + // Scenario 1: User types 'hello' (insertion) + onDidChangeTextDocument( + createChange({ text: 'hello', range: range(0, 0, 0, 0), rangeLength: 0 }) + ) - it('logs changes under "window_not_focused" when window is not focused', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - onDidChangeTextDocument(createChange({ text: 'foo', range: range(0, 0, 0, 0), rangeLength: 0 })) + // Advance time less than RAPID_CHANGE_TIMEOUT to simulate rapid change + vi.advanceTimersByTime(RAPID_CHANGE_TIMEOUT - 5) - mockWindowState.focused = false - onDidChangeWindowState(mockWindowState) + // Scenario 2: User deletes 'he' (deletion) + onDidChangeTextDocument(createChange({ text: '', range: range(0, 0, 0, 2), rangeLength: 2 })) - advanceTimerToPreventRapidChange() + // Now, advance time beyond SELECTION_TIMEOUT to make selection stale + vi.advanceTimersByTime(SELECTION_TIMEOUT + 1000) - onDidChangeTextDocument(createChange({ text: 'bar', range: range(0, 3, 0, 3), rangeLength: 0 })) - vi.advanceTimersByTime(LOG_INTERVAL) + // Scenario 3: User types 'there' (stale insertion) + onDidChangeTextDocument( + createChange({ text: 'there', range: range(0, 3, 0, 3), rangeLength: 0 }) + ) + // Should be counted as an insertion, stale change - expect(recordSpy).toHaveBeenNthCalledWith(1, 'cody.characters', 'char-counts', { - metadata: expectedCharCounters({ - xxs_change_inserted: 3, // 'foo' - window_not_focused_inserted: 3, // 'bar' - }), - }) - }) + // Scenario 4: Change outside of visible range + // Simulate that the change is outside the visible range + mockActiveTextEditor!.visibleRanges = [range(1, 0, 1, 0)] // Lines 1 to 1 (empty range) + onDidChangeActiveTextEditor(mockActiveTextEditor) - it.only('logs changes under "non_visible_document" when in non-visible documents', () => { - // Remove testDocument from visible editors - mockVisibleTextEditors = [] - onDidChangeVisibleTextEditors(mockVisibleTextEditors) + // User types 'hidden' at line 50 (outside visible range) + onDidChangeTextDocument( + createChange({ + text: 'hidden', + range: range(50, 0, 50, 0), // Line 50, start + rangeLength: 0, + }) + ) - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - onDidChangeTextDocument(createChange({ text: 'foo', range: range(0, 0, 0, 0), rangeLength: 0 })) + // Restore the visible ranges + mockActiveTextEditor!.visibleRanges = [range(0, 0, 1000, 0)] + // Flush the log vi.advanceTimersByTime(LOG_INTERVAL) - // Verify that changes are logged under 'non_visible_document' + // Expected counters: expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { metadata: expectedCharCounters({ - non_visible_document_inserted: 3, - non_visible_document_deleted: 0, - }), - }) - }) + xxs_change: 1, + xxs_change_inserted: 5, // 'hello' - it('logs changes under "stale_selection" when there has been no recent cursor movement', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - vi.advanceTimersByTime(SELECTION_TIMEOUT + 1) + rapid_xxxs_change: 1, + rapid_xxxs_change_deleted: 2, // 'he' - onDidChangeTextDocument(createChange({ text: 'foo', range: range(0, 0, 0, 0), rangeLength: 0 })) - vi.advanceTimersByTime(LOG_INTERVAL) + stale_xxs_change: 1, + stale_xxs_change_inserted: 5, // 'there' - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters({ - stale_selection_inserted: 3, - stale_selection_deleted: 0, + outside_of_visible_ranges: 1, + outside_of_visible_ranges_inserted: 6, // 'hidden' }), }) }) - it('logs undo and redo changes under their respective types', () => { + it('should handle undo, redo, window not focused, no active editor, outside of active editor, and document closing', () => { + onDidChangeActiveTextEditor(mockActiveTextEditor) onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - const textDocumentChangeReason = { - undo: 1, - redo: 2, - } - // Simulate undo change + const changeReasons = { Undo: 1, Redo: 2 } as const + + onDidChangeTextDocument(createChange({ text: 'test', range: range(0, 0, 0, 0), rangeLength: 0 })) + + // Simulate undo onDidChangeTextDocument( createChange({ text: '', - range: range(0, 3, 0, 0), - rangeLength: 3, - document: testDocument, - reason: textDocumentChangeReason.undo, + range: range(0, 4, 0, 0), + rangeLength: 4, + reason: changeReasons.Undo, }) ) - advanceTimerToPreventRapidChange() - - // Simulate redo change + // Simulate redo onDidChangeTextDocument( createChange({ - text: 'foo', + text: 'test', range: range(0, 0, 0, 0), rangeLength: 0, - document: testDocument, - reason: textDocumentChangeReason.redo, + reason: changeReasons.Redo, }) ) - vi.advanceTimersByTime(LOG_INTERVAL) - - // Verify that changes are logged under 'undo' and 'redo' - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters({ - undo_inserted: 0, - undo_deleted: 3, - redo_inserted: 3, - redo_deleted: 0, - }), - }) - }) - - it('logs rapid changes under "rapid_change"', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - onDidChangeTextDocument(createChange({ text: 'foo', range: range(0, 0, 0, 0), rangeLength: 0 })) - - vi.advanceTimersByTime(RAPID_CHANGE_TIMEOUT / 2) - - // Simulate second change within rapid change timeout - onDidChangeTextDocument(createChange({ text: 'bar', range: range(0, 3, 0, 3), rangeLength: 0 })) - vi.advanceTimersByTime(LOG_INTERVAL) - - // TODO: both changes should be logged as rapid. - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters({ - rapid_change_inserted: 3, - rapid_change_deleted: 0, - xs_change_inserted: 3, - xs_change_deleted: 0, - }), - }) - }) - - it('counts large changes according to their sizes if they are not rapid', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) + mockWindowState.focused = false + onDidChangeWindowState(mockWindowState) - advanceTimerToPreventRapidChange() - const largeText = 'a'.repeat(1005) + // User types ' window not focused' when window not focused onDidChangeTextDocument( - createChange({ text: largeText, range: range(0, 0, 0, 0), rangeLength: 0 }) + createChange({ text: 'window not focused', range: range(0, 4, 0, 4), rangeLength: 0 }) ) - vi.advanceTimersByTime(LOG_INTERVAL) - - // Verify that the large change is logged under 'xxl_change' - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters({ - xxl_change_inserted: 1005, - xxl_change_deleted: 0, - }), - }) - }) - - it('resets counter after flushing', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - onDidChangeTextDocument(createChange({ text: 'foo', range: range(0, 0, 0, 0), rangeLength: 0 })) - - vi.advanceTimersByTime(LOG_INTERVAL) - - expect(recordSpy).toHaveBeenCalledTimes(1) - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters({ - xs_change_inserted: 3, - xs_change_deleted: 0, - }), - }) - - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - advanceTimerToPreventRapidChange() - onDidChangeTextDocument(createChange({ text: 'bar', range: range(0, 3, 0, 3), rangeLength: 0 })) - - vi.advanceTimersByTime(LOG_INTERVAL) - - expect(recordSpy).toHaveBeenCalledTimes(2) - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters({ - xs_change_inserted: 3, - xs_change_deleted: 0, - }), - }) - }) + // Simulate window gaining focus + mockWindowState.focused = true + onDidChangeWindowState(mockWindowState) - it('logs user typing under size-based change types after cursor movement', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) + // Simulate no active editor + onDidChangeActiveTextEditor(undefined) onDidChangeTextDocument( - createChange({ text: 'abcde', range: range(0, 0, 0, 0), rangeLength: 0 }) + createChange({ text: 'no active editor', range: range(0, 21, 0, 21), rangeLength: 0 }) ) - vi.advanceTimersByTime(LOG_INTERVAL) + // Simulate editor for different document + const anotherDocument = { + uri: { toString: () => 'file://anotherdocument' }, + } as vscode.TextDocument - // 'abcde' is 5 characters, which falls under 'xs_change' (2-5) - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters({ - xs_change_inserted: 5, - xs_change_deleted: 0, - }), - }) - }) - - it('handles multiple documents and selections', () => { - const anotherDocument = document('bar') - mockVisibleTextEditors.push({ + const anotherEditor = { document: anotherDocument, - } as vscode.TextEditor) - onDidChangeVisibleTextEditors(mockVisibleTextEditors) + visibleRanges: [range(0, 0, 1000, 0)], + } as unknown as vscode.TextEditor - onDidChangeTextEditorSelection({ - textEditor: { - document: testDocument, - } as vscode.TextEditor, - selections: [], - kind: undefined, - }) - - onDidChangeTextEditorSelection({ - textEditor: { - document: anotherDocument, - } as vscode.TextEditor, - selections: [], - kind: undefined, - }) + onDidChangeActiveTextEditor(anotherEditor) + // User types in original document (not the active editor's document) onDidChangeTextDocument( createChange({ - text: 'foo', - range: range(0, 0, 0, 0), + text: 'outside active editor', + range: range(0, 21, 0, 21), rangeLength: 0, document: testDocument, }) ) - advanceTimerToPreventRapidChange() - + onDidCloseTextDocument(testDocument) onDidChangeTextDocument( createChange({ - text: 'baz', - range: range(0, 0, 0, 0), + text: '!', + range: range(0, 50, 0, 50), rangeLength: 0, - document: anotherDocument, + document: testDocument, }) ) vi.advanceTimersByTime(LOG_INTERVAL) + // Expected counters: expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { metadata: expectedCharCounters({ - xs_change_inserted: 6, // 'foo' + 'baz' - xs_change_deleted: 0, - }), - }) - }) - - it('removes document from tracking on close', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - onDidChangeTextDocument(createChange({ text: 'foo', range: range(0, 0, 0, 0), rangeLength: 0 })) - - onDidCloseTextDocument(testDocument) - - onDidChangeTextDocument(createChange({ text: 'bar', range: range(0, 3, 0, 3), rangeLength: 0 })) - vi.advanceTimersByTime(LOG_INTERVAL) - - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters({ - xs_change_inserted: 3, // Only 'foo' is counted - xs_change_deleted: 0, - non_visible_document_inserted: 3, - }), - }) - }) + xxs_change: 1, + xxs_change_inserted: 4, // 'test' - it('correctly classifies changes at boundary sizes', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - - for (const [_, boundaries] of Object.entries(changeBoundaries)) { - advanceTimerToPreventRapidChange() - const text = 'a'.repeat(boundaries.min || boundaries.max) - onDidChangeTextDocument(createChange({ text, range: range(0, 0, 0, 0), rangeLength: 0 })) - } - - vi.advanceTimersByTime(LOG_INTERVAL) - - const expected: any = {} - for (const [type, boundary] of Object.entries(changeBoundaries)) { - expected[`${type}_inserted`] = boundary.min || boundary.max - expected[`${type}_deleted`] = 0 - } + undo: 1, + undo_deleted: 4, // 'test' deleted - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters(expected), - }) - }) + redo: 1, + redo_inserted: 4, // 'test' re-inserted - it('handles a mix of different change sizes in one interval', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - onDidChangeTextDocument(createChange({ text: 'abc', range: range(0, 0, 0, 0), rangeLength: 0 })) + window_not_focused: 1, + window_not_focused_inserted: 18, // ' window not focused' - advanceTimerToPreventRapidChange() + no_active_editor: 1, + no_active_editor_inserted: 16, // 'no active editor' - // Simulate a medium change of 30 characters - const mediumText = 'a'.repeat(30) - onDidChangeTextDocument( - createChange({ text: mediumText, range: range(0, 3, 0, 3), rangeLength: 0 }) - ) - - advanceTimerToPreventRapidChange() - - const largeText = 'b'.repeat(60) - onDidChangeTextDocument( - createChange({ text: largeText, range: range(0, 33, 0, 33), rangeLength: 0 }) - ) - - vi.advanceTimersByTime(LOG_INTERVAL) - - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters({ - xs_change_inserted: 3, // 'abc' - m_change_inserted: 30, // 30 'a's - l_change_inserted: 60, // 60 'b's - xs_change_deleted: 0, - m_change_deleted: 0, - l_change_deleted: 0, - }), - }) - }) - - it('prioritizes rapid changes over size-based classification', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - onDidChangeTextDocument( - createChange({ text: 'hello', range: range(0, 0, 0, 0), rangeLength: 0 }) - ) - - vi.advanceTimersByTime(RAPID_CHANGE_TIMEOUT - 10) - onDidChangeTextDocument( - createChange({ text: 'world', range: range(0, 5, 0, 5), rangeLength: 0 }) - ) - - advanceTimerToPreventRapidChange() - - const text = 'a'.repeat(10) - onDidChangeTextDocument(createChange({ text, range: range(0, 10, 0, 10), rangeLength: 0 })) - - vi.advanceTimersByTime(LOG_INTERVAL) - - // 'hello' and 'world' should be counted under 'rapid_change' - // The last change should be counted under 's_change' (size 10) - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters({ - rapid_change_inserted: 5, // 'world' - xs_change_inserted: 5, // 'hello' - s_change_inserted: 10, // 10 'a's - }), - }) - }) - - it('handles window focus loss and regain within an interval', () => { - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - onDidChangeTextDocument( - createChange({ text: 'focus', range: range(0, 0, 0, 0), rangeLength: 0 }) - ) - - mockWindowState.focused = false - onDidChangeWindowState(mockWindowState) - - advanceTimerToPreventRapidChange() - onDidChangeTextDocument(createChange({ text: 'blur', range: range(0, 5, 0, 5), rangeLength: 0 })) - - mockWindowState.focused = true - onDidChangeWindowState(mockWindowState) - onDidChangeTextEditorSelection(mockTextEditorSelectionEvent) - - advanceTimerToPreventRapidChange() - onDidChangeTextDocument(createChange({ text: 'gain', range: range(0, 9, 0, 9), rangeLength: 0 })) - - vi.advanceTimersByTime(LOG_INTERVAL) - - expect(recordSpy).toHaveBeenCalledWith('cody.characters', 'flush', { - metadata: expectedCharCounters({ - window_not_focused_inserted: 4, // 'blur' - xs_change_inserted: 4 + 5, // 'gain' (after regaining focus) - xs_change_deleted: 0, - window_not_focused_deleted: 0, + outside_of_active_editor: 2, + outside_of_active_editor_inserted: 22, //'outside active editor' }), }) }) diff --git a/vscode/src/services/CharactersLogger.ts b/vscode/src/services/CharactersLogger.ts index bcdcdf8b9ba..7c3dde8bea4 100644 --- a/vscode/src/services/CharactersLogger.ts +++ b/vscode/src/services/CharactersLogger.ts @@ -20,24 +20,24 @@ export const changeBoundaries = { } as const const changeBoundariesKeys = Object.keys(changeBoundaries) as (keyof typeof changeBoundaries)[] -const staleChangeBoundariesKeys = changeBoundariesKeys.map(key => `stale_selection_${key}` as const) +const staleAndRapidChangeBoundariesKeys = changeBoundariesKeys.flatMap( + key => [`stale_${key}`, `rapid_${key}`, `rapid_stale_${key}`] as const +) const SPECIAL_DOCUMENT_CHANGE_TYPES = [ 'undo', 'redo', 'window_not_focused', - 'non_visible_document', - 'non_active_editor', + 'no_active_editor', + 'outside_of_active_editor', 'outside_of_visible_ranges', - 'stale_selection', - 'rapid_change', // occurred in less than RAPID_CHANGE_TIMEOUT after/before another change 'unexpected', // should not be logged because all the change sizes are covered by the keys above ] as const const DOCUMENT_CHANGE_TYPES = [ ...SPECIAL_DOCUMENT_CHANGE_TYPES, ...changeBoundariesKeys, - ...staleChangeBoundariesKeys, + ...staleAndRapidChangeBoundariesKeys, ] as const type DocumentChangeType = (typeof DOCUMENT_CHANGE_TYPES)[number] @@ -65,8 +65,6 @@ export class CharactersLogger implements vscode.Disposable { private windowFocused = true private activeTextEditor: vscode.TextEditor | undefined - private visibleDocuments = new Set() - private visibleRangesMap: Map> = new Map() private lastChangeTimestamp = 0 private lastSelectionTimestamps = new Map() @@ -77,22 +75,17 @@ export class CharactersLogger implements vscode.Disposable { > = vscode.workspace, window: Pick< typeof vscode.window, + | 'state' | 'activeTextEditor' | 'onDidChangeWindowState' | 'onDidChangeActiveTextEditor' - | 'onDidChangeVisibleTextEditors' - | 'onDidChangeTextEditorVisibleRanges' | 'onDidChangeTextEditorSelection' - | 'visibleTextEditors' > = vscode.window ) { this.disposables.push( workspace.onDidChangeTextDocument(this.onDidChangeTextDocument.bind(this)), workspace.onDidCloseTextDocument(document => { - const uri = document.uri.toString() - this.lastSelectionTimestamps.delete(uri) - this.visibleDocuments.delete(uri) - this.visibleRangesMap.delete(uri) + this.lastSelectionTimestamps.delete(document.uri.toString()) }), window.onDidChangeWindowState(state => { this.windowFocused = state.focused @@ -100,20 +93,13 @@ export class CharactersLogger implements vscode.Disposable { window.onDidChangeActiveTextEditor(editor => { this.activeTextEditor = editor }), - window.onDidChangeVisibleTextEditors(editors => { - this.updateVisibleDocuments(editors) - }), window.onDidChangeTextEditorSelection(event => { const documentUri = event.textEditor.document.uri.toString() this.lastSelectionTimestamps.set(documentUri, Date.now()) - }), - window.onDidChangeTextEditorVisibleRanges(event => { - const documentUri = event.textEditor.document.uri.toString() - this.visibleRangesMap.set(documentUri, event.visibleRanges) }) ) - this.updateVisibleDocuments(window.visibleTextEditors) + this.windowFocused = window.state.focused this.activeTextEditor = window.activeTextEditor this.nextTimeoutId = setTimeout(() => this.flush(), LOG_INTERVAL) } @@ -142,19 +128,19 @@ export class CharactersLogger implements vscode.Disposable { }, 0) const changeType = this.getDocumentChangeType(event, totalChangeSize) - this.changeCounters[changeType]++ const { activeTextEditor } = this for (const change of event.contentChanges) { - // TODO: manually test active test editor visible ranges staleness - const isChangeVisible = activeTextEditor?.visibleRanges.some(range => - range.contains(change.range) - ) + const isChangeVisible = activeTextEditor?.visibleRanges.some(range => { + return range.contains(change.range) + }) const isSpecialChangeType = SPECIAL_DOCUMENT_CHANGE_TYPES.find(v => v === changeType) const contentChangeType = !isSpecialChangeType && !isChangeVisible ? 'outside_of_visible_ranges' : changeType + this.changeCounters[contentChangeType]++ + // We use change.rangeLength for deletions because: // 1. It represents the length of the text being replaced, including newline characters. // 2. It accurately accounts for multi-line deletions. @@ -177,10 +163,10 @@ export class CharactersLogger implements vscode.Disposable { if (totalChangeSize > 0) { this.lastChangeTimestamp = Date.now() } - console.log( - `changes: [${event.contentChanges.map(c => c.text).join(',')}]`, - JSON.stringify(this.changeCounters, null, 2) - ) + // console.log( + // `changes: [${event.contentChanges.map(c => c.text).join(',')}]`, + // JSON.stringify(this.changeCounters, null, 2) + // ) } private getDocumentChangeType( @@ -202,38 +188,30 @@ export class CharactersLogger implements vscode.Disposable { return 'window_not_focused' } - if (!vscode.window.activeTextEditor) { - return 'non_active_editor' + if (!this.activeTextEditor) { + return 'no_active_editor' } - const timeSinceLastChange = currentTimestamp - this.lastChangeTimestamp - if (timeSinceLastChange < RAPID_CHANGE_TIMEOUT) { - return 'rapid_change' + if (this.activeTextEditor.document.uri.toString() !== documentUri) { + return 'outside_of_active_editor' } const lastSelectionTimestamp = this.lastSelectionTimestamps.get(documentUri) || 0 const isSelectionStale = currentTimestamp - lastSelectionTimestamp > SELECTION_TIMEOUT + const isRapidChange = currentTimestamp - this.lastChangeTimestamp < RAPID_CHANGE_TIMEOUT + + const rapidPrefix = isRapidChange ? 'rapid_' : '' + const stalePrefix = isSelectionStale ? 'stale_' : '' for (const [changeSizeType, boundaries] of Object.entries(changeBoundaries)) { if (boundaries.min <= totalChangeSize && totalChangeSize <= boundaries.max) { - return ( - isSelectionStale ? `stale_selection_${changeSizeType}` : changeSizeType - ) as DocumentChangeType + return `${rapidPrefix}${stalePrefix}${changeSizeType as keyof typeof changeBoundaries}` } } return 'unexpected' } - private updateVisibleDocuments(editors: Readonly): void { - this.visibleDocuments.clear() - for (const editor of editors) { - const uri = editor.document.uri.toString() - this.visibleDocuments.add(uri) - this.visibleRangesMap.set(uri, editor.visibleRanges) - } - } - public dispose(): void { this.flush() if (this.nextTimeoutId) { diff --git a/vscode/src/testutils/mocks.ts b/vscode/src/testutils/mocks.ts index f8b94062931..7c370655361 100644 --- a/vscode/src/testutils/mocks.ts +++ b/vscode/src/testutils/mocks.ts @@ -357,6 +357,14 @@ export class Location implements VSCodeLocation { } } +const isSmallerOrEqualPosition = (a: Position, b: Position): boolean => { + return a.line < b.line || (a.line === b.line && a.character <= b.character) +} + +const isBiggerOrEqualPosition = (a: Position, b: Position): boolean => { + return a.line > b.line || (a.line === b.line && a.character >= b.character) +} + export class Range implements VSCodeRange { public start: Position public end: Position @@ -418,21 +426,17 @@ export class Range implements VSCodeRange { return this.start.line === this.end.line } public contains(positionOrRange: Position | Range): boolean { - const isSmallerOrEqual = (a: Position, b: Position): boolean => { - return a.line < b.line || (a.line === b.line && a.character <= b.character) - } - if ('line' in positionOrRange) { return ( - isSmallerOrEqual(this.start, positionOrRange) && - isSmallerOrEqual(positionOrRange, this.end) + isSmallerOrEqualPosition(this.start, positionOrRange) && + isSmallerOrEqualPosition(positionOrRange, this.end) ) } if ('start' in positionOrRange && 'end' in positionOrRange) { return ( - isSmallerOrEqual(this.start, positionOrRange.start) && - isSmallerOrEqual(positionOrRange.start, this.end) && - isSmallerOrEqual(this.end, positionOrRange.end) + isSmallerOrEqualPosition(this.start, positionOrRange.start) && + isSmallerOrEqualPosition(positionOrRange.start, this.end) && + isBiggerOrEqualPosition(this.end, positionOrRange.end) ) }