Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 123 additions & 3 deletions src/vs/editor/common/cursor/cursorMoveCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,9 @@ export class CursorMoveCommands {
if (unit === CursorMove.Unit.WrappedLine) {
// Move up by view lines
return this._moveUpByViewLines(viewModel, cursors, inSelectionMode, value);
} else if (unit === CursorMove.Unit.FoldedLine) {
// Move up by model lines, skipping over folded regions
return this._moveUpByFoldedLines(viewModel, cursors, inSelectionMode, value);
} else {
// Move up by model lines
return this._moveUpByModelLines(viewModel, cursors, inSelectionMode, value);
Expand All @@ -303,6 +306,9 @@ export class CursorMoveCommands {
if (unit === CursorMove.Unit.WrappedLine) {
// Move down by view lines
return this._moveDownByViewLines(viewModel, cursors, inSelectionMode, value);
} else if (unit === CursorMove.Unit.FoldedLine) {
// Move down by model lines, skipping over folded regions
return this._moveDownByFoldedLines(viewModel, cursors, inSelectionMode, value);
} else {
// Move down by model lines
return this._moveDownByModelLines(viewModel, cursors, inSelectionMode, value);
Expand Down Expand Up @@ -515,6 +521,113 @@ export class CursorMoveCommands {
return result;
}

private static _moveDownByFoldedLines(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, count: number): PartialCursorState[] {
const model = viewModel.model;
const lineCount = model.getLineCount();
const hiddenAreas = viewModel.getHiddenAreas();

return cursors.map(cursor => {
const startLine = cursor.modelState.hasSelection() && !inSelectionMode
? cursor.modelState.selection.endLineNumber
: cursor.modelState.position.lineNumber;

const targetLine = CursorMoveCommands._targetFoldedDown(startLine, count, hiddenAreas, lineCount);
const delta = targetLine - startLine;
if (delta === 0) {
return CursorState.fromModelState(cursor.modelState);
}
return CursorState.fromModelState(MoveOperations.moveDown(viewModel.cursorConfig, model, cursor.modelState, inSelectionMode, delta));
});
}

private static _moveUpByFoldedLines(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, count: number): PartialCursorState[] {
const model = viewModel.model;
const hiddenAreas = viewModel.getHiddenAreas();

return cursors.map(cursor => {
const startLine = cursor.modelState.hasSelection() && !inSelectionMode
? cursor.modelState.selection.startLineNumber
: cursor.modelState.position.lineNumber;

const targetLine = CursorMoveCommands._targetFoldedUp(startLine, count, hiddenAreas);
const delta = startLine - targetLine;
if (delta === 0) {
return CursorState.fromModelState(cursor.modelState);
}
return CursorState.fromModelState(MoveOperations.moveUp(viewModel.cursorConfig, model, cursor.modelState, inSelectionMode, delta));
});
}

// Compute the target line after moving `count` steps downward from `startLine`,
// treating each folded region as a single step.
private static _targetFoldedDown(startLine: number, count: number, hiddenAreas: Range[], lineCount: number): number {
let line = startLine;
let i = 0;

while (i < hiddenAreas.length && hiddenAreas[i].endLineNumber < line + 1) {
i++;
}

for (let step = 0; step < count; step++) {
if (line >= lineCount) {
return lineCount;
}

let candidate = line + 1;
while (i < hiddenAreas.length && hiddenAreas[i].endLineNumber < candidate) {
i++;
}

if (i < hiddenAreas.length && hiddenAreas[i].startLineNumber <= candidate) {
candidate = hiddenAreas[i].endLineNumber + 1;
}

if (candidate > lineCount) {
// The next visible line does not exist (e.g. a fold reaches EOF).
return line;
}

line = candidate;
}

return line;
}

// Compute the target line after moving `count` steps upward from `startLine`,
// treating each folded region as a single step.
private static _targetFoldedUp(startLine: number, count: number, hiddenAreas: Range[]): number {
let line = startLine;
let i = hiddenAreas.length - 1;

while (i >= 0 && hiddenAreas[i].startLineNumber > line - 1) {
i--;
}

for (let step = 0; step < count; step++) {
if (line <= 1) {
return 1;
}

let candidate = line - 1;
while (i >= 0 && hiddenAreas[i].startLineNumber > candidate) {
i--;
}

if (i >= 0 && hiddenAreas[i].endLineNumber >= candidate) {
candidate = hiddenAreas[i].startLineNumber - 1;
}

if (candidate < 1) {
// The previous visible line does not exist (e.g. a fold reaches BOF).
return line;
}

line = candidate;
}

return line;
}

private static _moveToViewPosition(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean, toViewLineNumber: number, toViewColumn: number): PartialCursorState {
return CursorState.fromViewState(cursor.viewState.move(inSelectionMode, toViewLineNumber, toViewColumn, 0));
}
Expand Down Expand Up @@ -626,8 +739,10 @@ export namespace CursorMove {
\`\`\`
* 'by': Unit to move. Default is computed based on 'to' value.
\`\`\`
'line', 'wrappedLine', 'character', 'halfLine'
'line', 'wrappedLine', 'character', 'halfLine', 'foldedLine'
\`\`\`
Use 'foldedLine' with 'up'/'down' to move by logical lines while treating each
folded region as a single step.
* 'value': Number of units to move. Default is '1'.
* 'select': If 'true' makes the selection. Default is 'false'.
* 'noHistory': If 'true' does not add the movement to navigation history. Default is 'false'.
Expand All @@ -643,7 +758,7 @@ export namespace CursorMove {
},
'by': {
'type': 'string',
'enum': ['line', 'wrappedLine', 'character', 'halfLine']
'enum': ['line', 'wrappedLine', 'character', 'halfLine', 'foldedLine']
},
'value': {
'type': 'number',
Expand Down Expand Up @@ -695,7 +810,8 @@ export namespace CursorMove {
Line: 'line',
WrappedLine: 'wrappedLine',
Character: 'character',
HalfLine: 'halfLine'
HalfLine: 'halfLine',
FoldedLine: 'foldedLine'
};

/**
Expand Down Expand Up @@ -781,6 +897,9 @@ export namespace CursorMove {
case RawUnit.HalfLine:
unit = Unit.HalfLine;
break;
case RawUnit.FoldedLine:
unit = Unit.FoldedLine;
break;
}

return {
Expand Down Expand Up @@ -855,6 +974,7 @@ export namespace CursorMove {
WrappedLine,
Character,
HalfLine,
FoldedLine,
}

}
2 changes: 1 addition & 1 deletion src/vs/editor/contrib/find/browser/findDecorations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class FindDecorations implements IDisposable {
const candidates = this._editor.getModel().getDecorationsInRange(desiredRange);
for (const candidate of candidates) {
const candidateOpts = candidate.options;
if (candidateOpts === FindDecorations._FIND_MATCH_DECORATION || candidateOpts === FindDecorations._CURRENT_FIND_MATCH_DECORATION) {
if (candidateOpts === FindDecorations._FIND_MATCH_DECORATION || candidateOpts === FindDecorations._FIND_MATCH_NO_OVERVIEW_DECORATION || candidateOpts === FindDecorations._CURRENT_FIND_MATCH_DECORATION) {
return this._getDecorationIndex(candidate.id);
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/vs/editor/contrib/find/test/browser/findModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2383,4 +2383,23 @@ suite('FindModel', () => {

});

test('issue #288515: Wrong current index in find widget if matches > 1000', () => {
// Create 1001 lines of 'hello'
const textArr = Array(1001).fill('hello');
withTestCodeEditor(textArr, {}, (_editor) => {
const editor = _editor as IActiveCodeEditor;

// Place cursor at line 900, selecting 'hello'
editor.setSelection(new Selection(900, 1, 900, 6));

const findState = disposables.add(new FindReplaceState());
findState.change({ searchString: 'hello' }, false);
disposables.add(new FindModelBoundToEditorModel(editor, findState));

assert.strictEqual(findState.matchesCount, 1001);
// With cursor selecting 'hello' at line 900, matchesPosition should be 900
assert.strictEqual(findState.matchesPosition, 900);
});
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { RunOnceScheduler } from '../../../../base/common/async.js';
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
import * as errors from '../../../../base/common/errors.js';
import { Disposable, IDisposable, dispose } from '../../../../base/common/lifecycle.js';
import { Disposable, DisposableStore, dispose } from '../../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../../base/common/map.js';
import { StopWatch } from '../../../../base/common/stopwatch.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
Expand All @@ -27,6 +27,7 @@ import { SEMANTIC_HIGHLIGHTING_SETTING_ID, isSemanticColoringEnabled } from '../
export class DocumentSemanticTokensFeature extends Disposable {

private readonly _watchers = new ResourceMap<ModelSemanticColoring>();
private readonly _providerChangeListeners = this._register(new DisposableStore());

constructor(
@ISemanticTokensStylingService semanticTokensStylingService: ISemanticTokensStylingService,
Expand All @@ -38,6 +39,8 @@ export class DocumentSemanticTokensFeature extends Disposable {
) {
super();

const provider = languageFeaturesService.documentSemanticTokensProvider;

const register = (model: ITextModel) => {
this._watchers.get(model.uri)?.dispose();
this._watchers.set(model.uri, new ModelSemanticColoring(model, semanticTokensStylingService, themeService, languageFeatureDebounceService, languageFeaturesService));
Expand All @@ -60,6 +63,20 @@ export class DocumentSemanticTokensFeature extends Disposable {
}
}
};

const bindProviderChangeListeners = () => {
this._providerChangeListeners.clear();
for (const p of provider.allNoModel()) {
if (typeof p.onDidChange === 'function') {
this._providerChangeListeners.add(p.onDidChange(() => {
for (const watcher of this._watchers.values()) {
watcher.handleProviderDidChange(p);
}
}));
}
}
};

modelService.getModels().forEach(model => {
if (isSemanticColoringEnabled(model, themeService, configurationService)) {
register(model);
Expand All @@ -82,6 +99,13 @@ export class DocumentSemanticTokensFeature extends Disposable {
}
}));
this._register(themeService.onDidColorThemeChange(handleSettingOrThemeChange));
bindProviderChangeListeners();
this._register(provider.onDidChange(() => {
bindProviderChangeListeners();
for (const watcher of this._watchers.values()) {
watcher.handleRegistryChange();
}
}));
}

override dispose(): void {
Expand All @@ -104,7 +128,7 @@ class ModelSemanticColoring extends Disposable {
private readonly _fetchDocumentSemanticTokens: RunOnceScheduler;
private _currentDocumentResponse: SemanticTokensResponse | null;
private _currentDocumentRequestCancellationTokenSource: CancellationTokenSource | null;
private _documentProvidersChangeListeners: IDisposable[];
private _relevantProviders = new Set<DocumentSemanticTokensProvider>();
private _providersChangedDuringRequest: boolean;

constructor(
Expand All @@ -123,8 +147,8 @@ class ModelSemanticColoring extends Disposable {
this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), ModelSemanticColoring.REQUEST_MIN_DELAY));
this._currentDocumentResponse = null;
this._currentDocumentRequestCancellationTokenSource = null;
this._documentProvidersChangeListeners = [];
this._providersChangedDuringRequest = false;
this._updateRelevantProviders();

this._register(this._model.onDidChangeContent(() => {
if (!this._fetchDocumentSemanticTokens.isScheduled()) {
Expand All @@ -147,31 +171,10 @@ class ModelSemanticColoring extends Disposable {
this._currentDocumentRequestCancellationTokenSource = null;
}
this._setDocumentSemanticTokens(null, null, null, []);
this._updateRelevantProviders();
this._fetchDocumentSemanticTokens.schedule(0);
}));

const bindDocumentChangeListeners = () => {
dispose(this._documentProvidersChangeListeners);
this._documentProvidersChangeListeners = [];
for (const provider of this._provider.all(model)) {
if (typeof provider.onDidChange === 'function') {
this._documentProvidersChangeListeners.push(provider.onDidChange(() => {
if (this._currentDocumentRequestCancellationTokenSource) {
// there is already a request running,
this._providersChangedDuringRequest = true;
return;
}
this._fetchDocumentSemanticTokens.schedule(0);
}));
}
}
};
bindDocumentChangeListeners();
this._register(this._provider.onDidChange(() => {
bindDocumentChangeListeners();
this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));
}));

this._register(themeService.onDidColorThemeChange(_ => {
// clear out existing tokens
this._setDocumentSemanticTokens(null, null, null, []);
Expand All @@ -181,6 +184,27 @@ class ModelSemanticColoring extends Disposable {
this._fetchDocumentSemanticTokens.schedule(0);
}

public handleRegistryChange(): void {
this._updateRelevantProviders();
this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model));
}

public handleProviderDidChange(provider: DocumentSemanticTokensProvider): void {
if (!this._relevantProviders.has(provider)) {
return;
}
if (this._currentDocumentRequestCancellationTokenSource) {
// there is already a request running,
this._providersChangedDuringRequest = true;
return;
}
this._fetchDocumentSemanticTokens.schedule(0);
}

private _updateRelevantProviders(): void {
this._relevantProviders = new Set(this._provider.all(this._model));
}

public override dispose(): void {
if (this._currentDocumentResponse) {
this._currentDocumentResponse.dispose();
Expand All @@ -190,8 +214,6 @@ class ModelSemanticColoring extends Disposable {
this._currentDocumentRequestCancellationTokenSource.cancel();
this._currentDocumentRequestCancellationTokenSource = null;
}
dispose(this._documentProvidersChangeListeners);
this._documentProvidersChangeListeners = [];
this._setDocumentSemanticTokens(null, null, null, []);
this._isDisposed = true;

Expand Down
Loading
Loading