Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Debt - Hook up a generic textual document highlight provider for single and multi file settings #224884

Merged
merged 3 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/core/wordHelper';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { DocumentHighlight, DocumentHighlightKind, MultiDocumentHighlightProvider, ProviderResult } from 'vs/editor/common/languages';
import { DocumentHighlight, DocumentHighlightKind, DocumentHighlightProvider, MultiDocumentHighlightProvider, ProviderResult } from 'vs/editor/common/languages';
import { ITextModel } from 'vs/editor/common/model';
import { Position } from 'vs/editor/common/core/position';
import { CancellationToken } from 'vs/base/common/cancellation';
Expand All @@ -14,10 +14,33 @@ import { ResourceMap } from 'vs/base/common/map';
import { LanguageFilter } from 'vs/editor/common/languageSelector';


class TextualDocumentHighlightProvider implements MultiDocumentHighlightProvider {
class TextualDocumentHighlightProvider implements DocumentHighlightProvider, MultiDocumentHighlightProvider {

selector: LanguageFilter = { language: '*' };

provideDocumentHighlights(model: ITextModel, position: Position, token: CancellationToken): ProviderResult<DocumentHighlight[]> {
const result: DocumentHighlight[] = [];

const word = model.getWordAtPosition({
lineNumber: position.lineNumber,
column: position.column
});

if (!word) {
return Promise.resolve(result);
}

if (model.isDisposed()) {
return;
}

const matches = model.findMatches(word.word, true, false, true, USUAL_WORD_SEPARATORS, false);
return matches.map(m => ({
range: m.range,
kind: DocumentHighlightKind.Text
}));
}

provideMultiDocumentHighlights(primaryModel: ITextModel, position: Position, otherModels: ITextModel[], token: CancellationToken): ProviderResult<ResourceMap<DocumentHighlight[]>> {

const result = new ResourceMap<DocumentHighlight[]>();
Expand Down Expand Up @@ -57,7 +80,7 @@ export class TextualMultiDocumentHighlightFeature extends Disposable {
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
) {
super();

this._register(languageFeaturesService.documentHighlightProvider.register('*', new TextualDocumentHighlightProvider()));
this._register(languageFeaturesService.multiDocumentHighlightProvider.register('*', new TextualDocumentHighlightProvider()));
}
}
89 changes: 13 additions & 76 deletions src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
*--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
import * as arrays from 'vs/base/common/arrays';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { CancelablePromise, createCancelablePromise, Delayer, first, timeout } from 'vs/base/common/async';
import { CancelablePromise, createCancelablePromise, Delayer, first } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
Expand All @@ -23,7 +22,7 @@ import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/commo
import { IDiffEditor, IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry';
import { DocumentHighlight, DocumentHighlightKind, DocumentHighlightProvider, MultiDocumentHighlightProvider } from 'vs/editor/common/languages';
import { DocumentHighlight, DocumentHighlightProvider, MultiDocumentHighlightProvider } from 'vs/editor/common/languages';
import { IModelDeltaDecoration, ITextModel, shouldSynchronizeModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { getHighlightDecorationOptions } from 'vs/editor/contrib/wordHighlighter/browser/highlightDecorations';
Expand All @@ -34,8 +33,8 @@ import { Schemas } from 'vs/base/common/network';
import { ResourceMap } from 'vs/base/common/map';
import { score } from 'vs/editor/common/languageSelector';
import { isEqual } from 'vs/base/common/resources';
// import { TextualMultiDocumentHighlightFeature } from 'vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider';
// import { registerEditorFeature } from 'vs/editor/common/editorFeatures';
import { TextualMultiDocumentHighlightFeature } from 'vs/editor/contrib/wordHighlighter/browser/textualHighlightProvider';
import { registerEditorFeature } from 'vs/editor/common/editorFeatures';

const ctxHasWordHighlights = new RawContextKey<boolean>('hasWordHighlights', false);

Expand All @@ -44,11 +43,12 @@ export function getOccurrencesAtPosition(registry: LanguageFeatureRegistry<Docum

// in order of score ask the occurrences provider
// until someone response with a good result
// (good = none empty array)
// (good = non undefined and non null value)
// (result of size == 0 is valid, no highlights is a valid/expected result -- not a signal to fall back to other providers)
return first<DocumentHighlight[] | null | undefined>(orderedByScore.map(provider => () => {
return Promise.resolve(provider.provideDocumentHighlights(model, position, token))
.then(undefined, onUnexpectedExternalError);
}), arrays.isNonEmptyArray).then(result => {
}), (result): result is DocumentHighlight[] => result !== undefined && result !== null).then(result => {
if (result) {
const map = new ResourceMap<DocumentHighlight[]>();
map.set(model.uri, result);
Expand All @@ -63,17 +63,17 @@ export function getOccurrencesAcrossMultipleModels(registry: LanguageFeatureRegi

// in order of score ask the occurrences provider
// until someone response with a good result
// (good = none empty array)
// (good = non undefined and non null ResourceMap)
// (result of size == 0 is valid, no highlights is a valid/expected result -- not a signal to fall back to other providers)
return first<ResourceMap<DocumentHighlight[]> | null | undefined>(orderedByScore.map(provider => () => {
const filteredModels = otherModels.filter(otherModel => {
return shouldSynchronizeModel(otherModel);
}).filter(otherModel => {
return score(provider.selector, otherModel.uri, otherModel.getLanguageId(), true, undefined, undefined) > 0;
});

return Promise.resolve(provider.provideMultiDocumentHighlights(model, position, filteredModels, token))
.then(undefined, onUnexpectedExternalError);
}), (t: ResourceMap<DocumentHighlight[]> | null | undefined): t is ResourceMap<DocumentHighlight[]> => t instanceof ResourceMap && t.size > 0);
}), (result): result is ResourceMap<DocumentHighlight[]> => result !== undefined && result !== null);
}

interface IOccurenceAtPositionRequest {
Expand Down Expand Up @@ -184,76 +184,13 @@ class MultiModelOccurenceRequest extends OccurenceAtPositionRequest {
}
}

class TextualOccurenceRequest extends OccurenceAtPositionRequest {

private readonly _otherModels: ITextModel[];
private readonly _selectionIsEmpty: boolean;
private readonly _word: IWordAtPosition | null;

constructor(model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string, otherModels: ITextModel[]) {
super(model, selection, wordSeparators);
this._otherModels = otherModels;
this._selectionIsEmpty = selection.isEmpty();
this._word = word;
}

protected _compute(model: ITextModel, selection: Selection, wordSeparators: string, token: CancellationToken): Promise<ResourceMap<DocumentHighlight[]>> {
return timeout(250, token).then(() => {
const result = new ResourceMap<DocumentHighlight[]>();

let wordResult;
if (this._word) {
wordResult = this._word;
} else {
wordResult = model.getWordAtPosition(selection.getPosition());
}

if (!wordResult) {
return new ResourceMap<DocumentHighlight[]>();
}

const allModels = [model, ...this._otherModels];

for (const otherModel of allModels) {
if (otherModel.isDisposed()) {
continue;
}

const matches = otherModel.findMatches(wordResult.word, true, false, true, wordSeparators, false);
const highlights = matches.map(m => ({
range: m.range,
kind: DocumentHighlightKind.Text
}));

if (highlights) {
result.set(otherModel.uri, highlights);
}
}
return result;
});
}

public override isValid(model: ITextModel, selection: Selection, decorations: IEditorDecorationsCollection): boolean {
const currentSelectionIsEmpty = selection.isEmpty();
if (this._selectionIsEmpty !== currentSelectionIsEmpty) {
return false;
}
return super.isValid(model, selection, decorations);
}
}

function computeOccurencesAtPosition(registry: LanguageFeatureRegistry<DocumentHighlightProvider>, model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string): IOccurenceAtPositionRequest {
if (registry.has(model)) {
return new SemanticOccurenceAtPositionRequest(model, selection, wordSeparators, registry);
}
return new TextualOccurenceRequest(model, selection, word, wordSeparators, []);
return new SemanticOccurenceAtPositionRequest(model, selection, wordSeparators, registry);
}

function computeOccurencesMultiModel(registry: LanguageFeatureRegistry<MultiDocumentHighlightProvider>, model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string, otherModels: ITextModel[]): IOccurenceAtPositionRequest {
if (registry.has(model)) {
return new MultiModelOccurenceRequest(model, selection, wordSeparators, registry, otherModels);
}
return new TextualOccurenceRequest(model, selection, word, wordSeparators, otherModels);
return new MultiModelOccurenceRequest(model, selection, wordSeparators, registry, otherModels);
}

registerModelAndPositionCommand('_executeDocumentHighlights', async (accessor, model, position) => {
Expand Down Expand Up @@ -973,4 +910,4 @@ registerEditorContribution(WordHighlighterContribution.ID, WordHighlighterContri
registerEditorAction(NextWordHighlightAction);
registerEditorAction(PrevWordHighlightAction);
registerEditorAction(TriggerWordHighlightAction);
// registerEditorFeature(TextualMultiDocumentHighlightFeature);
registerEditorFeature(TextualMultiDocumentHighlightFeature);
5 changes: 3 additions & 2 deletions src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { createStringDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from 'vs/base/common/dataTransfer';
Expand Down Expand Up @@ -321,7 +320,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread
selector: selector,
provideMultiDocumentHighlights: (model: ITextModel, position: EditorPosition, otherModels: ITextModel[], token: CancellationToken): Promise<Map<URI, languages.DocumentHighlight[]> | undefined> => {
return this._proxy.$provideMultiDocumentHighlights(handle, model.uri, position, otherModels.map(model => model.uri), token).then(dto => {
if (isFalsyOrEmpty(dto)) {
// dto should be non-null + non-undefined
// dto length of 0 is valid, just no highlights, pass this through.
if (dto === undefined || dto === null) {
return undefined;
}
const result = new ResourceMap<languages.DocumentHighlight[]>();
Expand Down
Loading