diff --git a/src/vs/editor/common/model/prefixSumComputer.ts b/src/vs/editor/common/model/prefixSumComputer.ts index 08376d50d8e63..04cf4ccc1e493 100644 --- a/src/vs/editor/common/model/prefixSumComputer.ts +++ b/src/vs/editor/common/model/prefixSumComputer.ts @@ -235,6 +235,12 @@ export class ConstantTimePrefixSumComputer { public getIndexOf(sum: number): PrefixSumIndexOfResult { this._ensureValid(); const idx = this._indexBySum[sum]; + if (idx === undefined) { + // sum does not have a direct entry in _indexBySum (e.g. sum >= getTotalSum() or the array is empty / all values are zero) + const lastIdx = Math.max(0, this._values.length - 1); + const lastPrefixSum = lastIdx > 0 ? this._prefixSum[lastIdx - 1] : 0; + return new PrefixSumIndexOfResult(lastIdx, sum - lastPrefixSum); + } const viewLinesAbove = idx > 0 ? this._prefixSum[idx - 1] : 0; return new PrefixSumIndexOfResult(idx, sum - viewLinesAbove); } @@ -271,7 +277,7 @@ export class ConstantTimePrefixSumComputer { // trim things this._prefixSum.length = this._values.length; - this._indexBySum.length = this._prefixSum[this._prefixSum.length - 1]; + this._indexBySum.length = this._values.length > 0 ? this._prefixSum[this._values.length - 1] : 0; // mark as valid this._isValid = true; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index bb3a86c98575b..638fafb925af2 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1680,10 +1680,18 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private _onDidChangeContentOrInjectedText(e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void { for (const viewModel of this._viewModels) { - viewModel.onDidChangeContentOrInjectedText(e); + try { + viewModel.onDidChangeContentOrInjectedText(e); + } catch (error) { + onUnexpectedError(error); + } } for (const viewModel of this._viewModels) { - viewModel.emitContentChangeEvent(e); + try { + viewModel.emitContentChangeEvent(e); + } catch (error) { + onUnexpectedError(error); + } } } diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index f3f33283bfc11..db795ac7371d1 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -781,10 +781,6 @@ export class ViewModel extends Disposable implements IViewModel { * Gives a hint that a lot of requests are about to come in for these line numbers. */ public setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void { - if (this._lines.getViewLineCount() === 0) { - // No visible lines to set viewport on - return; - } this._viewportStart.update(this, startLineNumber); } diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index 4a27a5a077ba0..dc14410a12d72 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -165,6 +165,8 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { this._validModelVersionId = this.model.getVersionId(); this.projectedModelLineLineCounts = new ConstantTimePrefixSumComputer(values); + + this._ensureAtLeastOneVisibleLine(); } public getHiddenAreas(): Range[] { @@ -423,9 +425,13 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { public acceptVersionId(versionId: number): void { this._validModelVersionId = versionId; - if (this.modelLineProjections.length === 1 && !this.modelLineProjections[0].isVisible()) { - // At least one line must be visible => reset hidden areas - this.setHiddenAreas([]); + this._ensureAtLeastOneVisibleLine(); + } + + private _ensureAtLeastOneVisibleLine(): void { + if (this.getViewLineCount() === 0 && this.modelLineProjections.length > 0) { + this.modelLineProjections[0] = this.modelLineProjections[0].setVisible(true); + this.projectedModelLineLineCounts.setValue(0, this.modelLineProjections[0].getViewLineCount()); } } diff --git a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts index 2b9655e91504d..06ee277c8f68b 100644 --- a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts @@ -426,4 +426,227 @@ suite('ViewModel', () => { } ); }); + + suite('hidden areas must always leave at least one visible line', () => { + + test('replacing the only visible line content does not make it hidden', () => { + const text = [ + 'line1', + 'line2', + 'line3', + ]; + testViewModel(text, {}, (viewModel, model) => { + // Hide lines 1 and 3, leaving only line 2 visible + viewModel.setHiddenAreas([ + new Range(1, 1, 1, 1), + new Range(3, 1, 3, 1), + ]); + assert.strictEqual(viewModel.getLineCount(), 1); + + // Replace line 2 content entirely + model.applyEdits([{ + range: new Range(2, 1, 2, 6), + text: 'new content' + }]); + + assert.ok(viewModel.getLineCount() >= 1, `expected at least 1 view line but got ${viewModel.getLineCount()}`); + }); + }); + + test('deleting the only visible line when it is the last line', () => { + const text = [ + 'line1', + 'line2', + 'line3', + ]; + testViewModel(text, {}, (viewModel, model) => { + // Hide lines 1-2, leaving only line 3 visible + viewModel.setHiddenAreas([new Range(1, 1, 2, 1)]); + assert.strictEqual(viewModel.getLineCount(), 1); + + // Delete line 3 by merging it into line 2 + model.applyEdits([{ + range: new Range(2, 6, 3, 6), + text: null + }]); + + assert.ok(viewModel.getLineCount() >= 1, `expected at least 1 view line but got ${viewModel.getLineCount()}`); + }); + }); + + test('deleting the only visible line when it is in the middle', () => { + const text = [ + 'line1', + 'line2', + 'line3', + 'line4', + 'line5', + ]; + testViewModel(text, {}, (viewModel, model) => { + // Hide lines 1-2 and 4-5, leaving only line 3 visible + viewModel.setHiddenAreas([ + new Range(1, 1, 2, 1), + new Range(4, 1, 5, 1), + ]); + assert.strictEqual(viewModel.getLineCount(), 1); + + // Delete line 3 by merging adjacent lines + model.applyEdits([{ + range: new Range(2, 6, 4, 1), + text: null + }]); + + assert.ok(viewModel.getLineCount() >= 1, `expected at least 1 view line but got ${viewModel.getLineCount()}`); + }); + }); + + test('undo that removes the only visible line', () => { + const text = [ + 'line1', + ]; + testViewModel(text, {}, (viewModel, model) => { + assert.strictEqual(viewModel.getLineCount(), 1); + + // Insert lines to create content + model.pushEditOperations([], [{ + range: new Range(1, 6, 1, 6), + text: '\nline2\nline3\nline4\nline5' + }], () => ([])); + + assert.strictEqual(viewModel.getLineCount(), 5); + + // Hide lines 1-2 and 4-5, leaving only line 3 visible + viewModel.setHiddenAreas([ + new Range(1, 1, 2, 1), + new Range(4, 1, 5, 1), + ]); + assert.strictEqual(viewModel.getLineCount(), 1); + + // Undo collapses back to 1 line, but hidden area decorations may grow + model.undo(); + + assert.ok(viewModel.getLineCount() >= 1, `expected at least 1 view line but got ${viewModel.getLineCount()}`); + }); + }); + + test('deleting the only visible line between two hidden areas leaves all lines hidden', () => { + const text = [ + 'line1', + 'line2', + 'line3', + 'line4', + 'line5', + 'line6', + 'line7', + 'line8', + ]; + testViewModel(text, {}, (viewModel, model) => { + assert.strictEqual(viewModel.getLineCount(), 8); + + // Hide lines 1-5 and 7-8, leaving only line 6 visible + viewModel.setHiddenAreas([ + new Range(1, 1, 5, 1), + new Range(7, 1, 8, 1), + ]); + assert.strictEqual(viewModel.getLineCount(), 1); + + // Delete lines 6, 7, 8 — the only visible line plus some hidden ones + model.applyEdits([{ + range: new Range(6, 1, 8, 5), + text: null + }]); + + // The view model must still have at least one visible line + assert.ok(viewModel.getLineCount() >= 1, `expected at least 1 view line but got ${viewModel.getLineCount()}`); + }); + }); + + test('multiple visible lines deleted leaving only hidden lines', () => { + const text = [ + 'hidden1', + 'hidden2', + 'visible1', + 'visible2', + 'hidden3', + 'hidden4', + ]; + testViewModel(text, {}, (viewModel, model) => { + viewModel.setHiddenAreas([ + new Range(1, 1, 2, 1), + new Range(5, 1, 6, 1), + ]); + assert.strictEqual(viewModel.getLineCount(), 2); + + // Delete visible lines 3 and 4 + model.applyEdits([{ + range: new Range(2, 8, 5, 1), + text: null + }]); + + assert.ok(viewModel.getLineCount() >= 1, `expected at least 1 view line but got ${viewModel.getLineCount()}`); + }); + }); + + test('hidden areas from multiple sources that overlap produce valid merged result', () => { + const text: string[] = []; + for (let i = 1; i <= 10; i++) { + text.push(`line${i}`); + } + testViewModel(text, {}, (viewModel, model) => { + // Source A hides a large range [1-8]. + // Source B hides small ranges [2-3] and [5-6] that are subsumed by A. + // mergeLineRangeArray has a bug where it advances both pointers after + // merging [1-8]+[2-3]=[1-8], leaving [5-6] and [8,9] as separate entries + // that overlap with or are subsumed by [1-8]. + // normalizeLineRanges in setHiddenAreas cleans this up, so the result + // should still be correct: lines 1-8 hidden, lines 9-10 visible. + viewModel.setHiddenAreas([new Range(1, 1, 8, 1)], 'sourceA'); + viewModel.setHiddenAreas([new Range(2, 1, 3, 1), new Range(5, 1, 6, 1), new Range(8, 1, 9, 1)], 'sourceB'); + + // Lines 1-9 should be hidden (merged from [1-8] and [8-9]), line 10 visible + assert.strictEqual(viewModel.getLineCount(), 1, 'only line 10 should be visible'); + + // The hidden areas returned should be non-overlapping and sorted + const hiddenAreas = viewModel.getHiddenAreas(); + for (let i = 1; i < hiddenAreas.length; i++) { + assert.ok( + hiddenAreas[i].startLineNumber > hiddenAreas[i - 1].endLineNumber, + `hidden areas should not overlap: [${hiddenAreas[i - 1].startLineNumber}-${hiddenAreas[i - 1].endLineNumber}] and [${hiddenAreas[i].startLineNumber}-${hiddenAreas[i].endLineNumber}]` + ); + } + }); + }); + + test('tab size change with drifted hidden area decorations must not leave 0 visible lines', () => { + const text = [ + 'line1', + 'line2', + 'line3', + ]; + testViewModel(text, {}, (viewModel, model) => { + // Hide lines 1-2, leaving only line 3 visible. + viewModel.setHiddenAreas([new Range(1, 1, 2, 1)]); + assert.strictEqual(viewModel.getLineCount(), 1); + + // Insert at (2,1) — the end edge of the hidden area decoration. + // AlwaysGrowsWhenTypingAtEdges causes the decoration to grow from + // [1,1 → 2,1] to [1,1 → 3,1], covering what was the visible line 3. + // After this insert, the file has 4 lines, decoration covers [1-3], line 4 visible. + model.applyEdits([{ range: new Range(2, 1, 2, 1), text: 'x\n' }]); + // Insert again to push decoration further + model.applyEdits([{ range: new Range(3, 1, 3, 1), text: 'y\n' }]); + // Now file has 5 lines, decoration covers [1-4], line 5 visible. + + // Delete lines 4-5 to collapse back, making decoration cover everything + model.applyEdits([{ range: new Range(4, 1, 5, 6), text: '' }]); + // Now file has 4 lines. acceptVersionId ensures viewLines >= 1. + + // Tab size change: triggers _constructLines(resetHiddenAreas=false) + // which re-reads the decoration ranges (which may cover all lines). + model.updateOptions({ tabSize: 8 }); + + assert.ok(viewModel.getLineCount() >= 1, `expected at least 1 view line but got ${viewModel.getLineCount()}`); + }); + }); + }); }); diff --git a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts index a7f934a6247a1..bd7f3e99e5275 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -6,7 +6,19 @@ import assert from 'assert'; import { toUint32 } from '../../../../base/common/uint.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { PrefixSumComputer, PrefixSumIndexOfResult } from '../../../common/model/prefixSumComputer.js'; +import { ConstantTimePrefixSumComputer, PrefixSumComputer, PrefixSumIndexOfResult } from '../../../common/model/prefixSumComputer.js'; + +interface IPrefixSumComputer { + getTotalSum(): number; + /** + * Returns sum of first `count` values: SUM(0 <= j < count, values[j]). + */ + getPrefixSum(count: number): number; + getIndexOf(sum: number): PrefixSumIndexOfResult; + setValue(index: number, value: number): void; + insertValues(insertIndex: number, insertArr: number[]): void; + removeValues(start: number, deleteCount: number): void; +} function toUint32Array(arr: number[]): Uint32Array { const len = arr.length; @@ -17,166 +29,609 @@ function toUint32Array(arr: number[]): Uint32Array { return r; } +function createBoth(values: number[]): IPrefixSumComputer[] { + const psc = new PrefixSumComputer(toUint32Array(values)); + const wrapped: IPrefixSumComputer = { + getTotalSum: () => psc.getTotalSum(), + getPrefixSum: (count: number) => count === 0 ? 0 : psc.getPrefixSum(count - 1), + getIndexOf: (sum: number) => psc.getIndexOf(sum), + setValue: (index: number, value: number) => { psc.setValue(index, value); }, + insertValues: (insertIndex: number, insertArr: number[]) => { psc.insertValues(insertIndex, toUint32Array(insertArr)); }, + removeValues: (start: number, deleteCount: number) => { psc.removeValues(start, deleteCount); }, + }; + const ct = new ConstantTimePrefixSumComputer([...values]); + const wrappedCt: IPrefixSumComputer = { + getTotalSum: () => ct.getTotalSum(), + getPrefixSum: (count: number) => ct.getPrefixSum(count), + getIndexOf: (sum: number) => ct.getIndexOf(sum), + setValue: (index: number, value: number) => { ct.setValue(index, value); }, + insertValues: (insertIndex: number, insertArr: number[]) => { ct.insertValues(insertIndex, insertArr); }, + removeValues: (start: number, deleteCount: number) => { ct.removeValues(start, deleteCount); }, + }; + return [wrapped, wrappedCt]; +} + +function forBoth(values: number[], callback: (psc: IPrefixSumComputer) => void): void { + for (const psc of createBoth(values)) { + callback(psc); + } +} + suite('Editor ViewModel - PrefixSumComputer', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('PrefixSumComputer', () => { - let indexOfResult: PrefixSumIndexOfResult; - - const psc = new PrefixSumComputer(toUint32Array([1, 1, 2, 1, 3])); - assert.strictEqual(psc.getTotalSum(), 8); - assert.strictEqual(psc.getPrefixSum(-1), 0); - assert.strictEqual(psc.getPrefixSum(0), 1); - assert.strictEqual(psc.getPrefixSum(1), 2); - assert.strictEqual(psc.getPrefixSum(2), 4); - assert.strictEqual(psc.getPrefixSum(3), 5); - assert.strictEqual(psc.getPrefixSum(4), 8); - indexOfResult = psc.getIndexOf(0); - assert.strictEqual(indexOfResult.index, 0); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(1); - assert.strictEqual(indexOfResult.index, 1); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(2); - assert.strictEqual(indexOfResult.index, 2); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(3); - assert.strictEqual(indexOfResult.index, 2); - assert.strictEqual(indexOfResult.remainder, 1); - indexOfResult = psc.getIndexOf(4); - assert.strictEqual(indexOfResult.index, 3); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(5); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(6); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 1); - indexOfResult = psc.getIndexOf(7); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 2); - indexOfResult = psc.getIndexOf(8); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 3); - - // [1, 2, 2, 1, 3] - psc.setValue(1, 2); - assert.strictEqual(psc.getTotalSum(), 9); - assert.strictEqual(psc.getPrefixSum(0), 1); - assert.strictEqual(psc.getPrefixSum(1), 3); - assert.strictEqual(psc.getPrefixSum(2), 5); - assert.strictEqual(psc.getPrefixSum(3), 6); - assert.strictEqual(psc.getPrefixSum(4), 9); - - // [1, 0, 2, 1, 3] - psc.setValue(1, 0); - assert.strictEqual(psc.getTotalSum(), 7); - assert.strictEqual(psc.getPrefixSum(0), 1); - assert.strictEqual(psc.getPrefixSum(1), 1); - assert.strictEqual(psc.getPrefixSum(2), 3); - assert.strictEqual(psc.getPrefixSum(3), 4); - assert.strictEqual(psc.getPrefixSum(4), 7); - indexOfResult = psc.getIndexOf(0); - assert.strictEqual(indexOfResult.index, 0); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(1); - assert.strictEqual(indexOfResult.index, 2); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(2); - assert.strictEqual(indexOfResult.index, 2); - assert.strictEqual(indexOfResult.remainder, 1); - indexOfResult = psc.getIndexOf(3); - assert.strictEqual(indexOfResult.index, 3); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(4); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(5); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 1); - indexOfResult = psc.getIndexOf(6); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 2); - indexOfResult = psc.getIndexOf(7); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 3); - - // [1, 0, 0, 1, 3] - psc.setValue(2, 0); - assert.strictEqual(psc.getTotalSum(), 5); - assert.strictEqual(psc.getPrefixSum(0), 1); - assert.strictEqual(psc.getPrefixSum(1), 1); - assert.strictEqual(psc.getPrefixSum(2), 1); - assert.strictEqual(psc.getPrefixSum(3), 2); - assert.strictEqual(psc.getPrefixSum(4), 5); - indexOfResult = psc.getIndexOf(0); - assert.strictEqual(indexOfResult.index, 0); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(1); - assert.strictEqual(indexOfResult.index, 3); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(2); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(3); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 1); - indexOfResult = psc.getIndexOf(4); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 2); - indexOfResult = psc.getIndexOf(5); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 3); - - // [1, 0, 0, 0, 3] - psc.setValue(3, 0); - assert.strictEqual(psc.getTotalSum(), 4); - assert.strictEqual(psc.getPrefixSum(0), 1); - assert.strictEqual(psc.getPrefixSum(1), 1); - assert.strictEqual(psc.getPrefixSum(2), 1); - assert.strictEqual(psc.getPrefixSum(3), 1); - assert.strictEqual(psc.getPrefixSum(4), 4); - indexOfResult = psc.getIndexOf(0); - assert.strictEqual(indexOfResult.index, 0); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(1); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(2); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 1); - indexOfResult = psc.getIndexOf(3); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 2); - indexOfResult = psc.getIndexOf(4); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 3); - - // [1, 1, 0, 1, 1] - psc.setValue(1, 1); - psc.setValue(3, 1); - psc.setValue(4, 1); - assert.strictEqual(psc.getTotalSum(), 4); - assert.strictEqual(psc.getPrefixSum(0), 1); - assert.strictEqual(psc.getPrefixSum(1), 2); - assert.strictEqual(psc.getPrefixSum(2), 2); - assert.strictEqual(psc.getPrefixSum(3), 3); - assert.strictEqual(psc.getPrefixSum(4), 4); - indexOfResult = psc.getIndexOf(0); - assert.strictEqual(indexOfResult.index, 0); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(1); - assert.strictEqual(indexOfResult.index, 1); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(2); - assert.strictEqual(indexOfResult.index, 3); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(3); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 0); - indexOfResult = psc.getIndexOf(4); - assert.strictEqual(indexOfResult.index, 4); - assert.strictEqual(indexOfResult.remainder, 1); + test('comprehensive setValue and getIndexOf', () => { + forBoth([1, 1, 2, 1, 3], psc => { + assert.strictEqual(psc.getTotalSum(), 8); + assert.strictEqual(psc.getPrefixSum(0), 0); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 2); + assert.strictEqual(psc.getPrefixSum(3), 4); + assert.strictEqual(psc.getPrefixSum(4), 5); + assert.strictEqual(psc.getPrefixSum(5), 8); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(2, 0)); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(2, 1)); + assert.deepStrictEqual(psc.getIndexOf(4), new PrefixSumIndexOfResult(3, 0)); + assert.deepStrictEqual(psc.getIndexOf(5), new PrefixSumIndexOfResult(4, 0)); + assert.deepStrictEqual(psc.getIndexOf(6), new PrefixSumIndexOfResult(4, 1)); + assert.deepStrictEqual(psc.getIndexOf(7), new PrefixSumIndexOfResult(4, 2)); + assert.deepStrictEqual(psc.getIndexOf(8), new PrefixSumIndexOfResult(4, 3)); + + // [1, 2, 2, 1, 3] + psc.setValue(1, 2); + assert.strictEqual(psc.getTotalSum(), 9); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 3); + assert.strictEqual(psc.getPrefixSum(3), 5); + assert.strictEqual(psc.getPrefixSum(4), 6); + assert.strictEqual(psc.getPrefixSum(5), 9); + + // [1, 0, 2, 1, 3] + psc.setValue(1, 0); + assert.strictEqual(psc.getTotalSum(), 7); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 1); + assert.strictEqual(psc.getPrefixSum(3), 3); + assert.strictEqual(psc.getPrefixSum(4), 4); + assert.strictEqual(psc.getPrefixSum(5), 7); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(2, 0)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(2, 1)); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(3, 0)); + assert.deepStrictEqual(psc.getIndexOf(4), new PrefixSumIndexOfResult(4, 0)); + assert.deepStrictEqual(psc.getIndexOf(5), new PrefixSumIndexOfResult(4, 1)); + assert.deepStrictEqual(psc.getIndexOf(6), new PrefixSumIndexOfResult(4, 2)); + + // [1, 0, 0, 1, 3] + psc.setValue(2, 0); + assert.strictEqual(psc.getTotalSum(), 5); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 1); + assert.strictEqual(psc.getPrefixSum(3), 1); + assert.strictEqual(psc.getPrefixSum(4), 2); + assert.strictEqual(psc.getPrefixSum(5), 5); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(3, 0)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(4, 0)); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(4, 1)); + assert.deepStrictEqual(psc.getIndexOf(4), new PrefixSumIndexOfResult(4, 2)); + + // [1, 0, 0, 0, 3] + psc.setValue(3, 0); + assert.strictEqual(psc.getTotalSum(), 4); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 1); + assert.strictEqual(psc.getPrefixSum(3), 1); + assert.strictEqual(psc.getPrefixSum(4), 1); + assert.strictEqual(psc.getPrefixSum(5), 4); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(4, 0)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(4, 1)); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(4, 2)); + + // [1, 1, 0, 1, 1] + psc.setValue(1, 1); + psc.setValue(3, 1); + psc.setValue(4, 1); + assert.strictEqual(psc.getTotalSum(), 4); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 2); + assert.strictEqual(psc.getPrefixSum(3), 2); + assert.strictEqual(psc.getPrefixSum(4), 3); + assert.strictEqual(psc.getPrefixSum(5), 4); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(3, 0)); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(4, 0)); + }); + }); + + // --- getTotalSum --- + + test('getTotalSum with typical values', () => { + forBoth([1, 1, 2, 1, 3], psc => assert.strictEqual(psc.getTotalSum(), 8)); + forBoth([10], psc => assert.strictEqual(psc.getTotalSum(), 10)); + forBoth([5, 5, 5], psc => assert.strictEqual(psc.getTotalSum(), 15)); + }); + + test('getTotalSum with all zeroes', () => { + forBoth([0, 0, 0], psc => assert.strictEqual(psc.getTotalSum(), 0)); + forBoth([0], psc => assert.strictEqual(psc.getTotalSum(), 0)); + }); + + test('getTotalSum with empty array', () => { + forBoth([], psc => assert.strictEqual(psc.getTotalSum(), 0)); + }); + + test('getTotalSum with single element', () => { + forBoth([0], psc => assert.strictEqual(psc.getTotalSum(), 0)); + forBoth([1], psc => assert.strictEqual(psc.getTotalSum(), 1)); + forBoth([100], psc => assert.strictEqual(psc.getTotalSum(), 100)); + }); + + // --- getPrefixSum --- + + test('getPrefixSum with typical values', () => { + forBoth([1, 1, 2, 1, 3], psc => { + assert.strictEqual(psc.getPrefixSum(0), 0); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 2); + assert.strictEqual(psc.getPrefixSum(3), 4); + assert.strictEqual(psc.getPrefixSum(4), 5); + assert.strictEqual(psc.getPrefixSum(5), 8); + }); + }); + + test('getPrefixSum with all zeroes', () => { + forBoth([0, 0, 0], psc => { + assert.strictEqual(psc.getPrefixSum(0), 0); + assert.strictEqual(psc.getPrefixSum(1), 0); + assert.strictEqual(psc.getPrefixSum(2), 0); + assert.strictEqual(psc.getPrefixSum(3), 0); + }); + }); + + test('getPrefixSum with single element', () => { + forBoth([7], psc => { + assert.strictEqual(psc.getPrefixSum(0), 0); + assert.strictEqual(psc.getPrefixSum(1), 7); + }); + }); + + test('getPrefixSum with empty array', () => { + forBoth([], psc => { + assert.strictEqual(psc.getPrefixSum(0), 0); + }); + }); + + test('getPrefixSum with leading/trailing zeroes', () => { + forBoth([0, 0, 3, 0, 0], psc => { + assert.strictEqual(psc.getPrefixSum(0), 0); + assert.strictEqual(psc.getPrefixSum(1), 0); + assert.strictEqual(psc.getPrefixSum(2), 0); + assert.strictEqual(psc.getPrefixSum(3), 3); + assert.strictEqual(psc.getPrefixSum(4), 3); + assert.strictEqual(psc.getPrefixSum(5), 3); + }); + }); + + // --- getIndexOf --- + + test('getIndexOf with typical values', () => { + forBoth([1, 1, 2, 1, 3], psc => { + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(2, 0)); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(2, 1)); + assert.deepStrictEqual(psc.getIndexOf(4), new PrefixSumIndexOfResult(3, 0)); + assert.deepStrictEqual(psc.getIndexOf(5), new PrefixSumIndexOfResult(4, 0)); + assert.deepStrictEqual(psc.getIndexOf(6), new PrefixSumIndexOfResult(4, 1)); + assert.deepStrictEqual(psc.getIndexOf(7), new PrefixSumIndexOfResult(4, 2)); + assert.deepStrictEqual(psc.getIndexOf(8), new PrefixSumIndexOfResult(4, 3)); + }); + }); + + test('getIndexOf with all zeroes', () => { + forBoth([0, 0, 0], psc => { + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(2, 0)); + }); + }); + + test('getIndexOf with single zero', () => { + forBoth([0], psc => { + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + }); + }); + + test('getIndexOf with single element', () => { + forBoth([5], psc => { + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(0, 1)); + assert.deepStrictEqual(psc.getIndexOf(4), new PrefixSumIndexOfResult(0, 4)); + }); + }); + + test('getIndexOf with leading zeroes', () => { + forBoth([0, 0, 3], psc => { + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(2, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(2, 1)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(2, 2)); + }); + }); + + test('getIndexOf with trailing zeroes', () => { + forBoth([3, 0, 0], psc => { + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(0, 1)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(0, 2)); + }); + }); + + test('getIndexOf with interleaved zeroes', () => { + forBoth([0, 1, 0, 2, 0], psc => { + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(3, 0)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(3, 1)); + }); + }); + + test('getIndexOf with all ones', () => { + forBoth([1, 1, 1, 1, 1], psc => { + for (let i = 0; i < 5; i++) { + assert.deepStrictEqual(psc.getIndexOf(i), new PrefixSumIndexOfResult(i, 0)); + } + }); + }); + + test('getIndexOf with large value in single element', () => { + forBoth([1000], psc => { + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(500), new PrefixSumIndexOfResult(0, 500)); + assert.deepStrictEqual(psc.getIndexOf(999), new PrefixSumIndexOfResult(0, 999)); + }); + }); + + // --- setValue --- + + test('setValue no-op when value unchanged', () => { + forBoth([1, 2, 3], psc => { + assert.strictEqual(psc.getTotalSum(), 6); + psc.setValue(1, 2); + assert.strictEqual(psc.getTotalSum(), 6); + }); + }); + + test('setValue increase', () => { + forBoth([1, 2, 3], psc => { + psc.setValue(1, 5); + assert.strictEqual(psc.getTotalSum(), 9); + assert.strictEqual(psc.getPrefixSum(2), 6); + assert.strictEqual(psc.getPrefixSum(3), 9); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(5), new PrefixSumIndexOfResult(1, 4)); + assert.deepStrictEqual(psc.getIndexOf(6), new PrefixSumIndexOfResult(2, 0)); + }); + }); + + test('setValue decrease', () => { + forBoth([1, 5, 3], psc => { + psc.setValue(1, 2); + assert.strictEqual(psc.getTotalSum(), 6); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(1, 1)); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(2, 0)); + }); + }); + + test('setValue to zero', () => { + forBoth([1, 2, 3], psc => { + psc.setValue(1, 0); + assert.strictEqual(psc.getTotalSum(), 4); + assert.strictEqual(psc.getPrefixSum(2), 1); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(2, 0)); + }); + }); + + test('setValue from zero', () => { + forBoth([0, 0, 0], psc => { + psc.setValue(1, 3); + assert.strictEqual(psc.getTotalSum(), 3); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(1, 2)); + }); + }); + + test('setValue on first element', () => { + forBoth([1, 2, 3], psc => { + psc.setValue(0, 10); + assert.strictEqual(psc.getTotalSum(), 15); + assert.strictEqual(psc.getPrefixSum(1), 10); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(9), new PrefixSumIndexOfResult(0, 9)); + assert.deepStrictEqual(psc.getIndexOf(10), new PrefixSumIndexOfResult(1, 0)); + }); + }); + + test('setValue on last element', () => { + forBoth([1, 2, 3], psc => { + psc.setValue(2, 10); + assert.strictEqual(psc.getTotalSum(), 13); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(2, 0)); + assert.deepStrictEqual(psc.getIndexOf(12), new PrefixSumIndexOfResult(2, 9)); + }); + }); + + test('set all values to zero then restore', () => { + forBoth([1, 2, 3], psc => { + psc.setValue(0, 0); + psc.setValue(1, 0); + psc.setValue(2, 0); + assert.strictEqual(psc.getTotalSum(), 0); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(2, 0)); + + psc.setValue(0, 4); + assert.strictEqual(psc.getTotalSum(), 4); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(0, 3)); + }); + }); + + test('setValue multiple times on same index', () => { + forBoth([1, 1, 1], psc => { + psc.setValue(1, 5); + psc.setValue(1, 2); + psc.setValue(1, 10); + assert.strictEqual(psc.getTotalSum(), 12); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(10), new PrefixSumIndexOfResult(1, 9)); + assert.deepStrictEqual(psc.getIndexOf(11), new PrefixSumIndexOfResult(2, 0)); + }); + }); + + // --- insertValues --- + + test('insertValues at beginning', () => { + forBoth([3, 4], psc => { + psc.insertValues(0, [1, 2]); + assert.strictEqual(psc.getTotalSum(), 10); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 3); + assert.strictEqual(psc.getPrefixSum(3), 6); + assert.strictEqual(psc.getPrefixSum(4), 10); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(2, 0)); + }); + }); + + test('insertValues at end', () => { + forBoth([1, 2], psc => { + psc.insertValues(2, [3, 4]); + assert.strictEqual(psc.getTotalSum(), 10); + assert.strictEqual(psc.getPrefixSum(3), 6); + assert.strictEqual(psc.getPrefixSum(4), 10); + }); + }); + + test('insertValues in the middle', () => { + forBoth([1, 4], psc => { + psc.insertValues(1, [2, 3]); + assert.strictEqual(psc.getTotalSum(), 10); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 3); + assert.strictEqual(psc.getPrefixSum(3), 6); + assert.strictEqual(psc.getPrefixSum(4), 10); + }); + }); + + test('insertValues with zeroes', () => { + forBoth([1, 2], psc => { + psc.insertValues(1, [0, 0]); + assert.strictEqual(psc.getTotalSum(), 3); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 1); + assert.strictEqual(psc.getPrefixSum(3), 1); + assert.strictEqual(psc.getPrefixSum(4), 3); + }); + }); + + test('insertValues into all-zeroes', () => { + forBoth([0, 0, 0], psc => { + psc.insertValues(1, [2, 3]); + assert.strictEqual(psc.getTotalSum(), 5); + assert.strictEqual(psc.getPrefixSum(1), 0); + assert.strictEqual(psc.getPrefixSum(2), 2); + assert.strictEqual(psc.getPrefixSum(3), 5); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(2, 0)); + assert.deepStrictEqual(psc.getIndexOf(4), new PrefixSumIndexOfResult(2, 2)); + }); + }); + + test('insertValues into empty computer', () => { + forBoth([], psc => { + psc.insertValues(0, [5, 3]); + assert.strictEqual(psc.getTotalSum(), 8); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(4), new PrefixSumIndexOfResult(0, 4)); + assert.deepStrictEqual(psc.getIndexOf(5), new PrefixSumIndexOfResult(1, 0)); + }); + }); + + // --- removeValues --- + + test('removeValues from beginning', () => { + forBoth([1, 2, 3, 4], psc => { + psc.removeValues(0, 2); + assert.strictEqual(psc.getTotalSum(), 7); + assert.strictEqual(psc.getPrefixSum(1), 3); + assert.strictEqual(psc.getPrefixSum(2), 7); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(1, 0)); + }); + }); + + test('removeValues from end', () => { + forBoth([1, 2, 3, 4], psc => { + psc.removeValues(2, 2); + assert.strictEqual(psc.getTotalSum(), 3); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 3); + }); + }); + + test('removeValues from the middle', () => { + forBoth([1, 2, 3, 4], psc => { + psc.removeValues(1, 2); + assert.strictEqual(psc.getTotalSum(), 5); + assert.strictEqual(psc.getPrefixSum(1), 1); + assert.strictEqual(psc.getPrefixSum(2), 5); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(4), new PrefixSumIndexOfResult(1, 3)); + }); + }); + + test('removeValues all', () => { + forBoth([1, 2, 3], psc => { + psc.removeValues(0, 3); + assert.strictEqual(psc.getTotalSum(), 0); + }); + }); + + test('removeValues single element', () => { + forBoth([5, 10, 15], psc => { + psc.removeValues(1, 1); + assert.strictEqual(psc.getTotalSum(), 20); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(5), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(19), new PrefixSumIndexOfResult(1, 14)); + }); + }); + + test('removeValues zero-valued elements', () => { + forBoth([0, 0, 5, 0, 0], psc => { + psc.removeValues(0, 2); + assert.strictEqual(psc.getTotalSum(), 5); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(4), new PrefixSumIndexOfResult(0, 4)); + }); + }); + + // --- combined operations --- + + test('insert then remove', () => { + forBoth([1, 2, 3], psc => { + psc.insertValues(1, [10, 20]); + assert.strictEqual(psc.getTotalSum(), 36); + psc.removeValues(1, 2); + assert.strictEqual(psc.getTotalSum(), 6); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(3), new PrefixSumIndexOfResult(2, 0)); + }); + }); + + test('remove then insert at same position', () => { + forBoth([1, 2, 3], psc => { + psc.removeValues(1, 1); + psc.insertValues(1, [5]); + assert.strictEqual(psc.getTotalSum(), 9); + assert.deepStrictEqual(psc.getIndexOf(1), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(5), new PrefixSumIndexOfResult(1, 4)); + assert.deepStrictEqual(psc.getIndexOf(6), new PrefixSumIndexOfResult(2, 0)); + }); + }); + + test('setValue then insert then remove', () => { + forBoth([1, 1, 1], psc => { + psc.setValue(0, 5); + psc.insertValues(1, [10]); + psc.removeValues(3, 1); + // [5, 10, 1] + assert.strictEqual(psc.getTotalSum(), 16); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(4), new PrefixSumIndexOfResult(0, 4)); + assert.deepStrictEqual(psc.getIndexOf(5), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(14), new PrefixSumIndexOfResult(1, 9)); + assert.deepStrictEqual(psc.getIndexOf(15), new PrefixSumIndexOfResult(2, 0)); + }); + }); + + test('multiple queries between mutations are consistent', () => { + forBoth([2, 3, 5], psc => { + assert.strictEqual(psc.getTotalSum(), 10); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + + psc.setValue(1, 0); + assert.strictEqual(psc.getTotalSum(), 7); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(2, 0)); + + psc.setValue(1, 3); + assert.strictEqual(psc.getTotalSum(), 10); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(1, 0)); + }); + }); + + // --- edge cases --- + + test('large values', () => { + forBoth([100, 200, 300], psc => { + assert.strictEqual(psc.getTotalSum(), 600); + assert.strictEqual(psc.getPrefixSum(1), 100); + assert.strictEqual(psc.getPrefixSum(2), 300); + assert.strictEqual(psc.getPrefixSum(3), 600); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(0, 0)); + assert.deepStrictEqual(psc.getIndexOf(99), new PrefixSumIndexOfResult(0, 99)); + assert.deepStrictEqual(psc.getIndexOf(100), new PrefixSumIndexOfResult(1, 0)); + assert.deepStrictEqual(psc.getIndexOf(299), new PrefixSumIndexOfResult(1, 199)); + assert.deepStrictEqual(psc.getIndexOf(300), new PrefixSumIndexOfResult(2, 0)); + assert.deepStrictEqual(psc.getIndexOf(599), new PrefixSumIndexOfResult(2, 299)); + }); + }); + + test('many elements', () => { + forBoth(new Array(100).fill(1), psc => { + assert.strictEqual(psc.getTotalSum(), 100); + assert.strictEqual(psc.getPrefixSum(50), 50); + + for (let i = 0; i < 100; i++) { + assert.deepStrictEqual(psc.getIndexOf(i), new PrefixSumIndexOfResult(i, 0)); + } + }); + }); + + test('many elements all zeroes', () => { + forBoth(new Array(100).fill(0), psc => { + assert.strictEqual(psc.getTotalSum(), 0); + for (let i = 0; i <= 100; i++) { + assert.strictEqual(psc.getPrefixSum(i), 0); + } + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(99, 0)); + }); + }); + + test('setValue between queries re-validates correctly', () => { + forBoth([1, 1, 1, 1, 1], psc => { + assert.strictEqual(psc.getTotalSum(), 5); + + psc.setValue(2, 10); + assert.strictEqual(psc.getTotalSum(), 14); + assert.strictEqual(psc.getPrefixSum(3), 12); + assert.deepStrictEqual(psc.getIndexOf(2), new PrefixSumIndexOfResult(2, 0)); + assert.deepStrictEqual(psc.getIndexOf(11), new PrefixSumIndexOfResult(2, 9)); + assert.deepStrictEqual(psc.getIndexOf(12), new PrefixSumIndexOfResult(3, 0)); + assert.deepStrictEqual(psc.getIndexOf(13), new PrefixSumIndexOfResult(4, 0)); + + psc.setValue(0, 0); + assert.strictEqual(psc.getTotalSum(), 13); + assert.deepStrictEqual(psc.getIndexOf(0), new PrefixSumIndexOfResult(1, 0)); + }); }); }); diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 431e7e953d200..ccb09bd0b06f1 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -53,6 +53,7 @@ import { IGitService } from '../../../../workbench/contrib/git/common/gitService import { IsolationModePicker, SessionTargetPicker } from './sessionTargetPicker.js'; import { BranchPicker } from './branchPicker.js'; import { INewSession } from './newSession.js'; +import { getErrorMessage } from '../../../../base/common/errors.js'; // #region --- Chat Welcome Widget --- @@ -257,7 +258,8 @@ class NewChatWidget extends Disposable { this.gitService.openRepository(folderUri).then(repository => { this._isolationModePicker.setRepository(repository); this._branchPicker.setRepository(repository); - }).catch(() => { + }).catch(e => { + this.logService.warn(`Failed to open repository at ${folderUri.toString()}`, getErrorMessage(e)); this._isolationModePicker.setRepository(undefined); this._branchPicker.setRepository(undefined); }); diff --git a/src/vs/workbench/api/common/extHostGitExtensionService.ts b/src/vs/workbench/api/common/extHostGitExtensionService.ts index 8029798f00f0d..61a64e83bf375 100644 --- a/src/vs/workbench/api/common/extHostGitExtensionService.ts +++ b/src/vs/workbench/api/common/extHostGitExtensionService.ts @@ -6,7 +6,6 @@ import type * as vscode from 'vscode'; import { Event } from '../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; -import { observableFromEvent, waitForState } from '../../../base/common/observable.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; @@ -50,6 +49,7 @@ interface Repository { readonly rootUri: vscode.Uri; readonly state: RepositoryState; + status(): Promise; getRefs(query: GitRefQuery, token?: vscode.CancellationToken): Promise; } @@ -154,12 +154,15 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi }; } - // Ensure that the repository state is initialized - const repositoryStateObs = observableFromEvent(this, - repository.state.onDidChange, () => repository.state); - await waitForState(repositoryStateObs, state => !!state.HEAD); - - const repositoryState = repositoryStateObs.get(); + let repositoryState = repository.state; + if (repositoryState.HEAD === undefined) { + // Opening the repository does not wait for the repository state to be + // initialized so we need to wait for the first change event to ensure + // that the repository state is fully loaded before we return it to the + // main thread. + await Event.toPromise(repositoryState.onDidChange, this._disposables); + repositoryState = repository.state; + } // Store the repository and its handle in the maps const handle = ExtHostGitExtensionService._handlePool++; @@ -176,8 +179,8 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi handle, rootUri: repository.rootUri, state: { - HEAD: repositoryState.HEAD - ? toGitBranchDto(repositoryState.HEAD) + HEAD: repository.state.HEAD + ? toGitBranchDto(repository.state.HEAD) : undefined } }; diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 961340c6548c7..cedb8c9537371 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -107,6 +107,7 @@ export const NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID = 'workbench.action.newEmptyEdit export const CLOSE_MODAL_EDITOR_COMMAND_ID = 'workbench.action.closeModalEditor'; export const MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID = 'workbench.action.moveModalEditorToMain'; +export const MOVE_MODAL_EDITOR_TO_WINDOW_COMMAND_ID = 'workbench.action.moveModalEditorToWindow'; export const TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID = 'workbench.action.toggleModalEditorMaximized'; export const NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID = 'workbench.action.navigateModalEditorPrevious'; export const NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID = 'workbench.action.navigateModalEditorNext'; @@ -1439,6 +1440,42 @@ function registerModalEditorCommands(): void { } }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: MOVE_MODAL_EDITOR_TO_WINDOW_COMMAND_ID, + title: localize2('moveModalEditorToWindow', 'Open Modal Editor in New Window'), + category: Categories.View, + f1: true, + icon: Codicon.emptyWindow, + precondition: EditorPartModalContext, + menu: { + id: MenuId.ModalEditorTitle, + group: 'navigation', + order: 0, + when: IsSessionsWindowContext + } + }); + } + async run(accessor: ServicesAccessor): Promise { + const editorGroupsService = accessor.get(IEditorGroupsService); + + for (const part of editorGroupsService.parts) { + if (isModalEditorPart(part)) { + const auxiliaryEditorPart = await editorGroupsService.createAuxiliaryEditorPart(); + + for (const group of part.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { + group.moveEditors(group.editors.map(editor => ({ editor, options: { preserveFocus: true } })), auxiliaryEditorPart.activeGroup); + } + + auxiliaryEditorPart.activeGroup.focus(); + part.close(); + break; + } + } + } + }); + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index 1f6e93ba311e1..d75fcd17e5a17 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -32,7 +32,7 @@ import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser import { mainWindow } from '../../../../base/browser/window.js'; import { localize } from '../../../../nls.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID, NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID, TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID } from './editorCommands.js'; +import { CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, MOVE_MODAL_EDITOR_TO_WINDOW_COMMAND_ID, NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID, NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID, TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID } from './editorCommands.js'; import { IModalEditorNavigation, IModalEditorPartOptions } from '../../../../platform/editor/common/editor.js'; const defaultModalEditorAllowableCommands = new Set([ @@ -44,6 +44,7 @@ const defaultModalEditorAllowableCommands = new Set([ 'workbench.action.files.saveAll', CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, + MOVE_MODAL_EDITOR_TO_WINDOW_COMMAND_ID, TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID, NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID, NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID, diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index b1e5fdc159459..384baff52d4e3 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -301,7 +301,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo } private async showAgentSessionContextMenu(session: IAgentSession, anchor: HTMLElement | IMouseEvent): Promise { - await this.chatSessionsService.activateChatSessionItemProvider(session.providerType); + this.chatSessionsService.activateChatSessionItemProvider(session.providerType); const contextOverlay: Array<[string, boolean | string]> = []; contextOverlay.push([ChatContextKeys.isArchivedAgentSession.key, session.isArchived()]); diff --git a/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts b/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts index fe6a33802b2fe..865cb028552ff 100644 --- a/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts +++ b/src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts @@ -583,5 +583,35 @@ suite('Modal Editor Group', () => { }); }); + test('modal editor part editors can be moved to another group', async () => { + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); + const parts = await createEditorParts(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, parts); + + // Create modal and open editors + const modalPart = await parts.createModalEditorPart(); + const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/baz'), TEST_EDITOR_INPUT_ID); + await modalPart.activeGroup.openEditor(input1, { pinned: true }); + await modalPart.activeGroup.openEditor(input2, { pinned: true }); + + assert.strictEqual(modalPart.activeGroup.count, 2); + + // Move editors from modal to main part group + const targetGroup = parts.mainPart.activeGroup; + for (const group of modalPart.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { + group.moveEditors(group.editors.map(editor => ({ editor, options: { preserveFocus: true } })), targetGroup); + } + + // Editors should be in the target group now + assert.strictEqual(targetGroup.count, 2); + assert.strictEqual(modalPart.activeGroup.count, 0); + + // Close modal + modalPart.close(); + assert.strictEqual(parts.activeModalEditorPart, undefined); + }); + ensureNoDisposablesAreLeakedInTestSuite(); });